PrefixSearch: Avoid notice when no subpage exists
[mediawiki.git] / resources / src / mediawiki / mediawiki.debug.profile.js
blob64ec6c394a75a447be75af231b4e85694b894131
1 /*!
2  * JavaScript for the debug toolbar profiler, enabled through $wgDebugToolbar
3  * and StartProfiler.php.
4  *
5  * @author Erik Bernhardson
6  * @since 1.23
7  */
9 ( function ( mw, $ ) {
10         'use strict';
12         /**
13          * @singleton
14          * @class mw.Debug.profile
15          */
16         var profile = mw.Debug.profile = {
17                 /**
18                  * Object containing data for the debug toolbar
19                  *
20                  * @property ProfileData
21                  */
22                 data: null,
24                 /**
25                  * @property DOMElement
26                  */
27                 container: null,
29                 /**
30                  * Initializes the profiling pane.
31                  */
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();
44                         } else {
45                                 // generate a flyout
46                                 profile.data = new ProfileData( data, profile.width, mergeThresholdPx, dropThresholdPx );
47                                 // draw it
48                                 profile.container = profile.buildSvg( profile.container );
49                                 profile.attachFlyout();
50                         }
52                         return profile.container;
53                 },
55                 buildRequiresES5: function () {
56                         return $( '<div>' )
57                                 .text( 'An ES5 compatible javascript engine is required for the profile visualization.' )
58                                 .get( 0 );
59                 },
61                 buildNoData: function () {
62                         return $( '<div>' ).addClass( 'mw-debug-profile-no-data' )
63                                 .text( 'No events recorded, ensure profiling is enabled in StartProfiler.php.' )
64                                 .get( 0 );
65                 },
67                 /**
68                  * Creates DOM nodes appropriately namespaced for SVG.
69                  *
70                  * @param string tag to create
71                  * @return DOMElement
72                  */
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' ); },
78                 /**
79                  * @param DOMElement|undefined
80                  */
81                 buildSvg: function ( node ) {
82                         var container, group, i, g,
83                                 timespan = profile.data.timespan,
84                                 gapPerEvent = 38,
85                                 space = 10.5,
86                                 currentHeight = space,
87                                 totalHeight = 0;
89                         profile.ratio = ( profile.width - space * 2 ) / ( timespan.end - timespan.start );
90                         totalHeight += gapPerEvent * profile.data.groups.length;
92                         if ( node ) {
93                                 $( node ).empty();
94                         } else {
95                                 node = profile.createSvgElement( 'svg' );
96                                 node.setAttribute( 'version', '1.2' );
97                                 node.setAttribute( 'baseProfile', 'tiny' );
98                         }
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;
114                         }
116                         return node;
117                 },
119                 /**
120                  * @param Object group of periods to transform into graphics
121                  */
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' );
130                         // draw label
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 );
137                         // draw metadata
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] ) );
145                         }
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 );
156                         return timeline;
157                 },
159                 /**
160                  * @param Object period to transform into graphics
161                  */
162                 buildPeriod: function ( period ) {
163                         var node,
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 );
185                         } else {
186                                 node = profile.createSvgElement( 'polygon' );
187                                 node.setAttribute( 'points', pointList( [
188                                         [ head, 8 ],
189                                         [ head, 19 ],
190                                         [ head + 8, 8 ],
191                                         [ head, 8]
192                                 ] ) );
193                                 g.appendChild( node );
195                                 node = profile.createSvgElement( 'polygon' );
196                                 node.setAttribute( 'points', pointList( [
197                                         [ tail, 8 ],
198                                         [ tail, 19 ],
199                                         [ tail - 8, 8 ],
200                                         [ tail, 8 ]
201                                 ] ) );
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 );
210                         }
212                         return g;
213                 },
215                 /**
216                  * @param Object
217                  */
218                 buildFlyout: function ( period ) {
219                         var contained, sum, ms, mem, i,
220                                 node = $( '<div>' );
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' ) )
230                                         .appendTo( node );
231                         }
233                         return node;
234                 },
236                 /**
237                  * Attach a hover flyout to all .mw-debug-profile-period groups.
238                  */
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;
248                                         if ( idx === -1 ) {
249                                                 node.setAttribute( 'class', current ? ( current + ' ' + value ) : value );
250                                         }
251                                 },
252                                 removeClass = function ( node, value ) {
253                                         var current = node.getAttribute( 'class' ),
254                                                 list = current ? current.split( ' ' ) : false,
255                                                 idx = list ? list.indexOf( value ) : -1;
257                                         if ( idx !== -1 ) {
258                                                 list.splice( idx, 1 );
259                                                 node.setAttribute( 'class', list.join( ' ' ) );
260                                         }
261                                 },
262                                 // hide all tipsy flyouts
263                                 hide = function () {
264                                         $container.find( '.mw-debug-profile-period.tipsy-visible' )
265                                                 .each( function () {
266                                                         removeClass( this, 'tipsy-visible' );
267                                                         $( this ).tipsy( 'hide' );
268                                                 } );
269                                 };
271                         $container.find( '.mw-debug-profile-period' ).tipsy( {
272                                 fade: true,
273                                 gravity: function () {
274                                         return $.fn.tipsy.autoNS.call( this ) + $.fn.tipsy.autoWE.call( this );
275                                 },
276                                 className: 'mw-debug-profile-tipsy',
277                                 center: false,
278                                 html: true,
279                                 trigger: 'manual',
280                                 title: function () {
281                                         return profile.buildFlyout( $( this ).data( 'period' ) ).html();
282                                 }
283                         } ).on( 'mouseenter', function () {
284                                 hide();
285                                 addClass( this, 'tipsy-visible' );
286                                 $( this ).tipsy( 'show' );
287                         } );
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'
296                                 ) {
297                                         hide();
298                                 }
299                         } ).on( 'click', function () {
300                                 // convenience method for closing
301                                 hide();
302                         } );
303                 },
305                 /**
306                  * @return number the x co-ordinate for the specified timestamp
307                  */
308                 xCoord: function ( msTimestamp ) {
309                         return ( msTimestamp - profile.data.timespan.start ) * profile.ratio;
310                 }
311         };
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;
321                         } );
322                         return event;
323                 } ).filter( function ( event ) {
324                         return event.name && event.periods.length > 0;
325                 } );
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 );
335                 return this;
336         }
338         /**
339          * There are too many unique events to display a line for each,
340          * so this does a basic grouping.
341          */
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 );
346                 }
348                 pos = [ '::', ':', '-' ].reduce( function ( result, separator ) {
349                         var pos = label.indexOf( separator );
350                         if ( pos === -1 ) {
351                                 return result;
352                         } else if ( result === -1 ) {
353                                 return pos;
354                         } else {
355                                 return Math.min( result, pos );
356                         }
357                 }, -1 );
359                 if ( pos === -1 ) {
360                         return label;
361                 } else {
362                         return label.substring( 0, pos );
363                 }
364         };
366         /**
367          * @return Array list of objects with `name` and `events` keys
368          */
369         ProfileData.groupEvents = function ( events ) {
370                 var group, i,
371                         groups = {};
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] );
378                         } else {
379                                 groups[group] = [events[i]];
380                         }
381                 }
383                 // Return an array of groups
384                 return Object.keys( groups ).map( function ( group ) {
385                         return {
386                                 name: group,
387                                 events: groups[group]
388                         };
389                 } );
390         };
392         ProfileData.periodSorter = function ( a, b ) {
393                 if ( a.start === b.start ) {
394                         return a.end - b.end;
395                 }
396                 return a.start - b.start;
397         };
399         ProfileData.genMergePeriodReducer = function ( mergeThresholdMs ) {
400                 return function ( result, period ) {
401                         if ( result.length === 0 ) {
402                                 // period is first result
403                                 return [{
404                                         start: period.start,
405                                         end: period.end,
406                                         contained: [period]
407                                 }];
408                         }
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 );
417                         } else {
418                                 // period is next result
419                                 result.push( {
420                                         start: period.start,
421                                         end: period.end,
422                                         contained: [period]
423                                 } );
424                         }
425                         return result;
426                 };
427         };
429         /**
430          * Collect all periods from the grouped events and apply merge and
431          * drop transformations
432          */
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 ) {
437                                         return result;
438                                 }
439                                 result.push.apply( result, event.periods.map( function ( period ) {
440                                         // maintain link from period to event
441                                         period.source = event;
442                                         return period;
443                                 } ) );
444                                 return result;
445                         }, [] )
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;
454                         } );
455         };
457         /**
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
461          * stat collection
462          */
463         ProfileData.reducePeriods = function ( group, callback, result ) {
464                 return group.periods.reduce( function ( result, period ) {
465                         return period.contained.reduce( callback, result );
466                 }, result );
467         };
469         /**
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.
473          */
474         ProfileData.prototype.collate = function ( width, mergeThresholdPx, dropThresholdPx ) {
475                 // ms to pixel ratio
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 );
493                                                 return result;
494                                         }, [] )
495                                         // sort by start time
496                                         .sort( ProfileData.periodSorter )
497                                         // merge overlapping
498                                         .reduce( ProfileData.genMergePeriodReducer( 0 ), [] )
499                                         // sum
500                                         .reduce( function ( result, period ) {
501                                                 return result + period.end - period.start;
502                                         }, 0 );
504                                 return group;
505                         }, this )
506                         // remove groups that have had all their periods filtered
507                         .filter( function ( group ) {
508                                 return group.periods.length > 0;
509                         } )
510                         // sort events by first start
511                         .sort( function ( a, b ) {
512                                 return ProfileData.periodSorter( a.timespan, b.timespan );
513                         } );
514         };
516         // reducer to find edges of period array
517         function periodMinMax( result, period ) {
518                 if ( period.start < result.start ) {
519                         result.start = period.start;
520                 }
521                 if ( period.end > result.end ) {
522                         result.end = period.end;
523                 }
524                 return result;
525         }
527         periodMinMax.initial = function () {
528                 return { start: Number.POSITIVE_INFINITY, end: Number.NEGATIVE_INFINITY };
529         };
531         function formatBytes( bytes ) {
532                 var i, sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
533                 if ( bytes === 0 ) {
534                         return '0 Bytes';
535                 }
536                 i = parseInt( Math.floor( Math.log( bytes ) / Math.log( 1024 ) ), 10 );
537                 return Math.round( bytes / Math.pow( 1024, i ), 2 ) + ' ' + sizes[i];
538         }
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( ',' );
546                 } ).join( ' ' );
547         }
548 }( mediaWiki, jQuery ) );