PluginProbe
Code Profiler – WordPress Performance Profiling and Debugging Made Easy / 1.8.2
Code Profiler – WordPress Performance Profiling and Debugging Made Easy v1.8.2
1.9.5 1.9.4 1.9.3 trunk 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.5 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.6 1.6.1 1.6.10 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 All 40 releases
code-profiler / lib / class-report.php

class-report.php in Code Profiler – WordPress Performance Profiling and Debugging Made Easy 1.8.2, at lib/class-report.php

563 lines 16.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 +=====================================================================+
4 | ____ _ ____ __ _ _ |
5 | / ___|___ __| | ___ | _ \ _ __ ___ / _(_) | ___ _ __ |
6 | | | / _ \ / _` |/ _ \ | |_) | '__/ _ \| |_| | |/ _ \ '__| |
7 | | |__| (_) | (_| | __/ | __/| | | (_) | _| | | __/ | |
8 | \____\___/ \__,_|\___| |_| |_| \___/|_| |_|_|\___|_| |
9 | |
10 | (c) Jerome Bruandet ~ https://nintechnet.com/codeprofiler/ |
11 +=====================================================================+
12 */
13
14 if (! defined('ABSPATH') ) { die('Forbidden'); }
15
16
17 class CodeProfiler_Report {
18
19 private $last_slug;
20 private $last_type;
21 private $last_time = 0;
22 private $themes = [];
23 private $plugins = [];
24 private $composer = [];
25 private $summary_list = [];
26 private $cx_buffer = [];
27 private $parsed_data = 0;
28 private $plugins_dir;
29 private $themes_dir;
30 private $mu_dir;
31 private $total_plugins;
32 private $total_io;
33 private $profile_name;
34 private $microtime;
35 private $tmp_iostats;
36 private $tmp_summary;
37 private $tmp_diskio;
38 private $tmp_connections;
39 private $tmp_calls;
40
41
42 /**
43 * Initialize
44 */
45 public function __construct( $profile_name, $microtime ) {
46
47 $this->tmp_summary = CODE_PROFILER_UPLOAD_DIR ."/$microtime." .
48 CODE_PROFILER_TMP_SUMMARY_LOG;
49 $this->tmp_iostats = CODE_PROFILER_UPLOAD_DIR ."/$microtime." .
50 CODE_PROFILER_TMP_IOSTATS_LOG;
51 $this->tmp_calls = CODE_PROFILER_UPLOAD_DIR ."/$microtime." .
52 CODE_PROFILER_TMP_CALLS_LOG;
53 $this->tmp_diskio = CODE_PROFILER_UPLOAD_DIR ."/$microtime." .
54 CODE_PROFILER_TMP_DISKIO_LOG;
55 $this->tmp_connections = CODE_PROFILER_UPLOAD_DIR ."/$microtime." .
56 CODE_PROFILER_TMP_CONNECTIONS_LOG;
57 // Used for naming, not metrics
58 $this->microtime = $microtime;
59
60 // Make sure all files are there
61 if (! file_exists( $this->tmp_calls ) ) {
62 $cp_error = 'calls';
63 } elseif (! file_exists( $this->tmp_iostats ) ) {
64 $cp_error = 'iostats';
65 }
66 if (! empty( $cp_error ) ) {
67 $error = sprintf(
68 esc_html__('Cannot create the report: the profiler did not generate a data file (%s). '.
69 'You may find more details about this error in the "Log" tab or your PHP error log.',
70 'code-profiler'),
71 $cp_error
72 );
73 $this->return_error( $error );
74 }
75
76 // Total data analyzed
77 $this->parsed_data += filesize( $this->tmp_iostats );
78 $this->parsed_data += filesize( $this->tmp_calls );
79
80 $this->profile_name = $profile_name;
81 $this->plugins_dir = preg_quote( realpath( WP_PLUGIN_DIR ) );
82 $this->themes_dir = preg_quote( realpath( WP_CONTENT_DIR .'/themes') );
83 $this->mu_dir = preg_quote( realpath( WPMU_PLUGIN_DIR ) );
84 }
85
86
87 /**
88 * Filter and save the profiler's data
89 */
90 public function prepare_report() {
91
92 $this->parse_calls();
93 $this->get_plugins_theme_name();
94 $this->save_iostats();
95 $this->save_connections();
96 $this->save_data();
97 $this->save_diskio();
98 $this->save_composer();
99
100 code_profiler_log_info( sprintf(
101 __('Volume of code and data analyzed: %1$sMB (%2$s plugins and '.
102 '1 theme) in %4$ss - Memory used: %3$sMB', 'code-profiler'),
103 number_format( $this->parsed_data / 1024 / 1024, 2 ),
104 (int) $this->total_plugins,
105 number_format( memory_get_peak_usage( false ) / 1024 / 1024, 2 ),
106 number_format( microtime( true ) - $this->microtime, 2 )
107 ));
108
109 return $this->microtime;
110 }
111
112
113 /**
114 * Return a json-encoded error for AJAX, write to log and quit.
115 */
116 private function return_error( $error ) {
117
118 $response['message'] = $error;
119 $response['status'] = 'error';
120 code_profiler_log_error( $error );
121 wp_send_json( $response );
122 }
123
124
125 /**
126 * Save all data to disk
127 */
128 private function save_data() {
129
130 $slugs_buffer = '';
131 $this->summary_list['time'] = 0;
132
133 $slugs_tsv = CODE_PROFILER_UPLOAD_DIR ."/{$this->microtime}.".
134 "{$this->profile_name}.slugs.profile";
135 if ( file_exists( $slugs_tsv ) ) { unlink( $slugs_tsv ); }
136
137 $summary_json = CODE_PROFILER_UPLOAD_DIR ."/{$this->microtime}.".
138 "{$this->profile_name}.summary.profile";
139 if ( file_exists( $summary_json ) ) { unlink( $summary_json ); }
140
141 foreach( $this->plugins as $content => $content_array ) {
142 $this->total_plugins++;
143 // Slugs
144 $this->summary_list['time'] += $content_array['time'];
145 if ( isset( $this->cx_buffer['plugin'][ $content ] ) ) {
146 $time = $this->convert_2_seconds(
147 $content_array['time'] + $this->cx_buffer['plugin'][ $content ]
148 );
149 $this->summary_list['time'] += $this->cx_buffer['plugin'][ $content ];
150 } else {
151 $time = $this->convert_2_seconds( $content_array['time'] );
152 }
153 if ( empty( $content_array['name'] ) ) {
154 // E.g., a plugin without a folder
155 $content_array['name'] = $content;
156 }
157 if ( isset( $content_array['mu'] ) ) {
158 $plugin = 'mu-plugin';
159 } else {
160 $plugin = 'plugin';
161 }
162 $slugs_buffer .= "$content\t$time\t{$content_array['name']}\t$plugin\n";
163 }
164 foreach( $this->themes as $content => $content_array ) {
165 // Slugs
166 $this->summary_list['time'] += $content_array['time'];
167 if ( isset( $this->cx_buffer['theme'][ $content ] ) ) {
168 $time = $this->convert_2_seconds(
169 $content_array['time'] + $this->cx_buffer['theme'][ $content ]
170 );
171 $this->summary_list['time'] += $this->cx_buffer['theme'][ $content ];
172 } else {
173 $time = $this->convert_2_seconds( $content_array['time'] );
174 }
175 $slugs_buffer .= "$content\t$time\t{$content_array['name']}\ttheme\n";
176 }
177
178 $error = '';
179 if ( empty( $slugs_buffer ) ) {
180 $error = esc_html__('Data is empty: no plugins or themes found', 'code-profiler');
181 }
182 if ( $error ) {
183 $this->return_error( $error );
184 }
185
186 // Save data
187 file_put_contents( $slugs_tsv, $slugs_buffer );
188
189 // Summary
190 $s = json_decode( file_get_contents( $this->tmp_summary ), true );
191 unlink( $this->tmp_summary );
192 if ( empty( $s['memory'] ) ) {
193 $this->summary_list['memory'] = 0;
194 } else {
195 $this->summary_list['memory'] = $s['memory'] / 1024 / 1024;
196 }
197 if ( empty( $s['queries'] ) ) {
198 $this->summary_list['queries']= 0;
199 } else {
200 $this->summary_list['queries']= $s['queries'];
201 }
202 if ( empty( $this->total_io ) ) {
203 $this->summary_list['io'] = 0;
204 } else {
205 $this->summary_list['io'] = $this->total_io;
206 }
207 if (! empty( $s['precision'] ) ) {
208 $this->summary_list['precision'] = $s['precision'];
209 }
210 $this->summary_list['items'] = $this->total_plugins + 1; // Add the theme
211 $this->summary_list['time'] = $this->convert_2_seconds( $this->summary_list['time'] );
212 $this->summary_list['time'] = number_format( $this->summary_list['time'], 4 );
213
214 // Re-run options
215 if (! empty( $s['rerun'] ) ) {
216 $this->summary_list['rerun'] = $s['rerun'];
217 $this->summary_list['rerun']['profile'] = $this->profile_name;
218 }
219
220 // Save Code Profiler, PHP and WordPress' versions
221 global $wp_version;
222 $this->summary_list['versions'] = [
223 'wp' => $wp_version,
224 'cp' => CODE_PROFILER_VERSION,
225 'php' => PHP_VERSION
226 ];
227
228 file_put_contents( $summary_json, json_encode( $this->summary_list ) );
229 }
230
231
232 /**
233 * Convert microtime (PHP<7.3) or hrtime (PHP>=7.3) to seconds
234 */
235 private function convert_2_seconds( $time ) {
236
237 if ( $time < 0 || empty( $time ) ) {
238 return '0';
239 }
240 if ( strpos( $time, '.' ) !== false ) {
241 // Looks like microtime
242 $time = number_format( $time, 6 );
243 } else {
244 // hrtime
245 $time = number_format( $time / 1000000000, 6 );
246 }
247 return $time;
248 }
249
250
251 /**
252 * Check if a slug is from a plugin or theme.
253 */
254 private function plugin_or_theme( $script ) {
255
256 $res = [
257 'theme' => '',
258 'plugin' => '',
259 'script' => ''
260 ];
261
262 if ( preg_match("`^{$this->plugins_dir}[\\\/](?:(.+?)[\\\/]|([^\\\/]+\.php)$)`", $script, $slug ) ) {
263 if (! empty( $slug[1] ) ) {
264 $res['plugin'] = $slug[1];
265 $res['script'] = $script;
266 } elseif (! empty( $slug[2] ) ) {
267 $res['plugin'] = $slug[2];
268 $res['script'] = $script;
269 }
270
271 } elseif ( preg_match("`^{$this->themes_dir}[\\\/]([^\\\/]+)[\\\/]`", $script, $slug ) ) {
272 $res['theme'] = $slug[1];
273 $res['script'] = $script;
274
275 } elseif ( preg_match("`^{$this->mu_dir}[\\\/]([^\\\/]+\.php)$`", $script, $slug ) ) {
276 $res['plugin'] = $slug[1];
277 $res['script'] = $script;
278 }
279 return $res;
280 }
281
282
283 /**
284 * Retrieve the full name for all plugins and themes.
285 */
286 private function get_plugins_theme_name() {
287
288 // Get installed plugins
289 if ( ! function_exists('get_plugins') ) {
290 require_once ABSPATH .'wp-admin/includes/plugin.php';
291 }
292 $installed_plugins = get_plugins();
293 foreach( $installed_plugins as $k => $v ) {
294 if ( preg_match('`^([^\\\/]+)[\\\/]`', $k, $match ) ) {
295 if ( isset( $this->plugins[ $match[1] ]['time'] ) ) {
296 $this->plugins[ $match[1] ]['name'] = $v['Name'];
297 }
298 }
299 }
300 // Get installed themes
301 if ( ! function_exists('wp_get_themes') ) {
302 require_once ABSPATH .'wp-includes/theme.php';
303 }
304 $installed_themes = wp_get_themes();
305 foreach( $installed_themes as $k => $v ) {
306 if ( isset( $this->themes[ $k ]['time'] ) ) {
307 $this->themes[ $k ]['name'] = $v->Name;
308 }
309 }
310 // MU plugins
311 $mu_plugins = get_mu_plugins();
312 foreach( $mu_plugins as $k => $v ) {
313 if ( isset( $this->plugins[ $k ]['time'] ) ) {
314 $this->plugins[ $k ]['name'] = $v['Name'];
315 $this->plugins[ $k ]['mu'] = 1;
316 }
317 }
318 }
319
320
321 /**
322 * Parse, filter and sort the data
323 */
324 private function parse_calls() {
325
326 $fh = fopen( $this->tmp_calls, 'rb');
327 if ( $fh === false ) {
328 $error = sprintf(
329 esc_html__('Cannot open file for reading: %s', 'code-profiler'),
330 $this->tmp_calls
331 );
332 $this->return_error( $error );
333 }
334 while(! feof( $fh ) ) {
335 $line = fgets( $fh );
336 if ( preg_match( '/^(.+?)\t(.+?)\t(.+?)\t(.+)$/', $line, $match ) ) {
337
338 $caller = $match[1]; $callee = $match[2];
339 $start = $match[3]; $stop = $match[4];
340
341 // Check if we have a plugin or theme
342 $slug = $this->plugin_or_theme( $callee );
343 if ( empty( $slug['theme'] ) && empty( $slug['plugin'] ) ) {
344 $slug = $this->plugin_or_theme( $caller );
345 }
346
347 // Elapsed time since last tick
348 if (! empty( $this->last_time ) && ! empty( $start ) ) {
349 $elapse = $start - $this->last_time;
350 } else {
351 $elapse = 0;
352 }
353
354 // Plugin: update/create stats (time)
355
356 // We have a slug
357 if (! empty( $slug['theme'] ) || ! empty( $slug['plugin'] ) ) {
358 // It's a theme
359 if (! empty( $slug['theme'] ) ) {
360 // Same theme as previous record, update its stats
361 if ( $this->last_slug == $slug['theme'] ) {
362 if ( isset( $this->themes[ $this->last_slug ]['time'] ) ) {
363 $this->themes[ $this->last_slug ]['time'] += $elapse;
364 }
365 // Update the old record and create the new one if it doesn't exist
366 } else {
367 if (! empty( $this->last_slug ) ) {
368 if ( isset( $this->themes[ $this->last_slug ]['time'] ) ) {
369 $this->themes[ $this->last_slug ]['time'] += $elapse;
370 }
371 }
372 if (! isset( $this->themes[ $slug['theme'] ]['time'] ) ) {
373 $this->themes[ $slug['theme'] ]['time'] = 0;
374 }
375 }
376 // It's a plugin
377 } elseif (! empty( $slug['plugin'] ) ) {
378 // Same plugin as previous record, update its stats
379 if ( $this->last_slug == $slug['plugin'] ) {
380 if ( isset( $this->plugins[ $this->last_slug ]['time'] ) ) {
381 $this->plugins[ $this->last_slug ]['time'] += $elapse;
382 }
383 // Update the old one and create the new one if it doesn't exist
384 } else {
385 if (! empty( $this->last_slug ) ) {
386 if ( isset( $this->plugins[ $this->last_slug ]['time'] ) ) {
387 $this->plugins[ $this->last_slug ]['time'] += $elapse;
388 }
389 }
390 if (! isset( $this->plugins[ $slug['plugin'] ]['time'] ) ) {
391 $this->plugins[ $slug['plugin'] ]['time'] = 0;
392 }
393 }
394
395 // Look for multiple copies of composer and warn the user
396 if ( preg_match("`^{$this->plugins_dir}[\\\/](?:[^\\\/]+)[\\\/].+?[\\\/]composer[\\\/]autoload_real\.php`", $caller ) ) {
397 // Save the slug first, we'll fetch the name later
398 if (! isset( $this->composer[ $slug['plugin'] ] ) ) {
399 $this->composer[ $slug['plugin'] ] = '';
400 }
401 }
402 }
403
404 // We don't have a slug: if there's an old record, update it
405 } else {
406 if (! empty( $this->last_type ) && ! empty( $this->last_slug ) ) {
407 if ( $this->last_type == 'plugin' ) {
408 if ( isset( $this->plugins[ $this->last_slug ]['time'] ) ) {
409 $this->plugins[ $this->last_slug ]['time'] += $elapse;
410 }
411 } elseif ( $this->last_type == 'theme' ) {
412 if ( isset( $this->themes[ $this->last_slug ]['time'] ) ) {
413 $this->themes[ $this->last_slug ]['time'] += $elapse;
414 }
415 }
416 }
417 }
418
419 // Update all last records
420 if (! empty( $slug['theme'] ) ) {
421 $this->last_type = 'theme';
422 $this->last_slug = $slug['theme'];
423 } elseif (! empty( $slug['plugin'] ) ) {
424 $this->last_type = 'plugin';
425 $this->last_slug = $slug['plugin'];
426 } else {
427 $this->last_type = '';
428 $this->last_slug = '';
429 }
430 $this->last_time = $stop;
431 }
432 }
433
434 fclose( $fh );
435 unlink( $this->tmp_calls );
436 }
437
438 /********************************************************************
439 * Save IO stats
440 */
441 private function save_iostats() {
442
443 $buffer = json_decode ( file_get_contents( $this->tmp_iostats ) , true );
444
445 if ( empty( $buffer ) ) {
446 $error = sprintf(
447 esc_html__('Cannot decode JSON-encode file (%s)', 'code-profiler'),
448 'CODE_PROFILER_TMP_IOSTATS_LOG'
449 );
450 $this->return_error( $error );
451 }
452
453 $lines = '';
454 foreach( $buffer as $key => $value ) {
455 $lines .= "$key\t$value\n";
456 $this->total_io += $value;
457 }
458
459 file_put_contents(
460 CODE_PROFILER_UPLOAD_DIR ."/{$this->microtime}.{$this->profile_name}.iostats.profile",
461 $lines
462 );
463
464 unlink( $this->tmp_iostats );
465 }
466
467
468 /**
469 * Save Read/Write stats
470 */
471 private function save_diskio() {
472
473 rename(
474 $this->tmp_diskio,
475 CODE_PROFILER_UPLOAD_DIR ."/{$this->microtime}.{$this->profile_name}.diskio.profile"
476 );
477 }
478
479
480 /**
481 * Save list of plugins using composer
482 */
483 private function save_composer() {
484
485 if (! empty( $this->composer ) ) {
486 // Try to get the plugin's name
487 foreach( $this->composer as $slug => $v ) {
488 if ( isset( $this->plugins[ $slug ]['name'] ) ) {
489 $this->composer[ $slug ] = $this->plugins[ $slug ]['name'];
490 }
491 }
492 file_put_contents(
493 CODE_PROFILER_UPLOAD_DIR ."/{$this->microtime}.{$this->profile_name}.composer.profile",
494 json_encode( $this->composer )
495 );
496 }
497 }
498
499 /**
500 * External connections.
501 */
502 private function save_connections() {
503
504 if (! file_exists( $this->tmp_connections ) ) {
505 return;
506 }
507
508 $connections = json_decode( file_get_contents( $this->tmp_connections ), true );
509 unlink( $this->tmp_connections );
510
511 if ( empty( $connections ) ) {
512 return;
513 }
514
515 foreach( $connections as $connection => $array ) {
516 $found_call = 0;
517 $found_caller = 0;
518
519 foreach( $array['bt'] as $k =>$v ) {
520 if ( isset( $v['file'] ) && isset( $v['function'] ) && isset( $v['line'] ) ) {
521
522 if (! $found_caller && ( $found_call ||
523 in_array( $v['function'], [
524 'wp_remote_get',
525 'wp_safe_remote_get',
526 'wp_remote_post',
527 'wp_safe_remote_post'
528 ] )
529 ) ) {
530
531 $found_call = 1;
532
533 $type = $this->plugin_or_theme( $v['file'] );
534
535 if (! empty( $type['theme'] ) ) {
536 // Save it for later use
537 if ( isset( $this->cx_buffer['theme'][ $type['theme'] ] ) ) {
538 $this->cx_buffer['theme'][ $type['theme'] ] += $array['stop'] - $connection;
539 } else {
540 $this->cx_buffer['theme'][ $type['theme'] ] = $array['stop'] - $connection;
541 }
542 $found_caller = 1;
543
544 } elseif (! empty( $type['plugin'] ) ) {
545 // Save it for later use
546 if ( isset( $this->cx_buffer['plugin'][ $type['plugin'] ] ) ) {
547 $this->cx_buffer['plugin'][ $type['plugin'] ] += $array['stop'] - $connection;
548 } else {
549 $this->cx_buffer['plugin'][ $type['plugin'] ] = $array['stop'] - $connection;
550 }
551 $found_caller = 1;
552 }
553 }
554 }
555 }
556 }
557 }
558
559 }
560
561 // =====================================================================
562 // EOF
563