EBernhardson has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/104318


Change subject: Debug toolbar profiler timeline
......................................................................

Debug toolbar profiler timeline

This adds a new pane for the debug toolbar labeled Profiler.  It groups
together events sent to wfProfileIn/Out and draws a timeline giving a
visual representation of what the page was doing at a particular point
in time.

This is mostly inspired by, and some code reused from, the
WebProfilerBundle for symfony2.

Change-Id: I23a0ce4137abcd74ad87967fb263214c08cb9685
---
M includes/debug/Debug.php
M includes/profiler/Profiler.php
M resources/Resources.php
M resources/mediawiki/mediawiki.debug.js
A resources/mediawiki/mediawiki.profile.js
5 files changed, 345 insertions(+), 3 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/18/104318/1

diff --git a/includes/debug/Debug.php b/includes/debug/Debug.php
index d0f8916..1ee541b 100644
--- a/includes/debug/Debug.php
+++ b/includes/debug/Debug.php
@@ -547,6 +547,7 @@
                        'memory' => $context->getLanguage()->formatSize( 
memory_get_usage() ),
                        'memoryPeak' => $context->getLanguage()->formatSize( 
memory_get_peak_usage() ),
                        'includes' => self::getFilesIncluded( $context ),
+                       'profile' => Profiler::instance()->getRawData(),
                );
        }
 }
diff --git a/includes/profiler/Profiler.php b/includes/profiler/Profiler.php
index 235a5ad..ef84bc0 100644
--- a/includes/profiler/Profiler.php
+++ b/includes/profiler/Profiler.php
@@ -96,7 +96,7 @@
  */
 class Profiler {
        protected $mStack = array(), $mWorkStack = array(), $mCollated = 
array(),
-               $mCalls = array(), $mTotals = array();
+               $mCalls = array(), $mTotals = array(), $mPeriods = array();
        protected $mTimeMetric = 'wall';
        protected $mProfileID = false, $mCollateDone = false, $mTemplated = 
false;
 
@@ -556,6 +556,7 @@
                                $this->mMin[$fname] = 1 << 24;
                                $this->mMax[$fname] = 0;
                                $this->mOverhead[$fname] = 0;
+                               $this->mPeriods[$fname] = array();
                        }
 
                        $this->mCollated[$fname] += $elapsed;
@@ -564,6 +565,7 @@
                        $this->mMin[$fname] = min( $this->mMin[$fname], 
$elapsed );
                        $this->mMax[$fname] = max( $this->mMax[$fname], 
$elapsed );
                        $this->mOverhead[$fname] += $subcalls;
+                       $this->mPeriods[$fname][] = compact( 'start', 'end' );
                }
 
                $this->mCalls['-overhead-total'] = $profileCount;
@@ -608,6 +610,33 @@
                return $prof;
        }
 
+       function getRawData() {
+               $this->collateData();
+
+               $output = array();
+               $total = isset( $this->mCollated['-total'] ) ? 
$this->mCollated['-total'] : 0;
+               foreach ( $this->mCollated as $fname => $elapsed ) {
+                       $periods = array();
+                       foreach ( $this->mPeriods[$fname] as $period ) {
+                               $periods[] = array(
+                                       'start' => $period['start'] * 1000,
+                                       'end' => $period['end'] * 1000,
+                               );
+                       }
+                       $output[$fname] = array(
+                               'calls' => $this->mCalls[$fname],
+                               'elapsed' => $elapsed * 1000,
+                               'percent' => $total ? 100. * $elapsed / $total 
: 0,
+                               'memory' => $this->mMemory[$fname],
+                               'min' => $this->mMin[$fname] * 1000,
+                               'max' => $this->mMax[$fname] * 1000,
+                               'overhead' => $this->mOverhead[$fname],
+                               'periods' => $periods,
+                       );
+               }
+               return $output;
+       }
+
        /**
         * Dummy calls to wfProfileIn/wfProfileOut to calculate its overhead
         */
diff --git a/resources/Resources.php b/resources/Resources.php
index d79b29b..e8ba975 100644
--- a/resources/Resources.php
+++ b/resources/Resources.php
@@ -700,7 +700,7 @@
                ),
        ),
        'mediawiki.debug' => array(
-               'scripts' => 'resources/mediawiki/mediawiki.debug.js',
+               'scripts' => array( 'resources/mediawiki/mediawiki.debug.js', 
'resources/mediawiki/mediawiki.profile.js' ),
                'styles' => 'resources/mediawiki/mediawiki.debug.css',
                'dependencies' => 'jquery.footHovzer',
                'position' => 'bottom',
diff --git a/resources/mediawiki/mediawiki.debug.js 
b/resources/mediawiki/mediawiki.debug.js
index 986917a..ff96c60 100644
--- a/resources/mediawiki/mediawiki.debug.js
+++ b/resources/mediawiki/mediawiki.debug.js
@@ -160,6 +160,8 @@
 
                        paneTriggerBitDiv( 'includes', 'PHP includes', 
this.data.includes.length );
 
+                       paneTriggerBitDiv( 'profile', 'Profile', 
this.data.profile.length );
+
                        gitInfo = '';
                        if ( this.data.gitRevision !== false ) {
                                gitInfo = '(' + 
this.data.gitRevision.substring( 0, 7 ) + ')';
@@ -196,7 +198,8 @@
                                querylist: this.buildQueryTable(),
                                debuglog: this.buildDebugLogTable(),
                                request: this.buildRequestPane(),
-                               includes: this.buildIncludesPane()
+                               includes: this.buildIncludesPane(),
+                               profile: this.buildProfilePane()
                        };
 
                        for ( id in panes ) {
@@ -359,6 +362,11 @@
                        }
 
                        return $table;
+               },
+
+               buildProfilePane: function() {
+                       mw.Debug.Profile.init();
+                       return mw.Debug.Profile.canvas;
                }
        };
 
diff --git a/resources/mediawiki/mediawiki.profile.js 
b/resources/mediawiki/mediawiki.profile.js
new file mode 100644
index 0000000..c888978
--- /dev/null
+++ b/resources/mediawiki/mediawiki.profile.js
@@ -0,0 +1,304 @@
+/**
+ * JavaScript for the debug toolbar profiler, enabled through $wgDebugToolbar
+ * and StartProfiler.php.
+ *
+ * @author Erik Bernhardson
+ * @since 1.23
+ */
+
+( function ( mw, $ ) {
+       'use strict';
+
+       var profile,
+               hovzer = $.getFootHovzer();
+
+       profile = mw.Debug.Profile = {
+               /**
+                * Object containing data for the debug toolbar
+                *
+                * @var {Object}
+                */
+               data: {},
+
+               /**
+                * Canvas timeline is drawn to
+                *
+                * @var {Element}
+                */
+               canvas: null,
+
+               /**
+                * Initializes the debugging pane.
+                * Shouldn't be called before the document is ready
+                * (since it binds to elements on the page).
+                *
+                * @param {Object} data, defaults to 'debugInfo' from mw.config
+                */
+               init: function ( data, width, mergeThreshold, dropThreshold ) {
+
+                       this.canvas = document.createElement( 'canvas' );
+                       this.data = data || mw.config.get( 'debugInfo' 
).profile;
+                       this.buildCanvas( width || $(window).width() - 100, 
mergeThreshold || 1, dropThreshold || 1 );
+               },
+
+               collate: function( width, mergeThreshold, dropThreshold ) {
+                       var label, pos, i, sum, minmax, ratio,
+                               groups = {},
+                               prefix = 'Profile section ended by close(): ';
+
+
+                       // group together by partial label and period
+                       for ( i in this.data ) {
+                               if ( 0 === i.indexOf( '-' ) || i == "" ) {
+                                       continue;
+                               }
+                               if ( 0 === i.indexOf( prefix ) ) {
+                                       i = i.substring( prefix.length );
+                               }
+                               pos = [ '::', ':', '-' ].reduce( function( 
result, separator ) {
+                                       var pos = i.indexOf( separator );
+                                       if ( pos === -1 ) {
+                                               return result;
+                                       } else if ( result === -1 ) {
+                                               return pos;
+                                       } else {
+                                               return Math.min( pos, result );
+                                       }
+                               }, -1 );
+                               if ( pos === -1 ) {
+                                       label = i;
+                               } else {
+                                       label = i.substring( 0, pos );
+                               }
+                               groups[label] = groups[label] || [];
+                               groups[label].push.apply( 
+                                       groups[label], 
+                                       // filter any bad data
+                                       this.data[i].periods.filter( function( 
period ) {
+                                               return period.start && 
period.end && period.end > period.start;
+                                       } )
+                               );
+                       }
+
+                       // find the min and max timestamp
+                       minmax = Object.keys( groups ).reduce( function ( 
result, label ) {
+                               return groups[label].reduce( periodMinMax, 
result );
+                       }, { start: Number.POSITIVE_INFINITY, end: 
Number.NEGATIVE_INFINITY} );
+
+                       // change mergeThreshold from pixels to ms
+                       ratio = ( minmax.end - minmax.start ) / width;
+                       mergeThreshold *= ratio;
+
+                       // merge periods closer than a step size 
+                       return  Object.keys( groups ).reduce( function( result, 
 label ) {
+                               var periods = groups[label].sort( function ( a, 
b ) {
+                                       if ( a.start == b.start ) {
+                                               return a.end - b.end;
+                                       }
+                                       return a.start - b.start;
+                               } ).reduce( function ( result, period ){
+                                       // shift all periods left by 
minmax.start
+                                       // only when period.start > 1000000, 
because sometimes mw profiler 
+                                       // outputs relative time(bug?)
+                                       if ( period.start > 1000000 ) {
+                                               period.start -= minmax.start;
+                                               period.end -= minmax.start;
+                                       }
+
+                                       if ( result.length == 0 ) {
+                                               return [period];
+                                       }
+                                       var last = result[result.length - 1];
+                                       if ( period.end < last.end ) {
+                                               // skip, end is contained 
within previous
+                                       } else if ( period.start - 
mergeThreshold < last.end ) {
+                                               // neighbors within merging 
distance
+                                               result[result.length - 1].end = 
period.end;
+                                       } else {
+                                               // period is next result
+                                               result.push( period );
+                                       }
+                                       return result;
+                               }, [] ).filter( function( period ) {
+                                       return period.end - period.start > 
dropThreshold;
+                               } );
+                               if ( periods.length ) {
+                                       result[label] = periods;
+                               }
+                               return result;
+                       }, {} );
+               },
+
+               /**
+                * Constructs the HTML for the profile toolbar
+                */
+               buildCanvas: function ( width, mergeThreshold, dropThreshold ) {
+                       var $container, $bits, i, pos, groups, label;
+
+                       $container = $( '<div id="mw-profile-toolbar" 
class="mw-profile" lang="en" dir="ltr"></div>' );
+
+                       $bits = $( '<div class="mw-debug-bits"></div>' );
+
+                       groups = this.collate( width, mergeThreshold, 
dropThreshold );
+
+                       // Calculate extra timespan based data
+                       var timespans = {}, first, last;
+                       for ( label in groups ) {
+                               timespans[label] = groups[label].reduce( 
+                                       periodMinMax,
+                                       { start: Number.POSITIVE_INFINITY, end: 
Number.NEGATIVE_INFINITY} 
+                               );
+                               timespans[label].length = timespans[label].end 
- timespans[label].start;
+                               timespans[label].sum = groups[label].reduce( 
function( result, period ) {
+                                       return result + period.end - 
period.start;
+                               }, 0 );
+                       }
+                       var total = Object.keys( timespans ).reduce(
+                               function( result, label ) { return 
periodMinMax( result, timespans[label] ); },
+                               { start: Number.POSITIVE_INFINITY, end: 
Number.NEGATIVE_INFINITY} 
+                       );
+                       var max = total.end - total.start;
+
+                       // Sort by contained time
+                       this.sortedLabels = Object.keys( groups ).sort( 
function( a, b ) {
+                               return timespans[a].start - timespans[b].start; 
+                       } );
+                       // for debugging
+                       this.other = groups;
+
+                       // start drawing
+                       var i, j,
+                               text,
+                               canvasHeight = 0,
+                               gapPerEvent = 38,
+                               colors = { 
+                                       default: 'red',
+                               },
+                               space = 10.5,
+                               ratio = ( width - space * 2 ) / max,
+                               h = space,
+                               x = 0,
+                               canvas = this.canvas,
+                               ctx = canvas.getContext( "2d" ),
+                               backingStoreRatio,
+                               scaleRatio,
+                               devicePixelRatio;
+
+
+                       // reset the canvas
+                       ctx.save();
+                       ctx.setTransform( 1, 0, 0, 1, 0, 0 );
+                       ctx.clearRect( 0, 0, canvas.width, canvas.height );
+                       ctx.restore();
+
+                       // Figure out our size
+                       canvasHeight += gapPerEvent * Object.keys( groups 
).length;
+                       devicePixelRatio = window.devicePixelRatio == 
"undefined" ? 1 : window.devicePixelRatio;
+                       backingStoreRatio = ctx.webkitBackingStorePixelRatio == 
"undefined" ? 1 : ctx.webkitBackingStorePixelRatio;
+                       scaleRatio = devicePixelRatio / 1;
+
+                       canvas.width = width * scaleRatio;
+                       canvas.height = canvasHeight * scaleRatio;
+
+                       canvas.style.width = width + 'px';
+                       canvas.style.height = canvasHeight + 'px';
+
+                       ctx.scale( scaleRatio, scaleRatio );
+
+                       ctx.textBaseline = "middle";
+                       ctx.lineWidth = 0;
+
+                       this.sortedLabels.forEach( function( label ) {
+                               h += 8;
+
+                               if ( colors[label] ) {
+                                       ctx.fillStyle = colors[label];
+                                       ctx.strokeStyle = colors[label];
+                               } else {
+                                       ctx.fillStyle = colors['default'];
+                                       ctx.strokeStyle = colors['default'];
+                               }
+
+                               groups[label].forEach( function( period ) {
+                                       
+                                       var timelineHeadPosition = x + 
period.start * ratio,
+                                               timelineTailPosition = x + 
period.end * ratio;
+
+                                       // if less than width draw a plain box
+                                       if ( timelineHeadPosition + 16 > 
timelineTailPosition ) {
+                                               ctx.fillRect( 
timelineHeadPosition, h, 2, 9 );
+                                               ctx.fillRect( 
timelineHeadPosition, h, ( period.end - period.start ) * ratio || 2, 6 );
+                                       } else {
+                                               ctx.beginPath();
+                                               ctx.moveTo( 
timelineHeadPosition, h );
+                                               ctx.lineTo( 
timelineHeadPosition, h + 11 );
+                                               ctx.lineTo( 
timelineHeadPosition + 8, h );
+                                               ctx.lineTo( 
timelineHeadPosition, h );
+                                               ctx.fill();
+                                               ctx.closePath();
+                                               ctx.stroke();
+
+                                               ctx.beginPath();
+                                               ctx.moveTo( 
timelineTailPosition, h );
+                                               ctx.lineTo( 
timelineTailPosition, h + 11 );
+                                               ctx.lineTo( 
timelineTailPosition - 8, h );
+                                               ctx.lineTo( 
timelineTailPosition, h );
+                                               ctx.fill();
+                                               ctx.closePath();
+                                               ctx.stroke();
+
+                                               ctx.beginPath();
+                                               ctx.moveTo( 
timelineHeadPosition, h );
+                                               ctx.lineTo( 
timelineTailPosition, h );
+                                               ctx.lineTo( 
timelineTailPosition, h + 2 );
+                                               ctx.lineTo( 
timelineHeadPosition, h + 2 );
+                                               ctx.lineTo( 
timelineHeadPosition, h );
+                                               ctx.fill();
+                                               ctx.closePath();
+                                               ctx.stroke();
+                                       }
+                               } );
+
+                               h += 30;
+
+                               ctx.beginPath();
+                               ctx.strokeStyle = "#dfdfdf";
+                               ctx.moveTo( 0, h - 10 );
+                               ctx.lineTo( width, h - 10 );
+                               ctx.closePath();
+                               ctx.stroke();
+                       } );
+
+                       h = space;
+
+                       this.sortedLabels.forEach( function( label ) {
+                               var ms = " ~ " + ( timespans[label].sum < 1 ? 
timespans[label].sum : parseInt( timespans[label].sum, 10 ) ) + " ms",
+                                       xc;
+
+                               ctx.fillStyle = "#444";
+                               ctx.font = "12px sans-serif";
+                               ctx.textAlign = "start";
+                               xc = x + timespans[label].start * ratio + 1;
+                               ctx.fillText( label, xc, h );
+                               
+                               xc += ctx.measureText( label ).width;
+                               ctx.font = "10px sans-serif";
+                               ctx.fillText( ms, xc, h );
+
+                               h += gapPerEvent;
+                       } );
+               }
+       };
+
+       // reducer to find edges of period array
+       function periodMinMax( result, period ) {
+               if ( period.start < result.start ) { 
+                       result.start = period.start;
+               }
+               if ( period.end > result.end ) {
+                       result.end = period.end;
+               }
+               return result;
+       };
+
+}( mediaWiki, jQuery ) );

-- 
To view, visit https://gerrit.wikimedia.org/r/104318
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I23a0ce4137abcd74ad87967fb263214c08cb9685
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: EBernhardson <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to