Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.18% covered (success)
90.18%
101 / 112
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Benchmarker
90.99% covered (success)
90.99%
101 / 111
57.14% covered (warning)
57.14%
4 / 7
29.61
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 bench
98.33% covered (success)
98.33%
59 / 60
0.00% covered (danger)
0.00%
0 / 1
17
 startBench
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 addResult
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
3
 verboseRun
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 formatSize
28.57% covered (danger)
28.57%
2 / 7
0.00% covered (danger)
0.00%
0 / 1
9.83
 loadFile
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * @defgroup Benchmark Benchmark
4 * @ingroup  Maintenance
5 */
6
7/**
8 * Base code for benchmark scripts.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License along
21 * with this program; if not, write to the Free Software Foundation, Inc.,
22 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23 * http://www.gnu.org/copyleft/gpl.html
24 *
25 * @file
26 * @ingroup Benchmark
27 */
28
29namespace MediaWiki\Maintenance;
30
31use Wikimedia\RunningStat;
32
33// @codeCoverageIgnoreStart
34require_once __DIR__ . '/../Maintenance.php';
35// @codeCoverageIgnoreEnd
36
37/**
38 * Base class for benchmark scripts.
39 *
40 * @ingroup Benchmark
41 */
42abstract class Benchmarker extends Maintenance {
43    /** @var int */
44    protected $defaultCount = 100;
45
46    public function __construct() {
47        parent::__construct();
48        $this->addOption( 'count', "How many times to run a benchmark. Default: {$this->defaultCount}", false, true );
49        $this->addOption( 'verbose', 'Verbose logging of resource usage', false, false, 'v' );
50    }
51
52    public function bench( array $benchs ) {
53        $this->startBench();
54        $count = $this->getOption( 'count', $this->defaultCount );
55        $verbose = $this->hasOption( 'verbose' );
56
57        $normBenchs = [];
58        $shortNames = [];
59
60        // Normalise
61        foreach ( $benchs as $key => $bench ) {
62            // Shortcut for simple functions
63            if ( is_callable( $bench ) ) {
64                $bench = [ 'function' => $bench ];
65            }
66
67            // Default to no arguments
68            if ( !isset( $bench['args'] ) ) {
69                $bench['args'] = [];
70            }
71
72            // Name defaults to name of called function
73            if ( is_string( $key ) ) {
74                $name = $key;
75            } else {
76                // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
77                if ( is_array( $bench['function'] ) ) {
78                    $class = $bench['function'][0];
79                    if ( is_object( $class ) ) {
80                        $class = get_class( $class );
81                    }
82                    $name = $class . '::' . $bench['function'][1];
83                } else {
84                    // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
85                    $name = strval( $bench['function'] );
86                }
87                $argsText = implode(
88                    ', ',
89                    array_map(
90                        static function ( $a ) {
91                            return var_export( $a, true );
92                        },
93                        // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
94                        $bench['args']
95                    )
96                );
97                $index = $shortNames[$name] = ( $shortNames[$name] ?? 0 ) + 1;
98                $shorten = strlen( $argsText ) > 80 || str_contains( $argsText, "\n" );
99                if ( !$shorten ) {
100                    $name = "$name($argsText)";
101                }
102                if ( $shorten || $index > 1 ) {
103                    $name = "$name@$index";
104                }
105            }
106
107            $normBenchs[$name] = $bench;
108        }
109
110        foreach ( $normBenchs as $name => $bench ) {
111            // Optional setup called outside time measure
112            if ( isset( $bench['setup'] ) ) {
113                call_user_func( $bench['setup'] );
114            }
115
116            // Run benchmarks
117            $stat = new RunningStat();
118            for ( $i = 0; $i < $count; $i++ ) {
119                // Setup outside of time measure for each loop
120                if ( isset( $bench['setupEach'] ) ) {
121                    $bench['setupEach']();
122                }
123                $t = microtime( true );
124                // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
125                call_user_func_array( $bench['function'], $bench['args'] );
126                $t = ( microtime( true ) - $t ) * 1000;
127                if ( $verbose ) {
128                    $this->verboseRun( $i );
129                }
130                $stat->addObservation( $t );
131            }
132
133            $this->addResult( [
134                'name' => $name,
135                'count' => $stat->getCount(),
136                // Get rate per second from mean (in ms)
137                'rate' => $stat->getMean() == 0 ? INF : ( 1.0 / ( $stat->getMean() / 1000.0 ) ),
138                'total' => $stat->getMean() * $stat->getCount(),
139                'mean' => $stat->getMean(),
140                'max' => $stat->max,
141                'stddev' => $stat->getStdDev(),
142                'usage' => [
143                    'mem' => memory_get_usage( true ),
144                    'mempeak' => memory_get_peak_usage( true ),
145                ],
146            ] );
147        }
148    }
149
150    public function startBench() {
151        $this->output(
152            sprintf( "Running PHP version %s (%s) on %s %s %s\n\n",
153                phpversion(),
154                php_uname( 'm' ),
155                php_uname( 's' ),
156                php_uname( 'r' ),
157                php_uname( 'v' )
158            )
159        );
160    }
161
162    public function addResult( $res ) {
163        $ret = sprintf( "%s\n  %' 6s: %d\n",
164            $res['name'],
165            'count',
166            $res['count']
167        );
168        $ret .= sprintf( "  %' 6s: %8.1f/s\n",
169            'rate',
170            $res['rate']
171        );
172        foreach ( [ 'total', 'mean', 'max', 'stddev' ] as $metric ) {
173            $ret .= sprintf( "  %' 6s: %8.2fms\n",
174                $metric,
175                $res[$metric]
176            );
177        }
178
179        foreach ( [
180            'mem' => 'Current memory usage',
181            'mempeak' => 'Peak memory usage'
182        ] as $key => $label ) {
183            $ret .= sprintf( "%' 20s: %s\n",
184                $label,
185                $this->formatSize( $res['usage'][$key] )
186            );
187        }
188
189        $this->output( "$ret\n" );
190    }
191
192    protected function verboseRun( $iteration ) {
193        $this->output( sprintf( "#%3d - memory: %-10s - peak: %-10s\n",
194            $iteration,
195            $this->formatSize( memory_get_usage( true ) ),
196            $this->formatSize( memory_get_peak_usage( true ) )
197        ) );
198    }
199
200    /**
201     * Format an amount of bytes into short human-readable string.
202     *
203     * This is simplified version of Language::formatSize() to avoid pulling
204     * all the general MediaWiki services, which can significantly influence
205     * measured memory use.
206     *
207     * @param int|float $bytes
208     * @return string Formatted in using IEC bytes (multiples of 1024)
209     */
210    private function formatSize( $bytes ): string {
211        if ( $bytes >= ( 1024 ** 3 ) ) {
212            return number_format( $bytes / ( 1024 ** 3 ), 2 ) . ' GiB';
213        }
214        if ( $bytes >= ( 1024 ** 2 ) ) {
215            return number_format( $bytes / ( 1024 ** 2 ), 2 ) . ' MiB';
216        }
217        if ( $bytes >= 1024 ) {
218            return number_format( $bytes / 1024, 1 ) . ' KiB';
219        }
220        return $bytes . ' B';
221    }
222
223    /**
224     * @since 1.32
225     * @param string $file Path to file (maybe compressed with gzip)
226     * @return string|false Contents of file, or false if file not found
227     */
228    protected function loadFile( $file ) {
229        $content = file_get_contents( $file );
230        // Detect GZIP compression header
231        if ( str_starts_with( $content, "\037\213" ) ) {
232            $content = gzdecode( $content );
233        }
234        return $content;
235    }
236}
237
238/** @deprecated class alias since 1.43 */
239class_alias( Benchmarker::class, 'Benchmarker' );