MediaWiki master
Benchmarker.php
Go to the documentation of this file.
1<?php
29namespace MediaWiki\Maintenance;
30
31use Wikimedia\RunningStat;
32
33// @codeCoverageIgnoreStart
34require_once __DIR__ . '/../Maintenance.php';
35// @codeCoverageIgnoreEnd
36
42abstract class Benchmarker extends Maintenance {
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
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
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
239class_alias( Benchmarker::class, 'Benchmarker' );
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Base class for benchmark scripts.
__construct()
Default constructor.
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
output( $out, $channel=null)
Throw some output to the user.
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
hasOption( $name)
Checks to see if a particular option was set.
getOption( $name, $default=null)
Get an option, or return the default.