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
74 ? document.createElementNS.bind( document, 'http://www.w3.org/2000/svg' )
75 // throw a error for browsers which does not support document.createElementNS (IE<8)
76 : function () { throw new Error( 'document.createElementNS not supported' ); },
79 * @param DOMElement|undefined
81 buildSvg: function ( node ) {
82 var container, group, i, g,
83 timespan = profile.data.timespan,
86 currentHeight = space,
89 profile.ratio = ( profile.width - space * 2 ) / ( timespan.end - timespan.start );
90 totalHeight += gapPerEvent * profile.data.groups.length;
95 node = profile.createSvgElement( 'svg' );
96 node.setAttribute( 'version', '1.2' );
97 node.setAttribute( 'baseProfile', 'tiny' );
99 node.style.height = totalHeight;
100 node.style.width = profile.width;
102 // use a container that can be transformed
103 container = profile.createSvgElement( 'g' );
104 node.appendChild( container );
106 for ( i = 0; i < profile.data.groups.length; i++ ) {
107 group = profile.data.groups[i];
108 g = profile.buildTimeline( group );
110 g.setAttribute( 'transform', 'translate( 0 ' + currentHeight + ' )' );
111 container.appendChild( g );
113 currentHeight += gapPerEvent;
120 * @param Object group of periods to transform into graphics
122 buildTimeline: function ( group ) {
123 var text, tspan, line, i,
124 sum = group.timespan.sum,
125 ms = ' ~ ' + ( sum < 1 ? sum.toFixed( 2 ) : sum.toFixed( 0 ) ) + ' ms',
126 timeline = profile.createSvgElement( 'g' );
128 timeline.setAttribute( 'class', 'mw-debug-profile-timeline' );
131 text = profile.createSvgElement( 'text' );
132 text.setAttribute( 'x', profile.xCoord( group.timespan.start ) );
133 text.setAttribute( 'y', 0 );
134 text.textContent = group.name;
135 timeline.appendChild( text );
138 tspan = profile.createSvgElement( 'tspan' );
139 tspan.textContent = ms;
140 text.appendChild( tspan );
142 // draw timeline periods
143 for ( i = 0; i < group.periods.length; i++ ) {
144 timeline.appendChild( profile.buildPeriod( group.periods[i] ) );
147 // full-width line under each timeline
148 line = profile.createSvgElement( 'line' );
149 line.setAttribute( 'class', 'mw-debug-profile-underline' );
150 line.setAttribute( 'x1', 0 );
151 line.setAttribute( 'y1', 28 );
152 line.setAttribute( 'x2', profile.width );
153 line.setAttribute( 'y2', 28 );
154 timeline.appendChild( line );
160 * @param Object period to transform into graphics
162 buildPeriod: function ( period ) {
164 head = profile.xCoord( period.start ),
165 tail = profile.xCoord( period.end ),
166 g = profile.createSvgElement( 'g' );
168 g.setAttribute( 'class', 'mw-debug-profile-period' );
169 $( g ).data( 'period', period );
171 if ( head + 16 > tail ) {
172 node = profile.createSvgElement( 'rect' );
173 node.setAttribute( 'x', head );
174 node.setAttribute( 'y', 8 );
175 node.setAttribute( 'width', 2 );
176 node.setAttribute( 'height', 9 );
177 g.appendChild( node );
179 node = profile.createSvgElement( 'rect' );
180 node.setAttribute( 'x', head );
181 node.setAttribute( 'y', 8 );
182 node.setAttribute( 'width', ( period.end - period.start ) * profile.ratio || 2 );
183 node.setAttribute( 'height', 6 );
184 g.appendChild( node );
186 node = profile.createSvgElement( 'polygon' );
187 node.setAttribute( 'points', pointList( [
193 g.appendChild( node );
195 node = profile.createSvgElement( 'polygon' );
196 node.setAttribute( 'points', pointList( [
202 g.appendChild( node );
204 node = profile.createSvgElement( 'line' );
205 node.setAttribute( 'x1', head );
206 node.setAttribute( 'y1', 9 );
207 node.setAttribute( 'x2', tail );
208 node.setAttribute( 'y2', 9 );
209 g.appendChild( node );
218 buildFlyout: function ( period ) {
219 var contained, sum, ms, mem, i,
222 for ( i = 0; i < period.contained.length; i++ ) {
223 contained = period.contained[i];
224 sum = contained.end - contained.start;
225 ms = '' + ( sum < 1 ? sum.toFixed( 2 ) : sum.toFixed( 0 ) ) + ' ms';
226 mem = formatBytes( contained.memory );
228 $( '<div>' ).text( contained.source.name )
229 .append( $( '<span>' ).text( ' ~ ' + ms + ' / ' + mem ).addClass( 'mw-debug-profile-meta' ) )
237 * Attach a hover flyout to all .mw-debug-profile-period groups.
239 attachFlyout: function () {
240 // for some reason addClass and removeClass from jQuery
241 // arn't working on svg elements in chrome <= 33.0 (possibly more)
242 var $container = $( profile.container ),
243 addClass = function ( node, value ) {
244 var current = node.getAttribute( 'class' ),
245 list = current ? current.split( ' ' ) : false,
246 idx = list ? list.indexOf( value ) : -1;
249 node.setAttribute( 'class', current ? ( current + ' ' + value ) : value );
252 removeClass = function ( node, value ) {
253 var current = node.getAttribute( 'class' ),
254 list = current ? current.split( ' ' ) : false,
255 idx = list ? list.indexOf( value ) : -1;
258 list.splice( idx, 1 );
259 node.setAttribute( 'class', list.join( ' ' ) );
262 // hide all tipsy flyouts
264 $container.find( '.mw-debug-profile-period.tipsy-visible' )
266 removeClass( this, 'tipsy-visible' );
267 $( this ).tipsy( 'hide' );
271 $container.find( '.mw-debug-profile-period' ).tipsy( {
273 gravity: function () {
274 return $.fn.tipsy.autoNS.call( this ) + $.fn.tipsy.autoWE.call( this );
276 className: 'mw-debug-profile-tipsy',
281 return profile.buildFlyout( $( this ).data( 'period' ) ).html();
283 } ).on( 'mouseenter', function () {
285 addClass( this, 'tipsy-visible' );
286 $( this ).tipsy( 'show' );
289 $container.on( 'mouseleave', function ( event ) {
290 var $from = $( event.relatedTarget ),
291 $to = $( event.target );
292 // only close the tipsy if we are not
293 if ( $from.closest( '.tipsy' ).length === 0 &&
294 $to.closest( '.tipsy' ).length === 0 &&
295 $to.get( 0 ).namespaceURI !== 'http://www.w4.org/2000/svg'
299 } ).on( 'click', function () {
300 // convenience method for closing
306 * @return number the x co-ordinate for the specified timestamp
308 xCoord: function ( msTimestamp ) {
309 return ( msTimestamp - profile.data.timespan.start ) * profile.ratio;
313 function ProfileData( data, width, mergeThresholdPx, dropThresholdPx ) {
314 // validate input data
315 this.data = data.map( function ( event ) {
316 event.periods = event.periods.filter( function ( period ) {
317 return period.start && period.end
318 && period.start < period.end
319 // period start must be a reasonable ms timestamp
320 && period.start > 1000000;
323 } ).filter( function ( event ) {
324 return event.name && event.periods.length > 0;
327 // start and end time of the data
328 this.timespan = this.data.reduce( function ( result, event ) {
329 return event.periods.reduce( periodMinMax, result );
330 }, periodMinMax.initial() );
332 // transform input data
333 this.groups = this.collate( width, mergeThresholdPx, dropThresholdPx );
339 * There are too many unique events to display a line for each,
340 * so this does a basic grouping.
342 ProfileData.groupOf = function ( label ) {
343 var pos, prefix = 'Profile section ended by close(): ';
344 if ( label.indexOf( prefix ) === 0 ) {
345 label = label.substring( prefix.length );
348 pos = [ '::', ':', '-' ].reduce( function ( result, separator ) {
349 var pos = label.indexOf( separator );
352 } else if ( result === -1 ) {
355 return Math.min( result, pos );
362 return label.substring( 0, pos );
367 * @return Array list of objects with `name` and `events` keys
369 ProfileData.groupEvents = function ( events ) {
373 // Group events together
374 for ( i = events.length - 1; i >= 0; i-- ) {
375 group = ProfileData.groupOf( events[i].name );
376 if ( groups[group] ) {
377 groups[group].push( events[i] );
379 groups[group] = [events[i]];
383 // Return an array of groups
384 return Object.keys( groups ).map( function ( group ) {
387 events: groups[group]
392 ProfileData.periodSorter = function ( a, b ) {
393 if ( a.start === b.start ) {
394 return a.end - b.end;
396 return a.start - b.start;
399 ProfileData.genMergePeriodReducer = function ( mergeThresholdMs ) {
400 return function ( result, period ) {
401 if ( result.length === 0 ) {
402 // period is first result
409 var last = result[result.length - 1];
410 if ( period.end < last.end ) {
411 // end is contained within previous
412 result[result.length - 1].contained.push( period );
413 } else if ( period.start - mergeThresholdMs < last.end ) {
414 // neighbors within merging distance
415 result[result.length - 1].end = period.end;
416 result[result.length - 1].contained.push( period );
418 // period is next result
430 * Collect all periods from the grouped events and apply merge and
431 * drop transformations
433 ProfileData.extractPeriods = function ( events, mergeThresholdMs, dropThresholdMs ) {
434 // collect the periods from all events
435 return events.reduce( function ( result, event ) {
436 if ( !event.periods.length ) {
439 result.push.apply( result, event.periods.map( function ( period ) {
440 // maintain link from period to event
441 period.source = event;
446 // sort combined periods
447 .sort( ProfileData.periodSorter )
448 // Apply merge threshold. Original periods
449 // are maintained in the `contained` property
450 .reduce( ProfileData.genMergePeriodReducer( mergeThresholdMs ), [] )
451 // Apply drop threshold
452 .filter( function ( period ) {
453 return period.end - period.start > dropThresholdMs;
458 * runs a callback on all periods in the group. Only valid after
459 * groups.periods[0..n].contained are populated. This runs against
460 * un-transformed data and is better suited to summing or other
463 ProfileData.reducePeriods = function ( group, callback, result ) {
464 return group.periods.reduce( function ( result, period ) {
465 return period.contained.reduce( callback, result );
470 * Transforms this.data grouping by labels, merging neighboring
471 * events in the groups, and drops events and groups below the
472 * display threshold. Groups are returned sorted by starting time.
474 ProfileData.prototype.collate = function ( width, mergeThresholdPx, dropThresholdPx ) {
476 var ratio = ( this.timespan.end - this.timespan.start ) / width,
477 // transform thresholds to ms
478 mergeThresholdMs = mergeThresholdPx * ratio,
479 dropThresholdMs = dropThresholdPx * ratio;
481 return ProfileData.groupEvents( this.data )
482 // generate data about the grouped events
483 .map( function ( group ) {
484 // Cleaned periods from all events
485 group.periods = ProfileData.extractPeriods( group.events, mergeThresholdMs, dropThresholdMs );
486 // min and max timestamp per group
487 group.timespan = ProfileData.reducePeriods( group, periodMinMax, periodMinMax.initial() );
488 // ms from first call to end of last call
489 group.timespan.length = group.timespan.end - group.timespan.start;
490 // collect the un-transformed periods
491 group.timespan.sum = ProfileData.reducePeriods( group, function ( result, period ) {
492 result.push( period );
495 // sort by start time
496 .sort( ProfileData.periodSorter )
498 .reduce( ProfileData.genMergePeriodReducer( 0 ), [] )
500 .reduce( function ( result, period ) {
501 return result + period.end - period.start;
506 // remove groups that have had all their periods filtered
507 .filter( function ( group ) {
508 return group.periods.length > 0;
510 // sort events by first start
511 .sort( function ( a, b ) {
512 return ProfileData.periodSorter( a.timespan, b.timespan );
516 // reducer to find edges of period array
517 function periodMinMax( result, period ) {
518 if ( period.start < result.start ) {
519 result.start = period.start;
521 if ( period.end > result.end ) {
522 result.end = period.end;
527 periodMinMax.initial = function () {
528 return { start: Number.POSITIVE_INFINITY, end: Number.NEGATIVE_INFINITY };
531 function formatBytes( bytes ) {
532 var i, sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
536 i = parseInt( Math.floor( Math.log( bytes ) / Math.log( 1024 ) ), 10 );
537 return Math.round( bytes / Math.pow( 1024, i ), 2 ) + ' ' + sizes[i];
540 // turns a 2d array into a point list for svg
541 // polygon points attribute
542 // ex: [[1,2],[3,4],[4,2]] = '1,2 3,4 4,2'
543 function pointList( pairs ) {
544 return pairs.map( function ( pair ) {
545 return pair.join( ',' );
548 }( mediaWiki, jQuery ) );