Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.99% covered (success)
90.99%
101 / 111
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
29use Wikimedia\RunningStat;
30
31// @codeCoverageIgnoreStart
32require_once __DIR__ . '/../Maintenance.php';
33// @codeCoverageIgnoreEnd
34
35/**
36 * Base class for benchmark scripts.
37 *
38 * @ingroup Benchmark
39 */
40abstract class Benchmarker extends Maintenance {
41    protected $defaultCount = 100;
42
43    public function __construct() {
44        parent::__construct();
45        $this->addOption( 'count', "How many times to run a benchmark. Default: {$this->defaultCount}", false, true );
46        $this->addOption( 'verbose', 'Verbose logging of resource usage', false, false, 'v' );
47    }
48
49    public function bench( array $benchs ) {
50        $this->startBench();
51        $count = $this->getOption( 'count', $this->defaultCount );
52        $verbose = $this->hasOption( 'verbose' );
53
54        $normBenchs = [];
55        $shortNames = [];
56
57        // Normalise
58        foreach ( $benchs as $key => $bench ) {
59            // Shortcut for simple functions
60            if ( is_callable( $bench ) ) {
61                $bench = [ 'function' => $bench ];
62            }
63
64            // Default to no arguments
65            if ( !isset( $bench['args'] ) ) {
66                $bench['args'] = [];
67            }
68
69            // Name defaults to name of called function
70            if ( is_string( $key ) ) {
71                $name = $key;
72            } else {
73                // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
74                if ( is_array( $bench['function'] ) ) {
75                    $class = $bench['function'][0];
76                    if ( is_object( $class ) ) {
77                        $class = get_class( $class );
78                    }
79                    $name = $class . '::' . $bench['function'][1];
80                } else {
81                    // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
82                    $name = strval( $bench['function'] );
83                }
84                $argsText = implode(
85                    ', ',
86                    array_map(
87                        static function ( $a ) {
88                            return var_export( $a, true );
89                        },
90                        // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
91                        $bench['args']
92                    )
93                );
94                $index = $shortNames[$name] = ( $shortNames[$name] ?? 0 ) + 1;
95                $shorten = strlen( $argsText ) > 80 || str_contains( $argsText, "\n" );
96                if ( !$shorten ) {
97                    $name = "$name($argsText)";
98                }
99                if ( $shorten || $index > 1 ) {
100                    $name = "$name@$index";
101                }
102            }
103
104            $normBenchs[$name] = $bench;
105        }
106
107        foreach ( $normBenchs as $name => $bench ) {
108            // Optional setup called outside time measure
109            if ( isset( $bench['setup'] ) ) {
110                call_user_func( $bench['setup'] );
111            }
112
113            // Run benchmarks
114            $stat = new RunningStat();
115            for ( $i = 0; $i < $count; $i++ ) {
116                // Setup outside of time measure for each loop
117                if ( isset( $bench['setupEach'] ) ) {
118                    $bench['setupEach']();
119                }
120                $t = microtime( true );
121                // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
122                call_user_func_array( $bench['function'], $bench['args'] );
123                $t = ( microtime( true ) - $t ) * 1000;
124                if ( $verbose ) {
125                    $this->verboseRun( $i );
126                }
127                $stat->addObservation( $t );
128            }
129
130            $this->addResult( [
131                'name' => $name,
132                'count' => $stat->getCount(),
133                // Get rate per second from mean (in ms)
134                'rate' => $stat->getMean() == 0 ? INF : ( 1.0 / ( $stat->getMean() / 1000.0 ) ),
135                'total' => $stat->getMean() * $stat->getCount(),
136                'mean' => $stat->getMean(),
137                'max' => $stat->max,
138                'stddev' => $stat->getStdDev(),
139                'usage' => [
140                    'mem' => memory_get_usage( true ),
141                    'mempeak' => memory_get_peak_usage( true ),
142                ],
143            ] );
144        }
145    }
146
147    public function startBench() {
148        $this->output(
149            sprintf( "Running PHP version %s (%s) on %s %s %s\n\n",
150                phpversion(),
151                php_uname( 'm' ),
152                php_uname( 's' ),
153                php_uname( 'r' ),
154                php_uname( 'v' )
155            )
156        );
157    }
158
159    public function addResult( $res ) {
160        $ret = sprintf( "%s\n  %' 6s: %d\n",
161            $res['name'],
162            'count',
163            $res['count']
164        );
165        $ret .= sprintf( "  %' 6s: %8.1f/s\n",
166            'rate',
167            $res['rate']
168        );
169        foreach ( [ 'total', 'mean', 'max', 'stddev' ] as $metric ) {
170            $ret .= sprintf( "  %' 6s: %8.2fms\n",
171                $metric,
172                $res[$metric]
173            );
174        }
175
176        foreach ( [
177            'mem' => 'Current memory usage',
178            'mempeak' => 'Peak memory usage'
179        ] as $key => $label ) {
180            $ret .= sprintf( "%' 20s: %s\n",
181                $label,
182                $this->formatSize( $res['usage'][$key] )
183            );
184        }
185
186        $this->output( "$ret\n" );
187    }
188
189    protected function verboseRun( $iteration ) {
190        $this->output( sprintf( "#%3d - memory: %-10s - peak: %-10s\n",
191            $iteration,
192            $this->formatSize( memory_get_usage( true ) ),
193            $this->formatSize( memory_get_peak_usage( true ) )
194        ) );
195    }
196
197    /**
198     * Format an amount of bytes into short human-readable string.
199     *
200     * This is simplified version of Language::formatSize() to avoid pulling
201     * all the general MediaWiki services, which can significantly influence
202     * measured memory use.
203     *
204     * @param int|float $bytes
205     * @return string Formatted in using IEC bytes (multiples of 1024)
206     */
207    private function formatSize( $bytes ): string {
208        if ( $bytes >= ( 1024 ** 3 ) ) {
209            return number_format( $bytes / ( 1024 ** 3 ), 2 ) . ' GiB';
210        }
211        if ( $bytes >= ( 1024 ** 2 ) ) {
212            return number_format( $bytes / ( 1024 ** 2 ), 2 ) . ' MiB';
213        }
214        if ( $bytes >= 1024 ) {
215            return number_format( $bytes / 1024, 1 ) . ' KiB';
216        }
217        return $bytes . ' B';
218    }
219
220    /**
221     * @since 1.32
222     * @param string $file Path to file (maybe compressed with gzip)
223     * @return string|false Contents of file, or false if file not found
224     */
225    protected function loadFile( $file ) {
226        $content = file_get_contents( $file );
227        // Detect GZIP compression header
228        if ( str_starts_with( $content, "\037\213" ) ) {
229            $content = gzdecode( $content );
230        }
231        return $content;
232    }
233}