├── .npmignore ├── .gitignore ├── test ├── assets │ └── grayscale.jpg ├── speedline.js └── test.js ├── .travis.yml ├── .editorconfig ├── lib ├── api-stubs.js ├── devtools-monkeypatches.js ├── timeline-model-treeview.js └── timeline-model.js ├── license ├── .eslintrc ├── package.json ├── index.js ├── example.js └── readme.md /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /test/assets/grayscale.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulirish/devtools-timeline-model/HEAD/test/assets/grayscale.jpg -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8.0" 4 | - "lts/*" 5 | - "node" 6 | cache: 7 | directories: 8 | - node_modules 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /lib/api-stubs.js: -------------------------------------------------------------------------------- 1 | /* global Protocol UI SDK DataGrid */ 2 | 3 | const noop = function() { }; 4 | 5 | // other neccessary stubs 6 | Protocol.TargetBase = noop; 7 | Protocol.Agents = {}; 8 | 9 | UI.VBox = noop; 10 | UI.TreeElement = noop; 11 | 12 | DataGrid.ViewportDataGrid = noop; 13 | DataGrid.ViewportDataGridNode = noop; 14 | 15 | SDK.targetManager = {}; 16 | SDK.targetManager.mainTarget = noop; 17 | -------------------------------------------------------------------------------- /lib/devtools-monkeypatches.js: -------------------------------------------------------------------------------- 1 | /* global Runtime Common Bindings */ 2 | 3 | Runtime.experiments = {}; 4 | Runtime.experiments.isEnabled = exp => exp === 'timelineLatencyInfo'; 5 | 6 | Common.moduleSetting = function(module) { 7 | return {get: _ => module === 'showNativeFunctionsInJSProfile'}; 8 | }; 9 | 10 | // DevTools makes a few assumptions about using backing storage to hold traces. 11 | Bindings.DeferredTempFile = function() {}; 12 | Bindings.DeferredTempFile.prototype = { 13 | write: _ => { }, 14 | remove: _ => { }, 15 | finishWriting: _ => { } 16 | }; 17 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright 2015 Google Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "google", 3 | "env": { 4 | "node": true 5 | }, 6 | "rules": { 7 | // "off" or 0 - turn the rule off 8 | // "warn" or 1 - turn the rule on as a warning (doesn’t affect exit code) 9 | // "error" or 2 - turn the rule on as an error (exit code is 1 when triggered) 10 | "no-multiple-empty-lines": 0, 11 | "padded-blocks": 0, 12 | "curly": [0, "multi-line"], 13 | "max-len": [1, 120, { 14 | "ignoreComments": true, 15 | "ignoreUrls": true, 16 | "tabWidth": 2 17 | }], 18 | "no-implicit-coercion": [2, { 19 | "boolean": false, 20 | "number": true, 21 | "string": true 22 | }], 23 | "no-unused-expressions": [2, { 24 | "allowShortCircuit": true, 25 | "allowTernary": false 26 | }], 27 | "no-unused-vars": [1, { 28 | "vars": "all", 29 | "args": "after-used", 30 | "argsIgnorePattern": "(^reject$|^_$)", 31 | "varsIgnorePattern": "(^_$|andboxedModel$)" 32 | }], 33 | "quotes": [2, "single"], 34 | "require-jsdoc": 0, 35 | "valid-jsdoc": 0 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devtools-timeline-model", 3 | "version": "1.4.0", 4 | "description": "Parse raw trace data into the Chrome DevTools' structured profiling data models", 5 | "license": "Apache-2.0", 6 | "repository": "paulirish/devtools-timeline-model", 7 | "author": { 8 | "name": "Paul Irish", 9 | "url": "github.com/paulirish" 10 | }, 11 | "scripts": { 12 | "test": "npm run unit && npm run lint", 13 | "unit": "mocha $(find ./test -name '*.js') --timeout 10000", 14 | "lint": "eslint .", 15 | "watch": "find . -name \"*.js\" | grep -v \"node_modules\" | grep -v \"test\" | entr npm run test", 16 | "watchlint": "find . -name \"*.js\" | grep -v \"node_modules\" | grep -v \"test\" | entr npm run lint" 17 | }, 18 | "main": "index.js", 19 | "keywords": [ 20 | "devtools", 21 | "chrome", 22 | "performance", 23 | "profiling", 24 | "timeline" 25 | ], 26 | "dependencies": { 27 | "chrome-devtools-frontend": "1.0.445684", 28 | "resolve": "1.1.7" 29 | }, 30 | "devDependencies": { 31 | "eslint": "^2.4.0", 32 | "eslint-config-google": "^0.4.0", 33 | "mocha": "^2.3.3", 34 | "speedline": "0.1.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/speedline.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const assert = require('assert'); 5 | 6 | const frame = require('speedline/lib/frame'); 7 | const speedIndex = require('speedline/lib/speed-index'); 8 | 9 | const tracefilename = './test/assets/devtools-homepage-w-screenshots-trace.json'; 10 | const tracejsonfilename = './test/assets/progressive-app.json'; 11 | const ssfilename = './test/assets/grayscale.jpg'; 12 | 13 | function calculateVisualProgressFromImages(images, delay) { 14 | const baseTs = new Date().getTime(); 15 | 16 | const frames = images.map((imgPath, i) => { 17 | const imgBuff = fs.readFileSync(imgPath); 18 | return frame.create(imgBuff, baseTs + i * delay); 19 | }); 20 | 21 | return speedIndex.calculateVisualProgress(frames); 22 | } 23 | 24 | /* global describe, it */ 25 | describe('speedline compat', function() { 26 | it('extract frames from timeline should returns an array of frames', done => { 27 | frame.extractFramesFromTimeline(tracefilename).then(frames => { 28 | assert.ok(Array.isArray(frames), 'Frames is an array'); 29 | done(); 30 | }); 31 | }); 32 | 33 | it('extract frames should support json', done => { 34 | const trace = JSON.parse(fs.readFileSync(tracejsonfilename, 'utf-8')); 35 | frame.extractFramesFromTimeline(trace).then(frames => { 36 | assert.ok(Array.isArray(frames), 'Frames is an array'); 37 | done(); 38 | }); 39 | }); 40 | 41 | it('visual progress should be 100 if there is a single frame only', done => { 42 | const frames = calculateVisualProgressFromImages([ssfilename]); 43 | assert.equal(frames[0].getProgress(), 100); 44 | done(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var vm = require('vm'); 5 | 6 | /* eslint-disable no-native-reassign */ 7 | if (typeof __dirname === 'undefined') { 8 | __dirname = ''; 9 | } 10 | /* eslint-enable no-native-reassign */ 11 | 12 | class ModelAPI { 13 | 14 | constructor(events) { 15 | 16 | // Everything happens in a sandboxed vm context, to keep globals and natives separate. 17 | // First, sandboxed contexts don't have any globals from node, so we whitelist a few we'll provide for it. 18 | var glob = {require: require, global: global, console: console, __dirname: __dirname}; 19 | // We read in our script to run, and create a vm.Script object 20 | /* eslint-disable no-path-concat */ 21 | var script = new vm.Script(fs.readFileSync(__dirname + '/lib/timeline-model.js', 'utf8')); 22 | /* eslint-enable no-path-concat */ 23 | // We create a new V8 context with our globals 24 | var ctx = vm.createContext(glob); 25 | // We evaluate the `vm.Script` in the new context 26 | script.runInContext(ctx); 27 | // We pull the local `instance` variable out, to use as our proxy object 28 | this.sandbox = ctx.sandboxedModel; 29 | this.sandbox.init(events); 30 | 31 | return this; 32 | } 33 | 34 | timelineModel() { 35 | return this.sandbox.timelineModel(); 36 | } 37 | 38 | tracingModel() { 39 | return this.sandbox.tracingModel(); 40 | } 41 | 42 | topDown(startTime = 0, endTime = Infinity) { 43 | return this.sandbox.topDown(startTime, endTime); 44 | } 45 | 46 | topDownGroupBy(grouping, startTime = 0, endTime = Infinity) { 47 | return this.sandbox.topDownGroupBy(grouping, startTime, endTime); 48 | } 49 | 50 | bottomUp(startTime = 0, endTime = Infinity) { 51 | return this.sandbox.bottomUp(startTime, endTime); 52 | } 53 | 54 | /** 55 | * @ param {!String} grouping Allowed values: None Category Subdomain Domain URL Name 56 | * @ return {!WebInspector.TimelineProfileTree.Node} A grouped and sorted tree 57 | */ 58 | bottomUpGroupBy(grouping, startTime = 0, endTime = Infinity) { 59 | return this.sandbox.bottomUpGroupBy(grouping, startTime, endTime); 60 | } 61 | 62 | frameModel() { 63 | return this.sandbox.frameModel(); 64 | } 65 | 66 | filmStripModel() { 67 | return this.sandbox.filmStripModel(); 68 | } 69 | 70 | 71 | interactionModel() { 72 | return this.sandbox.interactionModel(); 73 | } 74 | } 75 | 76 | module.exports = ModelAPI; 77 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const filenames = [ 2 | 'test/assets/mdn-fling.json', 3 | 'test/assets/devtools-homepage-w-screenshots-trace.json' 4 | ]; 5 | 6 | var fs = require('fs'); 7 | var TraceToTimelineModel = require('.'); 8 | 9 | if (!console.group) { 10 | console.group = n => console.log(n, ':'); 11 | console.groupEnd = _ => console.log(''); 12 | } 13 | 14 | function dumpScreenshot(filmStripModel) { 15 | var frames = filmStripModel.frames(); 16 | var framesLen = frames.length; 17 | if (framesLen >= 1) { 18 | frames[framesLen - 1].imageDataPromise() 19 | .then(data => Promise.resolve('data:image/jpg;base64,' + data)) 20 | .then(img => { 21 | console.log('Filmstrip model last screenshot:\n', img.substr(0, 50) + '...'); 22 | }); 23 | } 24 | } 25 | 26 | function dumpTree(tree, timeValue) { 27 | var result = new Map(); 28 | tree.children.forEach((value, key) => result.set(key, value[timeValue].toFixed(1))); 29 | return result; 30 | } 31 | 32 | function report(filename) { 33 | var events = fs.readFileSync(filename, 'utf8'); 34 | 35 | var model = new TraceToTimelineModel(events); 36 | 37 | console.group(filename); 38 | 39 | console.log('Timeline model events:\n', model.timelineModel().mainThreadEvents().length); 40 | console.log('IR model interactions\n', model.interactionModel().interactionRecords().length); 41 | console.log('Frame model frames:\n', model.frameModel().frames().length); 42 | console.log('Filmstrip model screenshots:\n', model.filmStripModel().frames().length); 43 | dumpScreenshot(model.filmStripModel()); 44 | 45 | var topDown = model.topDown(); 46 | console.log('Top down tree total time:\n', topDown.totalTime); 47 | console.log('Top down tree, not grouped:\n', dumpTree(topDown, 'totalTime')); 48 | 49 | console.log('Bottom up tree leaves:\n', [...model.bottomUp().children.entries()].length); 50 | var bottomUpURL = model.bottomUpGroupBy('URL'); 51 | var secondTopCost = [...bottomUpURL.children.values()][1]; 52 | console.log('bottom up tree, grouped by URL', dumpTree(bottomUpURL, 'selfTime')); 53 | console.log('Bottom up tree, grouped, 2nd top URL:\n', secondTopCost.totalTime.toFixed(2), secondTopCost.id); 54 | 55 | var bottomUpSubdomain = model.bottomUpGroupBy('Subdomain'); 56 | console.log('Bottom up tree, grouped by top subdomain:\n', dumpTree(bottomUpSubdomain, 'selfTime')); 57 | 58 | var bottomUpByName = model.bottomUpGroupBy('EventName'); 59 | console.log('Bottom up tree grouped by EventName:\n', dumpTree(bottomUpByName, 'selfTime')); 60 | 61 | // console.log('Tracing model:\n', model.tracingModel()) 62 | // console.log('Timeline model:\n', model.timelineModel()) 63 | // console.log('IR model:\n', model.interactionModel()) 64 | // console.log('Frame model:\n', model.frameModel()) 65 | // console.log('Filmstrip model:\n', model.filmStripModel()) 66 | 67 | // console.log('Top down tree:\n', model.topDown()) 68 | // console.log('Bottom up tree:\n', model.bottomUp()) 69 | // console.log('Top down tree, grouped by URL:\n', model.topDownGroupedUnsorted) 70 | // console.log('Bottom up tree grouped by URL:\n', model.bottomUpGroupBy('None')) 71 | console.groupEnd(filename); 72 | } 73 | 74 | filenames.forEach(report); 75 | -------------------------------------------------------------------------------- /lib/timeline-model-treeview.js: -------------------------------------------------------------------------------- 1 | 2 | // this duplicates some work inside of TimelineTreeView, SortedDataGrid and beyond. 3 | // It's pretty difficult to extract, so we forked. 4 | 5 | /* global Timeline DataGrid self */ 6 | 7 | function TimelineModelTreeView(model) { 8 | this._rootNode = model; 9 | } 10 | 11 | // from Timeline.TimelineTreeView._sortingChanged 12 | // but tweaked so this._dataGrid.sortColumnId() is set as to sortItem 13 | // and this._dataGrid.isSortOrderAscending() is set as (sortOrder !== 'asc') 14 | TimelineModelTreeView.prototype.sortingChanged = function(sortItem, sortOrder) { 15 | if (!sortItem) 16 | return; 17 | var sortFunction; 18 | switch (sortItem) { 19 | case 'startTime': 20 | sortFunction = compareStartTime; 21 | break; 22 | case 'self': 23 | sortFunction = compareNumericField.bind(null, 'selfTime'); 24 | break; 25 | case 'total': 26 | sortFunction = compareNumericField.bind(null, 'totalTime'); 27 | break; 28 | case 'activity': 29 | sortFunction = compareName; 30 | break; 31 | default: 32 | console.assert(false, 'Unknown sort field: ' + sortItem); 33 | return; 34 | } 35 | return this.sortNodes(sortFunction, sortOrder !== 'asc'); 36 | 37 | // these functions adjusted to handle Map entries() rather than objects 38 | function compareNumericField(field, a, b) { 39 | var nodeA = (a[1]); 40 | var nodeB = (b[1]); 41 | return nodeA[field] - nodeB[field]; 42 | } 43 | 44 | function compareStartTime(a, b) { 45 | var nodeA = (a[1]); 46 | var nodeB = (b[1]); 47 | return nodeA.event.startTime - nodeB.event.startTime; 48 | } 49 | 50 | function compareName(a, b) { 51 | var nodeA = (a[1]); 52 | var nodeB = (b[1]); 53 | var nameA = Timeline.TimelineTreeView.eventNameForSorting(nodeA.event); 54 | var nameB = Timeline.TimelineTreeView.eventNameForSorting(nodeB.event); 55 | return nameA.localeCompare(nameB); 56 | } 57 | 58 | }; 59 | 60 | // from SortableDataGrid.sortNodes() 61 | TimelineModelTreeView.prototype.sortNodes = function(comparator, reverseMode) { 62 | this._sortingFunction = DataGrid.SortableDataGrid.Comparator.bind(null, comparator, reverseMode); 63 | sortChildren(this._rootNode, this._sortingFunction, reverseMode); 64 | }; 65 | 66 | /** 67 | * sortChildren has major changes, as it now works on Maps rather than Arrays 68 | * from SortableDataGrid._sortChildren() 69 | * @param {WebInspector.TimelineProfileTree.Node} parent 70 | * @param {any} sortingFunction 71 | */ 72 | function sortChildren(parent, sortingFunction) { 73 | if (!parent.children) return; 74 | parent.children = new Map([...parent.children.entries()].sort(sortingFunction)); 75 | for (var i = 0; i < parent.children.length; ++i) 76 | recalculateSiblings(parent.children[i], i); 77 | for (var child of parent.children.values()) 78 | sortChildren(child, sortingFunction); 79 | } 80 | 81 | /** 82 | * from DataGrid.recalculateSiblings() 83 | * @param {WebInspector.TimelineProfileTree.Node} node 84 | * @param {any} myIndex 85 | */ 86 | function recalculateSiblings(node, myIndex) { 87 | if (!node.parent) 88 | return; 89 | 90 | var previousChild = node.parent.children[myIndex - 1] || null; 91 | if (previousChild) 92 | previousChild.nextSibling = node; 93 | node.previousSibling = previousChild; 94 | 95 | var nextChild = node.parent.children[myIndex + 1] || null; 96 | if (nextChild) 97 | nextChild.previousSibling = node; 98 | node.nextSibling = nextChild; 99 | } 100 | 101 | self.TimelineModelTreeView = TimelineModelTreeView; 102 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # devtools-timeline-model [![Build Status](https://travis-ci.org/paulirish/devtools-timeline-model.svg?branch=master)](https://travis-ci.org/paulirish/devtools-timeline-model) 2 | 3 | 4 | **Unsupported**. 5 | 6 | Now we recommend you use another library for parsing traces.. See https://github.com/jlfwong/speedscope/blob/main/src/import/chrome.ts and https://github.com/saucelabs/tracelib and https://github.com/GoogleChrome/lighthouse/tree/master/lighthouse-core/lib/tracehouse 7 | 8 | Cheers 9 | 10 | 11 | --------------------- 12 | 13 | > Parse raw trace data into the Chrome DevTools' structured profiling data models 14 | 15 | If you use something like [big-rig](https://github.com/googlechrome/big-rig) or [automated-chrome-profiling](https://github.com/paulirish/automated-chrome-profiling#timeline-recording) you may end up with raw trace data. It's pretty raw. This module will parse that stuff into something a bit more consumable, and should help you with higher level analysis. 16 | 17 | 18 | ## Install 19 | 20 | ```sh 21 | $ npm install --save devtools-timeline-model 22 | ``` 23 | [![NPM devtools-timeline-model package](https://img.shields.io/npm/v/devtools-timeline-model.svg)](https://npmjs.org/package/devtools-timeline-model) 24 | 25 | ## Usage 26 | 27 | ```js 28 | var filename = 'demo/mdn-fling.json' 29 | var events = require('fs').readFileSync(filename, 'utf8') 30 | 31 | var DevtoolsTimelineModel = require('devtools-timeline-model'); 32 | // events can be either a string of the trace data or the JSON.parse'd equivalent 33 | var model = new DevtoolsTimelineModel(events) 34 | 35 | // tracing model 36 | model.tracingModel() 37 | // timeline model, all events 38 | model.timelineModel() 39 | // interaction model, incl scroll, click, animations 40 | model.interactionModel() 41 | // frame model, incl frame durations 42 | model.frameModel() 43 | // filmstrip model, incl screenshots 44 | model.filmStripModel() 45 | 46 | // topdown tree 47 | model.topDown() 48 | // bottom up tree 49 | model.bottomUp() 50 | // bottom up tree, grouped by URL 51 | model.bottomUpGroupBy('URL') // accepts: None Category Subdomain Domain URL EventName 52 | 53 | // see example.js for API examples. 54 | ``` 55 | 56 | ![image](https://cloud.githubusercontent.com/assets/39191/13832447/7b4dffde-eb99-11e5-8f7e-f1afcf999fd6.png) 57 | 58 | These objects are huge. You'll want to explore them in a UI like [devtool](https://github.com/Jam3/devtool). 59 | ![image](https://cloud.githubusercontent.com/assets/39191/13832411/390270ec-eb99-11e5-8dc9-c647c1b62c9d.png) 60 | 61 | 62 | ## Dev 63 | 64 | ```sh 65 | npm i 66 | brew install entr 67 | gls index.js lib/*.js | entr node example.js 68 | ``` 69 | 70 | ## Sandboxing WebInspector for Node 71 | 72 | Requiring the DevTools frontend looks rather straightforward at first. (`global.WebInspector = {}`, then start `require()`ing the files, in dependency order). However, there are two problems that crop up: 73 | 74 | 1. The frontend requires ~five globals and they currently must be added to the global context to work. 75 | 2. `utilities.js` adds a number of methods to native object prototypes, such as Array, Object, and typed arrays. 76 | 77 | `devtools-timeline-model` addresses that by sandboxing the WebInspector into it's own context. Here's how it works: 78 | 79 | ##### index.js 80 | ```js 81 | // First, sandboxed contexts don't have any globals from node, so we whitelist a few we'll provide for it. 82 | var glob = { require: require, global: global, console: console, process, process, __dirname: __dirname } 83 | // We read in our script to run, and create a vm.Script object 84 | var script = new vm.Script(fs.readFileSync(__dirname + "/lib/timeline-model.js", 'utf8')) 85 | // We create a new V8 context with our globals 86 | var ctx = vm.createContext(glob) 87 | // We evaluate the `vm.Script` in the new context 88 | var output = script.runInContext(ctx) 89 | ``` 90 | ##### (sandboxed) timeline-model.js 91 | ```js 92 | // establish our sandboxed globals 93 | this.window = this.self = this.global = this 94 | 95 | // We locally eval, as the node module scope isn't appropriate for the browser-centric DevTools frontend 96 | function requireval(path){ 97 | var filesrc = fs.readFileSync(__dirname + '/node_modules/' + path, 'utf8'); 98 | eval(filesrc + '\n\n//# sourceURL=' + path); 99 | } 100 | 101 | // polyfills, then the real chrome devtools frontend 102 | requireval('../lib/api-stubs.js') 103 | requireval('chrome-devtools-frontend/front_end/common/Object.js') 104 | requireval('chrome-devtools-frontend/front_end/common/SegmentedRange.js') 105 | requireval('chrome-devtools-frontend/front_end/platform/utilities.js') 106 | requireval('chrome-devtools-frontend/front_end/sdk/Target.js') 107 | // ... 108 | ``` 109 | ##### index.js 110 | ``` 111 | // After that's all done, we pull the local `instance` variable out, to use as our proxy object 112 | this.sandbox = ctx.instance; 113 | ``` 114 | 115 | Debugging is harder, as most tools aren't used to this setup. While `devtool` doesn't work well, you can have it run `lib/devtools-timeline-model.js` directly, which is fairly succesful. The classic `node-inspector` does work pretty well with the sandboxed script, though the workflow is a little worse than `devtool`'s. 116 | 117 | 118 | 119 | ## License 120 | 121 | Apache © [Paul Irish](https://github.com/paulirish/) 122 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | const assert = require('assert'); 5 | 6 | var TimelineModel = require('../'); 7 | 8 | const traceInArrayFormatFilename = './test/assets/devtools-homepage-w-screenshots-trace.json'; 9 | const traceInObjectFormatFilename = './test/assets/trace-in-object-format.json'; 10 | const webpagetestTraceFilename = './test/assets/trace-from-webpagetest.json'; 11 | 12 | var events = fs.readFileSync(traceInArrayFormatFilename, 'utf8'); 13 | var model; 14 | 15 | /* global describe, it */ 16 | describe('Web Inspector obj', function() { 17 | it('Array native globals dont leak', () => { 18 | assert.equal(Array.prototype.peekLast, undefined); 19 | }); 20 | 21 | it('WebInspector global doesn\'t leak', () => { 22 | assert.equal('Runtime' in global, false); 23 | assert.equal('TreeElement' in global, false); 24 | assert.equal('WorkerRuntime' in global, false); 25 | assert.equal('Protocol' in global, false); 26 | }); 27 | }); 28 | 29 | 30 | describe('DevTools Timeline Model', function() { 31 | it('doesn\'t throw an exception', () => { 32 | model = new TimelineModel(events); 33 | }); 34 | 35 | it('Multiple instances don\'t conflict', () => { 36 | let model1; 37 | let model2; 38 | assert.doesNotThrow(_ => { 39 | model1 = new TimelineModel(events); 40 | model2 = new TimelineModel(events); 41 | }); 42 | const events1 = model1.timelineModel().mainThreadEvents().length; 43 | const events2 = model2.timelineModel().mainThreadEvents().length; 44 | assert.equal(events1, events2); 45 | }); 46 | 47 | it('metrics returned are expected', () => { 48 | assert.equal(model.timelineModel().mainThreadEvents().length, 7721); 49 | assert.equal(model.interactionModel().interactionRecords().length, 0); 50 | assert.equal(model.frameModel().frames().length, 16); 51 | }); 52 | 53 | it('top-down profile', () => { 54 | const leavesCount = model.topDown().children.size; 55 | // console.log([...model.topDown().children.values()].map(e => [e.id, e.totalTime.toFixed(1)])); 56 | 57 | assert.equal(leavesCount, 18); 58 | const time = model.topDown().totalTime.toFixed(2); 59 | assert.equal(time, '555.01'); 60 | }); 61 | 62 | it('bottom-up profile', () => { 63 | const leavesCount = model.bottomUp().children.size; 64 | assert.equal(leavesCount, 220); 65 | var bottomUpURL = model.bottomUpGroupBy('URL'); 66 | const topCosts = [...bottomUpURL.children.values()]; 67 | const time = topCosts[0].totalTime.toFixed(2); 68 | const url = topCosts[0].id; 69 | assert.equal(time, '76.26'); 70 | assert.equal(url, 'https://s.ytimg.com/yts/jsbin/www-embed-lightweight-vflu_2b1k/www-embed-lightweight.js'); 71 | }); 72 | 73 | it('bottom-up profile - group by eventname', () => { 74 | const bottomUpByName = model.bottomUpGroupBy('EventName'); 75 | const leavesCount = bottomUpByName.children.size; 76 | assert.equal(leavesCount, 13); 77 | const topCosts = [...bottomUpByName.children.values()]; 78 | const time = topCosts[0].selfTime.toFixed(2); 79 | const name = topCosts[0].id; 80 | assert.equal(time, '187.75'); 81 | assert.equal(name, 'Layout'); 82 | }); 83 | 84 | it('bottom-up profile - group by subdomain', () => { 85 | const bottomUpByName = model.bottomUpGroupBy('Subdomain'); 86 | const topCosts = [...bottomUpByName.children.values()]; 87 | const time = topCosts[1].selfTime.toFixed(2); 88 | const name = topCosts[1].id; 89 | assert.equal(time, '44.33'); 90 | assert.equal(name, 'developers.google.com'); 91 | }); 92 | 93 | it('frame model', () => { 94 | const frameModel = model.frameModel(); 95 | assert.equal(frameModel.frames().length, 16); 96 | }); 97 | 98 | it('film strip model', () => { 99 | const filmStrip = model.filmStripModel(); 100 | assert.equal(filmStrip.frames().length, 16); 101 | }); 102 | 103 | it('interaction model', () => { 104 | const interactionModel = model.interactionModel(); 105 | assert.equal(interactionModel.interactionRecords().length, 0); 106 | }); 107 | 108 | it('limits by startTime', () => { 109 | const bottomUpByURL = model.bottomUpGroupBy('URL', 316224076.300); 110 | const leavesCount = bottomUpByURL.children.size; 111 | assert.equal(leavesCount, 14); 112 | const topCosts = [...bottomUpByURL.children.values()]; 113 | const url = topCosts[1].id; 114 | assert.equal(url, 'https://www.google-analytics.com/analytics.js'); 115 | }); 116 | 117 | it('limits by endTime', () => { 118 | const bottomUpByURL = model.bottomUpGroupBy('URL', 0, 316223621.274); 119 | const leavesCount = bottomUpByURL.children.size; 120 | assert.equal(leavesCount, 1); 121 | const topCosts = [...bottomUpByURL.children.values()]; 122 | const url = topCosts[0].id; 123 | assert.equal(url, 'https://developers.google.com/web/tools/chrome-devtools/?hl=en'); 124 | }); 125 | }); 126 | 127 | 128 | // ideas for tests 129 | // bottom up tree returns in self desc order 130 | // top down tree returns in total desc order 131 | // no entry in trees with empty ID 132 | 133 | 134 | // https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.q8di1j2nawlp 135 | describe('Supports Trace Events in JSON Object format', function() { 136 | const events = fs.readFileSync(traceInObjectFormatFilename, 'utf8'); 137 | let model; 138 | 139 | it('does not throw an exception', () => { 140 | assert.doesNotThrow(_ => { 141 | model = new TimelineModel(events); 142 | }); 143 | }); 144 | 145 | it('creates correctly formatted model', () => { 146 | assert.equal(model.timelineModel().mainThreadEvents().length, 8254); 147 | assert.equal(model.interactionModel().interactionRecords().length, 0); 148 | assert.equal(model.frameModel().frames().length, 12); 149 | }); 150 | }); 151 | 152 | // WebPageTest generated trace 153 | describe('Strips initial empty object from WebPageTest trace', function() { 154 | const events = fs.readFileSync(webpagetestTraceFilename, 'utf8'); 155 | let model; 156 | 157 | it('does not throw an exception', () => { 158 | assert.doesNotThrow(_ => { 159 | model = new TimelineModel(events); 160 | }); 161 | }); 162 | 163 | it('creates correctly formatted model', () => { 164 | assert.equal(model.timelineModel().mainThreadEvents().length, 609); 165 | assert.equal(model.interactionModel().interactionRecords().length, 0); 166 | assert.equal(model.frameModel().frames().length, 0); 167 | }); 168 | 169 | }); 170 | -------------------------------------------------------------------------------- /lib/timeline-model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global TimelineModel SDK Bindings Timeline TimelineModelTreeView */ 4 | 5 | const fs = require('fs'); 6 | const resolve = require('resolve'); 7 | 8 | // In order to maintain consistent global scope across the files, 9 | // and share natives like Array, etc, We will eval things within our sandbox 10 | function requireval(path) { 11 | const res = resolve.sync(path, {basedir: __dirname}); 12 | const filesrc = fs.readFileSync(res, 'utf8'); 13 | // eslint-disable-next-line no-eval 14 | eval(filesrc + '\n\n//# sourceURL=' + path); 15 | } 16 | 17 | // establish our sandboxed globals 18 | this.window = this.self = this.global = this; 19 | this.console = console; 20 | 21 | // establish our sandboxed globals 22 | this.Runtime = class {}; 23 | this.Protocol = class {}; 24 | this.TreeElement = class {}; 25 | 26 | // from generated externs. 27 | // As of node 7.3, instantiating these globals must be here rather than in api-stubs.js 28 | this.Accessibility = {}; 29 | this.Animation = {}; 30 | this.Audits = {}; 31 | this.Audits2 = {}; 32 | this.Audits2Worker = {}; 33 | this.Bindings = {}; 34 | this.CmModes = {}; 35 | this.Common = {}; 36 | this.Components = {}; 37 | this.Console = {}; 38 | this.DataGrid = {}; 39 | this.Devices = {}; 40 | this.Diff = {}; 41 | this.Elements = {}; 42 | this.Emulation = {}; 43 | this.Extensions = {}; 44 | this.FormatterWorker = {}; 45 | this.Gonzales = {}; 46 | this.HeapSnapshotWorker = {}; 47 | this.Host = {}; 48 | this.LayerViewer = {}; 49 | this.Layers = {}; 50 | this.Main = {}; 51 | this.Network = {}; 52 | this.Persistence = {}; 53 | this.Platform = {}; 54 | this.Profiler = {}; 55 | this.Resources = {}; 56 | this.Sass = {}; 57 | this.Screencast = {}; 58 | this.SDK = {}; 59 | this.Security = {}; 60 | this.Services = {}; 61 | this.Settings = {}; 62 | this.Snippets = {}; 63 | this.SourceFrame = {}; 64 | this.Sources = {}; 65 | this.Terminal = {}; 66 | this.TextEditor = {}; 67 | this.Timeline = {}; 68 | this.TimelineModel = {}; 69 | this.ToolboxBootstrap = {}; 70 | this.UI = {}; 71 | this.UtilitySharedWorker = {}; 72 | this.WorkerService = {}; 73 | this.Workspace = {}; 74 | 75 | requireval('./lib/api-stubs.js'); 76 | 77 | // chrome devtools frontend 78 | requireval('chrome-devtools-frontend/front_end/common/Object.js'); 79 | requireval('chrome-devtools-frontend/front_end/common/Console.js'); 80 | requireval('chrome-devtools-frontend/front_end/platform/utilities.js'); 81 | requireval('chrome-devtools-frontend/front_end/common/ParsedURL.js'); 82 | requireval('chrome-devtools-frontend/front_end/common/UIString.js'); 83 | requireval('chrome-devtools-frontend/front_end/sdk/Target.js'); 84 | requireval('chrome-devtools-frontend/front_end/sdk/LayerTreeBase.js'); 85 | requireval('chrome-devtools-frontend/front_end/common/SegmentedRange.js'); 86 | requireval('chrome-devtools-frontend/front_end/bindings/TempFile.js'); 87 | requireval('chrome-devtools-frontend/front_end/sdk/TracingModel.js'); 88 | requireval('chrome-devtools-frontend/front_end/sdk/ProfileTreeModel.js'); 89 | requireval('chrome-devtools-frontend/front_end/timeline/TimelineUIUtils.js'); 90 | requireval('chrome-devtools-frontend/front_end/timeline_model/TimelineJSProfile.js'); 91 | requireval('chrome-devtools-frontend/front_end/sdk/CPUProfileDataModel.js'); 92 | requireval('chrome-devtools-frontend/front_end/layers/LayerTreeModel.js'); 93 | requireval('chrome-devtools-frontend/front_end/timeline_model/TimelineModel.js'); 94 | requireval('chrome-devtools-frontend/front_end/data_grid/SortableDataGrid.js'); 95 | 96 | requireval('chrome-devtools-frontend/front_end/timeline/TimelineTreeView.js'); 97 | requireval('chrome-devtools-frontend/front_end/timeline_model/TimelineProfileTree.js'); 98 | requireval('chrome-devtools-frontend/front_end/sdk/FilmStripModel.js'); 99 | requireval('chrome-devtools-frontend/front_end/timeline_model/TimelineIRModel.js'); 100 | requireval('chrome-devtools-frontend/front_end/timeline_model/TimelineFrameModel.js'); 101 | 102 | // minor configurations 103 | requireval('./lib/devtools-monkeypatches.js'); 104 | 105 | // polyfill the bottom-up and topdown tree sorting 106 | requireval('./lib/timeline-model-treeview.js'); 107 | 108 | class SandboxedModel { 109 | 110 | init(events) { 111 | // build empty models. (devtools) tracing model & timeline model 112 | // from Timeline.TimelinePanel() constructor 113 | const tracingModelBackingStorage = new Bindings.TempFileBackingStorage('tracing'); 114 | this._tracingModel = new SDK.TracingModel(tracingModelBackingStorage); 115 | this._timelineModel = new TimelineModel.TimelineModel(Timeline.TimelineUIUtils.visibleEventsFilter()); 116 | 117 | if (typeof events === 'string') events = JSON.parse(events); 118 | // WebPagetest trace files put events in object under key `traceEvents` 119 | if (events.hasOwnProperty('traceEvents')) events = events.traceEvents; 120 | // WebPageTest trace files often have an empty object at index 0 121 | if (Object.keys(events[0]).length === 0) events.shift(); 122 | 123 | 124 | // reset models 125 | // from Timeline.TimelinePanel._clear() 126 | this._timelineModel.reset(); 127 | 128 | // populates with events, and call TracingModel.tracingComplete() 129 | this._tracingModel.setEventsForTest(events); 130 | 131 | // generate timeline model 132 | // from Timeline.TimelinePanel.loadingComplete() 133 | const loadedFromFile = true; 134 | this._timelineModel.setEvents(this._tracingModel, loadedFromFile); 135 | 136 | return this; 137 | } 138 | 139 | _createGroupingFunction(groupBy) { 140 | return Timeline.AggregatedTimelineTreeView.prototype._groupingFunction(groupBy); 141 | } 142 | 143 | timelineModel() { 144 | return this._timelineModel; 145 | } 146 | 147 | tracingModel() { 148 | return this._tracingModel; 149 | } 150 | 151 | topDown(startTime = 0, endTime = Infinity) { 152 | return this.topDownGroupBy(Timeline.AggregatedTimelineTreeView.GroupBy.None, startTime, endTime); 153 | } 154 | 155 | topDownGroupBy(grouping, startTime = 0, endTime = Infinity) { 156 | const filters = []; 157 | filters.push(Timeline.TimelineUIUtils.visibleEventsFilter()); 158 | filters.push(new TimelineModel.ExcludeTopLevelFilter()); 159 | const nonessentialEvents = [ 160 | TimelineModel.TimelineModel.RecordType.EventDispatch, 161 | TimelineModel.TimelineModel.RecordType.FunctionCall, 162 | TimelineModel.TimelineModel.RecordType.TimerFire 163 | ]; 164 | filters.push(new TimelineModel.ExclusiveNameFilter(nonessentialEvents)); 165 | 166 | const groupingAggregator = this._createGroupingFunction(Timeline.AggregatedTimelineTreeView.GroupBy[grouping]); 167 | const topDownGrouped = TimelineModel.TimelineProfileTree.buildTopDown(this._timelineModel.mainThreadEvents(), 168 | filters, startTime, endTime, groupingAggregator); 169 | 170 | // from Timeline.CallTreeTimelineTreeView._buildTree() 171 | if (grouping !== Timeline.AggregatedTimelineTreeView.GroupBy.None) 172 | new TimelineModel.TimelineAggregator().performGrouping(topDownGrouped); // group in-place 173 | 174 | new TimelineModelTreeView(topDownGrouped).sortingChanged('total', 'desc'); 175 | return topDownGrouped; 176 | } 177 | 178 | bottomUp(startTime = 0, endTime = Infinity) { 179 | return this.bottomUpGroupBy(Timeline.AggregatedTimelineTreeView.GroupBy.None, startTime, endTime); 180 | } 181 | 182 | /** 183 | * @param {!string} grouping Allowed values: None Category Subdomain Domain URL EventName 184 | * @return {!TimelineModel.TimelineProfileTree.Node} A grouped and sorted tree 185 | */ 186 | bottomUpGroupBy(grouping, startTime = 0, endTime = Infinity) { 187 | const topDown = this.topDownGroupBy(grouping, startTime, endTime); 188 | 189 | const bottomUpGrouped = TimelineModel.TimelineProfileTree.buildBottomUp(topDown); 190 | new TimelineModelTreeView(bottomUpGrouped).sortingChanged('self', 'desc'); 191 | 192 | // todo: understand why an empty key'd entry is created here 193 | bottomUpGrouped.children.delete(''); 194 | return bottomUpGrouped; 195 | } 196 | 197 | frameModel() { 198 | const frameModel = new TimelineModel.TimelineFrameModel(event => 199 | Timeline.TimelineUIUtils.eventStyle(event).category.name 200 | ); 201 | frameModel.addTraceEvents({ /* target */ }, 202 | this._timelineModel.inspectedTargetEvents(), this._timelineModel.sessionId() || ''); 203 | return frameModel; 204 | } 205 | 206 | filmStripModel() { 207 | return new SDK.FilmStripModel(this._tracingModel); 208 | } 209 | 210 | interactionModel() { 211 | const irModel = new TimelineModel.TimelineIRModel(); 212 | irModel.populate(this._timelineModel); 213 | return irModel; 214 | } 215 | } 216 | 217 | var sandboxedModel = new SandboxedModel(); 218 | // no exports as we're a sandboxed/eval'd module. 219 | --------------------------------------------------------------------------------