From 922d9ce1fbb2c0d78f5b214370da4457b7103b94 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 27 Apr 2021 11:07:07 -0700 Subject: [PATCH] chore(tracing): fix some of the start/stop scenarios (#6337) --- src/server/browserContext.ts | 8 +- src/server/frames.ts | 8 +- src/server/snapshot/inMemorySnapshotter.ts | 6 +- src/server/snapshot/snapshotter.ts | 100 ++++++++++-------- src/server/snapshot/snapshotterInjected.ts | 52 ++++----- src/server/trace/recorder/traceSnapshotter.ts | 2 +- .../trace/recorder/{tracer.ts => tracing.ts} | 36 ++++--- src/web/traceViewer/ui/filmStrip.tsx | 2 +- tests/snapshotter.spec.ts | 61 +++-------- utils/check_deps.js | 2 +- 10 files changed, 133 insertions(+), 144 deletions(-) rename src/server/trace/recorder/{tracer.ts => tracing.ts} (90%) diff --git a/src/server/browserContext.ts b/src/server/browserContext.ts index e3ee7193ddc8b..97e1b579a39f2 100644 --- a/src/server/browserContext.ts +++ b/src/server/browserContext.ts @@ -29,7 +29,7 @@ import * as types from './types'; import path from 'path'; import { CallMetadata, internalCallMetadata, createInstrumentation, SdkObject } from './instrumentation'; import { Debugger } from './supplements/debugger'; -import { Tracer } from './trace/recorder/tracer'; +import { Tracing } from './trace/recorder/tracing'; import { HarTracer } from './supplements/har/harTracer'; import { RecorderSupplement } from './supplements/recorderSupplement'; import * as consoleApiSource from '../generated/consoleApiSource'; @@ -57,7 +57,7 @@ export abstract class BrowserContext extends SdkObject { private _selectors?: Selectors; private _origins = new Set(); private _harTracer: HarTracer | undefined; - readonly tracing: Tracer; + readonly tracing: Tracing; constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) { super(browser, 'browser-context'); @@ -70,7 +70,7 @@ export abstract class BrowserContext extends SdkObject { if (this._options.recordHar) this._harTracer = new HarTracer(this, this._options.recordHar); - this.tracing = new Tracer(this); + this.tracing = new Tracing(this); } _setSelectors(selectors: Selectors) { @@ -264,7 +264,7 @@ export abstract class BrowserContext extends SdkObject { this._closedStatus = 'closing'; await this._harTracer?.flush(); - await this.tracing.stop(); + await this.tracing.dispose(); // Cleanup. const promises: Promise[] = []; diff --git a/src/server/frames.ts b/src/server/frames.ts index f5becb57c7d5b..776ddab4c96b6 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -71,6 +71,7 @@ export class FrameManager { readonly _consoleMessageTags = new Map(); readonly _signalBarriers = new Set(); private _webSockets = new Map(); + readonly _responses: network.Response[] = []; constructor(page: Page) { this._page = page; @@ -198,6 +199,7 @@ export class FrameManager { frame._onClearLifecycle(); const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument }; frame.emit(Frame.Events.Navigation, navigationEvent); + this._responses.length = 0; if (!initial) { debugLogger.log('api', ` navigated to "${url}"`); this._page.frameNavigatedToNewDocument(frame); @@ -264,8 +266,10 @@ export class FrameManager { } requestReceivedResponse(response: network.Response) { - if (!response.request()._isFavicon) - this._page.emit(Page.Events.Response, response); + if (response.request()._isFavicon) + return; + this._responses.push(response); + this._page.emit(Page.Events.Response, response); } requestFinished(request: network.Request) { diff --git a/src/server/snapshot/inMemorySnapshotter.ts b/src/server/snapshot/inMemorySnapshotter.ts index f7aa1b81c0ee9..f0842aef770fd 100644 --- a/src/server/snapshot/inMemorySnapshotter.ts +++ b/src/server/snapshot/inMemorySnapshotter.ts @@ -38,7 +38,7 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot } async initialize(): Promise { - await this._snapshotter.initialize(); + await this._snapshotter.start(); return await this._server.start(); } @@ -62,10 +62,6 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot }); } - async setAutoSnapshotIntervalForTest(interval: number): Promise { - await this._snapshotter.setAutoSnapshotInterval(interval); - } - onBlob(blob: SnapshotterBlob): void { this._blobs.set(blob.sha1, blob.buffer); } diff --git a/src/server/snapshot/snapshotter.ts b/src/server/snapshot/snapshotter.ts index 846543fb999c7..15ef05cc15f4d 100644 --- a/src/server/snapshot/snapshotter.ts +++ b/src/server/snapshot/snapshotter.ts @@ -40,24 +40,46 @@ export class Snapshotter { private _context: BrowserContext; private _delegate: SnapshotterDelegate; private _eventListeners: RegisteredListener[] = []; - private _interval = 0; private _snapshotStreamer: string; private _snapshotBinding: string; + private _initialized = false; + private _started = false; + private _fetchedResponses = new Map(); constructor(context: BrowserContext, delegate: SnapshotterDelegate) { this._context = context; this._delegate = delegate; - for (const page of context.pages()) - this._onPage(page); - this._eventListeners = [ - helper.addEventListener(this._context, BrowserContext.Events.Page, this._onPage.bind(this)), - ]; const guid = createGuid(); this._snapshotStreamer = '__playwright_snapshot_streamer_' + guid; this._snapshotBinding = '__playwright_snapshot_binding_' + guid; } - async initialize() { + async start() { + this._started = true; + if (!this._initialized) { + this._initialized = true; + await this._initialize(); + } + this._runInAllFrames(`window["${this._snapshotStreamer}"].reset()`); + + // Replay resources loaded in all pages. + for (const page of this._context.pages()) { + for (const response of page._frameManager._responses) + this._saveResource(page, response).catch(e => debugLogger.log('error', e)); + } + } + + async stop() { + this._started = false; + } + + async _initialize() { + for (const page of this._context.pages()) + this._onPage(page); + this._eventListeners = [ + helper.addEventListener(this._context, BrowserContext.Events.Page, this._onPage.bind(this)), + ]; + await this._context.exposeBinding(this._snapshotBinding, false, (source, data: SnapshotData) => { const snapshot: FrameSnapshot = { snapshotName: data.snapshotName, @@ -87,11 +109,15 @@ export class Snapshotter { }); const initScript = `(${frameSnapshotStreamer})("${this._snapshotStreamer}", "${this._snapshotBinding}")`; await this._context._doAddInitScript(initScript); + this._runInAllFrames(initScript); + } + + private _runInAllFrames(expression: string) { const frames = []; for (const page of this._context.pages()) frames.push(...page.frames()); frames.map(frame => { - frame._existingMainContext()?.rawEvaluate(initScript).catch(debugExceptionHandler); + frame._existingMainContext()?.rawEvaluate(expression).catch(debugExceptionHandler); }); } @@ -112,37 +138,20 @@ export class Snapshotter { page.frames().map(frame => snapshotFrame(frame)); } - async setAutoSnapshotInterval(interval: number): Promise { - this._interval = interval; - const frames = []; - for (const page of this._context.pages()) - frames.push(...page.frames()); - await Promise.all(frames.map(frame => this._setIntervalInFrame(frame, interval))); - } - private _onPage(page: Page) { - const processNewFrame = (frame: Frame) => { - this._annotateFrameHierarchy(frame); - this._setIntervalInFrame(frame, this._interval); - const initScript = `(${frameSnapshotStreamer})("${this._snapshotStreamer}", "${this._snapshotBinding}")`; - frame._existingMainContext()?.rawEvaluate(initScript).catch(debugExceptionHandler); - }; + // Annotate frame hierarchy so that snapshots could include frame ids. for (const frame of page.frames()) - processNewFrame(frame); - this._eventListeners.push(helper.addEventListener(page, Page.Events.FrameAttached, processNewFrame)); - - // Push streamer interval on navigation. - this._eventListeners.push(helper.addEventListener(page, Page.Events.InternalFrameNavigatedToNewDocument, frame => { - this._setIntervalInFrame(frame, this._interval); - })); + this._annotateFrameHierarchy(frame); + this._eventListeners.push(helper.addEventListener(page, Page.Events.FrameAttached, frame => this._annotateFrameHierarchy(frame))); - // Capture resources. this._eventListeners.push(helper.addEventListener(page, Page.Events.Response, (response: network.Response) => { this._saveResource(page, response).catch(e => debugLogger.log('error', e)); })); } private async _saveResource(page: Page, response: network.Response) { + if (!this._started) + return; const isRedirect = response.status() >= 300 && response.status() <= 399; if (isRedirect) return; @@ -163,9 +172,25 @@ export class Snapshotter { const status = response.status(); const requestBody = original.postDataBuffer(); const requestSha1 = requestBody ? calculateSha1(requestBody) : ''; + if (requestBody) + this._delegate.onBlob({ sha1: requestSha1, buffer: requestBody }); const requestHeaders = original.headers(); - const body = await response.body().catch(e => debugLogger.log('error', e)); - const responseSha1 = body ? calculateSha1(body) : ''; + + // Only fetch response bodies once. + let responseSha1 = this._fetchedResponses.get(response); + { + if (responseSha1 === undefined) { + const body = await response.body().catch(e => debugLogger.log('error', e)); + // Bail out after each async hop. + if (!this._started) + return; + responseSha1 = body ? calculateSha1(body) : ''; + if (body) + this._delegate.onBlob({ sha1: responseSha1, buffer: body }); + this._fetchedResponses.set(response, responseSha1); + } + } + const resource: ResourceSnapshot = { pageId: page.guid, frameId: response.frame().guid, @@ -181,17 +206,6 @@ export class Snapshotter { timestamp: monotonicTime() }; this._delegate.onResourceSnapshot(resource); - if (requestBody) - this._delegate.onBlob({ sha1: requestSha1, buffer: requestBody }); - if (body) - this._delegate.onBlob({ sha1: responseSha1, buffer: body }); - } - - private async _setIntervalInFrame(frame: Frame, interval: number) { - const context = frame._existingMainContext(); - await context?.evaluate(({ snapshotStreamer, interval }) => { - (window as any)[snapshotStreamer].setSnapshotInterval(interval); - }, { snapshotStreamer: this._snapshotStreamer, interval }).catch(debugExceptionHandler); } private async _annotateFrameHierarchy(frame: Frame) { diff --git a/src/server/snapshot/snapshotterInjected.ts b/src/server/snapshot/snapshotterInjected.ts index 268c370ba303e..490d6f29cc72b 100644 --- a/src/server/snapshot/snapshotterInjected.ts +++ b/src/server/snapshot/snapshotterInjected.ts @@ -51,6 +51,11 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding: cssText?: string, // Text for stylesheets. cssRef?: number, // Previous snapshotNumber for overridden stylesheets. }; + + function resetCachedData(obj: any) { + delete obj[kCachedData]; + } + function ensureCachedData(obj: any): CachedData { if (!obj[kCachedData]) obj[kCachedData] = {}; @@ -69,14 +74,11 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding: class Streamer { private _removeNoScript = true; - private _timer: NodeJS.Timeout | undefined; private _lastSnapshotNumber = 0; private _staleStyleSheets = new Set(); - private _allStyleSheetsWithUrlOverride = new Set(); private _readingStyleSheet = false; // To avoid invalidating due to our own reads. private _fakeBase: HTMLBaseElement; private _observer: MutationObserver; - private _interval = 0; constructor() { this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'insertRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet)); @@ -125,8 +127,6 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding: if (this._readingStyleSheet) return; this._staleStyleSheets.add(sheet); - if (sheet.href !== null) - this._allStyleSheetsWithUrlOverride.add(sheet); } private _updateStyleElementStyleSheetTextIfNeeded(sheet: CSSStyleSheet): string | undefined { @@ -162,29 +162,29 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding: (iframeElement as any)[kSnapshotFrameId] = frameId; } - captureSnapshot(snapshotName?: string) { - this._streamSnapshot(snapshotName); - } + reset() { + this._staleStyleSheets.clear(); - setSnapshotInterval(interval: number) { - this._interval = interval; - if (interval) - this._streamSnapshot(); + const visitNode = (node: Node | ShadowRoot) => { + resetCachedData(node); + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as Element; + if (element.shadowRoot) + visitNode(element.shadowRoot); + } + for (let child = node.firstChild; child; child = child.nextSibling) + visitNode(child); + }; + visitNode(document.documentElement); } - private _streamSnapshot(snapshotName?: string) { - if (this._timer) { - clearTimeout(this._timer); - this._timer = undefined; - } + captureSnapshot(snapshotName: string) { try { const snapshot = this._captureSnapshot(snapshotName); if (snapshot) (window as any)[snapshotBinding](snapshot); } catch (e) { } - if (this._interval) - this._timer = setTimeout(() => this._streamSnapshot(), this._interval); } private _sanitizeUrl(url: string): string { @@ -298,11 +298,11 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding: const result: NodeSnapshot = [nodeName, attrs]; const visitChild = (child: Node) => { - const snapshotted = visitNode(child); - if (snapshotted) { - result.push(snapshotted.n); + const snapshot = visitNode(child); + if (snapshot) { + result.push(snapshot.n); expectValue(child); - equals = equals && snapshotted.equals; + equals = equals && snapshot.equals; } }; @@ -432,10 +432,12 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding: }; let allOverridesAreRefs = true; - for (const sheet of this._allStyleSheetsWithUrlOverride) { + for (const sheet of this._staleStyleSheets) { + if (sheet.href === null) + continue; const content = this._updateLinkStyleSheetTextIfNeeded(sheet, snapshotNumber); if (content === undefined) { - // Unable to capture stylsheet contents. + // Unable to capture stylesheet contents. continue; } if (typeof content !== 'number') diff --git a/src/server/trace/recorder/traceSnapshotter.ts b/src/server/trace/recorder/traceSnapshotter.ts index 3f21e4ebfb0ce..d50b6a40c8ace 100644 --- a/src/server/trace/recorder/traceSnapshotter.ts +++ b/src/server/trace/recorder/traceSnapshotter.ts @@ -43,7 +43,7 @@ export class TraceSnapshotter extends EventEmitter implements SnapshotterDelegat } async start(): Promise { - await this._snapshotter.initialize(); + await this._snapshotter.start(); } async dispose() { diff --git a/src/server/trace/recorder/tracer.ts b/src/server/trace/recorder/tracing.ts similarity index 90% rename from src/server/trace/recorder/tracer.ts rename to src/server/trace/recorder/tracing.ts index e1556709a6cd6..748dfd0ef192a 100644 --- a/src/server/trace/recorder/tracer.ts +++ b/src/server/trace/recorder/tracing.ts @@ -18,7 +18,7 @@ import fs from 'fs'; import path from 'path'; import util from 'util'; import yazl from 'yazl'; -import { createGuid, mkdirIfNeeded, monotonicTime } from '../../../utils/utils'; +import { calculateSha1, createGuid, mkdirIfNeeded, monotonicTime } from '../../../utils/utils'; import { Artifact } from '../../artifact'; import { BrowserContext } from '../../browserContext'; import { Dialog } from '../../dialog'; @@ -40,14 +40,14 @@ export type TracerOptions = { screenshots?: boolean; }; -export class Tracer implements InstrumentationListener { +export class Tracing implements InstrumentationListener { private _appendEventChain = Promise.resolve(); - private _snapshotter: TraceSnapshotter | undefined; + private _snapshotter: TraceSnapshotter; private _eventListeners: RegisteredListener[] = []; private _pendingCalls = new Map(); private _context: BrowserContext; private _traceFile: string | undefined; - private _resourcesDir: string | undefined; + private _resourcesDir: string; private _sha1s: string[] = []; private _started = false; private _traceDir: string | undefined; @@ -55,19 +55,18 @@ export class Tracer implements InstrumentationListener { constructor(context: BrowserContext) { this._context = context; this._traceDir = context._browser.options.traceDir; + this._resourcesDir = path.join(this._traceDir || '', 'resources'); + this._snapshotter = new TraceSnapshotter(this._context, this._resourcesDir, traceEvent => this._appendTraceEvent(traceEvent)); } async start(options: TracerOptions): Promise { + // context + page must be the first events added, this method can't have awaits before them. if (!this._traceDir) throw new Error('Tracing directory is not specified when launching the browser'); if (this._started) throw new Error('Tracing has already been started'); this._started = true; this._traceFile = path.join(this._traceDir, (options.name || createGuid()) + '.trace'); - if (options.screenshots || options.snapshots) { - this._resourcesDir = path.join(this._traceDir, 'resources'); - await fsMkdirAsync(this._resourcesDir, { recursive: true }); - } this._appendEventChain = mkdirIfNeeded(this._traceFile); const event: trace.ContextCreatedTraceEvent = { @@ -80,13 +79,18 @@ export class Tracer implements InstrumentationListener { debugName: this._context._options._debugName, }; this._appendTraceEvent(event); + for (const page of this._context.pages()) + this._onPage(options.screenshots, page); this._eventListeners.push( helper.addEventListener(this._context, BrowserContext.Events.Page, this._onPage.bind(this, options.screenshots)), ); + + // context + page must be the first events added, no awaits above this line. + await fsMkdirAsync(this._resourcesDir, { recursive: true }); + this._context.instrumentation.addListener(this); if (options.snapshots) - this._snapshotter = new TraceSnapshotter(this._context, this._resourcesDir!, traceEvent => this._appendTraceEvent(traceEvent)); - await this._snapshotter?.start(); + await this._snapshotter.start(); } async stop(): Promise { @@ -95,8 +99,6 @@ export class Tracer implements InstrumentationListener { this._started = false; this._context.instrumentation.removeListener(this); helper.removeEventListeners(this._eventListeners); - await this._snapshotter?.dispose(); - this._snapshotter = undefined; for (const { sdkObject, metadata } of this._pendingCalls.values()) this.onAfterCall(sdkObject, metadata); for (const page of this._context.pages()) @@ -106,6 +108,10 @@ export class Tracer implements InstrumentationListener { await this._appendEventChain; } + async dispose() { + await this._snapshotter.dispose(); + } + async export(): Promise { if (!this._traceFile) throw new Error('Tracing directory is not specified when launching the browser'); @@ -228,11 +234,11 @@ export class Tracer implements InstrumentationListener { }), helper.addEventListener(page, Page.Events.ScreencastFrame, params => { - const guid = createGuid(); + const sha1 = calculateSha1(createGuid()); // no need to compute sha1 for screenshots const event: trace.ScreencastFrameTraceEvent = { type: 'screencast-frame', pageId: page.guid, - sha1: guid, // no need to compute sha1 for screenshots + sha1, pageTimestamp: params.timestamp, width: params.width, height: params.height, @@ -240,7 +246,7 @@ export class Tracer implements InstrumentationListener { }; this._appendTraceEvent(event); this._appendEventChain = this._appendEventChain.then(async () => { - await fsWriteFileAsync(path.join(this._resourcesDir!, guid), params.buffer).catch(() => {}); + await fsWriteFileAsync(path.join(this._resourcesDir!, sha1), params.buffer).catch(() => {}); }); }), diff --git a/src/web/traceViewer/ui/filmStrip.tsx b/src/web/traceViewer/ui/filmStrip.tsx index 523539f4d6ff5..7d721ad0445f0 100644 --- a/src/web/traceViewer/ui/filmStrip.tsx +++ b/src/web/traceViewer/ui/filmStrip.tsx @@ -83,7 +83,7 @@ const FilmStripLane: React.FunctionComponent<{ const frames: JSX.Element[] = []; let i = 0; - for (let time = startTime; time <= endTime; time += frameDuration, ++i) { + for (let time = startTime; startTime && frameDuration && time <= endTime; time += frameDuration, ++i) { const index = upperBound(screencastFrames, time, timeComparator) - 1; frames.push(
{ expect(snapshots.length).toBe(2); }); - it('should only collect on change', async ({ page }) => { - await page.setContent(''); - const snapshots = []; - snapshotter.on('snapshot', snapshot => snapshots.push(snapshot)); - await Promise.all([ - new Promise(f => snapshotter.once('snapshot', f)), - snapshotter.setAutoSnapshotIntervalForTest(25), - ]); - await Promise.all([ - new Promise(f => snapshotter.once('snapshot', f)), - page.setContent('') - ]); - expect(snapshots.length).toBe(2); - }); - - it('should respect inline CSSOM change', async ({ page }) => { + it('should respect inline CSSOM change', async ({ page, toImpl }) => { await page.setContent(''); - const snapshots = []; - snapshotter.on('snapshot', snapshot => snapshots.push(snapshot)); - await Promise.all([ - new Promise(f => snapshotter.once('snapshot', f)), - snapshotter.setAutoSnapshotIntervalForTest(25), - ]); - expect(distillSnapshot(snapshots[0])).toBe(''); - - await Promise.all([ - new Promise(f => snapshotter.once('snapshot', f)), - page.evaluate(() => { - (document.styleSheets[0].cssRules[0] as any).style.color = 'blue'; - }) - ]); - expect(distillSnapshot(snapshots[1])).toBe(''); + const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1'); + expect(distillSnapshot(snapshot1)).toBe(''); + + await page.evaluate(() => { (document.styleSheets[0].cssRules[0] as any).style.color = 'blue'; }); + const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot2'); + expect(distillSnapshot(snapshot2)).toBe(''); }); - it('should respect subresource CSSOM change', async ({ page, server }) => { + it('should respect subresource CSSOM change', async ({ page, server, toImpl }) => { await page.goto(server.EMPTY_PAGE); await page.route('**/style.css', route => { route.fulfill({ body: 'button { color: red; }', }).catch(() => {}); }); await page.setContent(''); - const snapshots = []; - snapshotter.on('snapshot', snapshot => snapshots.push(snapshot)); - await Promise.all([ - new Promise(f => snapshotter.once('snapshot', f)), - snapshotter.setAutoSnapshotIntervalForTest(25), - ]); - expect(distillSnapshot(snapshots[0])).toBe(''); - - await Promise.all([ - new Promise(f => snapshotter.once('snapshot', f)), - page.evaluate(() => { - (document.styleSheets[0].cssRules[0] as any).style.color = 'blue'; - }) - ]); - const { resources } = snapshots[1].render(); + const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1'); + expect(distillSnapshot(snapshot1)).toBe(''); + + await page.evaluate(() => { (document.styleSheets[0].cssRules[0] as any).style.color = 'blue'; }); + const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1'); + const { resources } = snapshot2.render(); const cssHref = `http://localhost:${server.PORT}/style.css`; const { sha1 } = resources[cssHref]; expect(snapshotter.resourceContent(sha1).toString()).toBe('button { color: blue; }'); diff --git a/utils/check_deps.js b/utils/check_deps.js index cd37769d72ef8..53fb915b0b6a0 100644 --- a/utils/check_deps.js +++ b/utils/check_deps.js @@ -141,7 +141,7 @@ DEPS['src/server/android/'] = [...DEPS['src/server/'], 'src/server/chromium/', ' DEPS['src/server/electron/'] = [...DEPS['src/server/'], 'src/server/chromium/']; DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/', 'src/server/android/', 'src/server/electron/']; -DEPS['src/server/browserContext.ts'] = [...DEPS['src/server/'], 'src/server/trace/recorder/tracer.ts']; +DEPS['src/server/browserContext.ts'] = [...DEPS['src/server/'], 'src/server/trace/recorder/tracing.ts']; DEPS['src/cli/driver.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerImpl.ts'] = ['src/**']; // Tracing is a client/server plugin, nothing should depend on it.