Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Profile
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 15
756
0.00% covered (danger)
0.00%
0 / 1
 start
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 end
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pushNestedProfile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 bumpProfileTimeUse
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 bumpTimeUse
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 bumpMWTime
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 bumpCount
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 formatLine
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 cmpProfile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 formatProfile
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
30
 printProfile
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
6
 swallowArray
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 reduce
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 reduceProfileTree
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 print
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Config;
5
6/**
7 * Records time profiling information
8 */
9class Profile {
10    /** @var int|float - int on 64-bit systems */
11    public $startTime;
12
13    /** @var int|float - int on 64-bit systems */
14    public $endTime;
15
16    private array $timeProfile = [];
17    private array $mwProfile = [];
18    private array $timeCategories = [];
19    private array $counts = [];
20
21    /**
22     * Array of profiles for nested pipelines. So, we effectively end up with
23     * a profile tree with the top-level-doc profile as the root profile.
24     * @var array<Profile>
25     */
26    private array $nestedProfiles = [];
27
28    /**
29     * This is the most recently pushed nested profile from a nested pipeline.
30     * @var ?Profile
31     */
32    private ?Profile $recentNestedProfile = null;
33
34    public function start(): void {
35        $this->startTime = hrtime( true );
36    }
37
38    public function end(): void {
39        $this->endTime = hrtime( true );
40    }
41
42    public function pushNestedProfile( Profile $p ): void {
43        $this->nestedProfiles[] = $this->recentNestedProfile = $p;
44    }
45
46    /**
47     * @param array &$profile
48     * @param string $resource
49     * @param int|float $time Time in nanoseconds
50     * @param ?string $cat
51     */
52    private function bumpProfileTimeUse(
53        array &$profile, string $resource, $time, ?string $cat
54    ): void {
55        $time /= 1000000; // ns --> ms
56        if ( $profile === $this->timeProfile && $this->recentNestedProfile ) {
57            // Eliminate double-counting
58            $time -= ( $this->recentNestedProfile->endTime - $this->recentNestedProfile->startTime ) / 1000000;
59            $this->recentNestedProfile = null;
60        }
61
62        $profile[$resource] ??= 0;
63        $profile[$resource] += $time;
64        if ( $cat ) {
65            $this->timeCategories[$cat] ??= 0;
66            $this->timeCategories[$cat] += $time;
67        }
68    }
69
70    /**
71     * Update a profile timer.
72     *
73     * @param string $resource
74     * @param int|float $time Time in nanoseconds
75     * @param string|null $cat
76     */
77    public function bumpTimeUse( string $resource, $time, ?string $cat = null ): void {
78        $this->bumpProfileTimeUse( $this->timeProfile, $resource, $time, $cat );
79    }
80
81    /**
82     * Update profile usage for "MW API" requests
83     *
84     * @param string $resource
85     * @param int|float $time Time in nanoseconds
86     * @param string|null $cat
87     */
88    public function bumpMWTime( string $resource, $time, ?string $cat = null ): void {
89        // FIXME: For now, skip the category since this leads to double counting
90        // when reportind time by categories since this time is part of other
91        // '$this->timeProfile' categories already.
92        $this->bumpProfileTimeUse( $this->mwProfile, $resource, $time, null );
93    }
94
95    /**
96     * Update a profile counter.
97     *
98     * @param string $resource
99     * @param int $n The amount to increment the counter; defaults to 1.
100     */
101    public function bumpCount( string $resource, int $n = 1 ): void {
102        $this->counts[$resource] ??= 0;
103        $this->counts[$resource] += $n;
104    }
105
106    /**
107     * @param string $k
108     * @param mixed $v
109     * @param string $comment
110     * @return string
111     */
112    private function formatLine( string $k, $v, string $comment = '' ): string {
113        $buf = str_pad( $k, 60, " ", STR_PAD_LEFT ) . ':';
114        if ( $v === round( $v ) ) {
115            $v = (string)$v;
116        } else {
117            $v = str_pad( (string)( floor( $v * 10000 ) / 10000 ), 5, ' ', STR_PAD_LEFT );
118        }
119        return $buf . str_pad( $v, 10, " ", STR_PAD_LEFT ) . ( $comment ? ' (' . $comment . ')' : '' );
120    }
121
122    /**
123     * Sort comparison function
124     * @param array $a
125     * @param array $b
126     * @return float
127     */
128    private static function cmpProfile( array $a, array $b ): float {
129        return $b[1] - $a[1];
130    }
131
132    private function formatProfile( array $profile, array $options = [] ): array {
133        // Sort time profile in descending order
134
135        $total = 0;
136        $outLines = [];
137        foreach ( $profile as $k => $v ) {
138            $total += $v;
139            $outLines[] = [ $k, $v ];
140        }
141
142        usort( $outLines, [ self::class, 'cmpProfile' ] );
143
144        $lines = [];
145        foreach ( $outLines as $line ) {
146            $k = $line[0];
147            $v = $line[1];
148            $lineComment = '';
149            if ( isset( $options['printPercentage'] ) ) {
150                $lineComment = (string)( round( $v * 1000 / $total ) / 10 ) . '%';
151            }
152
153            $buf = $this->formatLine( $k, $v, $lineComment );
154            if ( isset( $this->counts[$k] ) ) {
155                $buf .= str_pad( '; count: ' .
156                    str_pad( (string)( $this->counts[$k] ), 6, ' ', STR_PAD_LEFT ),
157                    6, " ", STR_PAD_LEFT );
158                $buf .= str_pad( '; per-instance: ' .
159                    str_pad(
160                        (string)( floor( $v * 10000 / $this->counts[$k] ) / 10000 ), 5, ' ', STR_PAD_LEFT
161                    ), 10 );
162            }
163            $lines[] = $buf;
164        }
165        return [ 'buf' => implode( "\n", $lines ), 'total' => $total ];
166    }
167
168    private function printProfile(): string {
169        $outLines = [];
170        $mwOut = $this->formatProfile( $this->mwProfile );
171        $cpuOut = $this->formatProfile( $this->timeProfile );
172
173        $outLines[] = str_repeat( "-", 85 );
174        $outLines[] = "Recorded times (in ms) for various parse components";
175        $outLines[] = "";
176        $outLines[] = $cpuOut['buf'];
177        $outLines[] = str_repeat( "-", 85 );
178        $outLines[] = 'Recorded times (in ms) for various "MW API" requests';
179        $outLines[] = "";
180        $outLines[] = $mwOut['buf'];
181        $outLines[] = str_repeat( "-", 85 );
182        $parseTime = ( $this->endTime - $this->startTime ) / 1000000;
183        $outLines[] = $this->formatLine( 'TOTAL PARSE TIME (1)', $parseTime );
184        $outLines[] = $this->formatLine( 'TOTAL PARSOID CPU TIME (2)', $cpuOut['total'] );
185        if ( $mwOut['total'] > 0 ) {
186            $outLines[] = $this->formatLine( 'TOTAL "MW API" TIME', $mwOut['total'] );
187        }
188        $outLines[] = $this->formatLine( 'Un/over-accounted parse time: (1) - (2)',
189            $parseTime - $cpuOut['total'] );
190        $outLines[] = "";
191        $catOut = $this->formatProfile( $this->timeCategories, [ 'printPercentage' => true ] );
192        $outLines[] = $catOut['buf'];
193        $outLines[] = "";
194        $outLines[] = str_repeat( "-", 85 );
195
196        // dump to stderr via error_log
197        return implode( "\n", $outLines );
198    }
199
200    private static function swallowArray( array $a, array &$res ): void {
201        foreach ( $a as $k => $v ) {
202            $res[$k] ??= 0;
203            $res[$k] += $v;
204        }
205    }
206
207    private function reduce( Profile $reducedProfile ): void {
208        self::swallowArray( $this->counts, $reducedProfile->counts );
209        self::swallowArray( $this->timeCategories, $reducedProfile->timeCategories );
210        self::swallowArray( $this->timeProfile, $reducedProfile->timeProfile );
211        self::swallowArray( $this->mwProfile, $reducedProfile->mwProfile );
212
213        foreach ( $this->nestedProfiles as $p ) {
214            $p->reduce( $reducedProfile );
215        }
216    }
217
218    private function reduceProfileTree(): Profile {
219        $reducedProfile = new Profile();
220        $reducedProfile->startTime = $this->startTime;
221        $reducedProfile->endTime = $this->endTime;
222        $this->reduce( $reducedProfile );
223        return $reducedProfile;
224    }
225
226    public function print(): string {
227        return $this->reduceProfileTree()->printProfile();
228    }
229}