diff --git a/lib/chrome/webdriver/chromium.js b/lib/chrome/webdriver/chromium.js index d6ca27b38..dcd367425 100644 --- a/lib/chrome/webdriver/chromium.js +++ b/lib/chrome/webdriver/chromium.js @@ -16,11 +16,7 @@ import { pathToFolder } from '../../support/pathToFolder.js'; import { ChromeDevtoolsProtocol } from '../chromeDevtoolsProtocol.js'; import { NetworkManager } from '../networkManager.js'; import { Android, isAndroidConfigured } from '../../android/index.js'; -import { - getFirstContentFulPaintEvent, - getLargestContentfulPaintEvent, - getRecalculateStyleElementsAndTimeBefore -} from './traceUtilities.js'; +import { getRenderBlocking } from './traceUtilities.js'; const unlink = promisify(_unlink); const rm = promisify(_rm); @@ -161,7 +157,11 @@ export class Chromium { await this.android.resetPowerUsage(); } - if (this.collectTracingEvents && !this.isTracing) { + if ( + this.collectTracingEvents && + !this.isTracing && + this.options.chrome.timelineRecordingType !== 'custom' + ) { this.isTracing = true; return this.cdpClient.startTrace(); } @@ -175,7 +175,11 @@ export class Chromium { */ async afterPageCompleteCheck(runner, index, url, alias) { const result = { url, alias }; - if (this.collectTracingEvents && this.isTracing) { + if ( + this.collectTracingEvents && + this.isTracing && + this.options.chrome.timelineRecordingType !== 'custom' + ) { // We are ready and can stop collecting events this.isTracing = false; this.events = await this.cdpClient.stopTrace(); @@ -335,7 +339,10 @@ export class Chromium { await this.storageManager.gzip(filename, gzFilename, true); } - if (this.collectTracingEvents) { + if ( + this.collectTracingEvents && + this.options.chrome.timelineRecordingType !== 'custom' + ) { const trace = parse(this.events, result.url); const name = this.options.enableProfileRun ? `trace-${index}-extra-run.json` @@ -346,50 +353,19 @@ export class Chromium { result.cpu = cpu; // Collect render blocking info - const renderBlockingInfo = {}; - const urlsWithBlockingInfo = trace.traceEvents.filter( - task => - task.cat === 'devtools.timeline' && - task.name === 'ResourceSendRequest' && - task.args.data.url && - task.args.data.renderBlocking - ); - for (let asset of urlsWithBlockingInfo) { - renderBlockingInfo[asset.args.data.url] = - asset.args.data.renderBlocking; - } - - const fcpEvent = getFirstContentFulPaintEvent(trace.traceEvents); - const lcpEvent = getLargestContentfulPaintEvent(trace.traceEvents); - - result.renderBlocking = { recalculateStyle: {}, requests: {} }; - - if (fcpEvent) { - const beforeFCP = getRecalculateStyleElementsAndTimeBefore( - trace.traceEvents, - fcpEvent.ts - ); - result.renderBlocking.recalculateStyle.beforeFCP = beforeFCP; - } - - if (lcpEvent) { - const beforeLCP = getRecalculateStyleElementsAndTimeBefore( - trace.traceEvents, - lcpEvent.ts - ); - result.renderBlocking.recalculateStyle.beforeLCP = beforeLCP; - } + const render = await getRenderBlocking(trace); + result.renderBlocking = render.renderBlocking; if (!this.options.skipHar) { for (let harRequest of this.hars[index - 1].log.entries) { - if (renderBlockingInfo[harRequest.request.url]) { + if (render.renderBlockingInfo[harRequest.request.url]) { harRequest._renderBlocking = - renderBlockingInfo[harRequest.request.url]; + render.renderBlockingInfo[harRequest.request.url]; } } } - result.renderBlocking.requests = renderBlockingInfo; + result.renderBlocking.requests = render.renderBlockingInfo; } // Google Web Vitals hacksery diff --git a/lib/chrome/webdriver/traceUtilities.js b/lib/chrome/webdriver/traceUtilities.js index 2ca1c8022..be11658bf 100644 --- a/lib/chrome/webdriver/traceUtilities.js +++ b/lib/chrome/webdriver/traceUtilities.js @@ -1,7 +1,7 @@ import intel from 'intel'; const log = intel.getLogger('browsertime.chrome'); -export function getLargestContentfulPaintEvent(traceEvents) { +function getLargestContentfulPaintEvent(traceEvents) { const lcpCandidates = traceEvents.filter( task => task.name === 'largestContentfulPaint::Candidate' ); @@ -20,7 +20,7 @@ export function getLargestContentfulPaintEvent(traceEvents) { } } -export function getFirstContentFulPaintEvent(traceEvents) { +function getFirstContentFulPaintEvent(traceEvents) { // Get first contentful paint const fcpEvent = traceEvents.find( task => task.name === 'firstContentfulPaint' @@ -33,10 +33,7 @@ export function getFirstContentFulPaintEvent(traceEvents) { } } -export function getRecalculateStyleElementsAndTimeBefore( - traceEvents, - timestamp -) { +function getRecalculateStyleElementsAndTimeBefore(traceEvents, timestamp) { const recalculatesBefore = traceEvents.filter( task => task.cat === 'disabled-by-default-devtools.timeline' && @@ -62,3 +59,41 @@ export function getRecalculateStyleElementsAndTimeBefore( return { elements, durationInMillis: duration / 1000 }; } + +export async function getRenderBlocking(trace) { + const renderBlockingInfo = {}; + + const urlsWithBlockingInfo = trace.traceEvents.filter( + task => + task.cat === 'devtools.timeline' && + task.name === 'ResourceSendRequest' && + task.args.data.url && + task.args.data.renderBlocking + ); + for (let asset of urlsWithBlockingInfo) { + renderBlockingInfo[asset.args.data.url] = asset.args.data.renderBlocking; + } + + const fcpEvent = getFirstContentFulPaintEvent(trace.traceEvents); + const lcpEvent = getLargestContentfulPaintEvent(trace.traceEvents); + + const renderBlocking = { recalculateStyle: {}, requests: {} }; + + if (fcpEvent) { + const beforeFCP = getRecalculateStyleElementsAndTimeBefore( + trace.traceEvents, + fcpEvent.ts + ); + renderBlocking.recalculateStyle.beforeFCP = beforeFCP; + } + + if (lcpEvent) { + const beforeLCP = getRecalculateStyleElementsAndTimeBefore( + trace.traceEvents, + lcpEvent.ts + ); + renderBlocking.recalculateStyle.beforeLCP = beforeLCP; + } + + return { renderBlockingInfo, renderBlocking }; +} diff --git a/lib/core/engine/command/chromeTrace.js b/lib/core/engine/command/chromeTrace.js new file mode 100644 index 000000000..97512ac2b --- /dev/null +++ b/lib/core/engine/command/chromeTrace.js @@ -0,0 +1,63 @@ +import intel from 'intel'; + +import { getRenderBlocking } from '../../../chrome/webdriver/traceUtilities.js'; +import { parse } from '../../../chrome/traceCategoriesParser.js'; +import { parseCPUTrace } from '../../../chrome/parseCpuTrace.js'; +const log = intel.getLogger('browsertime.command.chrometrace'); +export class ChromeTrace { + constructor(engineDelegate, index, options, result) { + this.engineDelegate = engineDelegate; + this.options = options; + this.result = result; + this.index = index; + } + + async start() { + if (this.options.browser === 'chrome') { + if (this.options.chrome.timelineRecordingType === 'custom') { + return this.engineDelegate.getCDPClient().startTrace(); + } else { + log.info( + 'You need to set traceRecordingType to custom to turn on the profiler in scripting' + ); + } + } else { + throw new Error('Trace only works in Chrome'); + } + } + + async stop() { + if (this.options.browser === 'chrome') { + if (this.options.chrome.timelineRecordingType === 'custom') { + let result = this.result[0]; + + this.events = []; + this.events = await this.engineDelegate.getCDPClient().stopTrace(); + const trace = parse(this.events, result.url); + const name = this.options.enableProfileRun + ? `trace-${this.index}-extra-run.json` + : `trace-${this.index}.json`; + result.extraJson[name] = trace; + + const cpu = await parseCPUTrace(trace, result.url); + result.cpu = cpu; + + // Collect render blocking info + const render = await getRenderBlocking(trace); + + result.renderBlocking = render.renderBlocking; + + if (!this.options.skipHar) { + for (let harRequest of this.hars[this.index - 1].log.entries) { + if (render.renderBlockingInfo[harRequest.request.url]) { + harRequest._renderBlocking = + render.renderBlockingInfo[harRequest.request.url]; + } + } + } + } + } else { + throw new Error('Trace only works in Chrome'); + } + } +} diff --git a/lib/core/engine/iteration.js b/lib/core/engine/iteration.js index a953f77f6..72dc50222 100644 --- a/lib/core/engine/iteration.js +++ b/lib/core/engine/iteration.js @@ -24,6 +24,7 @@ import { Select } from './command/select.js'; import { Debug } from './command/debug.js'; import { AndroidCommand } from './command/android.js'; import { ChromeDevelopmentToolsProtocol } from './command/chromeDevToolsProtocol.js'; +import { ChromeTrace } from './command/chromeTrace.js'; import { addConnectivity, removeConnectivity @@ -164,10 +165,12 @@ export class Iteration { engineDelegate, options.browser ); + const trace = new ChromeTrace(engineDelegate, index, options, result); const android = new Android(options); const debug = new Debug(browser, options); const commands = { profiler: profiler, + trace: trace, click: new Click(browser, this.pageCompleteCheck), scroll: new Scroll(browser, options), addText: new AddText(browser), diff --git a/lib/support/cli.js b/lib/support/cli.js index c1b88f4b3..477b8c78a 100644 --- a/lib/support/cli.js +++ b/lib/support/cli.js @@ -281,6 +281,14 @@ export function parseCommandLine() { type: 'boolean', group: 'chrome' }) + .option('chrome.timelineRecordingType', { + alias: 'chrome.traceRecordingType', + describe: 'Expose the start/stop commands for the chrome trace', + default: 'pageload', + choices: ['pageload', 'custom'], + type: 'string', + group: 'chrome' + }) .option('chrome.collectPerfLog', { type: 'boolean', describe: diff --git a/test/commandtests/chromeTest.js b/test/commandtests/chromeTest.js index b3094ac40..0b1f8d97b 100644 --- a/test/commandtests/chromeTest.js +++ b/test/commandtests/chromeTest.js @@ -25,7 +25,12 @@ after.always('Stop the HTTP server', () => { serial.beforeEach('Start the browser', async t => { t.timeout(timeout); - engine = getEngine({ browser: 'chrome' }); + engine = getEngine({ + browser: 'chrome', + chrome: { + timelineRecordingType: 'custom' + } + }); return engine.start(); }); diff --git a/test/data/commandscripts/chrome.cjs b/test/data/commandscripts/chrome.cjs index 2d701695a..0633e935b 100644 --- a/test/data/commandscripts/chrome.cjs +++ b/test/data/commandscripts/chrome.cjs @@ -5,5 +5,10 @@ module.exports = async function (context, commands) { }); await commands.measure.start('http://127.0.0.1:3000/simple/'); await commands.cdp.send('Network.clearBrowserCookies'); + + await commands.chromeTrace.start(); + await commands.measure.start('http://127.0.0.1:3000/dimple/'); + await commands.chromeTrace.stop(); + return commands.cdp.sendAndGet('Memory.getDOMCounters'); };