chore: extract snapshotter from trace viewer (#5618)

This commit is contained in:
Pavel Feldman 2021-02-25 09:33:32 -08:00 committed by GitHub
parent af89ab7a6f
commit 2ff6d54f26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 95 additions and 77 deletions

View File

@ -0,0 +1,41 @@
/**
* 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.
*/
export 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 ];
export type ResourceOverride = {
url: string,
sha1?: string,
ref?: number
};
export type FrameSnapshot = {
doctype?: string,
html: NodeSnapshot,
resourceOverrides: ResourceOverride[],
viewport: { width: number, height: number },
};

View File

@ -14,34 +14,30 @@
* limitations under the License. * limitations under the License.
*/ */
import * as trace from '../common/traceEvents'; import { FrameSnapshot, NodeSnapshot } from './snapshot';
import { ContextResources } from './traceModel';
export * as trace from '../common/traceEvents';
export type SerializedFrameSnapshot = { export type ContextResources = Map<string, { resourceId: string, frameId: string }[]>;
export type RenderedFrameSnapshot = {
html: string; html: string;
resources: { [key: string]: { resourceId: string, sha1?: string } }; resources: { [key: string]: { resourceId: string, sha1?: string } };
}; };
export class FrameSnapshot { export class SnapshotRenderer {
private _snapshots: trace.FrameSnapshotTraceEvent[]; private _snapshots: FrameSnapshot[];
private _index: number; private _index: number;
private _contextResources: ContextResources; private _contextResources: ContextResources;
private _frameId: string; private _frameId: string;
constructor(frameId: string, contextResources: ContextResources, events: trace.FrameSnapshotTraceEvent[], index: number) { constructor(frameId: string, contextResources: ContextResources, snapshots: FrameSnapshot[], index: number) {
this._frameId = frameId; this._frameId = frameId;
this._contextResources = contextResources; this._contextResources = contextResources;
this._snapshots = events; this._snapshots = snapshots;
this._index = index; this._index = index;
} }
traceEvent(): trace.FrameSnapshotTraceEvent { render(): RenderedFrameSnapshot {
return this._snapshots[this._index]; const visit = (n: NodeSnapshot, snapshotIndex: number): string => {
}
serialize(): SerializedFrameSnapshot {
const visit = (n: trace.NodeSnapshot, snapshotIndex: number): string => {
// Text node. // Text node.
if (typeof n === 'string') if (typeof n === 'string')
return escapeText(n); return escapeText(n);
@ -51,7 +47,7 @@ export class FrameSnapshot {
// Node reference. // Node reference.
const referenceIndex = snapshotIndex - n[0][0]; const referenceIndex = snapshotIndex - n[0][0];
if (referenceIndex >= 0 && referenceIndex < snapshotIndex) { if (referenceIndex >= 0 && referenceIndex < snapshotIndex) {
const nodes = snapshotNodes(this._snapshots[referenceIndex].snapshot); const nodes = snapshotNodes(this._snapshots[referenceIndex]);
const nodeIndex = n[0][1]; const nodeIndex = n[0][1];
if (nodeIndex >= 0 && nodeIndex < nodes.length) if (nodeIndex >= 0 && nodeIndex < nodes.length)
(n as any)._string = visit(nodes[nodeIndex], referenceIndex); (n as any)._string = visit(nodes[nodeIndex], referenceIndex);
@ -76,7 +72,7 @@ export class FrameSnapshot {
return (n as any)._string; return (n as any)._string;
}; };
const snapshot = this._snapshots[this._index].snapshot; const snapshot = this._snapshots[this._index];
let html = visit(snapshot.html, this._index); let html = visit(snapshot.html, this._index);
if (snapshot.doctype) if (snapshot.doctype)
html = `<!DOCTYPE ${snapshot.doctype}>` + html; html = `<!DOCTYPE ${snapshot.doctype}>` + html;
@ -88,7 +84,7 @@ export class FrameSnapshot {
if (contextResource) if (contextResource)
resources[url] = { resourceId: contextResource.resourceId }; resources[url] = { resourceId: contextResource.resourceId };
} }
for (const o of this.traceEvent().snapshot.resourceOverrides) { for (const o of snapshot.resourceOverrides) {
const resource = resources[o.url]; const resource = resources[o.url];
resource.sha1 = o.sha1; resource.sha1 = o.sha1;
} }
@ -106,10 +102,10 @@ function escapeText(s: string): string {
return s.replace(/[&<]/ug, char => (escaped as any)[char]); return s.replace(/[&<]/ug, char => (escaped as any)[char]);
} }
function snapshotNodes(snapshot: trace.FrameSnapshot): trace.NodeSnapshot[] { function snapshotNodes(snapshot: FrameSnapshot): NodeSnapshot[] {
if (!(snapshot as any)._nodes) { if (!(snapshot as any)._nodes) {
const nodes: trace.NodeSnapshot[] = []; const nodes: NodeSnapshot[] = [];
const visit = (n: trace.NodeSnapshot) => { const visit = (n: NodeSnapshot) => {
if (typeof n === 'string') { if (typeof n === 'string') {
nodes.push(n); nodes.push(n);
} else if (typeof n[0] === 'string') { } else if (typeof n[0] === 'string') {

View File

@ -16,14 +16,19 @@
import * as http from 'http'; import * as http from 'http';
import querystring from 'querystring'; import querystring from 'querystring';
import type { NetworkResourceTraceEvent } from '../common/traceEvents'; import { SnapshotRenderer, RenderedFrameSnapshot } from './snapshotRenderer';
import type { FrameSnapshot, SerializedFrameSnapshot } from './frameSnapshot'; import { HttpServer } from '../../utils/httpServer';
import { HttpServer } from '../../../utils/httpServer';
export type NetworkResponse = {
contentType: string;
responseHeaders: { name: string, value: string }[];
responseSha1: string;
};
export interface SnapshotStorage { export interface SnapshotStorage {
resourceContent(sha1: string): Buffer; resourceContent(sha1: string): Buffer;
resourceById(resourceId: string): NetworkResourceTraceEvent; resourceById(resourceId: string): NetworkResponse;
snapshotByName(snapshotName: string): FrameSnapshot | undefined; snapshotByName(snapshotName: string): SnapshotRenderer | undefined;
} }
export class SnapshotServer { export class SnapshotServer {
@ -149,7 +154,7 @@ export class SnapshotServer {
} }
if (request.mode === 'navigate') { if (request.mode === 'navigate') {
const htmlResponse = await fetch(`/snapshot-data?snapshotName=${snapshotId}`); const htmlResponse = await fetch(`/snapshot-data?snapshotName=${snapshotId}`);
const { html, resources }: SerializedFrameSnapshot = await htmlResponse.json(); const { html, resources }: RenderedFrameSnapshot = await htmlResponse.json();
if (!html) if (!html)
return respondNotAvailable(); return respondNotAvailable();
snapshotResources.set(snapshotId, resources); snapshotResources.set(snapshotId, resources);
@ -203,7 +208,7 @@ export class SnapshotServer {
response.setHeader('Content-Type', 'application/json'); response.setHeader('Content-Type', 'application/json');
const parsed: any = querystring.parse(request.url!.substring(request.url!.indexOf('?') + 1)); const parsed: any = querystring.parse(request.url!.substring(request.url!.indexOf('?') + 1));
const snapshot = this._snapshotStorage.snapshotByName(parsed.snapshotName); const snapshot = this._snapshotStorage.snapshotByName(parsed.snapshotName);
const snapshotData: any = snapshot ? snapshot.serialize() : { html: '' }; const snapshotData: any = snapshot ? snapshot.render() : { html: '' };
response.end(JSON.stringify(snapshotData)); response.end(JSON.stringify(snapshotData));
return true; return true;
} }

View File

@ -14,15 +14,15 @@
* limitations under the License. * limitations under the License.
*/ */
import { BrowserContext } from '../../browserContext'; import { BrowserContext } from '../browserContext';
import { Page } from '../../page'; import { Page } from '../page';
import * as network from '../../network'; import * as network from '../network';
import { helper, RegisteredListener } from '../../helper'; import { helper, RegisteredListener } from '../helper';
import { debugLogger } from '../../../utils/debugLogger'; import { debugLogger } from '../../utils/debugLogger';
import { Frame } from '../../frames'; import { Frame } from '../frames';
import { SnapshotData, frameSnapshotStreamer, kSnapshotBinding, kSnapshotStreamer } from './snapshotterInjected'; import { SnapshotData, frameSnapshotStreamer, kSnapshotBinding, kSnapshotStreamer } from './snapshotterInjected';
import { calculateSha1 } from '../../../utils/utils'; import { calculateSha1 } from '../../utils/utils';
import { FrameSnapshot } from '../common/traceEvents'; import { FrameSnapshot } from './snapshot';
export type SnapshotterResource = { export type SnapshotterResource = {
pageId: string, pageId: string,

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import type { NodeSnapshot } from '../common/traceEvents'; import { NodeSnapshot } from './snapshot';
export type SnapshotData = { export type SnapshotData = {
doctype?: string, doctype?: string,

View File

@ -15,18 +15,7 @@
*/ */
import { StackFrame } from '../../../common/types'; import { StackFrame } from '../../../common/types';
import { FrameSnapshot } from '../../snapshot/snapshot';
export 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 ];
export type ContextCreatedTraceEvent = { export type ContextCreatedTraceEvent = {
timestamp: number, timestamp: number,
@ -157,16 +146,3 @@ export type TraceEvent =
NavigationEvent | NavigationEvent |
LoadEvent | LoadEvent |
FrameSnapshotTraceEvent; FrameSnapshotTraceEvent;
export type ResourceOverride = {
url: string,
sha1?: string,
ref?: number
};
export type FrameSnapshot = {
doctype?: string,
html: NodeSnapshot,
resourceOverrides: ResourceOverride[],
viewport: { width: number, height: number },
};

View File

@ -15,18 +15,19 @@
*/ */
import { BrowserContext, Video } from '../../browserContext'; import { BrowserContext, Video } from '../../browserContext';
import type { SnapshotterResource as SnapshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter'; import type { SnapshotterResource as SnapshotterResource, SnapshotterBlob, SnapshotterDelegate } from '../../snapshot/snapshotter';
import * as trace from '../common/traceEvents'; import * as trace from '../common/traceEvents';
import path from 'path'; import path from 'path';
import * as util from 'util'; import * as util from 'util';
import fs from 'fs'; import fs from 'fs';
import { createGuid, getFromENV, mkdirIfNeeded, monotonicTime } from '../../../utils/utils'; import { createGuid, getFromENV, mkdirIfNeeded, monotonicTime } from '../../../utils/utils';
import { Page } from '../../page'; import { Page } from '../../page';
import { Snapshotter } from './snapshotter'; import { Snapshotter } from '../../snapshot/snapshotter';
import { helper, RegisteredListener } from '../../helper'; import { helper, RegisteredListener } from '../../helper';
import { Dialog } from '../../dialog'; import { Dialog } from '../../dialog';
import { Frame, NavigationEvent } from '../../frames'; import { Frame, NavigationEvent } from '../../frames';
import { CallMetadata, InstrumentationListener, SdkObject } from '../../instrumentation'; import { CallMetadata, InstrumentationListener, SdkObject } from '../../instrumentation';
import { FrameSnapshot } from '../../snapshot/snapshot';
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs)); const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs));
@ -133,7 +134,7 @@ class ContextTracer implements SnapshotterDelegate {
this._appendTraceEvent(event); this._appendTraceEvent(event);
} }
onFrameSnapshot(frame: Frame, frameUrl: string, snapshot: trace.FrameSnapshot, snapshotId?: string): void { onFrameSnapshot(frame: Frame, frameUrl: string, snapshot: FrameSnapshot, snapshotId?: string): void {
const event: trace.FrameSnapshotTraceEvent = { const event: trace.FrameSnapshotTraceEvent = {
timestamp: monotonicTime(), timestamp: monotonicTime(),
type: 'snapshot', type: 'snapshot',

View File

@ -19,7 +19,7 @@ import path from 'path';
import * as playwright from '../../../..'; import * as playwright from '../../../..';
import * as util from 'util'; import * as util from 'util';
import { ActionEntry, ContextEntry, TraceModel } from './traceModel'; import { ActionEntry, ContextEntry, TraceModel } from './traceModel';
import { SnapshotServer } from './snapshotServer'; import { SnapshotServer } from '../../snapshot/snapshotServer';
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs)); const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));

View File

@ -16,7 +16,7 @@
import { createGuid } from '../../../utils/utils'; import { createGuid } from '../../../utils/utils';
import * as trace from '../common/traceEvents'; import * as trace from '../common/traceEvents';
import { FrameSnapshot } from './frameSnapshot'; import { ContextResources, SnapshotRenderer } from '../../snapshot/snapshotRenderer';
export * as trace from '../common/traceEvents'; export * as trace from '../common/traceEvents';
export class TraceModel { export class TraceModel {
@ -152,16 +152,16 @@ export class TraceModel {
return { contextEntry, pageEntry }; return { contextEntry, pageEntry };
} }
findSnapshotById(pageId: string, frameId: string, snapshotId: string): FrameSnapshot | undefined { findSnapshotById(pageId: string, frameId: string, snapshotId: string): SnapshotRenderer | undefined {
const { pageEntry, contextEntry } = this.pageEntries.get(pageId)!; const { pageEntry, contextEntry } = this.pageEntries.get(pageId)!;
const frameSnapshots = pageEntry.snapshotsByFrameId[frameId]; const frameSnapshots = pageEntry.snapshotsByFrameId[frameId];
for (let index = 0; index < frameSnapshots.length; index++) { for (let index = 0; index < frameSnapshots.length; index++) {
if (frameSnapshots[index].snapshotId === snapshotId) if (frameSnapshots[index].snapshotId === snapshotId)
return new FrameSnapshot(frameId, this.contextResources.get(contextEntry.created.contextId)!, frameSnapshots, index); return new SnapshotRenderer(frameId, this.contextResources.get(contextEntry.created.contextId)!, frameSnapshots.map(fs => fs.snapshot), index);
} }
} }
findSnapshotByTime(pageId: string, frameId: string, timestamp: number): FrameSnapshot | undefined { findSnapshotByTime(pageId: string, frameId: string, timestamp: number): SnapshotRenderer | undefined {
const { pageEntry, contextEntry } = this.pageEntries.get(pageId)!; const { pageEntry, contextEntry } = this.pageEntries.get(pageId)!;
const frameSnapshots = pageEntry.snapshotsByFrameId[frameId]; const frameSnapshots = pageEntry.snapshotsByFrameId[frameId];
let snapshotIndex = -1; let snapshotIndex = -1;
@ -170,7 +170,7 @@ export class TraceModel {
if (timestamp && snapshot.timestamp <= timestamp) if (timestamp && snapshot.timestamp <= timestamp)
snapshotIndex = index; snapshotIndex = index;
} }
return snapshotIndex >= 0 ? new FrameSnapshot(frameId, this.contextResources.get(contextEntry.created.contextId)!, frameSnapshots, snapshotIndex) : undefined; return snapshotIndex >= 0 ? new SnapshotRenderer(frameId, this.contextResources.get(contextEntry.created.contextId)!, frameSnapshots.map(fs => fs.snapshot), snapshotIndex) : undefined;
} }
} }
@ -183,8 +183,6 @@ export type ContextEntry = {
pages: PageEntry[]; pages: PageEntry[];
} }
export type ContextResources = Map<string, { resourceId: string, frameId: string }[]>;
export type InterestingPageEvent = trace.DialogOpenedEvent | trace.DialogClosedEvent | trace.NavigationEvent | trace.LoadEvent; export type InterestingPageEvent = trace.DialogOpenedEvent | trace.DialogClosedEvent | trace.NavigationEvent | trace.LoadEvent;
export type PageEntry = { export type PageEntry = {

View File

@ -21,9 +21,9 @@ import * as util from 'util';
import { ScreenshotGenerator } from './screenshotGenerator'; import { ScreenshotGenerator } from './screenshotGenerator';
import { TraceModel } from './traceModel'; import { TraceModel } from './traceModel';
import { NetworkResourceTraceEvent, TraceEvent } from '../common/traceEvents'; import { NetworkResourceTraceEvent, TraceEvent } from '../common/traceEvents';
import { SnapshotServer, SnapshotStorage } from './snapshotServer';
import { ServerRouteHandler, HttpServer } from '../../../utils/httpServer'; import { ServerRouteHandler, HttpServer } from '../../../utils/httpServer';
import { FrameSnapshot } from './frameSnapshot'; import { SnapshotServer, SnapshotStorage } from '../../snapshot/snapshotServer';
import { SnapshotRenderer } from '../../snapshot/snapshotRenderer';
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs)); const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
@ -146,7 +146,7 @@ class TraceViewer implements SnapshotStorage {
return traceModel.resourceById.get(resourceId)!; return traceModel.resourceById.get(resourceId)!;
} }
snapshotByName(snapshotName: string): FrameSnapshot | undefined { snapshotByName(snapshotName: string): SnapshotRenderer | undefined {
const traceModel = this._document!.model; const traceModel = this._document!.model;
const parsed = parseSnapshotName(snapshotName); const parsed = parseSnapshotName(snapshotName);
const snapshot = parsed.snapshotId ? traceModel.findSnapshotById(parsed.pageId, parsed.frameId, parsed.snapshotId) : traceModel.findSnapshotByTime(parsed.pageId, parsed.frameId, parsed.timestamp!); const snapshot = parsed.snapshotId ? traceModel.findSnapshotById(parsed.pageId, parsed.frameId, parsed.snapshotId) : traceModel.findSnapshotByTime(parsed.pageId, parsed.frameId, parsed.timestamp!);

View File

@ -158,8 +158,9 @@ DEPS['src/server/supplements/recorder/recorderApp.ts'] = ['src/common/', 'src/ut
DEPS['src/utils/'] = ['src/common/']; DEPS['src/utils/'] = ['src/common/'];
// Trace viewer // Trace viewer
DEPS['src/server/trace/recorder/'] = ['src/server/trace/common/', ...DEPS['src/server/']]; DEPS['src/server/trace/common/'] = ['src/server/snapshot/', ...DEPS['src/server/']];
DEPS['src/server/trace/viewer/'] = ['src/server/trace/common/', ...DEPS['src/server/']]; DEPS['src/server/trace/recorder/'] = ['src/server/trace/common/', ...DEPS['src/server/trace/common/']];
DEPS['src/server/trace/viewer/'] = ['src/server/trace/common/', ...DEPS['src/server/trace/common/']];
checkDeps().catch(e => { checkDeps().catch(e => {
console.error(e && e.stack ? e.stack : e); console.error(e && e.stack ? e.stack : e);