chore(tracing): simplify resource treatment (#6571)

This commit is contained in:
Pavel Feldman 2021-05-13 20:41:32 -07:00 committed by GitHub
parent 9b0aeeffae
commit 7b844c5fab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 148 additions and 259 deletions

View file

@ -56,13 +56,11 @@ export class Dialog extends SdkObject {
assert(!this._handled, 'Cannot accept dialog which is already handled!'); assert(!this._handled, 'Cannot accept dialog which is already handled!');
this._handled = true; this._handled = true;
await this._onHandle(true, promptText); await this._onHandle(true, promptText);
this._page.emit(Page.Events.InternalDialogClosed, this);
} }
async dismiss() { async dismiss() {
assert(!this._handled, 'Cannot dismiss dialog which is already handled!'); assert(!this._handled, 'Cannot dismiss dialog which is already handled!');
this._handled = true; this._handled = true;
await this._onHandle(false); await this._onHandle(false);
this._page.emit(Page.Events.InternalDialogClosed, this);
} }
} }

View file

@ -97,7 +97,6 @@ export class Page extends SdkObject {
Crash: 'crash', Crash: 'crash',
Console: 'console', Console: 'console',
Dialog: 'dialog', Dialog: 'dialog',
InternalDialogClosed: 'internaldialogclosed',
Download: 'download', Download: 'download',
FileChooser: 'filechooser', FileChooser: 'filechooser',
DOMContentLoaded: 'domcontentloaded', DOMContentLoaded: 'domcontentloaded',

View file

@ -66,35 +66,6 @@ export type FrameSnapshotTraceEvent = {
snapshot: FrameSnapshot, snapshot: FrameSnapshot,
}; };
export type DialogOpenedEvent = {
timestamp: number,
type: 'dialog-opened',
pageId: string,
dialogType: string,
message?: string,
};
export type DialogClosedEvent = {
timestamp: number,
type: 'dialog-closed',
pageId: string,
dialogType: string,
};
export type NavigationEvent = {
timestamp: number,
type: 'navigation',
pageId: string,
url: string,
sameDocument: boolean,
};
export type LoadEvent = {
timestamp: number,
type: 'load',
pageId: string,
};
export type TraceEvent = export type TraceEvent =
ContextCreatedTraceEvent | ContextCreatedTraceEvent |
PageCreatedTraceEvent | PageCreatedTraceEvent |
@ -102,8 +73,4 @@ export type TraceEvent =
ScreencastFrameTraceEvent | ScreencastFrameTraceEvent |
ActionTraceEvent | ActionTraceEvent |
ResourceSnapshotTraceEvent | ResourceSnapshotTraceEvent |
FrameSnapshotTraceEvent | FrameSnapshotTraceEvent;
DialogOpenedEvent |
DialogClosedEvent |
NavigationEvent |
LoadEvent;

View file

@ -21,9 +21,7 @@ import yazl from 'yazl';
import { calculateSha1, createGuid, mkdirIfNeeded, monotonicTime } from '../../../utils/utils'; import { calculateSha1, createGuid, mkdirIfNeeded, monotonicTime } from '../../../utils/utils';
import { Artifact } from '../../artifact'; import { Artifact } from '../../artifact';
import { BrowserContext } from '../../browserContext'; import { BrowserContext } from '../../browserContext';
import { Dialog } from '../../dialog';
import { ElementHandle } from '../../dom'; import { ElementHandle } from '../../dom';
import { Frame, NavigationEvent } from '../../frames';
import { helper, RegisteredListener } from '../../helper'; import { helper, RegisteredListener } from '../../helper';
import { CallMetadata, InstrumentationListener, SdkObject } from '../../instrumentation'; import { CallMetadata, InstrumentationListener, SdkObject } from '../../instrumentation';
import { Page } from '../../page'; import { Page } from '../../page';
@ -188,51 +186,6 @@ export class Tracing implements InstrumentationListener {
page.setScreencastOptions({ width: 800, height: 600, quality: 90 }); page.setScreencastOptions({ width: 800, height: 600, quality: 90 });
this._eventListeners.push( this._eventListeners.push(
helper.addEventListener(page, Page.Events.Dialog, (dialog: Dialog) => {
const event: trace.DialogOpenedEvent = {
timestamp: monotonicTime(),
type: 'dialog-opened',
pageId,
dialogType: dialog.type(),
message: dialog.message(),
};
this._appendTraceEvent(event);
}),
helper.addEventListener(page, Page.Events.InternalDialogClosed, (dialog: Dialog) => {
const event: trace.DialogClosedEvent = {
timestamp: monotonicTime(),
type: 'dialog-closed',
pageId,
dialogType: dialog.type(),
};
this._appendTraceEvent(event);
}),
helper.addEventListener(page.mainFrame(), Frame.Events.Navigation, (navigationEvent: NavigationEvent) => {
if (page.mainFrame().url() === 'about:blank')
return;
const event: trace.NavigationEvent = {
timestamp: monotonicTime(),
type: 'navigation',
pageId,
url: navigationEvent.url,
sameDocument: !navigationEvent.newDocument,
};
this._appendTraceEvent(event);
}),
helper.addEventListener(page, Page.Events.Load, () => {
if (page.mainFrame().url() === 'about:blank')
return;
const event: trace.LoadEvent = {
timestamp: monotonicTime(),
type: 'load',
pageId,
};
this._appendTraceEvent(event);
}),
helper.addEventListener(page, Page.Events.ScreencastFrame, params => { helper.addEventListener(page, Page.Events.ScreencastFrame, params => {
const sha1 = calculateSha1(createGuid()); // no need to compute sha1 for screenshots const sha1 = calculateSha1(createGuid()); // no need to compute sha1 for screenshots
const event: trace.ScreencastFrameTraceEvent = { const event: trace.ScreencastFrameTraceEvent = {
@ -248,7 +201,6 @@ export class Tracing implements InstrumentationListener {
await fsWriteFileAsync(path.join(this._resourcesDir!, sha1), params.buffer).catch(() => {}); await fsWriteFileAsync(path.join(this._resourcesDir!, sha1), params.buffer).catch(() => {});
}); });
}), }),
helper.addEventListener(page, Page.Events.Close, () => { helper.addEventListener(page, Page.Events.Close, () => {
const event: trace.PageDestroyedTraceEvent = { const event: trace.PageDestroyedTraceEvent = {
timestamp: monotonicTime(), timestamp: monotonicTime(),

View file

@ -34,18 +34,10 @@ export class TraceModel {
appendEvents(events: trace.TraceEvent[], snapshotStorage: SnapshotStorage) { appendEvents(events: trace.TraceEvent[], snapshotStorage: SnapshotStorage) {
for (const event of events) for (const event of events)
this.appendEvent(event); this.appendEvent(event);
const actions: ActionEntry[] = []; const actions: trace.ActionTraceEvent[] = [];
for (const page of this.contextEntry!.pages) for (const page of this.contextEntry!.pages)
actions.push(...page.actions); actions.push(...page.actions);
this.contextEntry!.resources = snapshotStorage.resources();
const resources = snapshotStorage.resources().reverse();
actions.reverse();
for (const action of actions) {
while (resources.length && resources[0].timestamp > action.timestamp)
action.resources.push(resources.shift()!);
action.resources.reverse();
}
} }
appendEvent(event: trace.TraceEvent) { appendEvent(event: trace.TraceEvent) {
@ -56,6 +48,7 @@ export class TraceModel {
endTime: Number.MIN_VALUE, endTime: Number.MIN_VALUE,
created: event, created: event,
pages: [], pages: [],
resources: []
}; };
break; break;
} }
@ -64,7 +57,7 @@ export class TraceModel {
created: event, created: event,
destroyed: undefined as any, destroyed: undefined as any,
actions: [], actions: [],
interestingEvents: [], events: [],
screencastFrames: [], screencastFrames: [],
}; };
this.pageEntries.set(event.pageId, pageEntry); this.pageEntries.set(event.pageId, pageEntry);
@ -82,20 +75,14 @@ export class TraceModel {
case 'action': { case 'action': {
const metadata = event.metadata; const metadata = event.metadata;
const pageEntry = this.pageEntries.get(metadata.pageId!)!; const pageEntry = this.pageEntries.get(metadata.pageId!)!;
const action: ActionEntry = { pageEntry.actions.push(event);
actionId: metadata.id,
resources: [],
...event,
};
pageEntry.actions.push(action);
break; break;
} }
case 'dialog-opened': case 'event': {
case 'dialog-closed': const metadata = event.metadata;
case 'navigation': const pageEntry = this.pageEntries.get(metadata.pageId!);
case 'load': { if (pageEntry)
const pageEntry = this.pageEntries.get(event.pageId)!; pageEntry.events.push(event);
pageEntry.interestingEvents.push(event);
break; break;
} }
case 'resource-snapshot': case 'resource-snapshot':
@ -105,8 +92,10 @@ export class TraceModel {
this._snapshotStorage.addFrameSnapshot(event.snapshot); this._snapshotStorage.addFrameSnapshot(event.snapshot);
break; break;
} }
this.contextEntry!.startTime = Math.min(this.contextEntry!.startTime, event.timestamp); if (event.type === 'action' || event.type === 'event') {
this.contextEntry!.endTime = Math.max(this.contextEntry!.endTime, event.timestamp); this.contextEntry!.startTime = Math.min(this.contextEntry!.startTime, event.metadata.startTime);
this.contextEntry!.endTime = Math.max(this.contextEntry!.endTime, event.metadata.endTime);
}
} }
} }
@ -115,15 +104,14 @@ export type ContextEntry = {
endTime: number; endTime: number;
created: trace.ContextCreatedTraceEvent; created: trace.ContextCreatedTraceEvent;
pages: PageEntry[]; pages: PageEntry[];
resources: ResourceSnapshot[];
} }
export type InterestingPageEvent = trace.DialogOpenedEvent | trace.DialogClosedEvent | trace.NavigationEvent | trace.LoadEvent;
export type PageEntry = { export type PageEntry = {
created: trace.PageCreatedTraceEvent; created: trace.PageCreatedTraceEvent;
destroyed: trace.PageDestroyedTraceEvent; destroyed: trace.PageDestroyedTraceEvent;
actions: ActionEntry[]; actions: trace.ActionTraceEvent[];
interestingEvents: InterestingPageEvent[]; events: trace.ActionTraceEvent[];
screencastFrames: { screencastFrames: {
sha1: string, sha1: string,
timestamp: number, timestamp: number,
@ -132,11 +120,6 @@ export type PageEntry = {
}[] }[]
} }
export type ActionEntry = trace.ActionTraceEvent & {
actionId: string;
resources: ResourceSnapshot[]
};
export class PersistentSnapshotStorage extends BaseSnapshotStorage { export class PersistentSnapshotStorage extends BaseSnapshotStorage {
private _resourcesDir: string; private _resourcesDir: string;

View file

@ -14,17 +14,17 @@
limitations under the License. limitations under the License.
*/ */
import { ActionEntry } from '../../../server/trace/viewer/traceModel';
import './actionList.css'; import './actionList.css';
import './tabbedPane.css'; import './tabbedPane.css';
import * as React from 'react'; import * as React from 'react';
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
export interface ActionListProps { export interface ActionListProps {
actions: ActionEntry[], actions: ActionTraceEvent[],
selectedAction: ActionEntry | undefined, selectedAction: ActionTraceEvent | undefined,
highlightedAction: ActionEntry | undefined, highlightedAction: ActionTraceEvent | undefined,
onSelected: (action: ActionEntry) => void, onSelected: (action: ActionTraceEvent) => void,
onHighlighted: (action: ActionEntry | undefined) => void, onHighlighted: (action: ActionTraceEvent | undefined) => void,
} }
export const ActionList: React.FC<ActionListProps> = ({ export const ActionList: React.FC<ActionListProps> = ({
@ -68,16 +68,16 @@ export const ActionList: React.FC<ActionListProps> = ({
}} }}
ref={actionListRef} ref={actionListRef}
> >
{actions.map(actionEntry => { {actions.map(action => {
const { metadata, actionId } = actionEntry; const { metadata } = action;
const selectedSuffix = actionEntry === selectedAction ? ' selected' : ''; const selectedSuffix = action === selectedAction ? ' selected' : '';
const highlightedSuffix = actionEntry === highlightedAction ? ' highlighted' : ''; const highlightedSuffix = action === highlightedAction ? ' highlighted' : '';
return <div return <div
className={'action-entry' + selectedSuffix + highlightedSuffix} className={'action-entry' + selectedSuffix + highlightedSuffix}
key={actionId} key={metadata.id}
onClick={() => onSelected(actionEntry)} onClick={() => onSelected(action)}
onMouseEnter={() => onHighlighted(actionEntry)} onMouseEnter={() => onHighlighted(action)}
onMouseLeave={() => (highlightedAction === actionEntry) && onHighlighted(undefined)} onMouseLeave={() => (highlightedAction === action) && onHighlighted(undefined)}
> >
<div className={'action-error codicon codicon-issues'} hidden={!metadata.error} /> <div className={'action-error codicon codicon-issues'} hidden={!metadata.error} />
<div className='action-title'>{metadata.apiName || metadata.method}</div> <div className='action-title'>{metadata.apiName || metadata.method}</div>

View file

@ -14,18 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
import { ActionEntry } from '../../../server/trace/viewer/traceModel';
import * as React from 'react'; import * as React from 'react';
import './logsTab.css'; import './logsTab.css';
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
export const LogsTab: React.FunctionComponent<{ export const LogsTab: React.FunctionComponent<{
actionEntry: ActionEntry | undefined, action: ActionTraceEvent | undefined,
}> = ({ actionEntry }) => { }> = ({ action }) => {
let logs: string[] = []; let logs: string[] = [];
if (actionEntry) { if (action) {
logs = actionEntry.metadata.log || []; logs = action.metadata.log || [];
if (actionEntry.metadata.error) if (action.metadata.error)
logs = [actionEntry.metadata.error, ...logs]; logs = [action.metadata.error, ...logs];
} }
return <div className='logs-tab'>{ return <div className='logs-tab'>{
logs.map((logLine, index) => { logs.map((logLine, index) => {

View file

@ -14,18 +14,25 @@
* limitations under the License. * limitations under the License.
*/ */
import { ActionEntry } from '../../../server/trace/viewer/traceModel'; import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
import { ContextEntry } from '../../../server/trace/viewer/traceModel';
import './networkTab.css'; import './networkTab.css';
import * as React from 'react'; import * as React from 'react';
import { NetworkResourceDetails } from './networkResourceDetails'; import { NetworkResourceDetails } from './networkResourceDetails';
import { ResourceSnapshot } from '../../../server/snapshot/snapshotTypes';
export const NetworkTab: React.FunctionComponent<{ export const NetworkTab: React.FunctionComponent<{
actionEntry: ActionEntry | undefined, context: ContextEntry,
}> = ({ actionEntry }) => { action: ActionTraceEvent | undefined,
nextAction: ActionTraceEvent | undefined,
}> = ({ context, action, nextAction }) => {
const [selected, setSelected] = React.useState(0); const [selected, setSelected] = React.useState(0);
const resources: ResourceSnapshot[] = context.resources.filter(resource => {
return action && resource.timestamp > action.metadata.startTime && (!nextAction || resource.timestamp < nextAction.metadata.startTime);
});
return <div className='network-tab'>{ return <div className='network-tab'>{
(actionEntry ? actionEntry.resources : []).map((resource, index) => { resources.map((resource, index) => {
return <NetworkResourceDetails resource={resource} key={index} index={index} selected={selected === index} setSelected={setSelected} />; return <NetworkResourceDetails resource={resource} key={index} index={index} selected={selected === index} setSelected={setSelected} />;
}) })
}</div>; }</div>;

View file

@ -14,23 +14,23 @@
* limitations under the License. * limitations under the License.
*/ */
import { ActionEntry } from '../../../server/trace/viewer/traceModel';
import { Size } from '../geometry'; import { Size } from '../geometry';
import './snapshotTab.css'; import './snapshotTab.css';
import './tabbedPane.css'; import './tabbedPane.css';
import * as React from 'react'; import * as React from 'react';
import { useMeasure } from './helpers'; import { useMeasure } from './helpers';
import type { Point } from '../../../common/types'; import type { Point } from '../../../common/types';
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
export const SnapshotTab: React.FunctionComponent<{ export const SnapshotTab: React.FunctionComponent<{
actionEntry: ActionEntry | undefined, action: ActionTraceEvent | undefined,
snapshotSize: Size, snapshotSize: Size,
}> = ({ actionEntry, snapshotSize }) => { }> = ({ action, snapshotSize }) => {
const [measure, ref] = useMeasure<HTMLDivElement>(); const [measure, ref] = useMeasure<HTMLDivElement>();
let [snapshotIndex, setSnapshotIndex] = React.useState(0); let [snapshotIndex, setSnapshotIndex] = React.useState(0);
const snapshotMap = new Map<string, { title: string, snapshotName: string }>(); const snapshotMap = new Map<string, { title: string, snapshotName: string }>();
for (const snapshot of actionEntry?.metadata.snapshots || []) for (const snapshot of action?.metadata.snapshots || [])
snapshotMap.set(snapshot.title, snapshot); snapshotMap.set(snapshot.title, snapshot);
const actionSnapshot = snapshotMap.get('action') || snapshotMap.get('after'); const actionSnapshot = snapshotMap.get('action') || snapshotMap.get('after');
const snapshots = [actionSnapshot ? { ...actionSnapshot, title: 'action' } : undefined, snapshotMap.get('before'), snapshotMap.get('after')].filter(Boolean) as { title: string, snapshotName: string }[]; const snapshots = [actionSnapshot ? { ...actionSnapshot, title: 'action' } : undefined, snapshotMap.get('before'), snapshotMap.get('after')].filter(Boolean) as { title: string, snapshotName: string }[];
@ -44,12 +44,12 @@ export const SnapshotTab: React.FunctionComponent<{
return; return;
let snapshotUri = undefined; let snapshotUri = undefined;
let point: Point | undefined = undefined; let point: Point | undefined = undefined;
if (actionEntry) { if (action) {
const snapshot = snapshots[snapshotIndex]; const snapshot = snapshots[snapshotIndex];
if (snapshot && snapshot.snapshotName) { if (snapshot && snapshot.snapshotName) {
snapshotUri = `${actionEntry.metadata.pageId}?name=${snapshot.snapshotName}`; snapshotUri = `${action.metadata.pageId}?name=${snapshot.snapshotName}`;
if (snapshot.snapshotName.includes('action')) if (snapshot.snapshotName.includes('action'))
point = actionEntry.metadata.point; point = action.metadata.point;
} }
} }
const snapshotUrl = snapshotUri ? `${window.location.origin}/snapshot/${snapshotUri}` : 'data:text/html,<body style="background: #ddd"></body>'; const snapshotUrl = snapshotUri ? `${window.location.origin}/snapshot/${snapshotUri}` : 'data:text/html,<body style="background: #ddd"></body>';
@ -57,7 +57,7 @@ export const SnapshotTab: React.FunctionComponent<{
(iframeRef.current.contentWindow as any).showSnapshot(snapshotUrl, { point }); (iframeRef.current.contentWindow as any).showSnapshot(snapshotUrl, { point });
} catch (e) { } catch (e) {
} }
}, [actionEntry, snapshotIndex]); }, [action, snapshotIndex]);
const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height); const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height);
const scaledSize = { const scaledSize = {

View file

@ -14,7 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import { ActionEntry } from '../../../server/trace/viewer/traceModel';
import * as React from 'react'; import * as React from 'react';
import { useAsyncMemo } from './helpers'; import { useAsyncMemo } from './helpers';
import './sourceTab.css'; import './sourceTab.css';
@ -23,6 +22,7 @@ import { StackFrame } from '../../../common/types';
import { Source as SourceView } from '../../components/source'; import { Source as SourceView } from '../../components/source';
import { StackTraceView } from './stackTrace'; import { StackTraceView } from './stackTrace';
import { SplitView } from '../../components/splitView'; import { SplitView } from '../../components/splitView';
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
type StackInfo = string | { type StackInfo = string | {
frames: StackFrame[]; frames: StackFrame[];
@ -30,22 +30,22 @@ type StackInfo = string | {
}; };
export const SourceTab: React.FunctionComponent<{ export const SourceTab: React.FunctionComponent<{
actionEntry: ActionEntry | undefined, action: ActionTraceEvent | undefined,
}> = ({ actionEntry }) => { }> = ({ action }) => {
const [lastAction, setLastAction] = React.useState<ActionEntry | undefined>(); const [lastAction, setLastAction] = React.useState<ActionTraceEvent | undefined>();
const [selectedFrame, setSelectedFrame] = React.useState<number>(0); const [selectedFrame, setSelectedFrame] = React.useState<number>(0);
const [needReveal, setNeedReveal] = React.useState<boolean>(false); const [needReveal, setNeedReveal] = React.useState<boolean>(false);
if (lastAction !== actionEntry) { if (lastAction !== action) {
setLastAction(actionEntry); setLastAction(action);
setSelectedFrame(0); setSelectedFrame(0);
setNeedReveal(true); setNeedReveal(true);
} }
const stackInfo = React.useMemo<StackInfo>(() => { const stackInfo = React.useMemo<StackInfo>(() => {
if (!actionEntry) if (!action)
return ''; return '';
const { metadata } = actionEntry; const { metadata } = action;
if (!metadata.stack) if (!metadata.stack)
return ''; return '';
const frames = metadata.stack; const frames = metadata.stack;
@ -53,7 +53,7 @@ export const SourceTab: React.FunctionComponent<{
frames, frames,
fileContent: new Map(), fileContent: new Map(),
}; };
}, [actionEntry]); }, [action]);
const content = useAsyncMemo<string>(async () => { const content = useAsyncMemo<string>(async () => {
let value: string; let value: string;
@ -80,6 +80,6 @@ export const SourceTab: React.FunctionComponent<{
return <SplitView sidebarSize={100} orientation='vertical'> return <SplitView sidebarSize={100} orientation='vertical'>
<SourceView text={content} language='javascript' highlight={[{ line: targetLine, type: 'running' }]} revealLine={targetLine}></SourceView> <SourceView text={content} language='javascript' highlight={[{ line: targetLine, type: 'running' }]} revealLine={targetLine}></SourceView>
<StackTraceView actionEntry={actionEntry} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame}></StackTraceView> <StackTraceView action={action} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame}></StackTraceView>
</SplitView>; </SplitView>;
}; };

View file

@ -14,16 +14,16 @@
* limitations under the License. * limitations under the License.
*/ */
import { ActionEntry } from '../../../server/trace/viewer/traceModel';
import * as React from 'react'; import * as React from 'react';
import './stackTrace.css'; import './stackTrace.css';
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
export const StackTraceView: React.FunctionComponent<{ export const StackTraceView: React.FunctionComponent<{
actionEntry: ActionEntry | undefined, action: ActionTraceEvent | undefined,
selectedFrame: number, selectedFrame: number,
setSelectedFrame: (index: number) => void setSelectedFrame: (index: number) => void
}> = ({ actionEntry, setSelectedFrame, selectedFrame }) => { }> = ({ action, setSelectedFrame, selectedFrame }) => {
const frames = actionEntry?.metadata.stack || []; const frames = action?.metadata.stack || [];
return <div className='stack-trace'>{ return <div className='stack-trace'>{
frames.map((frame, index) => { frames.map((frame, index) => {
return <div return <div

View file

@ -71,10 +71,9 @@
position: absolute; position: absolute;
height: 9px; height: 9px;
top: 11px; top: 11px;
background-color: red;
border-radius: 2px; border-radius: 2px;
min-width: 3px; min-width: 3px;
--action-color: 'transparent'; --action-color: gray;
background-color: var(--action-color); background-color: var(--action-color);
} }
@ -83,46 +82,54 @@
box-shadow: 0 0 0 1px var(--action-color); box-shadow: 0 0 0 1px var(--action-color);
} }
.timeline-bar.click, .timeline-bar.frame_click,
.timeline-bar.dblclick, .timeline-bar.frame_dblclick,
.timeline-bar.hover, .timeline-bar.frame_hover,
.timeline-bar.check, .timeline-bar.frame_check,
.timeline-bar.uncheck, .timeline-bar.frame_uncheck,
.timeline-bar.tap { .timeline-bar.frame_tap {
--action-color: var(--green); --action-color: var(--green);
} }
.timeline-bar.fill, .timeline-bar.page_load,
.timeline-bar.press, .timeline-bar.page_domcontentloaded,
.timeline-bar.type, .timeline-bar.frame_fill,
.timeline-bar.selectOption, .timeline-bar.frame_press,
.timeline-bar.setInputFiles { .timeline-bar.frame_type,
.timeline-bar.frame_selectoption,
.timeline-bar.frame_setinputfiles {
--action-color: var(--orange); --action-color: var(--orange);
} }
.timeline-bar.goto, .timeline-bar.frame_loadstate {
.timeline-bar.setContent, display: none;
.timeline-bar.goBack, }
.timeline-bar.goForward,
.timeline-bar.frame_goto,
.timeline-bar.frame_setcontent,
.timeline-bar.frame_goback,
.timeline-bar.frame_goforward,
.timeline-bar.reload { .timeline-bar.reload {
--action-color: var(--blue); --action-color: var(--blue);
} }
.timeline-bar.evaluateExpression { .timeline-bar.frame_evaluateexpression {
--action-color: var(--yellow); --action-color: var(--yellow);
} }
.timeline-bar.dialog { .timeline-bar.frame_dialog {
top: 22px;
--action-color: var(--transparent-blue); --action-color: var(--transparent-blue);
} }
.timeline-bar.navigation { .timeline-bar.frame_navigated {
top: 22px;
--action-color: var(--blue); --action-color: var(--blue);
} }
.timeline-bar.waitForEventInfo { .timeline-bar.event {
top: 22px;
}
.timeline-bar.frame_waitforeventinfo {
bottom: inherit; bottom: inherit;
top: 0; top: 0;
--action-color: var(--gray); --action-color: var(--gray);

View file

@ -15,7 +15,8 @@
limitations under the License. limitations under the License.
*/ */
import { ContextEntry, InterestingPageEvent, ActionEntry, trace } from '../../../server/trace/viewer/traceModel'; import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
import { ContextEntry } from '../../../server/trace/viewer/traceModel';
import './timeline.css'; import './timeline.css';
import { Boundaries } from '../geometry'; import { Boundaries } from '../geometry';
import * as React from 'react'; import * as React from 'react';
@ -24,24 +25,24 @@ import { msToString } from '../../uiUtils';
import { FilmStrip } from './filmStrip'; import { FilmStrip } from './filmStrip';
type TimelineBar = { type TimelineBar = {
entry?: ActionEntry; action?: ActionTraceEvent;
event?: InterestingPageEvent; event?: ActionTraceEvent;
leftPosition: number; leftPosition: number;
rightPosition: number; rightPosition: number;
leftTime: number; leftTime: number;
rightTime: number; rightTime: number;
type: string; type: string;
label: string; label: string;
priority: number; className: string;
}; };
export const Timeline: React.FunctionComponent<{ export const Timeline: React.FunctionComponent<{
context: ContextEntry, context: ContextEntry,
boundaries: Boundaries, boundaries: Boundaries,
selectedAction: ActionEntry | undefined, selectedAction: ActionTraceEvent | undefined,
highlightedAction: ActionEntry | undefined, highlightedAction: ActionTraceEvent | undefined,
onSelected: (action: ActionEntry) => void, onSelected: (action: ActionTraceEvent) => void,
onHighlighted: (action: ActionEntry | undefined) => void, onHighlighted: (action: ActionTraceEvent | undefined) => void,
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onHighlighted }) => { }> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onHighlighted }) => {
const [measure, ref] = useMeasure<HTMLDivElement>(); const [measure, ref] = useMeasure<HTMLDivElement>();
const [previewPoint, setPreviewPoint] = React.useState<{ x: number, clientY: number } | undefined>(); const [previewPoint, setPreviewPoint] = React.useState<{ x: number, clientY: number } | undefined>();
@ -61,64 +62,36 @@ export const Timeline: React.FunctionComponent<{
if (entry.metadata.method === 'goto') if (entry.metadata.method === 'goto')
detail = entry.metadata.params.url || ''; detail = entry.metadata.params.url || '';
bars.push({ bars.push({
entry, action: entry,
leftTime: entry.metadata.startTime, leftTime: entry.metadata.startTime,
rightTime: entry.metadata.endTime, rightTime: entry.metadata.endTime,
leftPosition: timeToPosition(measure.width, boundaries, entry.metadata.startTime), leftPosition: timeToPosition(measure.width, boundaries, entry.metadata.startTime),
rightPosition: timeToPosition(measure.width, boundaries, entry.metadata.endTime), rightPosition: timeToPosition(measure.width, boundaries, entry.metadata.endTime),
label: entry.metadata.apiName + ' ' + detail, label: entry.metadata.apiName + ' ' + detail,
type: entry.metadata.method, type: entry.metadata.type + '.' + entry.metadata.method,
priority: 0, className: `${entry.metadata.type}_${entry.metadata.method}`.toLowerCase()
}); });
} }
let lastDialogOpened: trace.DialogOpenedEvent | undefined;
for (const event of page.interestingEvents) { for (const event of page.events) {
if (event.type === 'dialog-opened') { const startTime = event.metadata.startTime;
lastDialogOpened = event;
continue;
}
if (event.type === 'dialog-closed' && lastDialogOpened) {
bars.push({ bars.push({
event, event,
leftTime: lastDialogOpened.timestamp, leftTime: startTime,
rightTime: event.timestamp, rightTime: startTime,
leftPosition: timeToPosition(measure.width, boundaries, lastDialogOpened.timestamp), leftPosition: timeToPosition(measure.width, boundaries, startTime),
rightPosition: timeToPosition(measure.width, boundaries, event.timestamp), rightPosition: timeToPosition(measure.width, boundaries, startTime),
label: lastDialogOpened.message ? `${event.dialogType} "${lastDialogOpened.message}"` : event.dialogType, label: event.metadata.method,
type: 'dialog', type: event.metadata.type + '.' + event.metadata.method,
priority: -1, className: `${event.metadata.type}_${event.metadata.method}`.toLowerCase()
});
} else if (event.type === 'navigation') {
bars.push({
event,
leftTime: event.timestamp,
rightTime: event.timestamp,
leftPosition: timeToPosition(measure.width, boundaries, event.timestamp),
rightPosition: timeToPosition(measure.width, boundaries, event.timestamp),
label: `navigated to ${event.url}`,
type: event.type,
priority: 1,
});
} else if (event.type === 'load') {
bars.push({
event,
leftTime: event.timestamp,
rightTime: event.timestamp,
leftPosition: timeToPosition(measure.width, boundaries, event.timestamp),
rightPosition: timeToPosition(measure.width, boundaries, event.timestamp),
label: `load`,
type: event.type,
priority: 1,
}); });
} }
} }
}
bars.sort((a, b) => a.priority - b.priority);
return bars; return bars;
}, [context, boundaries, measure.width]); }, [context, boundaries, measure.width]);
const hoveredBar = hoveredBarIndex !== undefined ? bars[hoveredBarIndex] : undefined; const hoveredBar = hoveredBarIndex !== undefined ? bars[hoveredBarIndex] : undefined;
let targetBar: TimelineBar | undefined = bars.find(bar => bar.entry === (highlightedAction || selectedAction)); let targetBar: TimelineBar | undefined = bars.find(bar => bar.action === (highlightedAction || selectedAction));
targetBar = hoveredBar || targetBar; targetBar = hoveredBar || targetBar;
const findHoveredBarIndex = (x: number) => { const findHoveredBarIndex = (x: number) => {
@ -150,7 +123,7 @@ export const Timeline: React.FunctionComponent<{
setPreviewPoint({ x, clientY: event.clientY }); setPreviewPoint({ x, clientY: event.clientY });
setHoveredBarIndex(index); setHoveredBarIndex(index);
if (typeof index === 'number') if (typeof index === 'number')
onHighlighted(bars[index].entry); onHighlighted(bars[index].action);
}; };
const onMouseLeave = () => { const onMouseLeave = () => {
@ -167,7 +140,7 @@ export const Timeline: React.FunctionComponent<{
const index = findHoveredBarIndex(x); const index = findHoveredBarIndex(x);
if (index === undefined) if (index === undefined)
return; return;
const entry = bars[index].entry; const entry = bars[index].action;
if (entry) if (entry)
onSelected(entry); onSelected(entry);
}; };
@ -183,7 +156,7 @@ export const Timeline: React.FunctionComponent<{
<div className='timeline-lane timeline-labels'>{ <div className='timeline-lane timeline-labels'>{
bars.map((bar, index) => { bars.map((bar, index) => {
return <div key={index} return <div key={index}
className={'timeline-label ' + bar.type + (targetBar === bar ? ' selected' : '')} className={'timeline-label ' + bar.className + (targetBar === bar ? ' selected' : '')}
style={{ style={{
left: bar.leftPosition + 'px', left: bar.leftPosition + 'px',
width: Math.max(1, bar.rightPosition - bar.leftPosition) + 'px', width: Math.max(1, bar.rightPosition - bar.leftPosition) + 'px',
@ -196,7 +169,7 @@ export const Timeline: React.FunctionComponent<{
<div className='timeline-lane timeline-bars'>{ <div className='timeline-lane timeline-bars'>{
bars.map((bar, index) => { bars.map((bar, index) => {
return <div key={index} return <div key={index}
className={'timeline-bar ' + bar.type + (targetBar === bar ? ' selected' : '')} className={'timeline-bar ' + (bar.action ? 'action ' : '') + (bar.event ? 'event ' : '') + bar.className + (targetBar === bar ? ' selected' : '')}
style={{ style={{
left: bar.leftPosition + 'px', left: bar.leftPosition + 'px',
width: Math.max(1, bar.rightPosition - bar.leftPosition) + 'px', width: Math.max(1, bar.rightPosition - bar.leftPosition) + 'px',

View file

@ -14,7 +14,8 @@
limitations under the License. limitations under the License.
*/ */
import { ActionEntry, ContextEntry } from '../../../server/trace/viewer/traceModel'; import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
import { ContextEntry } from '../../../server/trace/viewer/traceModel';
import { ActionList } from './actionList'; import { ActionList } from './actionList';
import { TabbedPane } from './tabbedPane'; import { TabbedPane } from './tabbedPane';
import { Timeline } from './timeline'; import { Timeline } from './timeline';
@ -33,20 +34,21 @@ export const Workbench: React.FunctionComponent<{
debugNames: string[], debugNames: string[],
}> = ({ debugNames }) => { }> = ({ debugNames }) => {
const [debugName, setDebugName] = React.useState(debugNames[0]); const [debugName, setDebugName] = React.useState(debugNames[0]);
const [selectedAction, setSelectedAction] = React.useState<ActionEntry | undefined>(); const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>();
const [highlightedAction, setHighlightedAction] = React.useState<ActionEntry | undefined>(); const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
let context = useAsyncMemo(async () => { let context = useAsyncMemo(async () => {
return (await fetch(`/context/${debugName}`).then(response => response.json())) as ContextEntry; return (await fetch(`/context/${debugName}`).then(response => response.json())) as ContextEntry;
}, [debugName], emptyContext); }, [debugName], emptyContext);
const actions = React.useMemo(() => { const { actions, nextAction } = React.useMemo(() => {
const actions: ActionEntry[] = []; const actions: ActionTraceEvent[] = [];
for (const page of context.pages) for (const page of context.pages)
actions.push(...page.actions); actions.push(...page.actions);
actions.sort((a, b) => a.timestamp - b.timestamp); actions.sort((a, b) => a.timestamp - b.timestamp);
return actions; const nextAction = selectedAction ? actions[actions.indexOf(selectedAction) + 1] : undefined;
}, [context]); return { actions, nextAction };
}, [context, selectedAction]);
const snapshotSize = context.created.viewportSize || { width: 1280, height: 720 }; const snapshotSize = context.created.viewportSize || { width: 1280, height: 720 };
const boundaries = { minimum: context.startTime, maximum: context.endTime }; const boundaries = { minimum: context.startTime, maximum: context.endTime };
@ -77,11 +79,11 @@ export const Workbench: React.FunctionComponent<{
</div> </div>
<SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}> <SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
<SplitView sidebarSize={300} orientation='horizontal'> <SplitView sidebarSize={300} orientation='horizontal'>
<SnapshotTab actionEntry={selectedAction} snapshotSize={snapshotSize} /> <SnapshotTab action={selectedAction} snapshotSize={snapshotSize} />
<TabbedPane tabs={[ <TabbedPane tabs={[
{ id: 'logs', title: 'Log', render: () => <LogsTab actionEntry={selectedAction} /> }, { id: 'logs', title: 'Log', render: () => <LogsTab action={selectedAction} /> },
{ id: 'source', title: 'Source', render: () => <SourceTab actionEntry={selectedAction} /> }, { id: 'source', title: 'Source', render: () => <SourceTab action={selectedAction} /> },
{ id: 'network', title: 'Network', render: () => <NetworkTab actionEntry={selectedAction} /> }, { id: 'network', title: 'Network', render: () => <NetworkTab context={context} action={selectedAction} nextAction={nextAction}/> },
]}/> ]}/>
</SplitView> </SplitView>
<ActionList <ActionList
@ -110,5 +112,6 @@ const emptyContext: ContextEntry = {
viewportSize: { width: 1280, height: 800 }, viewportSize: { width: 1280, height: 800 },
debugName: '<empty>', debugName: '<empty>',
}, },
pages: [] pages: [],
resources: []
}; };

View file

@ -147,7 +147,7 @@ DEPS['src/cli/driver.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerIm
// Tracing is a client/server plugin, nothing should depend on it. // Tracing is a client/server plugin, nothing should depend on it.
DEPS['src/web/recorder/'] = ['src/common/', 'src/web/', 'src/web/components/', 'src/server/supplements/recorder/recorderTypes.ts']; DEPS['src/web/recorder/'] = ['src/common/', 'src/web/', 'src/web/components/', 'src/server/supplements/recorder/recorderTypes.ts'];
DEPS['src/web/traceViewer/'] = ['src/common/', 'src/web/']; DEPS['src/web/traceViewer/'] = ['src/common/', 'src/web/'];
DEPS['src/web/traceViewer/ui/'] = ['src/common/', 'src/web/traceViewer/', 'src/web/', 'src/server/trace/viewer/', 'src/server/trace/', 'src/server/trace/common/', 'src/server/snapshot/snapshotTypes.ts']; DEPS['src/web/traceViewer/ui/'] = ['src/common/', 'src/web/traceViewer/', 'src/web/', 'src/server/trace/viewer/', 'src/server/trace/', 'src/server/trace/common/', 'src/server/snapshot/snapshotTypes.ts', 'src/protocol/channels.ts'];
// The service is a cross-cutting feature, and so it depends on a bunch of things. // The service is a cross-cutting feature, and so it depends on a bunch of things.
DEPS['src/remote/'] = ['src/client/', 'src/debug/', 'src/dispatchers/', 'src/server/', 'src/server/supplements/', 'src/server/electron/', 'src/server/trace/']; DEPS['src/remote/'] = ['src/client/', 'src/debug/', 'src/dispatchers/', 'src/server/', 'src/server/supplements/', 'src/server/electron/', 'src/server/trace/'];