PluginProbe
Code Profiler – WordPress Performance Profiling and Debugging Made Easy / 1.7.4
Code Profiler – WordPress Performance Profiling and Debugging Made Easy v1.7.4
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.7.4, at lib/class-report.php

558 lines 15.8 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 // Save Code Profiler, PHP and WordPress' versions
215 global $wp_version;
216 $this->summary_list['versions'] = [
217 'wp' => $wp_version,
218 'cp' => CODE_PROFILER_VERSION,
219 'php' => PHP_VERSION
220 ];
221
222 file_put_contents( $summary_json, json_encode( $this->summary_list ) );
223
224 }
225
226
227 /**
228 * Convert microtime (PHP<7.3) or hrtime (PHP>=7.3) to seconds
229 */
230 private function convert_2_seconds( $time ) {
231
232 if ( $time < 0 || empty( $time ) ) {
233 return '0';
234 }
235 if ( strpos( $time, '.' ) !== false ) {
236 // Looks like microtime
237 $time = number_format( $time, 6 );
238 } else {
239 // hrtime
240 $time = number_format( $time / 1000000000, 6 );
241 }
242 return $time;
243 }
244
245
246 /**
247 * Check if a slug is from a plugin or theme.
248 */
249 private function plugin_or_theme( $script ) {
250
251 $res = [
252 'theme' => '',
253 'plugin' => '',
254 'script' => ''
255 ];
256
257 if ( preg_match("`^{$this->plugins_dir}[\\\/](?:(.+?)[\\\/]|([^\\\/]+\.php)$)`", $script, $slug ) ) {
258 if (! empty( $slug[1] ) ) {
259 $res['plugin'] = $slug[1];
260 $res['script'] = $script;
261 } elseif (! empty( $slug[2] ) ) {
262 $res['plugin'] = $slug[2];
263 $res['script'] = $script;
264 }
265
266 } elseif ( preg_match("`^{$this->themes_dir}[\\\/]([^\\\/]+)[\\\/]`", $script, $slug ) ) {
267 $res['theme'] = $slug[1];
268 $res['script'] = $script;
269
270 } elseif ( preg_match("`^{$this->mu_dir}[\\\/]([^\\\/]+\.php)$`", $script, $slug ) ) {
271 $res['plugin'] = $slug[1];
272 $res['script'] = $script;
273 }
274 return $res;
275 }
276
277
278 /**
279 * Retrieve the full name for all plugins and themes.
280 */
281 private function get_plugins_theme_name() {
282
283 // Get installed plugins
284 if ( ! function_exists('get_plugins') ) {
285 require_once ABSPATH .'wp-admin/includes/plugin.php';
286 }
287 $installed_plugins = get_plugins();
288 foreach( $installed_plugins as $k => $v ) {
289 if ( preg_match('`^([^\\\/]+)[\\\/]`', $k, $match ) ) {
290 if ( isset( $this->plugins[ $match[1] ]['time'] ) ) {
291 $this->plugins[ $match[1] ]['name'] = $v['Name'];
292 }
293 }
294 }
295 // Get installed themes
296 if ( ! function_exists('wp_get_themes') ) {
297 require_once ABSPATH .'wp-includes/theme.php';
298 }
299 $installed_themes = wp_get_themes();
300 foreach( $installed_themes as $k => $v ) {
301 if ( isset( $this->themes[ $k ]['time'] ) ) {
302 $this->themes[ $k ]['name'] = $v->Name;
303 }
304 }
305 // MU plugins
306 $mu_plugins = get_mu_plugins();
307 foreach( $mu_plugins as $k => $v ) {
308 if ( isset( $this->plugins[ $k ]['time'] ) ) {
309 $this->plugins[ $k ]['name'] = $v['Name'];
310 $this->plugins[ $k ]['mu'] = 1;
311 }
312 }
313 }
314
315
316 /**
317 * Parse, filter and sort the data
318 */
319 private function parse_calls() {
320
321 $fh = fopen( $this->tmp_calls, 'rb');
322 if ( $fh === false ) {
323 $error = sprintf(
324 esc_html__('Cannot open file for reading: %s', 'code-profiler'),
325 $this->tmp_calls
326 );
327 $this->return_error( $error );
328 }
329 while(! feof( $fh ) ) {
330 $line = fgets( $fh );
331 if ( preg_match( '/^(.+?)\t(.+?)\t(.+?)\t(.+)$/', $line, $match ) ) {
332
333 $caller = $match[1]; $callee = $match[2];
334 $start = $match[3]; $stop = $match[4];
335
336 // Check if we have a plugin or theme
337 $slug = $this->plugin_or_theme( $callee );
338 if ( empty( $slug['theme'] ) && empty( $slug['plugin'] ) ) {
339 $slug = $this->plugin_or_theme( $caller );
340 }
341
342 // Elapsed time since last tick
343 if (! empty( $this->last_time ) && ! empty( $start ) ) {
344 $elapse = $start - $this->last_time;
345 } else {
346 $elapse = 0;
347 }
348
349 // Plugin: update/create stats (time)
350
351 // We have a slug
352 if (! empty( $slug['theme'] ) || ! empty( $slug['plugin'] ) ) {
353 // It's a theme
354 if (! empty( $slug['theme'] ) ) {
355 // Same theme as previous record, update its stats
356 if ( $this->last_slug == $slug['theme'] ) {
357 if ( isset( $this->themes[ $this->last_slug ]['time'] ) ) {
358 $this->themes[ $this->last_slug ]['time'] += $elapse;
359 }
360 // Update the old record and create the new one if it doesn't exist
361 } else {
362 if (! empty( $this->last_slug ) ) {
363 if ( isset( $this->themes[ $this->last_slug ]['time'] ) ) {
364 $this->themes[ $this->last_slug ]['time'] += $elapse;
365 }
366 }
367 if (! isset( $this->themes[ $slug['theme'] ]['time'] ) ) {
368 $this->themes[ $slug['theme'] ]['time'] = 0;
369 }
370 }
371 // It's a plugin
372 } elseif (! empty( $slug['plugin'] ) ) {
373 // Same plugin as previous record, update its stats
374 if ( $this->last_slug == $slug['plugin'] ) {
375 if ( isset( $this->plugins[ $this->last_slug ]['time'] ) ) {
376 $this->plugins[ $this->last_slug ]['time'] += $elapse;
377 }
378 // Update the old one and create the new one if it doesn't exist
379 } else {
380 if (! empty( $this->last_slug ) ) {
381 if ( isset( $this->plugins[ $this->last_slug ]['time'] ) ) {
382 $this->plugins[ $this->last_slug ]['time'] += $elapse;
383 }
384 }
385 if (! isset( $this->plugins[ $slug['plugin'] ]['time'] ) ) {
386 $this->plugins[ $slug['plugin'] ]['time'] = 0;
387 }
388 }
389
390 // Look for multiple copies of composer and warn the user
391 if ( preg_match("`^{$this->plugins_dir}[\\\/](?:[^\\\/]+)[\\\/].+?[\\\/]composer[\\\/]autoload_real\.php`", $caller ) ) {
392 // Save the slug first, we'll fetch the name later
393 if (! isset( $this->composer[ $slug['plugin'] ] ) ) {
394 $this->composer[ $slug['plugin'] ] = '';
395 }
396 }
397 }
398
399 // We don't have a slug: if there's an old record, update it
400 } else {
401 if (! empty( $this->last_type ) && ! empty( $this->last_slug ) ) {
402 if ( $this->last_type == 'plugin' ) {
403 if ( isset( $this->plugins[ $this->last_slug ]['time'] ) ) {
404 $this->plugins[ $this->last_slug ]['time'] += $elapse;
405 }
406 } elseif ( $this->last_type == 'theme' ) {
407 if ( isset( $this->themes[ $this->last_slug ]['time'] ) ) {
408 $this->themes[ $this->last_slug ]['time'] += $elapse;
409 }
410 }
411 }
412 }
413
414 // Update all last records
415 if (! empty( $slug['theme'] ) ) {
416 $this->last_type = 'theme';
417 $this->last_slug = $slug['theme'];
418 } elseif (! empty( $slug['plugin'] ) ) {
419 $this->last_type = 'plugin';
420 $this->last_slug = $slug['plugin'];
421 } else {
422 $this->last_type = '';
423 $this->last_slug = '';
424 }
425 $this->last_time = $stop;
426 }
427 }
428
429 fclose( $fh );
430 unlink( $this->tmp_calls );
431 }
432
433 /********************************************************************
434 * Save IO stats
435 */
436 private function save_iostats() {
437
438 $buffer = json_decode ( file_get_contents( $this->tmp_iostats ) , true );
439
440 if ( empty( $buffer ) ) {
441 $error = sprintf(
442 esc_html__('Cannot decode JSON-encode file (%s)', 'code-profiler'),
443 'CODE_PROFILER_TMP_IOSTATS_LOG'
444 );
445 $this->return_error( $error );
446 }
447
448 $lines = '';
449 foreach( $buffer as $key => $value ) {
450 $lines .= "$key\t$value\n";
451 $this->total_io += $value;
452 }
453
454 file_put_contents(
455 CODE_PROFILER_UPLOAD_DIR ."/{$this->microtime}.{$this->profile_name}.iostats.profile",
456 $lines
457 );
458
459 unlink( $this->tmp_iostats );
460 }
461
462
463 /**
464 * Save Read/Write stats
465 */
466 private function save_diskio() {
467
468 rename(
469 $this->tmp_diskio,
470 CODE_PROFILER_UPLOAD_DIR ."/{$this->microtime}.{$this->profile_name}.diskio.profile"
471 );
472 }
473
474
475 /**
476 * Save list of plugins using composer
477 */
478 private function save_composer() {
479
480 if (! empty( $this->composer ) ) {
481 // Try to get the plugin's name
482 foreach( $this->composer as $slug => $v ) {
483 if ( isset( $this->plugins[ $slug ]['name'] ) ) {
484 $this->composer[ $slug ] = $this->plugins[ $slug ]['name'];
485 }
486 }
487 file_put_contents(
488 CODE_PROFILER_UPLOAD_DIR ."/{$this->microtime}.{$this->profile_name}.composer.profile",
489 json_encode( $this->composer )
490 );
491 }
492 }
493
494 /**
495 * External connections.
496 */
497 private function save_connections() {
498
499 if (! file_exists( $this->tmp_connections ) ) {
500 return;
501 }
502
503 $connections = json_decode( file_get_contents( $this->tmp_connections ), true );
504 unlink( $this->tmp_connections );
505
506 if ( empty( $connections ) ) {
507 return;
508 }
509
510 foreach( $connections as $connection => $array ) {
511 $found_call = 0;
512 $found_caller = 0;
513
514 foreach( $array['bt'] as $k =>$v ) {
515 if ( isset( $v['file'] ) && isset( $v['function'] ) && isset( $v['line'] ) ) {
516
517 if (! $found_caller && ( $found_call ||
518 in_array( $v['function'], [
519 'wp_remote_get',
520 'wp_safe_remote_get',
521 'wp_remote_post',
522 'wp_safe_remote_post'
523 ] )
524 ) ) {
525
526 $found_call = 1;
527
528 $type = $this->plugin_or_theme( $v['file'] );
529
530 if (! empty( $type['theme'] ) ) {
531 // Save it for later use
532 if ( isset( $this->cx_buffer['theme'][ $type['theme'] ] ) ) {
533 $this->cx_buffer['theme'][ $type['theme'] ] += $array['stop'] - $connection;
534 } else {
535 $this->cx_buffer['theme'][ $type['theme'] ] = $array['stop'] - $connection;
536 }
537 $found_caller = 1;
538
539 } elseif (! empty( $type['plugin'] ) ) {
540 // Save it for later use
541 if ( isset( $this->cx_buffer['plugin'][ $type['plugin'] ] ) ) {
542 $this->cx_buffer['plugin'][ $type['plugin'] ] += $array['stop'] - $connection;
543 } else {
544 $this->cx_buffer['plugin'][ $type['plugin'] ] = $array['stop'] - $connection;
545 }
546 $found_caller = 1;
547 }
548 }
549 }
550 }
551 }
552 }
553
554 }
555
556 // =====================================================================
557 // EOF
558