Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
ParserObserver
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
3 / 3
6
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
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 * @license GPL-2.0-or-later
7 * @since 1.38
8 *
9 * @file
10 * @ingroup Parser
11 *
12 * @author Cindy Cicalese
13 */
14
15namespace MediaWiki\Parser;
16
17use MediaWiki\Content\Content;
18use MediaWiki\Page\CacheKeyHelper;
19use MediaWiki\Page\PageReference;
20use MediaWiki\Title\Title;
21use Psr\Log\LoggerInterface;
22use RuntimeException;
23use Wikimedia\ObjectCache\MapCacheLRU;
24
25/**
26 * For observing and detecting parser behaviors, such as duplicate parses
27 * @internal
28 */
29class ParserObserver {
30    private readonly MapCacheLRU $previousParseStackTraces;
31
32    public function __construct( private readonly LoggerInterface $logger ) {
33        $this->previousParseStackTraces = new MapCacheLRU( 10 );
34    }
35
36    /**
37     * @param PageReference $page
38     * @param int|null $revId
39     * @param ParserOptions $options
40     * @param Content $content
41     * @param ParserOutput $output
42     */
43    public function notifyParse(
44        PageReference $page, ?int $revId, ParserOptions $options, Content $content, ParserOutput $output
45    ) {
46        $pageKey = CacheKeyHelper::getKeyForPage( $page );
47
48        $optionsHash = $options->optionsHash(
49            $output->getUsedOptions(),
50            Title::newFromPageReference( $page )
51        );
52
53        $contentStr = $content->isValid() ? $content->serialize() : null;
54        // $contentStr may be null if the content could not be serialized
55        $contentSha1 = $contentStr ? sha1( $contentStr ) : 'INVALID';
56
57        $index = $this->getParseId( $pageKey, $revId, $optionsHash, $contentSha1 );
58
59        $stackTrace = ( new RuntimeException() )->getTraceAsString();
60        if ( $this->previousParseStackTraces->has( $index ) ) {
61
62            // NOTE: there may be legitimate changes to re-parse the same WikiText content,
63            // e.g. if predicted revision ID for the REVISIONID magic word mismatched.
64            // But that should be rare.
65            $this->logger->debug(
66                __METHOD__ . ': Possibly redundant parse!',
67                [
68                    'page' => $pageKey,
69                    'rev' => $revId,
70                    'options-hash' => $optionsHash,
71                    'contentSha1' => $contentSha1,
72                    'trace' => $stackTrace,
73                    'previous-trace' => $this->previousParseStackTraces->get( $index ),
74                ]
75            );
76        }
77        $this->previousParseStackTraces->set( $index, $stackTrace );
78    }
79
80    /**
81     * @param string $titleStr
82     * @param int|null $revId
83     * @param string $optionsHash
84     * @param string $contentSha1
85     * @return string
86     */
87    private function getParseId( string $titleStr, ?int $revId, string $optionsHash, string $contentSha1 ): string {
88        // $revId may be null when previewing a new page
89        $revIdStr = $revId ?? "";
90
91        return "$titleStr.$revIdStr.$optionsHash.$contentSha1";
92    }
93
94}