chore(trace-viewer): introduce MultiTraceModel (#11922)

This commit is contained in:
Yury Semikhatsky 2022-02-08 12:27:29 -08:00 committed by GitHub
parent 39ed705904
commit 985f932033
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 65 additions and 53 deletions

View file

@ -34,8 +34,6 @@ export type ContextEntry = {
hasSource: boolean; hasSource: boolean;
}; };
export type MergedContexts = Pick<ContextEntry, 'startTime' | 'endTime' | 'browserName' | 'platform' | 'wallTime' | 'title' | 'options' | 'pages' | 'actions' | 'events' | 'hasSource'>;
export type PageEntry = { export type PageEntry = {
screencastFrames: { screencastFrames: {
sha1: string, sha1: string,

View file

@ -19,12 +19,13 @@ import { Boundaries, Size } from '../geometry';
import * as React from 'react'; import * as React from 'react';
import { useMeasure } from './helpers'; import { useMeasure } from './helpers';
import { upperBound } from '../../uiUtils'; import { upperBound } from '../../uiUtils';
import { MergedContexts, PageEntry } from '../entries'; import { PageEntry } from '../entries';
import { MultiTraceModel } from './modelUtil';
const tileSize = { width: 200, height: 45 }; const tileSize = { width: 200, height: 45 };
export const FilmStrip: React.FunctionComponent<{ export const FilmStrip: React.FunctionComponent<{
context: MergedContexts, context: MultiTraceModel,
boundaries: Boundaries, boundaries: Boundaries,
previewPoint?: { x: number, clientY: number }, previewPoint?: { x: number, clientY: number },
}> = ({ context, boundaries, previewPoint }) => { }> = ({ context, boundaries, previewPoint }) => {

View file

@ -16,14 +16,48 @@
import { ResourceSnapshot } from '../../../server/trace/common/snapshotTypes'; import { ResourceSnapshot } from '../../../server/trace/common/snapshotTypes';
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents'; import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
import { ContextEntry, MergedContexts, PageEntry } from '../entries'; import { ContextEntry, PageEntry } from '../entries';
import * as trace from '../../../server/trace/common/traceEvents';
const contextSymbol = Symbol('context'); const contextSymbol = Symbol('context');
const nextSymbol = Symbol('next'); const nextSymbol = Symbol('next');
const eventsSymbol = Symbol('events'); const eventsSymbol = Symbol('events');
const resourcesSymbol = Symbol('resources'); const resourcesSymbol = Symbol('resources');
export function indexModel(context: ContextEntry) { export class MultiTraceModel {
readonly startTime: number;
readonly endTime: number;
readonly browserName: string;
readonly platform?: string;
readonly wallTime?: number;
readonly title?: string;
readonly options: trace.BrowserContextEventOptions;
readonly pages: PageEntry[];
readonly actions: trace.ActionTraceEvent[];
readonly events: trace.ActionTraceEvent[];
readonly hasSource: boolean;
constructor(contexts: ContextEntry[]) {
contexts.forEach(contextEntry => indexModel(contextEntry));
this.browserName = contexts[0]?.browserName || '';
this.platform = contexts[0]?.platform || '';
this.title = contexts[0]?.title || '';
this.options = contexts[0]?.options || {};
this.wallTime = contexts.map(c => c.wallTime).reduce((prev, cur) => Math.min(prev || Number.MAX_VALUE, cur!), Number.MAX_VALUE);
this.startTime = contexts.map(c => c.startTime).reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE);
this.endTime = contexts.map(c => c.endTime).reduce((prev, cur) => Math.max(prev, cur), Number.MIN_VALUE);
this.pages = ([] as PageEntry[]).concat(...contexts.map(c => c.pages));
this.actions = ([] as ActionTraceEvent[]).concat(...contexts.map(c => c.actions));
this.events = ([] as ActionTraceEvent[]).concat(...contexts.map(c => c.events));
this.hasSource = contexts.some(c => c.hasSource);
this.actions.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
this.events.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
}
}
function indexModel(context: ContextEntry) {
for (const page of context.pages) for (const page of context.pages)
(page as any)[contextSymbol] = context; (page as any)[contextSymbol] = context;
for (let i = 0; i < context.actions.length; ++i) { for (let i = 0; i < context.actions.length; ++i) {
@ -87,22 +121,3 @@ export function resourcesForAction(action: ActionTraceEvent): ResourceSnapshot[]
(action as any)[resourcesSymbol] = result; (action as any)[resourcesSymbol] = result;
return result; return result;
} }
export function mergeContexts(contexts: ContextEntry[]): MergedContexts {
const newContext: MergedContexts = {
browserName: contexts[0].browserName,
platform: contexts[0].platform,
title: contexts[0].title,
options: contexts[0].options,
wallTime: contexts.map(c => c.wallTime).reduce((prev, cur) => Math.min(prev || Number.MAX_VALUE, cur!), Number.MAX_VALUE),
startTime: contexts.map(c => c.startTime).reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE),
endTime: contexts.map(c => c.endTime).reduce((prev, cur) => Math.max(prev, cur), Number.MIN_VALUE),
pages: ([] as PageEntry[]).concat(...contexts.map(c => c.pages)),
actions: ([] as ActionTraceEvent[]).concat(...contexts.map(c => c.actions)),
events: ([] as ActionTraceEvent[]).concat(...contexts.map(c => c.events)),
hasSource: contexts.some(c => c.hasSource)
};
newContext.actions.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
newContext.events.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
return newContext;
}

View file

@ -18,10 +18,10 @@
import * as React from 'react'; import * as React from 'react';
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents'; import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
import { msToString } from '../../uiUtils'; import { msToString } from '../../uiUtils';
import { MergedContexts } from '../entries';
import { Boundaries } from '../geometry'; import { Boundaries } from '../geometry';
import { FilmStrip } from './filmStrip'; import { FilmStrip } from './filmStrip';
import { useMeasure } from './helpers'; import { useMeasure } from './helpers';
import { MultiTraceModel } from './modelUtil';
import './timeline.css'; import './timeline.css';
type TimelineBar = { type TimelineBar = {
@ -37,7 +37,7 @@ type TimelineBar = {
}; };
export const Timeline: React.FunctionComponent<{ export const Timeline: React.FunctionComponent<{
context: MergedContexts, context: MultiTraceModel,
boundaries: Boundaries, boundaries: Boundaries,
selectedAction: ActionTraceEvent | undefined, selectedAction: ActionTraceEvent | undefined,
highlightedAction: ActionTraceEvent | undefined, highlightedAction: ActionTraceEvent | undefined,

View file

@ -15,7 +15,7 @@
*/ */
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents'; import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
import { ContextEntry, createEmptyContext, MergedContexts } from '../entries'; import { ContextEntry } from '../entries';
import { ActionList } from './actionList'; import { ActionList } from './actionList';
import { TabbedPane } from './tabbedPane'; import { TabbedPane } from './tabbedPane';
import { Timeline } from './timeline'; import { Timeline } from './timeline';
@ -29,12 +29,13 @@ import { SplitView } from '../../components/splitView';
import { ConsoleTab } from './consoleTab'; import { ConsoleTab } from './consoleTab';
import * as modelUtil from './modelUtil'; import * as modelUtil from './modelUtil';
import { msToString } from '../../uiUtils'; import { msToString } from '../../uiUtils';
import { MultiTraceModel } from './modelUtil';
export const Workbench: React.FunctionComponent<{ export const Workbench: React.FunctionComponent<{
}> = () => { }> = () => {
const [traceURLs, setTraceURLs] = React.useState<string[]>([]); const [traceURLs, setTraceURLs] = React.useState<string[]>([]);
const [uploadedTraceNames, setUploadedTraceNames] = React.useState<string[]>([]); const [uploadedTraceNames, setUploadedTraceNames] = React.useState<string[]>([]);
const [contextEntry, setContextEntry] = React.useState<MergedContexts>(emptyContext); const [model, setModel] = React.useState<MultiTraceModel>(emptyModel);
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>(); const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>();
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>(); const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions'); const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
@ -119,20 +120,19 @@ export const Workbench: React.FunctionComponent<{
return; return;
} }
const contextEntry = await response.json() as ContextEntry; const contextEntry = await response.json() as ContextEntry;
modelUtil.indexModel(contextEntry);
contextEntries.push(contextEntry); contextEntries.push(contextEntry);
} }
navigator.serviceWorker.removeEventListener('message', swListener); navigator.serviceWorker.removeEventListener('message', swListener);
const contextEntry = modelUtil.mergeContexts(contextEntries); const model = new MultiTraceModel(contextEntries);
setProgress({ done: 0, total: 0 }); setProgress({ done: 0, total: 0 });
setContextEntry(contextEntry!); setModel(model);
} else { } else {
setContextEntry(emptyContext); setModel(emptyModel);
} }
})(); })();
}, [traceURLs, uploadedTraceNames]); }, [traceURLs, uploadedTraceNames]);
const boundaries = { minimum: contextEntry.startTime, maximum: contextEntry.endTime }; const boundaries = { minimum: model.startTime, maximum: model.endTime };
// Leave some nice free space on the right hand side. // Leave some nice free space on the right hand side.
@ -147,19 +147,19 @@ export const Workbench: React.FunctionComponent<{
{ id: 'network', title: 'Network', count: networkCount, render: () => <NetworkTab action={selectedAction} /> }, { id: 'network', title: 'Network', count: networkCount, render: () => <NetworkTab action={selectedAction} /> },
]; ];
if (contextEntry.hasSource) if (model.hasSource)
tabs.push({ id: 'source', title: 'Source', count: 0, render: () => <SourceTab action={selectedAction} /> }); tabs.push({ id: 'source', title: 'Source', count: 0, render: () => <SourceTab action={selectedAction} /> });
return <div className='vbox workbench' onDragOver={event => { event.preventDefault(); setDragOver(true); }}> return <div className='vbox workbench' onDragOver={event => { event.preventDefault(); setDragOver(true); }}>
<div className='hbox header'> <div className='hbox header'>
<div className='logo'>🎭</div> <div className='logo'>🎭</div>
<div className='product'>Playwright</div> <div className='product'>Playwright</div>
{contextEntry.title && <div className='title'>{contextEntry.title}</div>} {model.title && <div className='title'>{model.title}</div>}
<div className='spacer'></div> <div className='spacer'></div>
</div> </div>
<div style={{ background: 'white', paddingLeft: '20px', flex: 'none', borderBottom: '1px solid #ddd' }}> <div style={{ background: 'white', paddingLeft: '20px', flex: 'none', borderBottom: '1px solid #ddd' }}>
<Timeline <Timeline
context={contextEntry} context={model}
boundaries={boundaries} boundaries={boundaries}
selectedAction={selectedAction} selectedAction={selectedAction}
highlightedAction={highlightedAction} highlightedAction={highlightedAction}
@ -175,7 +175,7 @@ export const Workbench: React.FunctionComponent<{
<TabbedPane tabs={ <TabbedPane tabs={
[ [
{ id: 'actions', title: 'Actions', count: 0, render: () => <ActionList { id: 'actions', title: 'Actions', count: 0, render: () => <ActionList
actions={contextEntry.actions} actions={model.actions}
selectedAction={selectedAction} selectedAction={selectedAction}
highlightedAction={highlightedAction} highlightedAction={highlightedAction}
onSelected={action => { onSelected={action => {
@ -186,21 +186,21 @@ export const Workbench: React.FunctionComponent<{
/> }, /> },
{ id: 'metadata', title: 'Metadata', count: 0, render: () => <div className='vbox'> { id: 'metadata', title: 'Metadata', count: 0, render: () => <div className='vbox'>
<div className='call-section' style={{ paddingTop: 2 }}>Time</div> <div className='call-section' style={{ paddingTop: 2 }}>Time</div>
{contextEntry.wallTime && <div className='call-line'>start time: <span className='datetime' title={new Date(contextEntry.wallTime).toLocaleString()}>{new Date(contextEntry.wallTime).toLocaleString()}</span></div>} {model.wallTime && <div className='call-line'>start time: <span className='datetime' title={new Date(model.wallTime).toLocaleString()}>{new Date(model.wallTime).toLocaleString()}</span></div>}
<div className='call-line'>duration: <span className='number' title={msToString(contextEntry.endTime - contextEntry.startTime)}>{msToString(contextEntry.endTime - contextEntry.startTime)}</span></div> <div className='call-line'>duration: <span className='number' title={msToString(model.endTime - model.startTime)}>{msToString(model.endTime - model.startTime)}</span></div>
<div className='call-section'>Browser</div> <div className='call-section'>Browser</div>
<div className='call-line'>engine: <span className='string' title={contextEntry.browserName}>{contextEntry.browserName}</span></div> <div className='call-line'>engine: <span className='string' title={model.browserName}>{model.browserName}</span></div>
{contextEntry.platform && <div className='call-line'>platform: <span className='string' title={contextEntry.platform}>{contextEntry.platform}</span></div>} {model.platform && <div className='call-line'>platform: <span className='string' title={model.platform}>{model.platform}</span></div>}
{contextEntry.options.userAgent && <div className='call-line'>user agent: <span className='datetime' title={contextEntry.options.userAgent}>{contextEntry.options.userAgent}</span></div>} {model.options.userAgent && <div className='call-line'>user agent: <span className='datetime' title={model.options.userAgent}>{model.options.userAgent}</span></div>}
<div className='call-section'>Viewport</div> <div className='call-section'>Viewport</div>
{contextEntry.options.viewport && <div className='call-line'>width: <span className='number' title={String(!!contextEntry.options.viewport?.width)}>{contextEntry.options.viewport.width}</span></div>} {model.options.viewport && <div className='call-line'>width: <span className='number' title={String(!!model.options.viewport?.width)}>{model.options.viewport.width}</span></div>}
{contextEntry.options.viewport && <div className='call-line'>height: <span className='number' title={String(!!contextEntry.options.viewport?.height)}>{contextEntry.options.viewport.height}</span></div>} {model.options.viewport && <div className='call-line'>height: <span className='number' title={String(!!model.options.viewport?.height)}>{model.options.viewport.height}</span></div>}
<div className='call-line'>is mobile: <span className='boolean' title={String(!!contextEntry.options.isMobile)}>{String(!!contextEntry.options.isMobile)}</span></div> <div className='call-line'>is mobile: <span className='boolean' title={String(!!model.options.isMobile)}>{String(!!model.options.isMobile)}</span></div>
{contextEntry.options.deviceScaleFactor && <div className='call-line'>device scale: <span className='number' title={String(contextEntry.options.deviceScaleFactor)}>{String(contextEntry.options.deviceScaleFactor)}</span></div>} {model.options.deviceScaleFactor && <div className='call-line'>device scale: <span className='number' title={String(model.options.deviceScaleFactor)}>{String(model.options.deviceScaleFactor)}</span></div>}
<div className='call-section'>Counts</div> <div className='call-section'>Counts</div>
<div className='call-line'>pages: <span className='number'>{contextEntry.pages.length}</span></div> <div className='call-line'>pages: <span className='number'>{model.pages.length}</span></div>
<div className='call-line'>actions: <span className='number'>{contextEntry.actions.length}</span></div> <div className='call-line'>actions: <span className='number'>{model.actions.length}</span></div>
<div className='call-line'>events: <span className='number'>{contextEntry.events.length}</span></div> <div className='call-line'>events: <span className='number'>{model.events.length}</span></div>
</div> }, </div> },
] ]
} selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/> } selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/>
@ -237,6 +237,4 @@ export const Workbench: React.FunctionComponent<{
</div>; </div>;
}; };
const emptyContext = createEmptyContext(); const emptyModel = new MultiTraceModel([]);
emptyContext.startTime = performance.now();
emptyContext.endTime = emptyContext.startTime;