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

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