[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
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 */ 8 9 ( function ( mw, $ ) { 10 'use strict'; 11 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, 23 24 /** 25 * @property DOMElement 26 */ 27 container: null, 28 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; 39 40 if ( 41 !Array.prototype.map || 42 !Array.prototype.reduce || 43 !Array.prototype.filter || 44 !document.createElementNS || 45 !document.createElementNS.bind 46 ) { 47 profile.container = profile.buildRequiresBrowserFeatures(); 48 } else if ( data.length === 0 ) { 49 profile.container = profile.buildNoData(); 50 } else { 51 // Initialize createSvgElement (now that we know we have 52 // document.createElementNS and bind) 53 this.createSvgElement = document.createElementNS.bind( document, 'http://www.w3.org/2000/svg' ); 54 55 // generate a flyout 56 profile.data = new ProfileData( data, profile.width, mergeThresholdPx, dropThresholdPx ); 57 // draw it 58 profile.container = profile.buildSvg( profile.container ); 59 profile.attachFlyout(); 60 } 61 62 return profile.container; 63 }, 64 65 buildRequiresBrowserFeatures: function () { 66 return $( '<div>' ) 67 .text( 'Certain browser features, including parts of ECMAScript 5 and document.createElementNS, are required for the profile visualization.' ) 68 .get( 0 ); 69 }, 70 71 buildNoData: function () { 72 return $( '<div>' ).addClass( 'mw-debug-profile-no-data' ) 73 .text( 'No events recorded, ensure profiling is enabled in StartProfiler.php.' ) 74 .get( 0 ); 75 }, 76 77 /** 78 * Creates DOM nodes appropriately namespaced for SVG. 79 * Initialized in init after checking support 80 * 81 * @param string tag to create 82 * @return DOMElement 83 */ 84 createSvgElement: null, 85 86 /** 87 * @param DOMElement|undefined 88 */ 89 buildSvg: function ( node ) { 90 var container, group, i, g, 91 timespan = profile.data.timespan, 92 gapPerEvent = 38, 93 space = 10.5, 94 currentHeight = space, 95 totalHeight = 0; 96 97 profile.ratio = ( profile.width - space * 2 ) / ( timespan.end - timespan.start ); 98 totalHeight += gapPerEvent * profile.data.groups.length; 99 100 if ( node ) { 101 $( node ).empty(); 102 } else { 103 node = profile.createSvgElement( 'svg' ); 104 node.setAttribute( 'version', '1.2' ); 105 node.setAttribute( 'baseProfile', 'tiny' ); 106 } 107 node.style.height = totalHeight; 108 node.style.width = profile.width; 109 110 // use a container that can be transformed 111 container = profile.createSvgElement( 'g' ); 112 node.appendChild( container ); 113 114 for ( i = 0; i < profile.data.groups.length; i++ ) { 115 group = profile.data.groups[i]; 116 g = profile.buildTimeline( group ); 117 118 g.setAttribute( 'transform', 'translate( 0 ' + currentHeight + ' )' ); 119 container.appendChild( g ); 120 121 currentHeight += gapPerEvent; 122 } 123 124 return node; 125 }, 126 127 /** 128 * @param Object group of periods to transform into graphics 129 */ 130 buildTimeline: function ( group ) { 131 var text, tspan, line, i, 132 sum = group.timespan.sum, 133 ms = ' ~ ' + ( sum < 1 ? sum.toFixed( 2 ) : sum.toFixed( 0 ) ) + ' ms', 134 timeline = profile.createSvgElement( 'g' ); 135 136 timeline.setAttribute( 'class', 'mw-debug-profile-timeline' ); 137 138 // draw label 139 text = profile.createSvgElement( 'text' ); 140 text.setAttribute( 'x', profile.xCoord( group.timespan.start ) ); 141 text.setAttribute( 'y', 0 ); 142 text.textContent = group.name; 143 timeline.appendChild( text ); 144 145 // draw metadata 146 tspan = profile.createSvgElement( 'tspan' ); 147 tspan.textContent = ms; 148 text.appendChild( tspan ); 149 150 // draw timeline periods 151 for ( i = 0; i < group.periods.length; i++ ) { 152 timeline.appendChild( profile.buildPeriod( group.periods[i] ) ); 153 } 154 155 // full-width line under each timeline 156 line = profile.createSvgElement( 'line' ); 157 line.setAttribute( 'class', 'mw-debug-profile-underline' ); 158 line.setAttribute( 'x1', 0 ); 159 line.setAttribute( 'y1', 28 ); 160 line.setAttribute( 'x2', profile.width ); 161 line.setAttribute( 'y2', 28 ); 162 timeline.appendChild( line ); 163 164 return timeline; 165 }, 166 167 /** 168 * @param Object period to transform into graphics 169 */ 170 buildPeriod: function ( period ) { 171 var node, 172 head = profile.xCoord( period.start ), 173 tail = profile.xCoord( period.end ), 174 g = profile.createSvgElement( 'g' ); 175 176 g.setAttribute( 'class', 'mw-debug-profile-period' ); 177 $( g ).data( 'period', period ); 178 179 if ( head + 16 > tail ) { 180 node = profile.createSvgElement( 'rect' ); 181 node.setAttribute( 'x', head ); 182 node.setAttribute( 'y', 8 ); 183 node.setAttribute( 'width', 2 ); 184 node.setAttribute( 'height', 9 ); 185 g.appendChild( node ); 186 187 node = profile.createSvgElement( 'rect' ); 188 node.setAttribute( 'x', head ); 189 node.setAttribute( 'y', 8 ); 190 node.setAttribute( 'width', ( period.end - period.start ) * profile.ratio || 2 ); 191 node.setAttribute( 'height', 6 ); 192 g.appendChild( node ); 193 } else { 194 node = profile.createSvgElement( 'polygon' ); 195 node.setAttribute( 'points', pointList( [ 196 [ head, 8 ], 197 [ head, 19 ], 198 [ head + 8, 8 ], 199 [ head, 8] 200 ] ) ); 201 g.appendChild( node ); 202 203 node = profile.createSvgElement( 'polygon' ); 204 node.setAttribute( 'points', pointList( [ 205 [ tail, 8 ], 206 [ tail, 19 ], 207 [ tail - 8, 8 ], 208 [ tail, 8 ] 209 ] ) ); 210 g.appendChild( node ); 211 212 node = profile.createSvgElement( 'line' ); 213 node.setAttribute( 'x1', head ); 214 node.setAttribute( 'y1', 9 ); 215 node.setAttribute( 'x2', tail ); 216 node.setAttribute( 'y2', 9 ); 217 g.appendChild( node ); 218 } 219 220 return g; 221 }, 222 223 /** 224 * @param Object 225 */ 226 buildFlyout: function ( period ) { 227 var contained, sum, ms, mem, i, 228 node = $( '<div>' ); 229 230 for ( i = 0; i < period.contained.length; i++ ) { 231 contained = period.contained[i]; 232 sum = contained.end - contained.start; 233 ms = '' + ( sum < 1 ? sum.toFixed( 2 ) : sum.toFixed( 0 ) ) + ' ms'; 234 mem = formatBytes( contained.memory ); 235 236 $( '<div>' ).text( contained.source.name ) 237 .append( $( '<span>' ).text( ' ~ ' + ms + ' / ' + mem ).addClass( 'mw-debug-profile-meta' ) ) 238 .appendTo( node ); 239 } 240 241 return node; 242 }, 243 244 /** 245 * Attach a hover flyout to all .mw-debug-profile-period groups. 246 */ 247 attachFlyout: function () { 248 // for some reason addClass and removeClass from jQuery 249 // arn't working on svg elements in chrome <= 33.0 (possibly more) 250 var $container = $( profile.container ), 251 addClass = function ( node, value ) { 252 var current = node.getAttribute( 'class' ), 253 list = current ? current.split( ' ' ) : false, 254 idx = list ? list.indexOf( value ) : -1; 255 256 if ( idx === -1 ) { 257 node.setAttribute( 'class', current ? ( current + ' ' + value ) : value ); 258 } 259 }, 260 removeClass = function ( node, value ) { 261 var current = node.getAttribute( 'class' ), 262 list = current ? current.split( ' ' ) : false, 263 idx = list ? list.indexOf( value ) : -1; 264 265 if ( idx !== -1 ) { 266 list.splice( idx, 1 ); 267 node.setAttribute( 'class', list.join( ' ' ) ); 268 } 269 }, 270 // hide all tipsy flyouts 271 hide = function () { 272 $container.find( '.mw-debug-profile-period.tipsy-visible' ) 273 .each( function () { 274 removeClass( this, 'tipsy-visible' ); 275 $( this ).tipsy( 'hide' ); 276 } ); 277 }; 278 279 $container.find( '.mw-debug-profile-period' ).tipsy( { 280 fade: true, 281 gravity: function () { 282 return $.fn.tipsy.autoNS.call( this ) + $.fn.tipsy.autoWE.call( this ); 283 }, 284 className: 'mw-debug-profile-tipsy', 285 center: false, 286 html: true, 287 trigger: 'manual', 288 title: function () { 289 return profile.buildFlyout( $( this ).data( 'period' ) ).html(); 290 } 291 } ).on( 'mouseenter', function () { 292 hide(); 293 addClass( this, 'tipsy-visible' ); 294 $( this ).tipsy( 'show' ); 295 } ); 296 297 $container.on( 'mouseleave', function ( event ) { 298 var $from = $( event.relatedTarget ), 299 $to = $( event.target ); 300 // only close the tipsy if we are not 301 if ( $from.closest( '.tipsy' ).length === 0 && 302 $to.closest( '.tipsy' ).length === 0 && 303 $to.get( 0 ).namespaceURI !== 'http://www.w4.org/2000/svg' 304 ) { 305 hide(); 306 } 307 } ).on( 'click', function () { 308 // convenience method for closing 309 hide(); 310 } ); 311 }, 312 313 /** 314 * @return number the x co-ordinate for the specified timestamp 315 */ 316 xCoord: function ( msTimestamp ) { 317 return ( msTimestamp - profile.data.timespan.start ) * profile.ratio; 318 } 319 }; 320 321 function ProfileData( data, width, mergeThresholdPx, dropThresholdPx ) { 322 // validate input data 323 this.data = data.map( function ( event ) { 324 event.periods = event.periods.filter( function ( period ) { 325 return period.start && period.end 326 && period.start < period.end 327 // period start must be a reasonable ms timestamp 328 && period.start > 1000000; 329 } ); 330 return event; 331 } ).filter( function ( event ) { 332 return event.name && event.periods.length > 0; 333 } ); 334 335 // start and end time of the data 336 this.timespan = this.data.reduce( function ( result, event ) { 337 return event.periods.reduce( periodMinMax, result ); 338 }, periodMinMax.initial() ); 339 340 // transform input data 341 this.groups = this.collate( width, mergeThresholdPx, dropThresholdPx ); 342 343 return this; 344 } 345 346 /** 347 * There are too many unique events to display a line for each, 348 * so this does a basic grouping. 349 */ 350 ProfileData.groupOf = function ( label ) { 351 var pos, prefix = 'Profile section ended by close(): '; 352 if ( label.indexOf( prefix ) === 0 ) { 353 label = label.slice( prefix.length ); 354 } 355 356 pos = [ '::', ':', '-' ].reduce( function ( result, separator ) { 357 var pos = label.indexOf( separator ); 358 if ( pos === -1 ) { 359 return result; 360 } else if ( result === -1 ) { 361 return pos; 362 } else { 363 return Math.min( result, pos ); 364 } 365 }, -1 ); 366 367 if ( pos === -1 ) { 368 return label; 369 } else { 370 return label.slice( 0, pos ); 371 } 372 }; 373 374 /** 375 * @return Array list of objects with `name` and `events` keys 376 */ 377 ProfileData.groupEvents = function ( events ) { 378 var group, i, 379 groups = {}; 380 381 // Group events together 382 for ( i = events.length - 1; i >= 0; i-- ) { 383 group = ProfileData.groupOf( events[i].name ); 384 if ( groups[group] ) { 385 groups[group].push( events[i] ); 386 } else { 387 groups[group] = [events[i]]; 388 } 389 } 390 391 // Return an array of groups 392 return Object.keys( groups ).map( function ( group ) { 393 return { 394 name: group, 395 events: groups[group] 396 }; 397 } ); 398 }; 399 400 ProfileData.periodSorter = function ( a, b ) { 401 if ( a.start === b.start ) { 402 return a.end - b.end; 403 } 404 return a.start - b.start; 405 }; 406 407 ProfileData.genMergePeriodReducer = function ( mergeThresholdMs ) { 408 return function ( result, period ) { 409 if ( result.length === 0 ) { 410 // period is first result 411 return [{ 412 start: period.start, 413 end: period.end, 414 contained: [period] 415 }]; 416 } 417 var last = result[result.length - 1]; 418 if ( period.end < last.end ) { 419 // end is contained within previous 420 result[result.length - 1].contained.push( period ); 421 } else if ( period.start - mergeThresholdMs < last.end ) { 422 // neighbors within merging distance 423 result[result.length - 1].end = period.end; 424 result[result.length - 1].contained.push( period ); 425 } else { 426 // period is next result 427 result.push( { 428 start: period.start, 429 end: period.end, 430 contained: [period] 431 } ); 432 } 433 return result; 434 }; 435 }; 436 437 /** 438 * Collect all periods from the grouped events and apply merge and 439 * drop transformations 440 */ 441 ProfileData.extractPeriods = function ( events, mergeThresholdMs, dropThresholdMs ) { 442 // collect the periods from all events 443 return events.reduce( function ( result, event ) { 444 if ( !event.periods.length ) { 445 return result; 446 } 447 result.push.apply( result, event.periods.map( function ( period ) { 448 // maintain link from period to event 449 period.source = event; 450 return period; 451 } ) ); 452 return result; 453 }, [] ) 454 // sort combined periods 455 .sort( ProfileData.periodSorter ) 456 // Apply merge threshold. Original periods 457 // are maintained in the `contained` property 458 .reduce( ProfileData.genMergePeriodReducer( mergeThresholdMs ), [] ) 459 // Apply drop threshold 460 .filter( function ( period ) { 461 return period.end - period.start > dropThresholdMs; 462 } ); 463 }; 464 465 /** 466 * runs a callback on all periods in the group. Only valid after 467 * groups.periods[0..n].contained are populated. This runs against 468 * un-transformed data and is better suited to summing or other 469 * stat collection 470 */ 471 ProfileData.reducePeriods = function ( group, callback, result ) { 472 return group.periods.reduce( function ( result, period ) { 473 return period.contained.reduce( callback, result ); 474 }, result ); 475 }; 476 477 /** 478 * Transforms this.data grouping by labels, merging neighboring 479 * events in the groups, and drops events and groups below the 480 * display threshold. Groups are returned sorted by starting time. 481 */ 482 ProfileData.prototype.collate = function ( width, mergeThresholdPx, dropThresholdPx ) { 483 // ms to pixel ratio 484 var ratio = ( this.timespan.end - this.timespan.start ) / width, 485 // transform thresholds to ms 486 mergeThresholdMs = mergeThresholdPx * ratio, 487 dropThresholdMs = dropThresholdPx * ratio; 488 489 return ProfileData.groupEvents( this.data ) 490 // generate data about the grouped events 491 .map( function ( group ) { 492 // Cleaned periods from all events 493 group.periods = ProfileData.extractPeriods( group.events, mergeThresholdMs, dropThresholdMs ); 494 // min and max timestamp per group 495 group.timespan = ProfileData.reducePeriods( group, periodMinMax, periodMinMax.initial() ); 496 // ms from first call to end of last call 497 group.timespan.length = group.timespan.end - group.timespan.start; 498 // collect the un-transformed periods 499 group.timespan.sum = ProfileData.reducePeriods( group, function ( result, period ) { 500 result.push( period ); 501 return result; 502 }, [] ) 503 // sort by start time 504 .sort( ProfileData.periodSorter ) 505 // merge overlapping 506 .reduce( ProfileData.genMergePeriodReducer( 0 ), [] ) 507 // sum 508 .reduce( function ( result, period ) { 509 return result + period.end - period.start; 510 }, 0 ); 511 512 return group; 513 }, this ) 514 // remove groups that have had all their periods filtered 515 .filter( function ( group ) { 516 return group.periods.length > 0; 517 } ) 518 // sort events by first start 519 .sort( function ( a, b ) { 520 return ProfileData.periodSorter( a.timespan, b.timespan ); 521 } ); 522 }; 523 524 // reducer to find edges of period array 525 function periodMinMax( result, period ) { 526 if ( period.start < result.start ) { 527 result.start = period.start; 528 } 529 if ( period.end > result.end ) { 530 result.end = period.end; 531 } 532 return result; 533 } 534 535 periodMinMax.initial = function () { 536 return { start: Number.POSITIVE_INFINITY, end: Number.NEGATIVE_INFINITY }; 537 }; 538 539 function formatBytes( bytes ) { 540 var i, sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; 541 if ( bytes === 0 ) { 542 return '0 Bytes'; 543 } 544 i = parseInt( Math.floor( Math.log( bytes ) / Math.log( 1024 ) ), 10 ); 545 return Math.round( bytes / Math.pow( 1024, i ), 2 ) + ' ' + sizes[i]; 546 } 547 548 // turns a 2d array into a point list for svg 549 // polygon points attribute 550 // ex: [[1,2],[3,4],[4,2]] = '1,2 3,4 4,2' 551 function pointList( pairs ) { 552 return pairs.map( function ( pair ) { 553 return pair.join( ',' ); 554 } ).join( ' ' ); 555 } 556 }( mediaWiki, jQuery ) );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |