3rd-party-cpu-abuser
Version:
From a Chrome Dev Tools recorded timeline saved as JSON, this will help you identify quickly wich domain name eats most of the CPU
150 lines (121 loc) • 4.5 kB
JavaScript
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
if(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)
}