Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
ParserObserver
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
3 / 3
6
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 notifyParse
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
4
 getParseId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Observer to detect parser behaviors such as duplicate parses
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
20 *
21 * @since 1.38
22 *
23 * @file
24 * @ingroup Parser
25 *
26 * @author Cindy Cicalese
27 */
28
29namespace MediaWiki\Parser;
30
31use Content;
32use MapCacheLRU;
33use MediaWiki\Cache\CacheKeyHelper;
34use MediaWiki\Page\PageReference;
35use MediaWiki\Title\Title;
36use ParserOptions;
37use Psr\Log\LoggerInterface;
38use RuntimeException;
39
40/**
41 * For observing and detecting parser behaviors, such as duplicate parses
42 * @internal
43 * @package MediaWiki\Parser
44 */
45class ParserObserver {
46    /**
47     * @var LoggerInterface
48     */
49    private $logger;
50
51    private MapCacheLRU $previousParseStackTraces;
52
53    /**
54     * @param LoggerInterface $logger
55     */
56    public function __construct( LoggerInterface $logger ) {
57        $this->logger = $logger;
58        $this->previousParseStackTraces = new MapCacheLRU( 10 );
59    }
60
61    /**
62     * @param PageReference $page
63     * @param int|null $revId
64     * @param ParserOptions $options
65     * @param Content $content
66     * @param ParserOutput $output
67     */
68    public function notifyParse(
69        PageReference $page, ?int $revId, ParserOptions $options, Content $content, ParserOutput $output
70    ) {
71        $pageKey = CacheKeyHelper::getKeyForPage( $page );
72
73        $optionsHash = $options->optionsHash(
74            $output->getUsedOptions(),
75            Title::newFromPageReference( $page )
76        );
77
78        $contentStr = $content->isValid() ? $content->serialize() : null;
79        // $contentStr may be null if the content could not be serialized
80        $contentSha1 = $contentStr ? sha1( $contentStr ) : 'INVALID';
81
82        $index = $this->getParseId( $pageKey, $revId, $optionsHash, $contentSha1 );
83
84        $stackTrace = ( new RuntimeException() )->getTraceAsString();
85        if ( $this->previousParseStackTraces->has( $index ) ) {
86
87            // NOTE: there may be legitimate changes to re-parse the same WikiText content,
88            // e.g. if predicted revision ID for the REVISIONID magic word mismatched.
89            // But that should be rare.
90            $this->logger->debug(
91                __METHOD__ . ': Possibly redundant parse!',
92                [
93                    'page' => $pageKey,
94                    'rev' => $revId,
95                    'options-hash' => $optionsHash,
96                    'contentSha1' => $contentSha1,
97                    'trace' => $stackTrace,
98                    'previous-trace' => $this->previousParseStackTraces->get( $index ),
99                ]
100            );
101        }
102        $this->previousParseStackTraces->set( $index, $stackTrace );
103    }
104
105    /**
106     * @param string $titleStr
107     * @param int|null $revId
108     * @param string $optionsHash
109     * @param string $contentSha1
110     * @return string
111     */
112    private function getParseId( string $titleStr, ?int $revId, string $optionsHash, string $contentSha1 ): string {
113        // $revId may be null when previewing a new page
114        $revIdStr = $revId ?? "";
115
116        return "$titleStr.$revIdStr.$optionsHash.$contentSha1";
117    }
118
119}