Feature: Add metrics output from puppeteer.

This commit is contained in:
Jason Gore 2019-10-11 14:10:53 -07:00
Родитель db037352ff
Коммит 05a9242aa9
56 изменённых файлов: 6195 добавлений и 1362 удалений

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -1,17 +1,34 @@
import fs from 'fs';
import path from 'path';
import * as tmp from 'tmp';
import puppeteer, { Browser, Page } from 'puppeteer';
import puppeteer, { Browser, Metrics, Page } from 'puppeteer';
import { cook, CookResult, Scenarios, ScenarioConfig } from '../flamegrill';
import { cook, Scenarios, ScenarioConfig } from '../flamegrill';
import { ScenarioProfile } from '../profile';
import { __unitTestHooks } from '../process/process';
import { findRegressions } from '../analyze/regression';
import { analyzeFunctions } from '../analyze/functional';
// TODO: these are black box tests for now but should be refactored to be unit tests
// TODO: modules that output files should be modified not to and wrapped by a centralized file output helper
// TODO: consider also making file output / github / CI integration another package within this repo
describe('flamegrill', () => {
const testMetrics: Metrics = {
Timestamp: 0,
Documents: 1,
Frames: 2,
JSEventListeners: 3,
Nodes: 4,
LayoutCount: 5,
RecalcStyleCount: 6,
LayoutDuration: 7,
RecalcStyleDuration: 8,
ScriptDuration: 9,
TaskDuration: 10,
JSHeapUsedSize: 11,
JSHeapTotalSize: 12
}
describe('cook', () => {
const profiles = require('../fixtures/profiles.json');
const util = require('../util');
@ -28,7 +45,7 @@ describe('flamegrill', () => {
goto: jest.fn(() => {
return Promise.resolve(null);
}),
metrics: jest.fn(() => Promise.resolve({})),
metrics: jest.fn(() => Promise.resolve(testMetrics)),
setDefaultTimeout: jest.fn(() => {})
} as unknown as Page;
@ -73,7 +90,7 @@ describe('flamegrill', () => {
it('generates expected output', async () => {
const snapshotsDir = path.join(__dirname, '../fixtures/snapshots');
const expectedResults = require('../fixtures/results.json');
const expectedResults = require(path.join(snapshotsDir, 'results.json'));
// Set up arr_diff to feed in predefined profiler log fixtures.
jest.spyOn(util, 'arr_diff').mockImplementation(() => {
@ -94,10 +111,11 @@ describe('flamegrill', () => {
const testResults = await cook(scenarios, scenarioConfig);
// The path will differ for every test run, so remove it before comparing results.
removePaths(expectedResults);
removePaths(testResults);
// Convenience line left commented out for updating expected output.
// fs.writeFileSync(path.join(outdir.name, "results.json"), JSON.stringify(testResults));
fs.writeFileSync(path.join(outdir.name, "results.json"), JSON.stringify(testResults));
expect(testResults).toEqual(expectedResults);
@ -107,19 +125,32 @@ describe('flamegrill', () => {
expect(testFiles).toEqual(snapshotFiles);
testFiles.forEach(file => {
// Some generated output creates files with \r\n. Some environments spit out \n.
// Ignore line break types when comparing results.
const analysis = fs.readFileSync(path.join(snapshotsDir, file), 'utf8').split(/\r?\n/g);
const output = fs.readFileSync(path.join(outdir.name, file), 'utf8').split(/\r?\n/g);
let expectedFileContents;
let testFileContents;
expect(output).toEqual(analysis);
if(file.includes('.json')) {
expectedFileContents = require(path.join(snapshotsDir, file));
testFileContents = require(path.join(outdir.name, file));
// The path will differ for every test run, so remove it before comparing results.
removePaths(expectedFileContents);
removePaths(testFileContents);
} else {
// Some generated output creates files with \r\n. Some environments spit out \n.
// Ignore line break types when comparing results.
expectedFileContents = fs.readFileSync(path.join(snapshotsDir, file), 'utf8').split(/\r?\n/g);
testFileContents = fs.readFileSync(path.join(outdir.name, file), 'utf8').split(/\r?\n/g);
}
expect(testFileContents).toEqual(expectedFileContents);
});
expect((testBrowser.close as jest.Mock).mock.calls.length).toEqual(1);
});
it('errors on invalid profile logs', async () => {
const expectedResults = require('../fixtures/errors/errors.json');
const snapshotsDir = path.join(__dirname, '../fixtures/errors/snapshots');
const expectedResults = require(path.join(snapshotsDir, 'results.json'));
// Set up arr_diff to feed in predefined profiler log fixtures.
jest.spyOn(util, 'arr_diff').mockImplementation(() => {
@ -130,6 +161,7 @@ describe('flamegrill', () => {
const testResults = await cook({ 'test': { scenario: 'testUrl' } }, scenarioConfig);
// The path will differ for every test run, so remove it before comparing results.
removePaths(expectedResults);
removePaths(testResults);
// Convenience line left commented out for updating expected output.
@ -137,19 +169,40 @@ describe('flamegrill', () => {
expect(testResults).toEqual(expectedResults);
const snapshotFiles = fs.readdirSync(snapshotsDir);
const testFiles = fs.readdirSync(outdir.name);
expect(testFiles).toEqual(['test.err.txt']);
const errorFile = fs.readFileSync(path.join(outdir.name, 'test.err.txt'));
expect(errorFile.includes('Error: dispatchLogRow_: Can\'t parse tick,0xfffffffdeb11c0e4,55914,0,0x0,6, integer too large.')).toBeTruthy();
expect(testFiles).toEqual(snapshotFiles);
testFiles.forEach(file => {
let expectedFileContent;
let testFileContent;
if(file.includes('.json')) {
expectedFileContent = require(path.join(snapshotsDir, file));
testFileContent = require(path.join(outdir.name, file));
// The path will differ for every test run, so remove it before comparing results.
removePaths(expectedFileContent);
removePaths(testFileContent);
expect(testFileContent).toEqual(expectedFileContent);
} else if (file === 'test.err.txt') {
const errorFile = fs.readFileSync(path.join(outdir.name, 'test.err.txt'));
expect(errorFile.includes('Error: dispatchLogRow_: Can\'t parse tick,0xfffffffdeb11c0e4,55914,0,0x0,6, integer too large.')).toBeTruthy();
} else {
// Unknown file, test should be updated to check results.
expect(file).toEqual('');
}
});
expect((testBrowser.close as jest.Mock).mock.calls.length).toEqual(1);
});
});
// These tests are technically redundant with cook tests but are left for now as they may be useful
// as unit tests are added.
describe('generateFlamegraph, findRegressions', () => {
describe('generateFlamegraph, analyzeFunctions', () => {
const { processProfile } = __unitTestHooks;
const profiles = require('../fixtures/profiles.json');
const snapshotsDir = path.join(__dirname, '../fixtures/snapshots');
@ -164,31 +217,37 @@ describe('flamegrill', () => {
// TODO: replace with processProfiles?
await Promise.all(Object.keys(profiles).map(key => {
let logfile = require.resolve(path.join('../fixtures', profiles[key].logFile));
let outfile = path.join(outdir.name, key);
return processProfile(logfile, outfile)
const profile: ScenarioProfile = {
logFile: require.resolve(path.join('../fixtures', profiles[key].logFile)),
metrics: testMetrics
};
const outfile = path.join(outdir.name, key);
return processProfile(profile, outfile)
}));
await Promise.all(Object.keys(profiles).map(key => {
let logfile = require.resolve(path.join('../fixtures', profiles[key].baseline.logFile));
let outfile = path.join(outdir.name, key + '_base');
return processProfile(logfile, outfile)
const profile: ScenarioProfile = {
logFile: require.resolve(path.join('../fixtures', profiles[key].baseline.logFile)),
metrics: testMetrics
};
const outfile = path.join(outdir.name, key + '_base');
return processProfile(profile, outfile)
}));
Object.keys(profiles).forEach(key => {
// TODO: this code block is duplicating code in flamegrill.ts and should be removed as code is refactored.
let datafileBefore = path.join(outdir.name, key + '_base.data.js');
let datafileAfter = path.join(outdir.name, key + '.data.js');
let datafileBasline = path.join(outdir.name, key + '_base.data.js');
let datafile = path.join(outdir.name, key + '.data.js');
let regressionfile = path.join(outdir.name, key + '.regression.txt');
const analysis = findRegressions(datafileBefore, datafileAfter);
const analysis = analyzeFunctions(datafileBasline, datafile);
if(analysis.isRegression) {
fs.writeFileSync(regressionfile, analysis.summary);
}
});
const snapshotFiles = fs.readdirSync(snapshotsDir);
const snapshotFiles = fs.readdirSync(snapshotsDir).filter(file => file !== 'results.json');
const testFiles = fs.readdirSync(outdir.name);
expect(testFiles).toEqual(snapshotFiles);
@ -213,7 +272,6 @@ describe('flamegrill', () => {
function removePaths<T>(obj: T) {
Object.keys(obj).forEach(key => {
if (key.includes('File')) {
console.log(`key = ${key}`);
obj[key as keyof T] = path.basename(obj[key as keyof T] as any) as any;
} else if (obj[key as keyof T] instanceof Object) {
removePaths(obj[key as keyof T]);

Просмотреть файл

@ -1,4 +1,4 @@
import { FunctionData, __unitTestHooks } from '../regression';
import { FunctionData, __unitTestHooks } from '../functional';
const { filterMinifiedNames, filterSystemNames } = __unitTestHooks;

Просмотреть файл

@ -2,15 +2,16 @@ import fs from 'fs';
import path from 'path';
import { ScenarioConfig } from '../flamegrill';
import { ScenarioProfiles } from '../profile';
import { ProcessedScenario, ProcessedScenarios } from '../process';
import { findRegressions, RegressionOutput } from './regression';
import { analyzeFunctions, FunctionalAnalysis } from './functional';
export interface Analysis {
numTicks: number;
}
export interface RegressionAnalysis extends RegressionOutput {
export interface RegressionAnalysis extends FunctionalAnalysis {
regressionFile?: string;
};
@ -46,7 +47,7 @@ function analyzeScenario(scenario: ProcessedScenario, scenarioName: string, conf
if (scenario.baseline && scenario.baseline.output) {
let numTicksBaseline = getTicks(scenario.baseline.output.dataFile);
let analysis: RegressionAnalysis = findRegressions(scenario.baseline.output.dataFile, scenario.output.dataFile);
let analysis: RegressionAnalysis = analyzeFunctions(scenario.baseline.output.dataFile, scenario.output.dataFile);
if (analysis.isRegression) {
analysis.regressionFile = `${scenarioName}.regression.txt`;
fs.writeFileSync(path.join(config.outDir, analysis.regressionFile), analysis.summary);

Просмотреть файл

@ -47,7 +47,7 @@ export interface ProcessedData {
numTicks: number;
}
export interface RegressionOutput {
export interface FunctionalAnalysis {
summary: string;
isRegression: boolean;
}
@ -58,11 +58,11 @@ export interface RegressionOutput {
* @param {string} datafileBaseline Baseline data.
* @param {string} datafile Scenario data.
*/
export function findRegressions(datafileBaseline: string, datafile: string): RegressionOutput {
export function analyzeFunctions(datafileBaseline: string, datafile: string): FunctionalAnalysis {
let summary = '';
let isRegression = false;
const dataBefore = readFlamegraphData(datafileBaseline);
const dataAfter = readFlamegraphData(datafile);
const dataBaseline = readFlamegraphData(datafileBaseline);
const data = readFlamegraphData(datafile);
// TODO: UNIT TESTS FOR EVERYONE!
// * function removed making other functions seem to be regressions when overall perf improves
@ -95,12 +95,12 @@ export function findRegressions(datafileBaseline: string, datafile: string): Reg
const regressions: FunctionRegression[] = [];
// Analyze data to find new functions and regressions.
Object.keys(dataAfter.functionsMap).forEach(name => {
Object.keys(data.functionsMap).forEach(name => {
// In some scenarios (such as native button), a tick can represent more time than our base thresholds.
// In these cases, elevate the base threshold to the tick base * 2.
// For example, if base threshold reports new functions at 2% time consumed, but 1 tick is 4% time,
// then new functions will only be reported when > 8%.
const minTicks = Math.min(dataBefore.numTicks, dataAfter.numTicks);
const minTicks = Math.min(dataBaseline.numTicks, data.numTicks);
const regressionNew = Math.max(1 / minTicks * 2, regressionNewBase);
const regressionDiff = Math.max(1 / minTicks * 2, regressionDiffBase);
@ -109,31 +109,31 @@ export function findRegressions(datafileBaseline: string, datafile: string): Reg
// console.log(`Modified base thresholds: regressionNew = ${regressionNew}, regressionDiff = ${regressionDiff}`);
// }
const after = dataAfter.functionsMap[name];
const before = dataBefore.functionsMap[name];
const functions = data.functionsMap[name];
const functionsBaseline = dataBaseline.functionsMap[name];
// TODO: This analysis should really account for the entire call hierarchy when comparing rather than just averaging.
// For now just ignore spurious 1 tick instances by using the same number of instances for both.
const instances = before ? Math.min(before.instances.length, after.instances.length) : after.instances.length;
const afterTicksNormalized = calcTotalTicksNormalized(after.instances) / instances;
const instances = functionsBaseline ? Math.min(functionsBaseline.instances.length, functions.instances.length) : functions.instances.length;
const ticksNormalized = calcTotalTicksNormalized(functions.instances) / instances;
if (!before) {
if(afterTicksNormalized > regressionNew) {
newFunctions.push({ name, displayName: after.displayName });
if (!functionsBaseline) {
if(ticksNormalized > regressionNew) {
newFunctions.push({ name, displayName: functions.displayName });
}
} else {
const beforeTicksNormalized = calcTotalTicksNormalized(before.instances) / instances;
const percDiff = afterTicksNormalized - beforeTicksNormalized;
const ticksNormalizedBaseline = calcTotalTicksNormalized(functionsBaseline.instances) / instances;
const percDiff = ticksNormalized - ticksNormalizedBaseline;
if (percDiff > regressionDiff) {
regressions.push({ name, displayName: after.displayName });
regressions.push({ name, displayName: functions.displayName });
}
}
});
summary += `Results for ${path.basename(datafileBaseline)} => ${path.basename(datafile)}\n\n`;
summary += `numTicks: ${dataBefore.numTicks} => ${dataAfter.numTicks}\n\n`;
summary += `numTicks: ${dataBaseline.numTicks} => ${data.numTicks}\n\n`;
if (regressions.length === 0 && newFunctions.length === 0) {
summary += 'OK!';
@ -143,23 +143,23 @@ export function findRegressions(datafileBaseline: string, datafile: string): Reg
isRegression = true;
summary += 'Potential Regressions: \n';
regressions.forEach(regression => {
const after = dataAfter.functionsMap[regression.displayName];
const before = dataBefore.functionsMap[regression.displayName];
const functions = data.functionsMap[regression.displayName];
const functionsBaseline = dataBaseline.functionsMap[regression.displayName];
// Averaging prevents overexaggeration of recurisve functions, but can also underexaggerate their impact.
// Averaging can also cause spurious (1 tick samples of obj.computed, what causes these?) of functions to underexaggerate impact.
// TODO: Remove averaging when analysis takes into account call hierarchy.
// For now just ignore spurious 1 tick instances by using the same number of instances for both.
const instances = before ? Math.min(before.instances.length, after.instances.length) : after.instances.length;
const afterTicksNormalized = calcTotalTicksNormalized(after.instances) / instances;
const beforeTicksNormalized = before && calcTotalTicksNormalized(before.instances) / instances;
const instances = functionsBaseline ? Math.min(functionsBaseline.instances.length, functions.instances.length) : functions.instances.length;
const ticksNormalized = calcTotalTicksNormalized(functions.instances) / instances;
const ticksNormalizedBaseline = functionsBaseline && calcTotalTicksNormalized(functionsBaseline.instances) / instances;
const beforeTicksDisplay = beforeTicksNormalized ? (beforeTicksNormalized * 100).toFixed(0) : 'not present';
const afterTicksDisplay = afterTicksNormalized && (afterTicksNormalized * 100).toFixed(0);
const ticksDisplayBaseline = ticksNormalizedBaseline ? (ticksNormalizedBaseline * 100).toFixed(0) : 'not present';
const ticksDisplay = ticksNormalized && (ticksNormalized * 100).toFixed(0);
summary += ` ${regression.displayName}` +
`, time consumed: ${beforeTicksDisplay}% => ` +
`${afterTicksDisplay}% \n`;
`, time consumed: ${ticksDisplayBaseline}% => ` +
`${ticksDisplay}% \n`;
});
}
@ -167,7 +167,7 @@ export function findRegressions(datafileBaseline: string, datafile: string): Reg
isRegression = true;
summary += '\nNew Functions: \n';
newFunctions.forEach(newFunction => {
const ticksNormalized = calcTotalTicksNormalized(dataAfter.functionsMap[newFunction.displayName].instances);
const ticksNormalized = calcTotalTicksNormalized(data.functionsMap[newFunction.displayName].instances);
summary += ` ${newFunction.displayName}, time consumed = ${(ticksNormalized * 100).toFixed(0)}%\n`;
});
}
@ -327,7 +327,7 @@ if (require.main === module) {
});
scenarios.forEach(scenario => {
const analysis = findRegressions(path.join(process.cwd(), `${scenario}_base.data.js`), path.join(process.cwd(), `${scenario}.data.js`));
const analysis = analyzeFunctions(path.join(process.cwd(), `${scenario}_base.data.js`), path.join(process.cwd(), `${scenario}.data.js`));
console.log(JSON.stringify(analysis));
// processPerfData(path.join(process.cwd(), `${scenario}_base.js`), path.join(process.cwd(), `${scenario}.js`));
});

Просмотреть файл

@ -1,13 +0,0 @@
{
"test": {
"profile": {
"logFile": "error.prof",
"metrics": {}
},
"processed": {
"error": {
"errorFile": "test.err.txt"
}
}
}
}

Просмотреть файл

@ -0,0 +1,27 @@
{
"test": {
"profile": {
"logFile": "C:\\Users\\Jason\\src\\flamegrill\\packages\\flamegrill\\src\\fixtures\\errors\\error.prof",
"metrics": {
"Timestamp": 0,
"Documents": 1,
"Frames": 2,
"JSEventListeners": 3,
"Nodes": 4,
"LayoutCount": 5,
"RecalcStyleCount": 6,
"LayoutDuration": 7,
"RecalcStyleDuration": 8,
"ScriptDuration": 9,
"TaskDuration": 10,
"JSHeapUsedSize": 11,
"JSHeapTotalSize": 12
}
},
"processed": {
"error": {
"errorFile": "C:\\Users\\Jason\\AppData\\Local\\Temp\\tmp-31156P9K1pPOdnx3z\\test.err.txt"
}
}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
C:\Users\Jason\src\flamegrill\packages\flamegrill\lib\tickprocessor.js:880
throw e;
^
Error: dispatchLogRow_: Can't parse tick,0xfffffffdeb11c0e4,55914,0,0x0,6, integer too large.
at TickProcessor.LogReader.dispatchLogRow_ (C:\Users\Jason\src\flamegrill\packages\flamegrill\lib\tickprocessor.js:835:19)
at TickProcessor.LogReader.processLogLine_ (C:\Users\Jason\src\flamegrill\packages\flamegrill\lib\tickprocessor.js:876:12)
at TickProcessor.LogReader.processLogLine (C:\Users\Jason\src\flamegrill\packages\flamegrill\lib\tickprocessor.js:736:10)
at TickProcessor.processLogFile (C:\Users\Jason\src\flamegrill\packages\flamegrill\lib\tickprocessor.js:2793:10)
at Object.<anonymous> (C:\Users\Jason\src\flamegrill\packages\flamegrill\lib\tickprocessor.js:3565:15)
at Module._compile (internal/modules/cjs/loader.js:778:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)
at Module.load (internal/modules/cjs/loader.js:653:32)
at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
at Function.Module._load (internal/modules/cjs/loader.js:585:3)

Просмотреть файл

@ -1,678 +0,0 @@
{
"BaseButton": {
"profile": {
"logFile": "isolate-000001C8E897A980-9332-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-000002716F92D960-27048-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "BaseButton.data.js",
"flamegraphFile": "BaseButton.html"
},
"baseline": {
"output": {
"dataFile": "BaseButton_base.data.js",
"flamegraphFile": "BaseButton_base.html"
}
}
},
"analysis": {
"numTicks": 327,
"baseline": {
"numTicks": 345
},
"regression": {
"summary": "Results for BaseButton_base.data.js => BaseButton.data.js\n\nnumTicks: 345 => 327\n\nOK!",
"isRegression": false
}
}
},
"BaseButtonNew": {
"profile": {
"logFile": "isolate-00000150C15DDCB0-8200-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-000001C7CB0729F0-15316-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "BaseButtonNew.data.js",
"flamegraphFile": "BaseButtonNew.html"
},
"baseline": {
"output": {
"dataFile": "BaseButtonNew_base.data.js",
"flamegraphFile": "BaseButtonNew_base.html"
}
}
},
"analysis": {
"numTicks": 421,
"baseline": {
"numTicks": 426
},
"regression": {
"summary": "Results for BaseButtonNew_base.data.js => BaseButtonNew.data.js\n\nnumTicks: 426 => 421\n\nOK!",
"isRegression": false
}
}
},
"button": {
"profile": {
"logFile": "isolate-000001FA30C2C060-15348-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-00000218B8DE6280-33408-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "button.data.js",
"flamegraphFile": "button.html"
},
"baseline": {
"output": {
"dataFile": "button_base.data.js",
"flamegraphFile": "button_base.html"
}
}
},
"analysis": {
"numTicks": 26,
"baseline": {
"numTicks": 28
},
"regression": {
"summary": "Results for button_base.data.js => button.data.js\n\nnumTicks: 28 => 26\n\nOK!",
"isRegression": false
}
}
},
"DefaultButton": {
"profile": {
"logFile": "isolate-0000026F3D5C86F0-32220-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-000001A279165800-31988-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "DefaultButton.data.js",
"flamegraphFile": "DefaultButton.html"
},
"baseline": {
"output": {
"dataFile": "DefaultButton_base.data.js",
"flamegraphFile": "DefaultButton_base.html"
}
}
},
"analysis": {
"numTicks": 455,
"baseline": {
"numTicks": 528
},
"regression": {
"summary": "Results for DefaultButton_base.data.js => DefaultButton.data.js\n\nnumTicks: 528 => 455\n\nOK!",
"isRegression": false
}
}
},
"DefaultButtonNew": {
"profile": {
"logFile": "isolate-000001976475B760-12772-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-000001AFA4995FB0-8920-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "DefaultButtonNew.data.js",
"flamegraphFile": "DefaultButtonNew.html"
},
"baseline": {
"output": {
"dataFile": "DefaultButtonNew_base.data.js",
"flamegraphFile": "DefaultButtonNew_base.html"
}
}
},
"analysis": {
"numTicks": 882,
"baseline": {
"numTicks": 923
},
"regression": {
"summary": "Results for DefaultButtonNew_base.data.js => DefaultButtonNew.data.js\n\nnumTicks: 923 => 882\n\nOK!",
"isRegression": false
}
}
},
"DetailsRow": {
"profile": {
"logFile": "isolate-000001C75B30DD10-19448-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-0000016A4F24EAD0-27036-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "DetailsRow.data.js",
"flamegraphFile": "DetailsRow.html"
},
"baseline": {
"output": {
"dataFile": "DetailsRow_base.data.js",
"flamegraphFile": "DetailsRow_base.html"
}
}
},
"analysis": {
"numTicks": 2750,
"baseline": {
"numTicks": 1471
},
"regression": {
"summary": "Results for DetailsRow_base.data.js => DetailsRow.data.js\n\nnumTicks: 1471 => 2750\n\nPotential Regressions: \n ~DetailsRowBase.render, time consumed: 7% => 30% \n ~getClassNames, time consumed: 1% => 15% \n ~mergeStyleSets, time consumed: 0% => 37% \n ~styleToRegistration, time consumed: 0% => 27% \n",
"isRegression": true,
"regressionFile": "DetailsRow.regression.txt"
}
}
},
"DetailsRowFast": {
"profile": {
"logFile": "isolate-000001AB3AA2DE10-28068-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-0000025EBEA9C020-13384-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "DetailsRowFast.data.js",
"flamegraphFile": "DetailsRowFast.html"
},
"baseline": {
"output": {
"dataFile": "DetailsRowFast_base.data.js",
"flamegraphFile": "DetailsRowFast_base.html"
}
}
},
"analysis": {
"numTicks": 2695,
"baseline": {
"numTicks": 1409
},
"regression": {
"summary": "Results for DetailsRowFast_base.data.js => DetailsRowFast.data.js\n\nnumTicks: 1409 => 2695\n\nPotential Regressions: \n ~DetailsRowBase.render, time consumed: 6% => 27% \n ~extractRules, time consumed: 0% => 20% \n ~getClassNames, time consumed: 1% => 14% \n ~mergeStyleSets, time consumed: 0% => 36% \n ~styleToRegistration, time consumed: 0% => 27% \n",
"isRegression": true,
"regressionFile": "DetailsRowFast.regression.txt"
}
}
},
"DetailsRowNoStyles": {
"profile": {
"logFile": "isolate-000001D1BFE31800-24432-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-000001CEF69AFE10-4612-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "DetailsRowNoStyles.data.js",
"flamegraphFile": "DetailsRowNoStyles.html"
},
"baseline": {
"output": {
"dataFile": "DetailsRowNoStyles_base.data.js",
"flamegraphFile": "DetailsRowNoStyles_base.html"
}
}
},
"analysis": {
"numTicks": 1948,
"baseline": {
"numTicks": 1466
},
"regression": {
"summary": "Results for DetailsRowNoStyles_base.data.js => DetailsRowNoStyles.data.js\n\nnumTicks: 1466 => 1948\n\nPotential Regressions: \n ~CheckBase, time consumed: 1% => 15% \n ~DetailsRowCheckBase, time consumed: 3% => 14% \n\nNew Functions: \n ~mergeStyleSets, time consumed = 23%\n ~styleToRegistration, time consumed = 17%\n",
"isRegression": true,
"regressionFile": "DetailsRowNoStyles.regression.txt"
}
}
},
"DocumentCardTitle": {
"profile": {
"logFile": "isolate-000001658015DEE0-9140-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-000002A0A0FDD770-30228-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "DocumentCardTitle.data.js",
"flamegraphFile": "DocumentCardTitle.html"
},
"baseline": {
"output": {
"dataFile": "DocumentCardTitle_base.data.js",
"flamegraphFile": "DocumentCardTitle_base.html"
}
}
},
"analysis": {
"numTicks": 16012,
"baseline": {
"numTicks": 15919
},
"regression": {
"summary": "Results for DocumentCardTitle_base.data.js => DocumentCardTitle.data.js\n\nnumTicks: 15919 => 16012\n\nOK!",
"isRegression": false
}
}
},
"MenuButton": {
"profile": {
"logFile": "isolate-0000023E414D6CE0-28460-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-000002744B17D570-32064-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "MenuButton.data.js",
"flamegraphFile": "MenuButton.html"
},
"baseline": {
"output": {
"dataFile": "MenuButton_base.data.js",
"flamegraphFile": "MenuButton_base.html"
}
}
},
"analysis": {
"numTicks": 647,
"baseline": {
"numTicks": 590
},
"regression": {
"summary": "Results for MenuButton_base.data.js => MenuButton.data.js\n\nnumTicks: 590 => 647\n\nOK!",
"isRegression": false
}
}
},
"MenuButtonNew": {
"profile": {
"logFile": "isolate-0000021FD7B39BB0-28188-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-0000028C417C8AC0-24936-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "MenuButtonNew.data.js",
"flamegraphFile": "MenuButtonNew.html"
},
"baseline": {
"output": {
"dataFile": "MenuButtonNew_base.data.js",
"flamegraphFile": "MenuButtonNew_base.html"
}
}
},
"analysis": {
"numTicks": 1548,
"baseline": {
"numTicks": 1581
},
"regression": {
"summary": "Results for MenuButtonNew_base.data.js => MenuButtonNew.data.js\n\nnumTicks: 1581 => 1548\n\nOK!",
"isRegression": false
}
}
},
"PrimaryButton": {
"profile": {
"logFile": "isolate-00000224DD39CC70-27088-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-00000289B99F90F0-33752-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "PrimaryButton.data.js",
"flamegraphFile": "PrimaryButton.html"
},
"baseline": {
"output": {
"dataFile": "PrimaryButton_base.data.js",
"flamegraphFile": "PrimaryButton_base.html"
}
}
},
"analysis": {
"numTicks": 488,
"baseline": {
"numTicks": 496
},
"regression": {
"summary": "Results for PrimaryButton_base.data.js => PrimaryButton.data.js\n\nnumTicks: 496 => 488\n\nOK!",
"isRegression": false
}
}
},
"PrimaryButtonNew": {
"profile": {
"logFile": "isolate-000001D40015E310-20784-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-000001C3BAC2EA80-32776-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "PrimaryButtonNew.data.js",
"flamegraphFile": "PrimaryButtonNew.html"
},
"baseline": {
"output": {
"dataFile": "PrimaryButtonNew_base.data.js",
"flamegraphFile": "PrimaryButtonNew_base.html"
}
}
},
"analysis": {
"numTicks": 922,
"baseline": {
"numTicks": 1001
},
"regression": {
"summary": "Results for PrimaryButtonNew_base.data.js => PrimaryButtonNew.data.js\n\nnumTicks: 1001 => 922\n\nOK!",
"isRegression": false
}
}
},
"SplitButton": {
"profile": {
"logFile": "isolate-000001D2DD15DB40-22696-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-000001292C499B50-24060-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "SplitButton.data.js",
"flamegraphFile": "SplitButton.html"
},
"baseline": {
"output": {
"dataFile": "SplitButton_base.data.js",
"flamegraphFile": "SplitButton_base.html"
}
}
},
"analysis": {
"numTicks": 1293,
"baseline": {
"numTicks": 1355
},
"regression": {
"summary": "Results for SplitButton_base.data.js => SplitButton.data.js\n\nnumTicks: 1355 => 1293\n\nOK!",
"isRegression": false
}
}
},
"SplitButtonNew": {
"profile": {
"logFile": "isolate-0000027E6DE24B80-31272-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-000002EAD2E39E10-33780-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "SplitButtonNew.data.js",
"flamegraphFile": "SplitButtonNew.html"
},
"baseline": {
"output": {
"dataFile": "SplitButtonNew_base.data.js",
"flamegraphFile": "SplitButtonNew_base.html"
}
}
},
"analysis": {
"numTicks": 3155,
"baseline": {
"numTicks": 3048
},
"regression": {
"summary": "Results for SplitButtonNew_base.data.js => SplitButtonNew.data.js\n\nnumTicks: 3048 => 3155\n\nOK!",
"isRegression": false
}
}
},
"Stack": {
"profile": {
"logFile": "isolate-000002958EF69420-30340-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-00000199612E7F60-32084-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "Stack.data.js",
"flamegraphFile": "Stack.html"
},
"baseline": {
"output": {
"dataFile": "Stack_base.data.js",
"flamegraphFile": "Stack_base.html"
}
}
},
"analysis": {
"numTicks": 263,
"baseline": {
"numTicks": 251
},
"regression": {
"summary": "Results for Stack_base.data.js => Stack.data.js\n\nnumTicks: 251 => 263\n\nOK!",
"isRegression": false
}
}
},
"StackWithIntrinsicChildren": {
"profile": {
"logFile": "isolate-000002CB95DF62A0-29024-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-000001F8F52C6E60-30996-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "StackWithIntrinsicChildren.data.js",
"flamegraphFile": "StackWithIntrinsicChildren.html"
},
"baseline": {
"output": {
"dataFile": "StackWithIntrinsicChildren_base.data.js",
"flamegraphFile": "StackWithIntrinsicChildren_base.html"
}
}
},
"analysis": {
"numTicks": 563,
"baseline": {
"numTicks": 596
},
"regression": {
"summary": "Results for StackWithIntrinsicChildren_base.data.js => StackWithIntrinsicChildren.data.js\n\nnumTicks: 596 => 563\n\nOK!",
"isRegression": false
}
}
},
"StackWithTextChildren": {
"profile": {
"logFile": "isolate-000001E2F953C450-33588-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-000001AA84CA8D20-8016-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "StackWithTextChildren.data.js",
"flamegraphFile": "StackWithTextChildren.html"
},
"baseline": {
"output": {
"dataFile": "StackWithTextChildren_base.data.js",
"flamegraphFile": "StackWithTextChildren_base.html"
}
}
},
"analysis": {
"numTicks": 1978,
"baseline": {
"numTicks": 1928
},
"regression": {
"summary": "Results for StackWithTextChildren_base.data.js => StackWithTextChildren.data.js\n\nnumTicks: 1928 => 1978\n\nOK!",
"isRegression": false
}
}
},
"Text": {
"profile": {
"logFile": "isolate-00000257772F8820-33584-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-000002236CE1DD30-25244-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "Text.data.js",
"flamegraphFile": "Text.html"
},
"baseline": {
"output": {
"dataFile": "Text_base.data.js",
"flamegraphFile": "Text_base.html"
}
}
},
"analysis": {
"numTicks": 206,
"baseline": {
"numTicks": 168
},
"regression": {
"summary": "Results for Text_base.data.js => Text.data.js\n\nnumTicks: 168 => 206\n\nOK!",
"isRegression": false
}
}
},
"Toggle": {
"profile": {
"logFile": "isolate-00000248C0C3E090-14868-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-000001FCF48C0980-25140-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "Toggle.data.js",
"flamegraphFile": "Toggle.html"
},
"baseline": {
"output": {
"dataFile": "Toggle_base.data.js",
"flamegraphFile": "Toggle_base.html"
}
}
},
"analysis": {
"numTicks": 882,
"baseline": {
"numTicks": 454
},
"regression": {
"summary": "Results for Toggle_base.data.js => Toggle.data.js\n\nnumTicks: 454 => 882\n\nPotential Regressions: \n ~ToggleBase.render, time consumed: 9% => 53% \n ~getClassNames, time consumed: 2% => 47% \n ~mergeStyleSets, time consumed: 0% => 39% \n ~obj.<computed>, time consumed: 10% => 53% \n ~styleToRegistration, time consumed: 0% => 29% \n\nNew Functions: \n ~Toggle_styles_getStyles, time consumed = 6%\n ~_resolve, time consumed = 7%\n ~_styles, time consumed = 7%\n ~extractRules, time consumed = 42%\n ~getKeyForRules, time consumed = 8%\n",
"isRegression": true,
"regressionFile": "Toggle.regression.txt"
}
}
},
"ToggleNew": {
"profile": {
"logFile": "isolate-000002231747AC50-11576-puppeteer.prof",
"metrics": {},
"baseline": {
"logFile": "isolate-0000014BEA16EDB0-28552-puppeteer.prof",
"metrics": {}
}
},
"processed": {
"output": {
"dataFile": "ToggleNew.data.js",
"flamegraphFile": "ToggleNew.html"
},
"baseline": {
"output": {
"dataFile": "ToggleNew_base.data.js",
"flamegraphFile": "ToggleNew_base.html"
}
}
},
"analysis": {
"numTicks": 1165,
"baseline": {
"numTicks": 1102
},
"regression": {
"summary": "Results for ToggleNew_base.data.js => ToggleNew.data.js\n\nnumTicks: 1102 => 1165\n\nOK!",
"isRegression": false
}
}
}
}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -71,6 +71,8 @@ export async function cook(scenarios: Scenarios, userConfig?: ScenarioConfig): P
};
});
fs.writeFileSync(path.join(config.outDir, 'results.json'), JSON.stringify(results));
console.log('results: ');
console.dir(results, { depth: null });

Просмотреть файл

@ -0,0 +1,56 @@
import { Metrics } from 'puppeteer';
export function addMetrics(flamegraph: string, metrics: Metrics): string {
const metricsTableHeader = `
<details>
<summary>Metrics</summary>
<table>
<tr>
<th>
<a href="https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagemetrics">Metric</a>
</th>
<th>Value</th>
</tr>
`;
const metricsTableFooter = `
</table>
</details>
`;
const metricsTableContents = Object
.keys(metrics)
.map(key => {
return `
<tr>
<td>${key}</td>
<td>${getFormattedValue(metrics[key as keyof Metrics], key as keyof Metrics)}</td>
</tr>
`;
})
.join('');
const metricsTable = metricsTableHeader + metricsTableContents + metricsTableFooter;
// Use placeholder in string to add metrics.
flamegraph = flamegraph
.split('<!-- /* METRICS_PLACEHOLDER */ -->')
.join(metricsTable);
return flamegraph;
};
function getFormattedValue(value: number, key: keyof Metrics): string {
switch(key) {
case 'JSHeapUsedSize':
case 'JSHeapTotalSize':
return value.toLocaleString('en') + ' bytes';
case 'LayoutDuration':
case 'RecalcStyleDuration':
case 'ScriptDuration':
case 'TaskDuration':
return value.toLocaleString('en', { maximumSignificantDigits: 3 }) + ' s';
default:
return value.toString();
}
}

Просмотреть файл

@ -2,10 +2,12 @@ import fs from 'fs';
import path from 'path';
import cp from 'child_process';
import concat from 'concat-stream';
import flamebearer from './flamebearer';
import { ScenarioConfig } from '../flamegrill';
import { ScenarioProfiles } from '../profile';
import { ScenarioProfile, ScenarioProfiles } from '../profile';
import flamebearer from './flamebearer';
import { addMetrics } from './metrics';
// TODO: is file output good enough here as an API? should this return programmatic results?
export interface ProcessedOutput {
@ -42,10 +44,10 @@ export async function processProfiles(profiles: ScenarioProfiles, config: Requir
const outFileBaseline = path.join(config.outDir, `${scenarioName}_base`);
const result = profiles[scenarioName];
const processedScenario: ProcessedScenario = await processProfile(result.logFile, outFile);
const processedScenario: ProcessedScenario = await processProfile(result, outFile);
if(result.baseline) {
processedScenario.baseline = await processProfile(result.baseline.logFile, outFileBaseline);
processedScenario.baseline = await processProfile(result.baseline, outFileBaseline);
}
processedScenarios[scenarioName] = processedScenario;
@ -54,8 +56,8 @@ export async function processProfiles(profiles: ScenarioProfiles, config: Requir
return processedScenarios;
}
async function processProfile(logFile: string, outFile: string): Promise<Processed> {
return generateFlamegraph(logFile, outFile);
async function processProfile(profile: ScenarioProfile, outFile: string): Promise<Processed> {
return generateFlamegraph(profile, outFile);
}
/**
@ -66,10 +68,10 @@ async function processProfile(logFile: string, outFile: string): Promise<Process
* @param {string} outFile File path and base name for file output. Extensions will be added for each type of file output.
* @returns {Promise<OutputFiles>} Promise resolving to generated files.
*/
async function generateFlamegraph(logFile: string, outFile: string): Promise<Processed> {
async function generateFlamegraph(profile: ScenarioProfile, outFile: string): Promise<Processed> {
// TODO: account for --windows, --unix and --mac arguments? what is the output difference with and without these args?
// TODO: use cli
const preprocessProc = cp.spawn(process.execPath, [tickprocessor, '--preprocess', '-j', logFile]);
const preprocessProc = cp.spawn(process.execPath, [tickprocessor, '--preprocess', '-j', profile.logFile]);
const flamegraphFile = outFile + '.html'
const dataFile = outFile + '.data.js';
const errorFile = outFile + '.err.txt';
@ -86,7 +88,10 @@ async function generateFlamegraph(logFile: string, outFile: string): Promise<Pro
// This line writes intermediate streaming output for troubleshooting:
// fs.writeFileSync(flamegraphfile.split('.html').join('.preprocessed.log'), preprocessed);
const [flamegraph, data] = flamebearer(preprocessed);
let [flamegraph, data] = flamebearer(preprocessed);
flamegraph = addMetrics(flamegraph, profile.metrics);
fs.writeFileSync(flamegraphFile, flamegraph);
fs.writeFileSync(dataFile, data);

Просмотреть файл

@ -36,6 +36,7 @@ export async function profile(scenarios: Scenarios, config: Required<ScenarioCon
const browser = await puppeteer.launch({
headless: true,
// TODO: use --single-process arg?
args: [
'--flag-switches-begin',
'--no-sandbox',