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;
};
export type MergedContexts = Pick<ContextEntry, 'startTime' | 'endTime' | 'browserName' | 'platform' | 'wallTime' | 'title' | 'options' | 'pages' | 'actions' | 'events' | 'hasSource'>;
export type PageEntry = {
screencastFrames: {
sha1: string,

View file

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

View file

@ -16,14 +16,48 @@
import { ResourceSnapshot } from '../../../server/trace/common/snapshotTypes';
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 nextSymbol = Symbol('next');
const eventsSymbol = Symbol('events');
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)
(page as any)[contextSymbol] = context;
for (let i = 0; i < context.actions.length; ++i) {
@ -87,22 +121,3 @@ export function resourcesForAction(action: ActionTraceEvent): ResourceSnapshot[]
(action as any)[resourcesSymbol] = 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 { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
import { msToString } from '../../uiUtils';
import { MergedContexts } from '../entries';
import { Boundaries } from '../geometry';
import { FilmStrip } from './filmStrip';
import { useMeasure } from './helpers';
import { MultiTraceModel } from './modelUtil';
import './timeline.css';
type TimelineBar = {
@ -37,7 +37,7 @@ type TimelineBar = {
};
export const Timeline: React.FunctionComponent<{
context: MergedContexts,
context: MultiTraceModel,
boundaries: Boundaries,
selectedAction: ActionTraceEvent | undefined,
highlightedAction: ActionTraceEvent | undefined,

View file

@ -15,7 +15,7 @@
*/
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
import { ContextEntry, createEmptyContext, MergedContexts } from '../entries';
import { ContextEntry } from '../entries';
import { ActionList } from './actionList';
import { TabbedPane } from './tabbedPane';
import { Timeline } from './timeline';
@ -29,12 +29,13 @@ import { SplitView } from '../../components/splitView';
import { ConsoleTab } from './consoleTab';
import * as modelUtil from './modelUtil';
import { msToString } from '../../uiUtils';
import { MultiTraceModel } from './modelUtil';
export const Workbench: React.FunctionComponent<{
}> = () => {
const [traceURLs, setTraceURLs] = 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 [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
@ -119,20 +120,19 @@ export const Workbench: React.FunctionComponent<{
return;
}
const contextEntry = await response.json() as ContextEntry;
modelUtil.indexModel(contextEntry);
contextEntries.push(contextEntry);
}
navigator.serviceWorker.removeEventListener('message', swListener);
const contextEntry = modelUtil.mergeContexts(contextEntries);
const model = new MultiTraceModel(contextEntries);
setProgress({ done: 0, total: 0 });
setContextEntry(contextEntry!);
setModel(model);
} else {
setContextEntry(emptyContext);
setModel(emptyModel);
}
})();
}, [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.
@ -147,19 +147,19 @@ export const Workbench: React.FunctionComponent<{
{ 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} /> });
return <div className='vbox workbench' onDragOver={event => { event.preventDefault(); setDragOver(true); }}>
<div className='hbox header'>
<div className='logo'>🎭</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>
<div style={{ background: 'white', paddingLeft: '20px', flex: 'none', borderBottom: '1px solid #ddd' }}>
<Timeline
context={contextEntry}
context={model}
boundaries={boundaries}
selectedAction={selectedAction}
highlightedAction={highlightedAction}
@ -175,7 +175,7 @@ export const Workbench: React.FunctionComponent<{
<TabbedPane tabs={
[
{ id: 'actions', title: 'Actions', count: 0, render: () => <ActionList
actions={contextEntry.actions}
actions={model.actions}
selectedAction={selectedAction}
highlightedAction={highlightedAction}
onSelected={action => {
@ -186,21 +186,21 @@ export const Workbench: React.FunctionComponent<{
/> },
{ id: 'metadata', title: 'Metadata', count: 0, render: () => <div className='vbox'>
<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>}
<div className='call-line'>duration: <span className='number' title={msToString(contextEntry.endTime - contextEntry.startTime)}>{msToString(contextEntry.endTime - contextEntry.startTime)}</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(model.endTime - model.startTime)}>{msToString(model.endTime - model.startTime)}</span></div>
<div className='call-section'>Browser</div>
<div className='call-line'>engine: <span className='string' title={contextEntry.browserName}>{contextEntry.browserName}</span></div>
{contextEntry.platform && <div className='call-line'>platform: <span className='string' title={contextEntry.platform}>{contextEntry.platform}</span></div>}
{contextEntry.options.userAgent && <div className='call-line'>user agent: <span className='datetime' title={contextEntry.options.userAgent}>{contextEntry.options.userAgent}</span></div>}
<div className='call-line'>engine: <span className='string' title={model.browserName}>{model.browserName}</span></div>
{model.platform && <div className='call-line'>platform: <span className='string' title={model.platform}>{model.platform}</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>
{contextEntry.options.viewport && <div className='call-line'>width: <span className='number' title={String(!!contextEntry.options.viewport?.width)}>{contextEntry.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>}
<div className='call-line'>is mobile: <span className='boolean' title={String(!!contextEntry.options.isMobile)}>{String(!!contextEntry.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.viewport && <div className='call-line'>width: <span className='number' title={String(!!model.options.viewport?.width)}>{model.options.viewport.width}</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(!!model.options.isMobile)}>{String(!!model.options.isMobile)}</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-line'>pages: <span className='number'>{contextEntry.pages.length}</span></div>
<div className='call-line'>actions: <span className='number'>{contextEntry.actions.length}</span></div>
<div className='call-line'>events: <span className='number'>{contextEntry.events.length}</span></div>
<div className='call-line'>pages: <span className='number'>{model.pages.length}</span></div>
<div className='call-line'>actions: <span className='number'>{model.actions.length}</span></div>
<div className='call-line'>events: <span className='number'>{model.events.length}</span></div>
</div> },
]
} selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/>
@ -237,6 +237,4 @@ export const Workbench: React.FunctionComponent<{
</div>;
};
const emptyContext = createEmptyContext();
emptyContext.startTime = performance.now();
emptyContext.endTime = emptyContext.startTime;
const emptyModel = new MultiTraceModel([]);