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

544 lines 15.4 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://code-profiler.com/ |
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_ticks;
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_ticks = CODE_PROFILER_UPLOAD_DIR ."/$microtime." .
52 CODE_PROFILER_TMP_TICKS_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_ticks ) ) {
62 $cp_error = 1;
63 } elseif (! file_exists( $this->tmp_iostats ) ) {
64 $cp_error = 2;
65 }
66 if (! empty( $cp_error ) ) {
67 $error = sprintf(
68 esc_html__('Cannot create the report: the profiler did not '.
69 'generate a data file (#%s). Make sure the following directory '.
70 'is writable: %s. If you are using a caching plugin or an '.
71 'opcode cache, try to disable it. You may also find more '.
72 'details about the error in the "Log" tab.', 'code-profiler'),
73 $cp_error,
74 CODE_PROFILER_UPLOAD_DIR .'/'
75 );
76 $this->return_error( $error );
77 }
78
79 // Total data analyzed
80 $this->parsed_data += filesize( $this->tmp_iostats );
81 $this->parsed_data += filesize( $this->tmp_ticks );
82
83 $this->profile_name = $profile_name;
84 $this->plugins_dir = preg_quote( realpath( WP_PLUGIN_DIR ) );
85 $this->themes_dir = preg_quote( realpath( WP_CONTENT_DIR .'/themes') );
86 $this->mu_dir = preg_quote( realpath( WPMU_PLUGIN_DIR ) );
87 }
88
89
90 /**
91 * Filter and save the profiler's data
92 */
93 public function prepare_report() {
94
95 $this->parse_ticks();
96 $this->get_plugins_theme_name();
97 $this->save_iostats();
98 $this->save_connections();
99 $this->save_data();
100 $this->save_diskio();
101 $this->save_composer();
102
103 code_profiler_log_info( sprintf(
104 __('Volume of code and data analyzed: %1$sMB (%2$s plugins and '.
105 '1 theme) in %4$ss - Memory used: %3$sMB', 'code-profiler'),
106 number_format( $this->parsed_data / 1024 / 1024, 2 ),
107 (int) $this->total_plugins,
108 number_format( memory_get_peak_usage( false ) / 1024 / 1024, 2 ),
109 number_format( microtime( true ) - $this->microtime, 2 )
110 ));
111
112 return $this->microtime;
113 }
114
115
116 /**
117 * Return a json-encoded error for AJAX, write to log and quit.
118 */
119 private function return_error( $error ) {
120
121 $response['message'] = $error;
122 $response['status'] = 'error';
123 code_profiler_log_error( $error );
124 wp_send_json( $response );
125 }
126
127
128 /**
129 * Save all data to disk
130 */
131 private function save_data() {
132
133 $slugs_buffer = '';
134 $this->summary_list['time'] = 0;
135
136 $slugs_tsv = CODE_PROFILER_UPLOAD_DIR ."/{$this->microtime}.".
137 "{$this->profile_name}.slugs.profile";
138 if ( file_exists( $slugs_tsv ) ) { unlink( $slugs_tsv ); }
139
140 $summary_json = CODE_PROFILER_UPLOAD_DIR ."/{$this->microtime}.".
141 "{$this->profile_name}.summary.profile";
142 if ( file_exists( $summary_json ) ) { unlink( $summary_json ); }
143
144 foreach( $this->plugins as $content => $content_array ) {
145 $this->total_plugins++;
146 // Slugs
147 $this->summary_list['time'] += $content_array['time'];
148 if ( isset( $this->cx_buffer['plugin'][ $content ] ) ) {
149 $time = $this->convert_2_seconds(
150 $content_array['time'] + $this->cx_buffer['plugin'][ $content ]
151 );
152 $this->summary_list['time'] += $this->cx_buffer['plugin'][ $content ];
153 } else {
154 $time = $this->convert_2_seconds( $content_array['time'] );
155 }
156 if ( empty( $content_array['name'] ) ) {
157 // E.g., a plugin without a folder
158 $content_array['name'] = $content;
159 }
160 if ( isset( $content_array['mu'] ) ) {
161 $plugin = 'mu-plugin';
162 } else {
163 $plugin = 'plugin';
164 }
165 $slugs_buffer .= "$content\t$time\t{$content_array['name']}\t$plugin\n";
166 }
167 foreach( $this->themes as $content => $content_array ) {
168 // Slugs
169 $this->summary_list['time'] += $content_array['time'];
170 if ( isset( $this->cx_buffer['theme'][ $content ] ) ) {
171 $time = $this->convert_2_seconds(
172 $content_array['time'] + $this->cx_buffer['theme'][ $content ]
173 );
174 $this->summary_list['time'] += $this->cx_buffer['theme'][ $content ];
175 } else {
176 $time = $this->convert_2_seconds( $content_array['time'] );
177 }
178 $slugs_buffer .= "$content\t$time\t{$content_array['name']}\ttheme\n";
179 }
180
181 $error = '';
182 if ( empty( $slugs_buffer ) ) {
183 $error = esc_html__('Data is empty: no plugins or themes found', 'code-profiler');
184 }
185 if ( $error ) {
186 $this->return_error( $error );
187 }
188
189 // Save data
190 file_put_contents( $slugs_tsv, $slugs_buffer );
191
192 // Summary
193 $s = json_decode( file_get_contents( $this->tmp_summary ), true );
194 unlink( $this->tmp_summary );
195 if ( empty( $s['memory'] ) ) {
196 $this->summary_list['memory'] = '-';
197 } else {
198 $this->summary_list['memory'] = $s['memory'] / 1024 / 1024;
199 }
200 if ( empty( $s['queries'] ) ) {
201 $this->summary_list['queries']= '-';
202 } else {
203 $this->summary_list['queries']= $s['queries'];
204 }
205 if ( empty( $this->total_io ) ) {
206 $this->summary_list['io'] = '-';
207 } else {
208 $this->summary_list['io'] = $this->total_io;
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_ticks() {
320
321 $fh = fopen( $this->tmp_ticks, 'rb');
322 if ( $fh === false ) {
323 $error = sprintf(
324 esc_html__('Cannot open file for reading: %s', 'code-profiler'),
325 $this->tmp_ticks
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_ticks );
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 = 0;
512
513 foreach( $array['bt'] as $k =>$v ) {
514 if ( isset( $v['file'] ) && isset( $v['function'] ) && isset( $v['line'] ) ) {
515 if ( ! $found &&
516 in_array( $v['function'], [
517 'wp_remote_get',
518 'wp_safe_remote_get',
519 'wp_remote_post',
520 'wp_safe_remote_post'
521 ] )
522 ) {
523 $found = 1;
524
525 $type = $this->plugin_or_theme( $v['file'] );
526 if (! empty( $type['theme'] ) ) {
527 // Save it for later use
528 $this->cx_buffer['theme'][ $type['theme'] ] = $array['stop'] - $connection;
529
530 } elseif (! empty( $type['plugin'] ) ) {
531 // Save it for later use
532 $this->cx_buffer['plugin'][ $type['plugin'] ] = $array['stop'] - $connection;
533 }
534 }
535 }
536 }
537 }
538 }
539
540 }
541
542 // =====================================================================
543 // EOF
544