diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 774cd24163..2639aff91d 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -45,7 +45,7 @@ import type { ConsoleMessage } from '../../console'; import { Dispatcher } from '../../dispatchers/dispatcher'; import { serializeError } from '../../errors'; -const version: trace.VERSION = 6; +const version: trace.VERSION = 7; export type TracerOptions = { name?: string; @@ -100,10 +100,12 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this._contextCreatedEvent = { version, type: 'context-options', + origin: 'library', browserName: '', options: {}, platform: process.platform, wallTime: 0, + monotonicTime: 0, sdkLanguage: context.attribution.playwright.options.sdkLanguage, testIdAttributeName }; @@ -177,7 +179,12 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this._allocateNewTraceFile(this._state); this._fs.mkdir(path.dirname(this._state.traceFile)); - const event: trace.TraceEvent = { ...this._contextCreatedEvent, title: options.title, wallTime: Date.now() }; + const event: trace.TraceEvent = { + ...this._contextCreatedEvent, + title: options.title, + wallTime: Date.now(), + monotonicTime: monotonicTime() + }; this._fs.appendFile(this._state.traceFile, JSON.stringify(event) + '\n'); this._context.instrumentation.addListener(this, this._context); diff --git a/packages/playwright/src/worker/testTracing.ts b/packages/playwright/src/worker/testTracing.ts index 82c84000e7..5462564c49 100644 --- a/packages/playwright/src/worker/testTracing.ts +++ b/packages/playwright/src/worker/testTracing.ts @@ -28,6 +28,7 @@ import type { TestInfoImpl } from './testInfo'; export type Attachment = TestInfo['attachments'][0]; export const testTraceEntryName = 'test.trace'; +const version: trace.VERSION = 7; let traceOrdinal = 0; type TraceFixtureValue = PlaywrightWorkerOptions['trace'] | undefined; @@ -41,11 +42,24 @@ export class TestTracing { private _temporaryTraceFiles: string[] = []; private _artifactsDir: string; private _tracesDir: string; + private _contextCreatedEvent: trace.ContextCreatedTraceEvent; constructor(testInfo: TestInfoImpl, artifactsDir: string) { this._testInfo = testInfo; this._artifactsDir = artifactsDir; this._tracesDir = path.join(this._artifactsDir, 'traces'); + this._contextCreatedEvent = { + version, + type: 'context-options', + origin: 'testRunner', + browserName: '', + options: {}, + platform: process.platform, + wallTime: Date.now(), + monotonicTime: monotonicTime(), + sdkLanguage: 'javascript', + }; + this._appendTraceEvent(this._contextCreatedEvent); } private _shouldCaptureTrace() { diff --git a/packages/trace-viewer/src/entries.ts b/packages/trace-viewer/src/entries.ts index 543e36ec49..a53fd0314b 100644 --- a/packages/trace-viewer/src/entries.ts +++ b/packages/trace-viewer/src/entries.ts @@ -26,7 +26,7 @@ export type ContextEntry = { browserName: string; channel?: string; platform?: string; - wallTime?: number; + wallTime: number; sdkLanguage?: Language; testIdAttributeName?: string; title?: string; @@ -58,6 +58,7 @@ export function createEmptyContext(): ContextEntry { origin: 'testRunner', traceUrl: '', startTime: Number.MAX_SAFE_INTEGER, + wallTime: Number.MAX_SAFE_INTEGER, endTime: 0, browserName: '', options: { diff --git a/packages/trace-viewer/src/traceModernizer.ts b/packages/trace-viewer/src/traceModernizer.ts index 3ba3605973..a2a987318c 100644 --- a/packages/trace-viewer/src/traceModernizer.ts +++ b/packages/trace-viewer/src/traceModernizer.ts @@ -18,6 +18,7 @@ import type * as trace from '@trace/trace'; import type * as traceV3 from './versions/traceV3'; import type * as traceV4 from './versions/traceV4'; import type * as traceV5 from './versions/traceV5'; +import type * as traceV6 from './versions/traceV6'; import type { ActionEntry, ContextEntry, PageEntry } from './entries'; import type { SnapshotStorage } from './snapshotStorage'; @@ -71,12 +72,13 @@ export class TraceModernizer { switch (event.type) { case 'context-options': { this._version = event.version; - contextEntry.origin = 'library'; + contextEntry.origin = event.origin; contextEntry.browserName = event.browserName; contextEntry.channel = event.channel; contextEntry.title = event.title; contextEntry.platform = event.platform; contextEntry.wallTime = event.wallTime; + contextEntry.startTime = event.monotonicTime; contextEntry.sdkLanguage = event.sdkLanguage; contextEntry.options = event.options; contextEntry.testIdAttributeName = event.testIdAttributeName; @@ -145,11 +147,11 @@ export class TraceModernizer { break; } case 'resource-snapshot': - this._snapshotStorage!.addResource(event.snapshot); + this._snapshotStorage.addResource(event.snapshot); contextEntry.resources.push(event.snapshot); break; case 'frame-snapshot': - this._snapshotStorage!.addFrameSnapshot(event.snapshot); + this._snapshotStorage.addFrameSnapshot(event.snapshot); break; } // Make sure there is a page entry for each page, even without screencast frames, @@ -170,12 +172,18 @@ export class TraceModernizer { } } + private _processedContextCreatedEvent() { + return this._version !== undefined; + } + private _modernize(event: any): trace.TraceEvent[] { - if (this._version === undefined) + // In trace 6->7 we also need to modernize context-options event. + let version = this._version || event.version; + if (version === undefined) return [event]; - const lastVersion: trace.VERSION = 6; + const lastVersion: trace.VERSION = 7; let events = [event]; - for (let version = this._version; version < lastVersion; ++version) + for (; version < lastVersion; ++version) events = (this as any)[`_modernize_${version}_to_${version + 1}`].call(this, events); return events; } @@ -341,8 +349,8 @@ export class TraceModernizer { return event; } - _modernize_5_to_6(events: traceV5.TraceEvent[]): trace.TraceEvent[] { - const result: trace.TraceEvent[] = []; + _modernize_5_to_6(events: traceV5.TraceEvent[]): traceV6.TraceEvent[] { + const result: traceV6.TraceEvent[] = []; for (const event of events) { result.push(event); if (event.type !== 'after' || !event.log.length) @@ -358,4 +366,35 @@ export class TraceModernizer { } return result; } + + _modernize_6_to_7(events: traceV6.TraceEvent[]): trace.TraceEvent[] { + const result: trace.TraceEvent[] = []; + if (!this._processedContextCreatedEvent() && events[0].type !== 'context-options') { + const event: trace.ContextCreatedTraceEvent = { + type: 'context-options', + origin: 'testRunner', + version: 7, + browserName: '', + options: {}, + platform: process.platform, + wallTime: 0, + monotonicTime: 0, + sdkLanguage: 'javascript', + }; + result.push(event); + } + for (const event of events) { + if (event.type === 'context-options') { + result.push({ ...event, monotonicTime: 0, origin: 'library' }); + continue; + } + // Take wall and monotonic time from the first event. + if (!this._contextEntry.wallTime && event.type === 'before') + this._contextEntry.wallTime = event.wallTime; + if (!this._contextEntry.startTime && event.type === 'before') + this._contextEntry.startTime = event.startTime; + result.push(event); + } + return result; + } } diff --git a/packages/trace-viewer/src/versions/traceV6.ts b/packages/trace-viewer/src/versions/traceV6.ts new file mode 100644 index 0000000000..2bbbd447b3 --- /dev/null +++ b/packages/trace-viewer/src/versions/traceV6.ts @@ -0,0 +1,239 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Entry as ResourceSnapshot } from '../../../trace/src/har'; + +type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl'; +type Point = { x: number, y: number }; +type Size = { width: number, height: number }; + +type StackFrame = { + file: string, + line: number, + column: number, + function?: string, +}; + +type SerializedValue = { + n?: number, + b?: boolean, + s?: string, + v?: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0', + d?: string, + u?: string, + bi?: string, + m?: SerializedValue, + se?: SerializedValue, + r?: { + p: string, + f: string, + }, + a?: SerializedValue[], + o?: { + k: string, + v: SerializedValue, + }[], + h?: number, + id?: number, + ref?: number, +}; + +type SerializedError = { + error?: { + message: string, + name: string, + stack?: string, + }, + value?: SerializedValue, +}; + +type NodeSnapshot = + // Text node. + string | + // Subtree reference, "x snapshots ago, node #y". Could point to a text node. + // Only nodes that are not references are counted, starting from zero, using post-order traversal. + [ [number, number] ] | + // Just node name. + [ string ] | + // Node name, attributes, child nodes. + // Unfortunately, we cannot make this type definition recursive, therefore "any". + [ string, { [attr: string]: string }, ...any ]; + + +type ResourceOverride = { + url: string, + sha1?: string, + ref?: number +}; + +type FrameSnapshot = { + snapshotName?: string, + callId: string, + pageId: string, + frameId: string, + frameUrl: string, + timestamp: number, + collectionTime: number, + doctype?: string, + html: NodeSnapshot, + resourceOverrides: ResourceOverride[], + viewport: { width: number, height: number }, + isMainFrame: boolean, +}; + +export type BrowserContextEventOptions = { + viewport?: Size, + deviceScaleFactor?: number, + isMobile?: boolean, + userAgent?: string, +}; + +export type ContextCreatedTraceEvent = { + version: number, + type: 'context-options', + browserName: string, + channel?: string, + platform: string, + wallTime: number, + title?: string, + options: BrowserContextEventOptions, + sdkLanguage?: Language, + testIdAttributeName?: string, +}; + +export type ScreencastFrameTraceEvent = { + type: 'screencast-frame', + pageId: string, + sha1: string, + width: number, + height: number, + timestamp: number, +}; + +export type BeforeActionTraceEvent = { + type: 'before', + callId: string; + startTime: number; + apiName: string; + class: string; + method: string; + params: Record; + wallTime: number; + beforeSnapshot?: string; + stack?: StackFrame[]; + pageId?: string; + parentId?: string; +}; + +export type InputActionTraceEvent = { + type: 'input', + callId: string; + inputSnapshot?: string; + point?: Point; +}; + +export type AfterActionTraceEventAttachment = { + name: string; + contentType: string; + path?: string; + sha1?: string; + base64?: string; +}; + +export type AfterActionTraceEvent = { + type: 'after', + callId: string; + endTime: number; + afterSnapshot?: string; + error?: SerializedError['error']; + attachments?: AfterActionTraceEventAttachment[]; + result?: any; + point?: Point; +}; + +export type LogTraceEvent = { + type: 'log', + callId: string; + time: number; + message: string; +}; + +export type EventTraceEvent = { + type: 'event', + time: number; + class: string; + method: string; + params: any; + pageId?: string; +}; + +export type ConsoleMessageTraceEvent = { + type: 'console'; + time: number; + pageId?: string; + messageType: string, + text: string, + args?: { preview: string, value: any }[], + location: { + url: string, + lineNumber: number, + columnNumber: number, + }, +}; + +export type ResourceSnapshotTraceEvent = { + type: 'resource-snapshot', + snapshot: ResourceSnapshot, +}; + +export type FrameSnapshotTraceEvent = { + type: 'frame-snapshot', + snapshot: FrameSnapshot, +}; + +export type ActionTraceEvent = { + type: 'action', +} & Omit + & Omit + & Omit; + +export type StdioTraceEvent = { + type: 'stdout' | 'stderr'; + timestamp: number; + text?: string; + base64?: string; +}; + +export type ErrorTraceEvent = { + type: 'error'; + message: string; + stack?: StackFrame[]; +}; + +export type TraceEvent = + ContextCreatedTraceEvent | + ScreencastFrameTraceEvent | + ActionTraceEvent | + BeforeActionTraceEvent | + InputActionTraceEvent | + AfterActionTraceEvent | + EventTraceEvent | + LogTraceEvent | + ConsoleMessageTraceEvent | + ResourceSnapshotTraceEvent | + FrameSnapshotTraceEvent | + StdioTraceEvent | + ErrorTraceEvent; diff --git a/packages/trace/src/trace.ts b/packages/trace/src/trace.ts index 2f4974aae4..7387f612aa 100644 --- a/packages/trace/src/trace.ts +++ b/packages/trace/src/trace.ts @@ -21,7 +21,7 @@ import type { FrameSnapshot, ResourceSnapshot } from './snapshot'; export type Size = { width: number, height: number }; // Make sure you add _modernize_N_to_N1(event: any) to traceModel.ts. -export type VERSION = 6; +export type VERSION = 7; export type BrowserContextEventOptions = { viewport?: Size, @@ -33,10 +33,12 @@ export type BrowserContextEventOptions = { export type ContextCreatedTraceEvent = { version: number, type: 'context-options', + origin: 'testRunner' | 'library', browserName: string, channel?: string, platform: string, wallTime: number, + monotonicTime: number, title?: string, options: BrowserContextEventOptions, sdkLanguage?: Language,