2 * JavaScript for the debug toolbar profiler, enabled through $wgDebugToolbar
3 * and StartProfiler.php.
5 * @author Erik Bernhardson
14 * @class mw.Debug.profile
16 var profile
= mw
.Debug
.profile
= {
18 * Object containing data for the debug toolbar
20 * @property ProfileData
25 * @property DOMElement
30 * Initializes the profiling pane.
32 init: function ( data
, width
, mergeThresholdPx
, dropThresholdPx
) {
33 data
= data
|| mw
.config
.get( 'debugInfo' ).profile
;
34 profile
.width
= width
|| $(window
).width() - 20;
35 // merge events from same pixel(some events are very granular)
36 mergeThresholdPx
= mergeThresholdPx
|| 2;
37 // only drop events if requested
38 dropThresholdPx
= dropThresholdPx
|| 0;
40 if ( !Array
.prototype.map
|| !Array
.prototype.reduce
|| !Array
.prototype.filter
) {
41 profile
.container
= profile
.buildRequiresES5();
42 } else if ( data
.length
=== 0 ) {
43 profile
.container
= profile
.buildNoData();
46 profile
.data
= new ProfileData( data
, profile
.width
, mergeThresholdPx
, dropThresholdPx
);
48 profile
.container
= profile
.buildSvg( profile
.container
);
49 profile
.attachFlyout();
52 return profile
.container
;
55 buildRequiresES5: function () {
57 .text( 'An ES5 compatible javascript engine is required for the profile visualization.' )
61 buildNoData: function () {
62 return $( '<div>' ).addClass( 'mw-debug-profile-no-data' )
63 .text( 'No events recorded, ensure profiling is enabled in StartProfiler.php.' )
68 * Creates DOM nodes appropriately namespaced for SVG.
70 * @param string tag to create
73 createSvgElement
: document
.createElementNS
.bind( document
, 'http://www.w3.org/2000/svg' ),
76 * @param DOMElement|undefined
78 buildSvg: function ( node
) {
79 var container
, group
, i
, g
,
80 timespan
= profile
.data
.timespan
,
83 currentHeight
= space
,
86 profile
.ratio
= ( profile
.width
- space
* 2 ) / ( timespan
.end
- timespan
.start
);
87 totalHeight
+= gapPerEvent
* profile
.data
.groups
.length
;
92 node
= profile
.createSvgElement( 'svg' );
93 node
.setAttribute( 'version', '1.2' );
94 node
.setAttribute( 'baseProfile', 'tiny' );
96 node
.style
.height
= totalHeight
;
97 node
.style
.width
= profile
.width
;
99 // use a container that can be transformed
100 container
= profile
.createSvgElement( 'g' );
101 node
.appendChild( container
);
103 for ( i
= 0; i
< profile
.data
.groups
.length
; i
++ ) {
104 group
= profile
.data
.groups
[i
];
105 g
= profile
.buildTimeline( group
);
107 g
.setAttribute( 'transform', 'translate( 0 ' + currentHeight
+ ' )' );
108 container
.appendChild( g
);
110 currentHeight
+= gapPerEvent
;
117 * @param Object group of periods to transform into graphics
119 buildTimeline: function ( group
) {
120 var text
, tspan
, line
, i
,
121 sum
= group
.timespan
.sum
,
122 ms
= ' ~ ' + ( sum
< 1 ? sum
.toFixed( 2 ) : sum
.toFixed( 0 ) ) + ' ms',
123 timeline
= profile
.createSvgElement( 'g' );
125 timeline
.setAttribute( 'class', 'mw-debug-profile-timeline' );
128 text
= profile
.createSvgElement( 'text' );
129 text
.setAttribute( 'x', profile
.xCoord( group
.timespan
.start
) );
130 text
.setAttribute( 'y', 0 );
131 text
.textContent
= group
.name
;
132 timeline
.appendChild( text
);
135 tspan
= profile
.createSvgElement( 'tspan' );
136 tspan
.textContent
= ms
;
137 text
.appendChild( tspan
);
139 // draw timeline periods
140 for ( i
= 0; i
< group
.periods
.length
; i
++ ) {
141 timeline
.appendChild( profile
.buildPeriod( group
.periods
[i
] ) );
144 // full-width line under each timeline
145 line
= profile
.createSvgElement( 'line' );
146 line
.setAttribute( 'class', 'mw-debug-profile-underline' );
147 line
.setAttribute( 'x1', 0 );
148 line
.setAttribute( 'y1', 28 );
149 line
.setAttribute( 'x2', profile
.width
);
150 line
.setAttribute( 'y2', 28 );
151 timeline
.appendChild( line
);
157 * @param Object period to transform into graphics
159 buildPeriod: function ( period
) {
161 head
= profile
.xCoord( period
.start
),
162 tail
= profile
.xCoord( period
.end
),
163 g
= profile
.createSvgElement( 'g' );
165 g
.setAttribute( 'class', 'mw-debug-profile-period' );
166 $( g
).data( 'period', period
);
168 if ( head
+ 16 > tail
) {
169 node
= profile
.createSvgElement( 'rect' );
170 node
.setAttribute( 'x', head
);
171 node
.setAttribute( 'y', 8 );
172 node
.setAttribute( 'width', 2 );
173 node
.setAttribute( 'height', 9 );
174 g
.appendChild( node
);
176 node
= profile
.createSvgElement( 'rect' );
177 node
.setAttribute( 'x', head
);
178 node
.setAttribute( 'y', 8 );
179 node
.setAttribute( 'width', ( period
.end
- period
.start
) * profile
.ratio
|| 2 );
180 node
.setAttribute( 'height', 6 );
181 g
.appendChild( node
);
183 node
= profile
.createSvgElement( 'polygon' );
184 node
.setAttribute( 'points', pointList( [
190 g
.appendChild( node
);
192 node
= profile
.createSvgElement( 'polygon' );
193 node
.setAttribute( 'points', pointList( [
199 g
.appendChild( node
);
201 node
= profile
.createSvgElement( 'line' );
202 node
.setAttribute( 'x1', head
);
203 node
.setAttribute( 'y1', 9 );
204 node
.setAttribute( 'x2', tail
);
205 node
.setAttribute( 'y2', 9 );
206 g
.appendChild( node
);
215 buildFlyout: function ( period
) {
216 var contained
, sum
, ms
, mem
, i
,
219 for ( i
= 0; i
< period
.contained
.length
; i
++ ) {
220 contained
= period
.contained
[i
];
221 sum
= contained
.end
- contained
.start
;
222 ms
= '' + ( sum
< 1 ? sum
.toFixed( 2 ) : sum
.toFixed( 0 ) ) + ' ms';
223 mem
= formatBytes( contained
.memory
);
225 $( '<div>' ).text( contained
.source
.name
)
226 .append( $( '<span>' ).text( ' ~ ' + ms
+ ' / ' + mem
).addClass( 'mw-debug-profile-meta' ) )
234 * Attach a hover flyout to all .mw-debug-profile-period groups.
236 attachFlyout: function () {
237 // for some reason addClass and removeClass from jQuery
238 // arn't working on svg elements in chrome <= 33.0 (possibly more)
239 var $container
= $( profile
.container
),
240 addClass = function ( node
, value
) {
241 var current
= node
.getAttribute( 'class' ),
242 list
= current
? current
.split( ' ' ) : false,
243 idx
= list
? list
.indexOf( value
) : -1;
246 node
.setAttribute( 'class', current
? ( current
+ ' ' + value
) : value
);
249 removeClass = function ( node
, value
) {
250 var current
= node
.getAttribute( 'class' ),
251 list
= current
? current
.split( ' ' ) : false,
252 idx
= list
? list
.indexOf( value
) : -1;
255 list
.splice( idx
, 1 );
256 node
.setAttribute( 'class', list
.join( ' ' ) );
259 // hide all tipsy flyouts
261 $container
.find( '.mw-debug-profile-period.tipsy-visible' )
263 removeClass( this, 'tipsy-visible' );
264 $( this ).tipsy( 'hide' );
268 $container
.find( '.mw-debug-profile-period' ).tipsy( {
270 gravity: function () {
271 return $.fn
.tipsy
.autoNS
.call( this )
272 + $.fn
.tipsy
.autoWE
.call( this );
274 className
: 'mw-debug-profile-tipsy',
279 return profile
.buildFlyout( $( this ).data( 'period' ) ).html();
281 } ).on( 'mouseenter', function () {
283 addClass( this, 'tipsy-visible' );
284 $( this ).tipsy( 'show' );
287 $container
.on( 'mouseleave', function ( event
) {
288 var $from = $( event
.relatedTarget
),
289 $to
= $( event
.target
);
290 // only close the tipsy if we are not
291 if ( $from.closest( '.tipsy' ).length
=== 0 &&
292 $to
.closest( '.tipsy' ).length
=== 0 &&
293 $to
.get( 0 ).namespaceURI
!== 'http://www.w4.org/2000/svg'
297 } ).on( 'click', function () {
298 // convenience method for closing
304 * @return number the x co-ordinate for the specified timestamp
306 xCoord: function ( msTimestamp
) {
307 return ( msTimestamp
- profile
.data
.timespan
.start
) * profile
.ratio
;
311 function ProfileData( data
, width
, mergeThresholdPx
, dropThresholdPx
) {
312 // validate input data
313 this.data
= data
.map( function ( event
) {
314 event
.periods
= event
.periods
.filter( function ( period
) {
315 return period
.start
&& period
.end
316 && period
.start
< period
.end
317 // period start must be a reasonable ms timestamp
318 && period
.start
> 1000000;
321 } ).filter( function ( event
) {
322 return event
.name
&& event
.periods
.length
> 0;
325 // start and end time of the data
326 this.timespan
= this.data
.reduce( function ( result
, event
) {
327 return event
.periods
.reduce( periodMinMax
, result
);
328 }, periodMinMax
.initial() );
330 // transform input data
331 this.groups
= this.collate( width
, mergeThresholdPx
, dropThresholdPx
);
337 * There are too many unique events to display a line for each,
338 * so this does a basic grouping.
340 ProfileData
.groupOf = function ( label
) {
341 var pos
, prefix
= 'Profile section ended by close(): ';
342 if ( label
.indexOf( prefix
) === 0 ) {
343 label
= label
.substring( prefix
.length
);
346 pos
= [ '::', ':', '-' ].reduce( function ( result
, separator
) {
347 var pos
= label
.indexOf( separator
);
350 } else if ( result
=== -1 ) {
353 return Math
.min( result
, pos
);
360 return label
.substring( 0, pos
);
365 * @return Array list of objects with `name` and `events` keys
367 ProfileData
.groupEvents = function ( events
) {
371 // Group events together
372 for ( i
= events
.length
- 1; i
>= 0; i
-- ) {
373 group
= ProfileData
.groupOf( events
[i
].name
);
374 if ( groups
[group
] ) {
375 groups
[group
].push( events
[i
] );
377 groups
[group
] = [events
[i
]];
381 // Return an array of groups
382 return Object
.keys( groups
).map( function ( group
) {
385 events
: groups
[group
],
390 ProfileData
.periodSorter = function ( a
, b
) {
391 if ( a
.start
=== b
.start
) {
392 return a
.end
- b
.end
;
394 return a
.start
- b
.start
;
397 ProfileData
.genMergePeriodReducer = function ( mergeThresholdMs
) {
398 return function ( result
, period
) {
399 if ( result
.length
=== 0 ) {
400 // period is first result
407 var last
= result
[result
.length
- 1];
408 if ( period
.end
< last
.end
) {
409 // end is contained within previous
410 result
[result
.length
- 1].contained
.push( period
);
411 } else if ( period
.start
- mergeThresholdMs
< last
.end
) {
412 // neighbors within merging distance
413 result
[result
.length
- 1].end
= period
.end
;
414 result
[result
.length
- 1].contained
.push( period
);
416 // period is next result
428 * Collect all periods from the grouped events and apply merge and
429 * drop transformations
431 ProfileData
.extractPeriods = function ( events
, mergeThresholdMs
, dropThresholdMs
) {
432 // collect the periods from all events
433 return events
.reduce( function ( result
, event
) {
434 if ( !event
.periods
.length
) {
437 result
.push
.apply( result
, event
.periods
.map( function ( period
) {
438 // maintain link from period to event
439 period
.source
= event
;
444 // sort combined periods
445 .sort( ProfileData
.periodSorter
)
446 // Apply merge threshold. Original periods
447 // are maintained in the `contained` property
448 .reduce( ProfileData
.genMergePeriodReducer( mergeThresholdMs
), [] )
449 // Apply drop threshold
450 .filter( function ( period
) {
451 return period
.end
- period
.start
> dropThresholdMs
;
456 * runs a callback on all periods in the group. Only valid after
457 * groups.periods[0..n].contained are populated. This runs against
458 * un-transformed data and is better suited to summing or other
461 ProfileData
.reducePeriods = function ( group
, callback
, result
) {
462 return group
.periods
.reduce( function ( result
, period
) {
463 return period
.contained
.reduce( callback
, result
);
468 * Transforms this.data grouping by labels, merging neighboring
469 * events in the groups, and drops events and groups below the
470 * display threshold. Groups are returned sorted by starting time.
472 ProfileData
.prototype.collate = function ( width
, mergeThresholdPx
, dropThresholdPx
) {
474 var ratio
= ( this.timespan
.end
- this.timespan
.start
) / width
,
475 // transform thresholds to ms
476 mergeThresholdMs
= mergeThresholdPx
* ratio
,
477 dropThresholdMs
= dropThresholdPx
* ratio
;
479 return ProfileData
.groupEvents( this.data
)
480 // generate data about the grouped events
481 .map( function ( group
) {
482 // Cleaned periods from all events
483 group
.periods
= ProfileData
.extractPeriods( group
.events
, mergeThresholdMs
, dropThresholdMs
);
484 // min and max timestamp per group
485 group
.timespan
= ProfileData
.reducePeriods( group
, periodMinMax
, periodMinMax
.initial() );
486 // ms from first call to end of last call
487 group
.timespan
.length
= group
.timespan
.end
- group
.timespan
.start
;
488 // collect the un-transformed periods
489 group
.timespan
.sum
= ProfileData
.reducePeriods( group
, function ( result
, period
) {
490 result
.push( period
);
493 // sort by start time
494 .sort( ProfileData
.periodSorter
)
496 .reduce( ProfileData
.genMergePeriodReducer( 0 ), [] )
498 .reduce( function ( result
, period
) {
499 return result
+ period
.end
- period
.start
;
504 // remove groups that have had all their periods filtered
505 .filter( function ( group
) {
506 return group
.periods
.length
> 0;
508 // sort events by first start
509 .sort( function ( a
, b
) {
510 return ProfileData
.periodSorter( a
.timespan
, b
.timespan
);
514 // reducer to find edges of period array
515 function periodMinMax( result
, period
) {
516 if ( period
.start
< result
.start
) {
517 result
.start
= period
.start
;
519 if ( period
.end
> result
.end
) {
520 result
.end
= period
.end
;
525 periodMinMax
.initial = function () {
526 return { start
: Number
.POSITIVE_INFINITY
, end
: Number
.NEGATIVE_INFINITY
};
529 function formatBytes( bytes
) {
530 var i
, sizes
= ['Bytes', 'KB', 'MB', 'GB', 'TB'];
534 i
= parseInt( Math
.floor( Math
.log( bytes
) / Math
.log( 1024 ) ), 10 );
535 return Math
.round( bytes
/ Math
.pow( 1024, i
), 2 ) + ' ' + sizes
[i
];
538 // turns a 2d array into a point list for svg
539 // polygon points attribute
540 // ex: [[1,2],[3,4],[4,2]] = '1,2 3,4 4,2'
541 function pointList( pairs
) {
542 return pairs
.map( function ( pair
) {
543 return pair
.join( ',' );
546 }( mediaWiki
, jQuery
) );