2021-07-01 20:46:56 -07:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
2022-10-18 22:23:40 -04:00
|
|
|
import type { Language } from '@isomorphic/locatorGenerators';
|
2022-09-20 18:41:51 -07:00
|
|
|
import type { ResourceSnapshot } from '@trace/snapshot';
|
|
|
|
import type * as trace from '@trace/trace';
|
2023-02-27 15:29:20 -08:00
|
|
|
import type { ActionTraceEvent, EventTraceEvent } from '@trace/trace';
|
2022-03-25 13:12:00 -08:00
|
|
|
import type { ContextEntry, PageEntry } from '../entries';
|
2021-07-01 20:46:56 -07:00
|
|
|
|
|
|
|
const contextSymbol = Symbol('context');
|
2023-03-17 20:20:35 -07:00
|
|
|
const nextInContextSymbol = Symbol('next');
|
|
|
|
const prevInListSymbol = Symbol('prev');
|
2021-07-01 20:46:56 -07:00
|
|
|
const eventsSymbol = Symbol('events');
|
2021-07-02 14:33:38 -07:00
|
|
|
const resourcesSymbol = Symbol('resources');
|
2021-07-01 20:46:56 -07:00
|
|
|
|
2023-05-08 18:51:27 -07:00
|
|
|
export type SourceLocation = {
|
|
|
|
file: string;
|
|
|
|
line: number;
|
|
|
|
source: SourceModel;
|
|
|
|
};
|
|
|
|
|
2023-03-16 10:01:17 -07:00
|
|
|
export type SourceModel = {
|
2023-05-08 18:51:27 -07:00
|
|
|
errors: { line: number, message: string }[];
|
2023-03-16 10:01:17 -07:00
|
|
|
content: string | undefined;
|
|
|
|
};
|
|
|
|
|
2023-05-19 15:18:18 -07:00
|
|
|
export type ActionTraceEventInContext = ActionTraceEvent & {
|
|
|
|
context: ContextEntry;
|
|
|
|
};
|
|
|
|
|
2023-07-05 11:20:28 -07:00
|
|
|
export type ActionTreeItem = {
|
|
|
|
id: string;
|
|
|
|
children: ActionTreeItem[];
|
|
|
|
parent: ActionTreeItem | undefined;
|
|
|
|
action?: ActionTraceEventInContext;
|
|
|
|
};
|
|
|
|
|
2022-02-08 12:27:29 -08:00
|
|
|
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[];
|
2023-05-19 15:18:18 -07:00
|
|
|
readonly actions: ActionTraceEventInContext[];
|
2023-02-27 15:29:20 -08:00
|
|
|
readonly events: trace.EventTraceEvent[];
|
2023-07-10 18:36:28 -07:00
|
|
|
readonly stdio: trace.StdioTraceEvent[];
|
2022-02-08 12:27:29 -08:00
|
|
|
readonly hasSource: boolean;
|
2022-10-18 22:23:40 -04:00
|
|
|
readonly sdkLanguage: Language | undefined;
|
2023-02-17 11:19:53 -08:00
|
|
|
readonly testIdAttributeName: string | undefined;
|
2023-03-16 10:01:17 -07:00
|
|
|
readonly sources: Map<string, SourceModel>;
|
2023-07-10 12:56:56 -07:00
|
|
|
resources: ResourceSnapshot[];
|
2023-03-16 10:01:17 -07:00
|
|
|
|
2022-02-08 12:27:29 -08:00
|
|
|
|
|
|
|
constructor(contexts: ContextEntry[]) {
|
|
|
|
contexts.forEach(contextEntry => indexModel(contextEntry));
|
|
|
|
|
|
|
|
this.browserName = contexts[0]?.browserName || '';
|
2022-10-18 22:23:40 -04:00
|
|
|
this.sdkLanguage = contexts[0]?.sdkLanguage;
|
2023-02-17 11:19:53 -08:00
|
|
|
this.testIdAttributeName = contexts[0]?.testIdAttributeName;
|
2022-02-08 12:27:29 -08:00
|
|
|
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));
|
2023-05-05 15:12:18 -07:00
|
|
|
this.actions = mergeActions(contexts);
|
2023-02-27 15:29:20 -08:00
|
|
|
this.events = ([] as EventTraceEvent[]).concat(...contexts.map(c => c.events));
|
2023-07-10 18:36:28 -07:00
|
|
|
this.stdio = ([] as trace.StdioTraceEvent[]).concat(...contexts.map(c => c.stdio));
|
2022-02-08 12:27:29 -08:00
|
|
|
this.hasSource = contexts.some(c => c.hasSource);
|
2023-07-10 12:56:56 -07:00
|
|
|
this.resources = [...contexts.map(c => c.resources)].flat();
|
2022-02-08 12:27:29 -08:00
|
|
|
|
2023-02-27 15:29:20 -08:00
|
|
|
this.events.sort((a1, a2) => a1.time - a2.time);
|
2023-03-16 10:01:17 -07:00
|
|
|
this.sources = collectSources(this.actions);
|
2022-02-08 12:27:29 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function indexModel(context: ContextEntry) {
|
2021-10-15 14:22:49 -08:00
|
|
|
for (const page of context.pages)
|
2021-07-01 20:46:56 -07:00
|
|
|
(page as any)[contextSymbol] = context;
|
2021-10-15 14:22:49 -08:00
|
|
|
for (let i = 0; i < context.actions.length; ++i) {
|
|
|
|
const action = context.actions[i] as any;
|
|
|
|
action[contextSymbol] = context;
|
2023-06-06 17:38:44 -07:00
|
|
|
}
|
|
|
|
let lastNonRouteAction = undefined;
|
|
|
|
for (let i = context.actions.length - 1; i >= 0; i--) {
|
|
|
|
const action = context.actions[i] as any;
|
|
|
|
action[nextInContextSymbol] = lastNonRouteAction;
|
|
|
|
if (!action.apiName.includes('route.'))
|
|
|
|
lastNonRouteAction = action;
|
2021-07-01 20:46:56 -07:00
|
|
|
}
|
2021-10-15 14:22:49 -08:00
|
|
|
for (const event of context.events)
|
|
|
|
(event as any)[contextSymbol] = context;
|
2021-07-01 20:46:56 -07:00
|
|
|
}
|
|
|
|
|
2023-05-05 15:12:18 -07:00
|
|
|
function mergeActions(contexts: ContextEntry[]) {
|
2023-05-19 15:18:18 -07:00
|
|
|
const map = new Map<string, ActionTraceEventInContext>();
|
2023-05-05 15:12:18 -07:00
|
|
|
|
|
|
|
// Protocol call aka isPrimary contexts have startTime/endTime as server-side times.
|
|
|
|
// Step aka non-isPrimary contexts have startTime/endTime are client-side times.
|
|
|
|
// Adjust expect startTime/endTime on non-primary contexts to put them on a single timeline.
|
|
|
|
let offset = 0;
|
|
|
|
const primaryContexts = contexts.filter(context => context.isPrimary);
|
|
|
|
const nonPrimaryContexts = contexts.filter(context => !context.isPrimary);
|
|
|
|
|
|
|
|
for (const context of primaryContexts) {
|
|
|
|
for (const action of context.actions)
|
2023-05-19 15:18:18 -07:00
|
|
|
map.set(`${action.apiName}@${action.wallTime}`, { ...action, context });
|
2023-05-05 15:12:18 -07:00
|
|
|
if (!offset && context.actions.length)
|
|
|
|
offset = context.actions[0].startTime - context.actions[0].wallTime;
|
2023-02-28 13:26:23 -08:00
|
|
|
}
|
2023-05-05 15:12:18 -07:00
|
|
|
|
2023-06-06 17:38:44 -07:00
|
|
|
const nonPrimaryIdToPrimaryId = new Map<string, string>();
|
2023-05-05 15:12:18 -07:00
|
|
|
for (const context of nonPrimaryContexts) {
|
|
|
|
for (const action of context.actions) {
|
|
|
|
if (offset) {
|
|
|
|
const duration = action.endTime - action.startTime;
|
|
|
|
if (action.startTime)
|
|
|
|
action.startTime = action.wallTime + offset;
|
|
|
|
if (action.endTime)
|
|
|
|
action.endTime = action.startTime + duration;
|
|
|
|
}
|
|
|
|
|
2023-05-06 10:25:32 -07:00
|
|
|
const key = `${action.apiName}@${action.wallTime}`;
|
|
|
|
const existing = map.get(key);
|
2023-05-05 15:12:18 -07:00
|
|
|
if (existing && existing.apiName === action.apiName) {
|
2023-06-06 17:38:44 -07:00
|
|
|
nonPrimaryIdToPrimaryId.set(action.callId, existing.callId);
|
2023-05-05 15:12:18 -07:00
|
|
|
if (action.error)
|
|
|
|
existing.error = action.error;
|
|
|
|
if (action.attachments)
|
|
|
|
existing.attachments = action.attachments;
|
2023-05-09 14:50:28 -07:00
|
|
|
if (action.parentId)
|
2023-06-06 17:38:44 -07:00
|
|
|
existing.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId;
|
2023-05-05 15:12:18 -07:00
|
|
|
continue;
|
|
|
|
}
|
2023-06-14 09:37:19 -07:00
|
|
|
if (action.parentId)
|
|
|
|
action.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId;
|
2023-05-19 15:18:18 -07:00
|
|
|
map.set(key, { ...action, context });
|
2023-02-28 13:26:23 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-05 15:12:18 -07:00
|
|
|
const result = [...map.values()];
|
2023-05-12 19:15:31 -07:00
|
|
|
result.sort((a1, a2) => {
|
|
|
|
if (a2.parentId === a1.callId)
|
|
|
|
return -1;
|
|
|
|
if (a1.parentId === a2.callId)
|
|
|
|
return 1;
|
|
|
|
return a1.wallTime - a2.wallTime || a1.startTime - a2.startTime;
|
|
|
|
});
|
|
|
|
|
2023-03-17 20:20:35 -07:00
|
|
|
for (let i = 1; i < result.length; ++i)
|
|
|
|
(result[i] as any)[prevInListSymbol] = result[i - 1];
|
2023-05-05 15:12:18 -07:00
|
|
|
|
2023-03-17 20:20:35 -07:00
|
|
|
return result;
|
2023-02-28 13:26:23 -08:00
|
|
|
}
|
|
|
|
|
2023-07-05 11:20:28 -07:00
|
|
|
export function buildActionTree(actions: ActionTraceEventInContext[]): { rootItem: ActionTreeItem, itemMap: Map<string, ActionTreeItem> } {
|
|
|
|
const itemMap = new Map<string, ActionTreeItem>();
|
|
|
|
|
|
|
|
for (const action of actions) {
|
|
|
|
itemMap.set(action.callId, {
|
|
|
|
id: action.callId,
|
|
|
|
parent: undefined,
|
|
|
|
children: [],
|
|
|
|
action,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const rootItem: ActionTreeItem = { id: '', parent: undefined, children: [] };
|
|
|
|
for (const item of itemMap.values()) {
|
|
|
|
const parent = item.action!.parentId ? itemMap.get(item.action!.parentId) || rootItem : rootItem;
|
|
|
|
parent.children.push(item);
|
|
|
|
item.parent = parent;
|
|
|
|
}
|
|
|
|
return { rootItem, itemMap };
|
|
|
|
}
|
|
|
|
|
2023-03-31 18:34:51 -07:00
|
|
|
export function idForAction(action: ActionTraceEvent) {
|
|
|
|
return `${action.pageId || 'none'}:${action.callId}`;
|
|
|
|
}
|
|
|
|
|
2023-07-10 12:56:56 -07:00
|
|
|
export function context(action: ActionTraceEvent | EventTraceEvent): ContextEntry {
|
2021-07-01 20:46:56 -07:00
|
|
|
return (action as any)[contextSymbol];
|
|
|
|
}
|
|
|
|
|
2023-03-17 20:20:35 -07:00
|
|
|
function nextInContext(action: ActionTraceEvent): ActionTraceEvent {
|
|
|
|
return (action as any)[nextInContextSymbol];
|
|
|
|
}
|
|
|
|
|
|
|
|
export function prevInList(action: ActionTraceEvent): ActionTraceEvent {
|
|
|
|
return (action as any)[prevInListSymbol];
|
2021-07-01 20:46:56 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export function stats(action: ActionTraceEvent): { errors: number, warnings: number } {
|
|
|
|
let errors = 0;
|
|
|
|
let warnings = 0;
|
2021-10-15 14:22:49 -08:00
|
|
|
const c = context(action);
|
2021-07-01 20:46:56 -07:00
|
|
|
for (const event of eventsForAction(action)) {
|
2023-02-27 15:29:20 -08:00
|
|
|
if (event.method === 'console') {
|
|
|
|
const { guid } = event.params.message;
|
|
|
|
const type = c.initializers[guid]?.type;
|
2021-07-01 20:46:56 -07:00
|
|
|
if (type === 'warning')
|
|
|
|
++warnings;
|
|
|
|
else if (type === 'error')
|
|
|
|
++errors;
|
|
|
|
}
|
2023-02-27 15:29:20 -08:00
|
|
|
if (event.method === 'pageError')
|
2021-07-01 20:46:56 -07:00
|
|
|
++errors;
|
|
|
|
}
|
|
|
|
return { errors, warnings };
|
|
|
|
}
|
|
|
|
|
2023-02-27 15:29:20 -08:00
|
|
|
export function eventsForAction(action: ActionTraceEvent): EventTraceEvent[] {
|
|
|
|
let result: EventTraceEvent[] = (action as any)[eventsSymbol];
|
2021-07-01 20:46:56 -07:00
|
|
|
if (result)
|
|
|
|
return result;
|
|
|
|
|
2023-03-17 20:20:35 -07:00
|
|
|
const nextAction = nextInContext(action);
|
2021-10-15 14:22:49 -08:00
|
|
|
result = context(action).events.filter(event => {
|
2023-02-27 15:29:20 -08:00
|
|
|
return event.time >= action.startTime && (!nextAction || event.time < nextAction.startTime);
|
2021-07-01 20:46:56 -07:00
|
|
|
});
|
|
|
|
(action as any)[eventsSymbol] = result;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function resourcesForAction(action: ActionTraceEvent): ResourceSnapshot[] {
|
2021-07-02 14:33:38 -07:00
|
|
|
let result: ResourceSnapshot[] = (action as any)[resourcesSymbol];
|
|
|
|
if (result)
|
|
|
|
return result;
|
|
|
|
|
2023-03-17 20:20:35 -07:00
|
|
|
const nextAction = nextInContext(action);
|
2021-07-02 14:33:38 -07:00
|
|
|
result = context(action).resources.filter(resource => {
|
2023-02-27 15:29:20 -08:00
|
|
|
return typeof resource._monotonicTime === 'number' && resource._monotonicTime > action.startTime && (!nextAction || resource._monotonicTime < nextAction.startTime);
|
2021-07-01 20:46:56 -07:00
|
|
|
});
|
2021-07-02 14:33:38 -07:00
|
|
|
(action as any)[resourcesSymbol] = result;
|
|
|
|
return result;
|
2021-07-01 20:46:56 -07:00
|
|
|
}
|
2023-03-16 10:01:17 -07:00
|
|
|
|
|
|
|
function collectSources(actions: trace.ActionTraceEvent[]): Map<string, SourceModel> {
|
|
|
|
const result = new Map<string, SourceModel>();
|
|
|
|
for (const action of actions) {
|
|
|
|
for (const frame of action.stack || []) {
|
|
|
|
let source = result.get(frame.file);
|
|
|
|
if (!source) {
|
|
|
|
source = { errors: [], content: undefined };
|
|
|
|
result.set(frame.file, source);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (action.error && action.stack?.[0])
|
2023-05-08 18:51:27 -07:00
|
|
|
result.get(action.stack[0].file)!.errors.push({ line: action.stack?.[0].line || 0, message: action.error.message });
|
2023-03-16 10:01:17 -07:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|