diff options
author | Luke Shumaker <lukeshu@sbcglobal.net> | 2016-05-01 15:12:12 -0400 |
---|---|---|
committer | Luke Shumaker <lukeshu@sbcglobal.net> | 2016-05-01 15:12:12 -0400 |
commit | c9aa36da061816dee256a979c2ff8d2ee41824d9 (patch) | |
tree | 29f7002b80ee984b488bd047dbbd80b36bf892e9 /resources/src/mediawiki/mediawiki.debug.profile.js | |
parent | b4274e0e33eafb5e9ead9d949ebf031a9fb8363b (diff) | |
parent | d1ba966140d7a60cd5ae4e8667ceb27c1a138592 (diff) |
Merge branch 'archwiki'
# Conflicts:
# skins/ArchLinux.php
# skins/ArchLinux/archlogo.gif
Diffstat (limited to 'resources/src/mediawiki/mediawiki.debug.profile.js')
-rw-r--r-- | resources/src/mediawiki/mediawiki.debug.profile.js | 556 |
1 files changed, 556 insertions, 0 deletions
diff --git a/resources/src/mediawiki/mediawiki.debug.profile.js b/resources/src/mediawiki/mediawiki.debug.profile.js new file mode 100644 index 00000000..04f7acd0 --- /dev/null +++ b/resources/src/mediawiki/mediawiki.debug.profile.js @@ -0,0 +1,556 @@ +/*! + * JavaScript for the debug toolbar profiler, enabled through $wgDebugToolbar + * and StartProfiler.php. + * + * @author Erik Bernhardson + * @since 1.23 + */ + +( function ( mw, $ ) { + 'use strict'; + + /** + * @singleton + * @class mw.Debug.profile + */ + var profile = mw.Debug.profile = { + /** + * Object containing data for the debug toolbar + * + * @property ProfileData + */ + data: null, + + /** + * @property DOMElement + */ + container: null, + + /** + * Initializes the profiling pane. + */ + init: function ( data, width, mergeThresholdPx, dropThresholdPx ) { + data = data || mw.config.get( 'debugInfo' ).profile; + profile.width = width || $(window).width() - 20; + // merge events from same pixel(some events are very granular) + mergeThresholdPx = mergeThresholdPx || 2; + // only drop events if requested + dropThresholdPx = dropThresholdPx || 0; + + if ( + !Array.prototype.map || + !Array.prototype.reduce || + !Array.prototype.filter || + !document.createElementNS || + !document.createElementNS.bind + ) { + profile.container = profile.buildRequiresBrowserFeatures(); + } else if ( data.length === 0 ) { + profile.container = profile.buildNoData(); + } else { + // Initialize createSvgElement (now that we know we have + // document.createElementNS and bind) + this.createSvgElement = document.createElementNS.bind( document, 'http://www.w3.org/2000/svg' ); + + // generate a flyout + profile.data = new ProfileData( data, profile.width, mergeThresholdPx, dropThresholdPx ); + // draw it + profile.container = profile.buildSvg( profile.container ); + profile.attachFlyout(); + } + + return profile.container; + }, + + buildRequiresBrowserFeatures: function () { + return $( '<div>' ) + .text( 'Certain browser features, including parts of ECMAScript 5 and document.createElementNS, are required for the profile visualization.' ) + .get( 0 ); + }, + + buildNoData: function () { + return $( '<div>' ).addClass( 'mw-debug-profile-no-data' ) + .text( 'No events recorded, ensure profiling is enabled in StartProfiler.php.' ) + .get( 0 ); + }, + + /** + * Creates DOM nodes appropriately namespaced for SVG. + * Initialized in init after checking support + * + * @param string tag to create + * @return DOMElement + */ + createSvgElement: null, + + /** + * @param DOMElement|undefined + */ + buildSvg: function ( node ) { + var container, group, i, g, + timespan = profile.data.timespan, + gapPerEvent = 38, + space = 10.5, + currentHeight = space, + totalHeight = 0; + + profile.ratio = ( profile.width - space * 2 ) / ( timespan.end - timespan.start ); + totalHeight += gapPerEvent * profile.data.groups.length; + + if ( node ) { + $( node ).empty(); + } else { + node = profile.createSvgElement( 'svg' ); + node.setAttribute( 'version', '1.2' ); + node.setAttribute( 'baseProfile', 'tiny' ); + } + node.style.height = totalHeight; + node.style.width = profile.width; + + // use a container that can be transformed + container = profile.createSvgElement( 'g' ); + node.appendChild( container ); + + for ( i = 0; i < profile.data.groups.length; i++ ) { + group = profile.data.groups[i]; + g = profile.buildTimeline( group ); + + g.setAttribute( 'transform', 'translate( 0 ' + currentHeight + ' )' ); + container.appendChild( g ); + + currentHeight += gapPerEvent; + } + + return node; + }, + + /** + * @param Object group of periods to transform into graphics + */ + buildTimeline: function ( group ) { + var text, tspan, line, i, + sum = group.timespan.sum, + ms = ' ~ ' + ( sum < 1 ? sum.toFixed( 2 ) : sum.toFixed( 0 ) ) + ' ms', + timeline = profile.createSvgElement( 'g' ); + + timeline.setAttribute( 'class', 'mw-debug-profile-timeline' ); + + // draw label + text = profile.createSvgElement( 'text' ); + text.setAttribute( 'x', profile.xCoord( group.timespan.start ) ); + text.setAttribute( 'y', 0 ); + text.textContent = group.name; + timeline.appendChild( text ); + + // draw metadata + tspan = profile.createSvgElement( 'tspan' ); + tspan.textContent = ms; + text.appendChild( tspan ); + + // draw timeline periods + for ( i = 0; i < group.periods.length; i++ ) { + timeline.appendChild( profile.buildPeriod( group.periods[i] ) ); + } + + // full-width line under each timeline + line = profile.createSvgElement( 'line' ); + line.setAttribute( 'class', 'mw-debug-profile-underline' ); + line.setAttribute( 'x1', 0 ); + line.setAttribute( 'y1', 28 ); + line.setAttribute( 'x2', profile.width ); + line.setAttribute( 'y2', 28 ); + timeline.appendChild( line ); + + return timeline; + }, + + /** + * @param Object period to transform into graphics + */ + buildPeriod: function ( period ) { + var node, + head = profile.xCoord( period.start ), + tail = profile.xCoord( period.end ), + g = profile.createSvgElement( 'g' ); + + g.setAttribute( 'class', 'mw-debug-profile-period' ); + $( g ).data( 'period', period ); + + if ( head + 16 > tail ) { + node = profile.createSvgElement( 'rect' ); + node.setAttribute( 'x', head ); + node.setAttribute( 'y', 8 ); + node.setAttribute( 'width', 2 ); + node.setAttribute( 'height', 9 ); + g.appendChild( node ); + + node = profile.createSvgElement( 'rect' ); + node.setAttribute( 'x', head ); + node.setAttribute( 'y', 8 ); + node.setAttribute( 'width', ( period.end - period.start ) * profile.ratio || 2 ); + node.setAttribute( 'height', 6 ); + g.appendChild( node ); + } else { + node = profile.createSvgElement( 'polygon' ); + node.setAttribute( 'points', pointList( [ + [ head, 8 ], + [ head, 19 ], + [ head + 8, 8 ], + [ head, 8] + ] ) ); + g.appendChild( node ); + + node = profile.createSvgElement( 'polygon' ); + node.setAttribute( 'points', pointList( [ + [ tail, 8 ], + [ tail, 19 ], + [ tail - 8, 8 ], + [ tail, 8 ] + ] ) ); + g.appendChild( node ); + + node = profile.createSvgElement( 'line' ); + node.setAttribute( 'x1', head ); + node.setAttribute( 'y1', 9 ); + node.setAttribute( 'x2', tail ); + node.setAttribute( 'y2', 9 ); + g.appendChild( node ); + } + + return g; + }, + + /** + * @param Object + */ + buildFlyout: function ( period ) { + var contained, sum, ms, mem, i, + node = $( '<div>' ); + + for ( i = 0; i < period.contained.length; i++ ) { + contained = period.contained[i]; + sum = contained.end - contained.start; + ms = '' + ( sum < 1 ? sum.toFixed( 2 ) : sum.toFixed( 0 ) ) + ' ms'; + mem = formatBytes( contained.memory ); + + $( '<div>' ).text( contained.source.name ) + .append( $( '<span>' ).text( ' ~ ' + ms + ' / ' + mem ).addClass( 'mw-debug-profile-meta' ) ) + .appendTo( node ); + } + + return node; + }, + + /** + * Attach a hover flyout to all .mw-debug-profile-period groups. + */ + attachFlyout: function () { + // for some reason addClass and removeClass from jQuery + // arn't working on svg elements in chrome <= 33.0 (possibly more) + var $container = $( profile.container ), + addClass = function ( node, value ) { + var current = node.getAttribute( 'class' ), + list = current ? current.split( ' ' ) : false, + idx = list ? list.indexOf( value ) : -1; + + if ( idx === -1 ) { + node.setAttribute( 'class', current ? ( current + ' ' + value ) : value ); + } + }, + removeClass = function ( node, value ) { + var current = node.getAttribute( 'class' ), + list = current ? current.split( ' ' ) : false, + idx = list ? list.indexOf( value ) : -1; + + if ( idx !== -1 ) { + list.splice( idx, 1 ); + node.setAttribute( 'class', list.join( ' ' ) ); + } + }, + // hide all tipsy flyouts + hide = function () { + $container.find( '.mw-debug-profile-period.tipsy-visible' ) + .each( function () { + removeClass( this, 'tipsy-visible' ); + $( this ).tipsy( 'hide' ); + } ); + }; + + $container.find( '.mw-debug-profile-period' ).tipsy( { + fade: true, + gravity: function () { + return $.fn.tipsy.autoNS.call( this ) + $.fn.tipsy.autoWE.call( this ); + }, + className: 'mw-debug-profile-tipsy', + center: false, + html: true, + trigger: 'manual', + title: function () { + return profile.buildFlyout( $( this ).data( 'period' ) ).html(); + } + } ).on( 'mouseenter', function () { + hide(); + addClass( this, 'tipsy-visible' ); + $( this ).tipsy( 'show' ); + } ); + + $container.on( 'mouseleave', function ( event ) { + var $from = $( event.relatedTarget ), + $to = $( event.target ); + // only close the tipsy if we are not + if ( $from.closest( '.tipsy' ).length === 0 && + $to.closest( '.tipsy' ).length === 0 && + $to.get( 0 ).namespaceURI !== 'http://www.w4.org/2000/svg' + ) { + hide(); + } + } ).on( 'click', function () { + // convenience method for closing + hide(); + } ); + }, + + /** + * @return number the x co-ordinate for the specified timestamp + */ + xCoord: function ( msTimestamp ) { + return ( msTimestamp - profile.data.timespan.start ) * profile.ratio; + } + }; + + function ProfileData( data, width, mergeThresholdPx, dropThresholdPx ) { + // validate input data + this.data = data.map( function ( event ) { + event.periods = event.periods.filter( function ( period ) { + return period.start && period.end + && period.start < period.end + // period start must be a reasonable ms timestamp + && period.start > 1000000; + } ); + return event; + } ).filter( function ( event ) { + return event.name && event.periods.length > 0; + } ); + + // start and end time of the data + this.timespan = this.data.reduce( function ( result, event ) { + return event.periods.reduce( periodMinMax, result ); + }, periodMinMax.initial() ); + + // transform input data + this.groups = this.collate( width, mergeThresholdPx, dropThresholdPx ); + + return this; + } + + /** + * There are too many unique events to display a line for each, + * so this does a basic grouping. + */ + ProfileData.groupOf = function ( label ) { + var pos, prefix = 'Profile section ended by close(): '; + if ( label.indexOf( prefix ) === 0 ) { + label = label.slice( prefix.length ); + } + + pos = [ '::', ':', '-' ].reduce( function ( result, separator ) { + var pos = label.indexOf( separator ); + if ( pos === -1 ) { + return result; + } else if ( result === -1 ) { + return pos; + } else { + return Math.min( result, pos ); + } + }, -1 ); + + if ( pos === -1 ) { + return label; + } else { + return label.slice( 0, pos ); + } + }; + + /** + * @return Array list of objects with `name` and `events` keys + */ + ProfileData.groupEvents = function ( events ) { + var group, i, + groups = {}; + + // Group events together + for ( i = events.length - 1; i >= 0; i-- ) { + group = ProfileData.groupOf( events[i].name ); + if ( groups[group] ) { + groups[group].push( events[i] ); + } else { + groups[group] = [events[i]]; + } + } + + // Return an array of groups + return Object.keys( groups ).map( function ( group ) { + return { + name: group, + events: groups[group] + }; + } ); + }; + + ProfileData.periodSorter = function ( a, b ) { + if ( a.start === b.start ) { + return a.end - b.end; + } + return a.start - b.start; + }; + + ProfileData.genMergePeriodReducer = function ( mergeThresholdMs ) { + return function ( result, period ) { + if ( result.length === 0 ) { + // period is first result + return [{ + start: period.start, + end: period.end, + contained: [period] + }]; + } + var last = result[result.length - 1]; + if ( period.end < last.end ) { + // end is contained within previous + result[result.length - 1].contained.push( period ); + } else if ( period.start - mergeThresholdMs < last.end ) { + // neighbors within merging distance + result[result.length - 1].end = period.end; + result[result.length - 1].contained.push( period ); + } else { + // period is next result + result.push( { + start: period.start, + end: period.end, + contained: [period] + } ); + } + return result; + }; + }; + + /** + * Collect all periods from the grouped events and apply merge and + * drop transformations + */ + ProfileData.extractPeriods = function ( events, mergeThresholdMs, dropThresholdMs ) { + // collect the periods from all events + return events.reduce( function ( result, event ) { + if ( !event.periods.length ) { + return result; + } + result.push.apply( result, event.periods.map( function ( period ) { + // maintain link from period to event + period.source = event; + return period; + } ) ); + return result; + }, [] ) + // sort combined periods + .sort( ProfileData.periodSorter ) + // Apply merge threshold. Original periods + // are maintained in the `contained` property + .reduce( ProfileData.genMergePeriodReducer( mergeThresholdMs ), [] ) + // Apply drop threshold + .filter( function ( period ) { + return period.end - period.start > dropThresholdMs; + } ); + }; + + /** + * runs a callback on all periods in the group. Only valid after + * groups.periods[0..n].contained are populated. This runs against + * un-transformed data and is better suited to summing or other + * stat collection + */ + ProfileData.reducePeriods = function ( group, callback, result ) { + return group.periods.reduce( function ( result, period ) { + return period.contained.reduce( callback, result ); + }, result ); + }; + + /** + * Transforms this.data grouping by labels, merging neighboring + * events in the groups, and drops events and groups below the + * display threshold. Groups are returned sorted by starting time. + */ + ProfileData.prototype.collate = function ( width, mergeThresholdPx, dropThresholdPx ) { + // ms to pixel ratio + var ratio = ( this.timespan.end - this.timespan.start ) / width, + // transform thresholds to ms + mergeThresholdMs = mergeThresholdPx * ratio, + dropThresholdMs = dropThresholdPx * ratio; + + return ProfileData.groupEvents( this.data ) + // generate data about the grouped events + .map( function ( group ) { + // Cleaned periods from all events + group.periods = ProfileData.extractPeriods( group.events, mergeThresholdMs, dropThresholdMs ); + // min and max timestamp per group + group.timespan = ProfileData.reducePeriods( group, periodMinMax, periodMinMax.initial() ); + // ms from first call to end of last call + group.timespan.length = group.timespan.end - group.timespan.start; + // collect the un-transformed periods + group.timespan.sum = ProfileData.reducePeriods( group, function ( result, period ) { + result.push( period ); + return result; + }, [] ) + // sort by start time + .sort( ProfileData.periodSorter ) + // merge overlapping + .reduce( ProfileData.genMergePeriodReducer( 0 ), [] ) + // sum + .reduce( function ( result, period ) { + return result + period.end - period.start; + }, 0 ); + + return group; + }, this ) + // remove groups that have had all their periods filtered + .filter( function ( group ) { + return group.periods.length > 0; + } ) + // sort events by first start + .sort( function ( a, b ) { + return ProfileData.periodSorter( a.timespan, b.timespan ); + } ); + }; + + // reducer to find edges of period array + function periodMinMax( result, period ) { + if ( period.start < result.start ) { + result.start = period.start; + } + if ( period.end > result.end ) { + result.end = period.end; + } + return result; + } + + periodMinMax.initial = function () { + return { start: Number.POSITIVE_INFINITY, end: Number.NEGATIVE_INFINITY }; + }; + + function formatBytes( bytes ) { + var i, sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + if ( bytes === 0 ) { + return '0 Bytes'; + } + i = parseInt( Math.floor( Math.log( bytes ) / Math.log( 1024 ) ), 10 ); + return Math.round( bytes / Math.pow( 1024, i ), 2 ) + ' ' + sizes[i]; + } + + // turns a 2d array into a point list for svg + // polygon points attribute + // ex: [[1,2],[3,4],[4,2]] = '1,2 3,4 4,2' + function pointList( pairs ) { + return pairs.map( function ( pair ) { + return pair.join( ',' ); + } ).join( ' ' ); + } +}( mediaWiki, jQuery ) ); |