64ec6c394a75a447be75af231b4e85694b894131
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
) );