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 MapCacheLRU;
32use MediaWiki\Cache\CacheKeyHelper;
33use MediaWiki\Content\Content;
34use MediaWiki\Page\PageReference;
35use MediaWiki\Title\Title;
36use Psr\Log\LoggerInterface;
37use RuntimeException;
38
39/**
40 * For observing and detecting parser behaviors, such as duplicate parses
41 * @internal
42 * @package MediaWiki\Parser
43 */
44class ParserObserver {
45    /**
46     * @var LoggerInterface
47     */
48    private $logger;
49
50    private MapCacheLRU $previousParseStackTraces;
51
52    /**
53     * @param LoggerInterface $logger
54     */
55    public function __construct( LoggerInterface $logger ) {
56        $this->logger = $logger;
57        $this->previousParseStackTraces = new MapCacheLRU( 10 );
58    }
59
60    /**
61     * @param PageReference $page
62     * @param int|null $revId
63     * @param ParserOptions $options
64     * @param Content $content
65     * @param ParserOutput $output
66     */
67    public function notifyParse(
68        PageReference $page, ?int $revId, ParserOptions $options, Content $content, ParserOutput $output
69    ) {
70        $pageKey = CacheKeyHelper::getKeyForPage( $page );
71
72        $optionsHash = $options->optionsHash(
73            $output->getUsedOptions(),
74            Title::newFromPageReference( $page )
75        );
76
77        $contentStr = $content->isValid() ? $content->serialize() : null;
78        // $contentStr may be null if the content could not be serialized
79        $contentSha1 = $contentStr ? sha1( $contentStr ) : 'INVALID';
80
81        $index = $this->getParseId( $pageKey, $revId, $optionsHash, $contentSha1 );
82
83        $stackTrace = ( new RuntimeException() )->getTraceAsString();
84        if ( $this->previousParseStackTraces->has( $index ) ) {
85
86            // NOTE: there may be legitimate changes to re-parse the same WikiText content,
87            // e.g. if predicted revision ID for the REVISIONID magic word mismatched.
88            // But that should be rare.
89            $this->logger->debug(
90                __METHOD__ . ': Possibly redundant parse!',
91                [
92                    'page' => $pageKey,
93                    'rev' => $revId,
94                    'options-hash' => $optionsHash,
95                    'contentSha1' => $contentSha1,
96                    'trace' => $stackTrace,
97                    'previous-trace' => $this->previousParseStackTraces->get( $index ),
98                ]
99            );
100        }
101        $this->previousParseStackTraces->set( $index, $stackTrace );
102    }
103
104    /**
105     * @param string $titleStr
106     * @param int|null $revId
107     * @param string $optionsHash
108     * @param string $contentSha1
109     * @return string
110     */
111    private function getParseId( string $titleStr, ?int $revId, string $optionsHash, string $contentSha1 ): string {
112        // $revId may be null when previewing a new page
113        $revIdStr = $revId ?? "";
114
115        return "$titleStr.$revIdStr.$optionsHash.$contentSha1";
116    }
117
118}