[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/resources/src/mediawiki/ -> mediawiki.debug.profile.js (source)

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


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1