Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
90.18% |
101 / 112 |
|
57.14% |
4 / 7 |
CRAP | |
0.00% |
0 / 1 |
Benchmarker | |
90.99% |
101 / 111 |
|
57.14% |
4 / 7 |
29.61 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
bench | |
98.33% |
59 / 60 |
|
0.00% |
0 / 1 |
17 | |||
startBench | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
addResult | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
3 | |||
verboseRun | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
formatSize | |
28.57% |
2 / 7 |
|
0.00% |
0 / 1 |
9.83 | |||
loadFile | |
0.00% |
0 / 4 |
|
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 | |
29 | namespace MediaWiki\Maintenance; |
30 | |
31 | use Wikimedia\RunningStat; |
32 | |
33 | // @codeCoverageIgnoreStart |
34 | require_once __DIR__ . '/../Maintenance.php'; |
35 | // @codeCoverageIgnoreEnd |
36 | |
37 | /** |
38 | * Base class for benchmark scripts. |
39 | * |
40 | * @ingroup Benchmark |
41 | */ |
42 | abstract 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 */ |
239 | class_alias( Benchmarker::class, 'Benchmarker' ); |