chore(trace-viewer): introduce MultiTraceModel (#11922)
This commit is contained in:
parent
39ed705904
commit
985f932033
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue