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 )
275 + $.fn
.tipsy
.autoWE
.call( this );
277 className
: 'mw-debug-profile-tipsy',
282 return profile
.buildFlyout( $( this ).data( 'period' ) ).html();
284 } ).on( 'mouseenter', function () {
286 addClass( this, 'tipsy-visible' );
287 $( this ).tipsy( 'show' );
290 $container
.on( 'mouseleave', function ( event
) {
291 var $from = $( event
.relatedTarget
),
292 $to
= $( event
.target
);
293 // only close the tipsy if we are not
294 if ( $from.closest( '.tipsy' ).length
=== 0 &&
295 $to
.closest( '.tipsy' ).length
=== 0 &&
296 $to
.get( 0 ).namespaceURI
!== 'http://www.w4.org/2000/svg'
300 } ).on( 'click', function () {
301 // convenience method for closing
307 * @return number the x co-ordinate for the specified timestamp
309 xCoord: function ( msTimestamp
) {
310 return ( msTimestamp
- profile
.data
.timespan
.start
) * profile
.ratio
;
314 function ProfileData( data
, width
, mergeThresholdPx
, dropThresholdPx
) {
315 // validate input data
316 this.data
= data
.map( function ( event
) {
317 event
.periods
= event
.periods
.filter( function ( period
) {
318 return period
.start
&& period
.end
319 && period
.start
< period
.end
320 // period start must be a reasonable ms timestamp
321 && period
.start
> 1000000;
324 } ).filter( function ( event
) {
325 return event
.name
&& event
.periods
.length
> 0;
328 // start and end time of the data
329 this.timespan
= this.data
.reduce( function ( result
, event
) {
330 return event
.periods
.reduce( periodMinMax
, result
);
331 }, periodMinMax
.initial() );
333 // transform input data
334 this.groups
= this.collate( width
, mergeThresholdPx
, dropThresholdPx
);
340 * There are too many unique events to display a line for each,
341 * so this does a basic grouping.
343 ProfileData
.groupOf = function ( label
) {
344 var pos
, prefix
= 'Profile section ended by close(): ';
345 if ( label
.indexOf( prefix
) === 0 ) {
346 label
= label
.substring( prefix
.length
);
349 pos
= [ '::', ':', '-' ].reduce( function ( result
, separator
) {
350 var pos
= label
.indexOf( separator
);
353 } else if ( result
=== -1 ) {
356 return Math
.min( result
, pos
);
363 return label
.substring( 0, pos
);
368 * @return Array list of objects with `name` and `events` keys
370 ProfileData
.groupEvents = function ( events
) {
374 // Group events together
375 for ( i
= events
.length
- 1; i
>= 0; i
-- ) {
376 group
= ProfileData
.groupOf( events
[i
].name
);
377 if ( groups
[group
] ) {
378 groups
[group
].push( events
[i
] );
380 groups
[group
] = [events
[i
]];
384 // Return an array of groups
385 return Object
.keys( groups
).map( function ( group
) {
388 events
: groups
[group
]
393 ProfileData
.periodSorter = function ( a
, b
) {
394 if ( a
.start
=== b
.start
) {
395 return a
.end
- b
.end
;
397 return a
.start
- b
.start
;
400 ProfileData
.genMergePeriodReducer = function ( mergeThresholdMs
) {
401 return function ( result
, period
) {
402 if ( result
.length
=== 0 ) {
403 // period is first result
410 var last
= result
[result
.length
- 1];
411 if ( period
.end
< last
.end
) {
412 // end is contained within previous
413 result
[result
.length
- 1].contained
.push( period
);
414 } else if ( period
.start
- mergeThresholdMs
< last
.end
) {
415 // neighbors within merging distance
416 result
[result
.length
- 1].end
= period
.end
;
417 result
[result
.length
- 1].contained
.push( period
);
419 // period is next result
431 * Collect all periods from the grouped events and apply merge and
432 * drop transformations
434 ProfileData
.extractPeriods = function ( events
, mergeThresholdMs
, dropThresholdMs
) {
435 // collect the periods from all events
436 return events
.reduce( function ( result
, event
) {
437 if ( !event
.periods
.length
) {
440 result
.push
.apply( result
, event
.periods
.map( function ( period
) {
441 // maintain link from period to event
442 period
.source
= event
;
447 // sort combined periods
448 .sort( ProfileData
.periodSorter
)
449 // Apply merge threshold. Original periods
450 // are maintained in the `contained` property
451 .reduce( ProfileData
.genMergePeriodReducer( mergeThresholdMs
), [] )
452 // Apply drop threshold
453 .filter( function ( period
) {
454 return period
.end
- period
.start
> dropThresholdMs
;
459 * runs a callback on all periods in the group. Only valid after
460 * groups.periods[0..n].contained are populated. This runs against
461 * un-transformed data and is better suited to summing or other
464 ProfileData
.reducePeriods = function ( group
, callback
, result
) {
465 return group
.periods
.reduce( function ( result
, period
) {
466 return period
.contained
.reduce( callback
, result
);
471 * Transforms this.data grouping by labels, merging neighboring
472 * events in the groups, and drops events and groups below the
473 * display threshold. Groups are returned sorted by starting time.
475 ProfileData
.prototype.collate = function ( width
, mergeThresholdPx
, dropThresholdPx
) {
477 var ratio
= ( this.timespan
.end
- this.timespan
.start
) / width
,
478 // transform thresholds to ms
479 mergeThresholdMs
= mergeThresholdPx
* ratio
,
480 dropThresholdMs
= dropThresholdPx
* ratio
;
482 return ProfileData
.groupEvents( this.data
)
483 // generate data about the grouped events
484 .map( function ( group
) {
485 // Cleaned periods from all events
486 group
.periods
= ProfileData
.extractPeriods( group
.events
, mergeThresholdMs
, dropThresholdMs
);
487 // min and max timestamp per group
488 group
.timespan
= ProfileData
.reducePeriods( group
, periodMinMax
, periodMinMax
.initial() );
489 // ms from first call to end of last call
490 group
.timespan
.length
= group
.timespan
.end
- group
.timespan
.start
;
491 // collect the un-transformed periods
492 group
.timespan
.sum
= ProfileData
.reducePeriods( group
, function ( result
, period
) {
493 result
.push( period
);
496 // sort by start time
497 .sort( ProfileData
.periodSorter
)
499 .reduce( ProfileData
.genMergePeriodReducer( 0 ), [] )
501 .reduce( function ( result
, period
) {
502 return result
+ period
.end
- period
.start
;
507 // remove groups that have had all their periods filtered
508 .filter( function ( group
) {
509 return group
.periods
.length
> 0;
511 // sort events by first start
512 .sort( function ( a
, b
) {
513 return ProfileData
.periodSorter( a
.timespan
, b
.timespan
);
517 // reducer to find edges of period array
518 function periodMinMax( result
, period
) {
519 if ( period
.start
< result
.start
) {
520 result
.start
= period
.start
;
522 if ( period
.end
> result
.end
) {
523 result
.end
= period
.end
;
528 periodMinMax
.initial = function () {
529 return { start
: Number
.POSITIVE_INFINITY
, end
: Number
.NEGATIVE_INFINITY
};
532 function formatBytes( bytes
) {
533 var i
, sizes
= ['Bytes', 'KB', 'MB', 'GB', 'TB'];
537 i
= parseInt( Math
.floor( Math
.log( bytes
) / Math
.log( 1024 ) ), 10 );
538 return Math
.round( bytes
/ Math
.pow( 1024, i
), 2 ) + ' ' + sizes
[i
];
541 // turns a 2d array into a point list for svg
542 // polygon points attribute
543 // ex: [[1,2],[3,4],[4,2]] = '1,2 3,4 4,2'
544 function pointList( pairs
) {
545 return pairs
.map( function ( pair
) {
546 return pair
.join( ',' );
549 }( mediaWiki
, jQuery
) );