All files index.js

60.61% Statements 40/66
43.75% Branches 14/32
63.64% Functions 7/11
62.3% Lines 38/61
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 1502x 2x 2x     2x 2x             5x 5x         5x 2x   3x   5x 5x 5x       5x 5x     5x 5x   5x                                                                   5x 327x                       5x     327x 327x     327x     5x                 5x 129x     5x       1x 1x     1x         1x 1x   1x       1x 14x     1x 1x                                  
const fs = require('fs')
const path = require('path')
const Table = require('cli-table')
 
// full external API
exports.toTableConsole = options => toTableConsole(main(options))
exports.data = main
 
// actual code
// supported options :
// file: (String) Path to a file
// minTime: (Number) under this limit, the domain is ignored. Set to 0 to have them all
function main(options) {
  try {
    fs.accessSync(options.file, fs.constants.R_OK)
  } catch (e) {
    throw 'Please provide a .json file to analyze'
  }
  // in ms : under this limit, we ignore the domain
  if('minTime' in options && !Number.isNaN(Number(options.minTime)))
    var ignoreTime = Number(options.minTime)
  else
    var ignoreTime = 150
 
  const groupBy = options.groupBy || 'Subdomain'
  const startMark = options.startMark || null
  const endMark = options.endMark || null
 
  // GET DATA
  // uses https://github.com/paulirish/devtools-timeline-model
  const DevtoolsTimelineModel = require('devtools-timeline-model')
  const model = new DevtoolsTimelineModel(fs.readFileSync(options.file, 'utf8'))
 
  // Allow processing to be restricted by start/endMarks e.g. firstPaint
  let startTimeStamp = 0
  let endTimeStamp = Infinity
 
  Iif(startMark !== null || endMark !== null) {
 
    const events = rootFrameEvents(model)
 
    // Start timestamp
    if (startMark !== null) {
      let startMarks = events.filter(entry => entry.name === startMark)
      if (startMarks.length === 0) {
        throw 'No entry for ' + startMark
      }
 
      if (startMarks.length > 1) {
        console.warn('Found more than one entry for ' + startMark + ', using first')
      }
 
      startTimeStamp = startMarks[0].startTime;
    }
 
    // End timestamp
    if (endMark !== null) {
      let endMarks = events.filter(entry => entry.name === endMark)
      if (endMarks.length === 0) {
        throw 'No entry for ' + endMark
      }
 
      if (endMarks.length > 1) {
        console.warn('Found more than one entry for ' + endMark + ', using first')    
      }
 
      endTimeStamp = endMarks[0].startTime;
    }
  }
 
  // get all CPU time, already grouped by option (default FQDN)
  const topCosts = [...model.bottomUpGroupBy(groupBy, startTimeStamp, endTimeStamp).children.values()]
  const completeTime = topCosts.reduce((total, node) => total + node.totalTime, 0)
 
  // @TODO : test forced relayouts
  // https://github.com/paulirish/automated-chrome-profiling/blob/master/test-for-layout-thrashing.js
  /* console.log(model.frameModel())
  const forcedReflowEvents = model.frameModel().frames()
    .filter( e => e.name == 'UpdateLayoutTree' || e.name == 'Layout')
    .filter( e => e.args && e.args.beginData && e.args.beginData.stackTrace && e.args.beginData.stackTrace.length)
  console.log('forcedReflowEvents', forcedReflowEvents)
  */
 
  // ignore if < 150 ms, sort by time
  const filteredTopCosts = topCosts
    // rename the unassigned
    .map((node) => {
      node.id = node.id || '(no-domain)'
      return node
    })
    // ignore small contributions
    .filter((node) => node.totalTime > ignoreTime)
    // .sort((b, a) => a.time - b.time)
 
  const data = {
    // First, some stats
    totalCPUTime: completeTime,
    totalDomains: topCosts.length,
    totalOffendersDomains: filteredTopCosts.length,
    // then the filtered data
    cpuTimePerDomain:{}
  }
  // associate a domain to a time
  filteredTopCosts.forEach((node) => 
    data.cpuTimePerDomain[node.id] = node.totalTime
  )
 
  return data
}
 
function toTableConsole(data) {
  const tableSummary = new Table()
  let output = ''
 
 
  tableSummary.push(
    { 'Total CPU busy time (ms)': data.totalCPUTime.toFixed(2) },
    { 'Total number of domains': data.totalDomains },
    { 'Number of big offenders': data.totalOffendersDomains }
  )
  output += tableSummary.toString()
  output += '\n'
 
  const tableData = new Table({
    head: ['CPU Time (ms)', 'domain name']
  })
 
  for(domainName in data.cpuTimePerDomain) {
    tableData.push([data.cpuTimePerDomain[domainName].toFixed(2), domainName])
  }
 
  output += tableData.toString()
  return output
}
 
function rootFrameEvents(model) {
 
  const rootFrames = model.sandbox._timelineModel.rootFrames()
  if(rootFrames.length === 0) {
    throw 'No root frame in trace'
  }
  if(rootFrames.length > 1) {
    throw 'More than one root frame in trace'
  }
 
  const rootFrameId = rootFrames[0].frameId;
 
  return model.sandbox._timelineModel._mainThreadEvents.filter(entry => entry.args.frame === rootFrameId)
}