mirror of
https://github.com/SukkaW/Surge.git
synced 2026-09-13 02:54:38 +08:00
Chore: event loop wall build time tracking
This commit is contained in:
@@ -3,18 +3,26 @@ import { expect } from 'earl';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
|
||||
import { analyzeTraces } from './report';
|
||||
import type { CategoryStat, TraceAnalysis } from './report';
|
||||
import { SpanCategory, UNCATEGORIZED } from './types';
|
||||
import type { TraceResult } from './types';
|
||||
import type { CategoryStat, TraceAnalysis } from './report';
|
||||
|
||||
function span(
|
||||
name: string,
|
||||
start: number,
|
||||
end: number,
|
||||
extra: Partial<Pick<TraceResult, 'category' | 'thread' | 'sync' | 'timeOrigin'>> = {},
|
||||
children: TraceResult[] = []
|
||||
): TraceResult {
|
||||
return { name, start, end, thread: 0, children, ...extra };
|
||||
interface SpanExtra extends Partial<Pick<TraceResult, 'category' | 'thread' | 'sync' | 'timeOrigin'>> {
|
||||
/** cumulative loop idle (ms) at start and end; defaults to "loop never idle" */
|
||||
idle?: [atStart: number, atEnd: number]
|
||||
}
|
||||
|
||||
function span(name: string, start: number, end: number, extra: SpanExtra = {}, children: TraceResult[] = []): TraceResult {
|
||||
const { idle, ...rest } = extra;
|
||||
return {
|
||||
name,
|
||||
start,
|
||||
end,
|
||||
thread: 0,
|
||||
children,
|
||||
loopIdle: { atStart: idle?.[0] ?? 0, atEnd: idle?.[1] ?? 0 },
|
||||
...rest
|
||||
};
|
||||
}
|
||||
|
||||
function indexByCategory(analysis: TraceAnalysis) {
|
||||
@@ -24,42 +32,65 @@ function indexByCategory(analysis: TraceAnalysis) {
|
||||
}, {});
|
||||
}
|
||||
|
||||
function mainThread(analysis: TraceAnalysis) {
|
||||
return analysis.threads.find(t => t.thread === 0)!;
|
||||
}
|
||||
|
||||
describe('trace report analysis', () => {
|
||||
it('attributes self time as duration minus the union of (overlapping) children', () => {
|
||||
it('hands every instant of a thread out exactly once, so category totals add up to wall', () => {
|
||||
// Two concurrent downloads (10-50, 30-70) and a sync compute (80-90) under one
|
||||
// task. The loop idled 40ms in total, all of it while the downloads were in flight.
|
||||
const task = span('task', 0, 100, { idle: [0, 40] }, [
|
||||
span('a', 10, 50, { category: SpanCategory.Network, idle: [0, 20] }),
|
||||
span('b', 30, 70, { category: SpanCategory.Network, idle: [10, 40] }),
|
||||
span('c', 80, 90, { category: SpanCategory.Compute, sync: true, idle: [40, 40] })
|
||||
]);
|
||||
|
||||
const analysis = analyzeTraces([task]);
|
||||
const main = mainThread(analysis);
|
||||
const byCategory = indexByCategory(analysis);
|
||||
|
||||
expect(analysis.wall).toEqual(100);
|
||||
expect(main.attributed.cpu + main.attributed.wait).toEqual(100);
|
||||
expect(main.attributed.wait).toEqual(40);
|
||||
|
||||
// downloads were innermost for 60ms of wall (not 80ms: the overlap is shared, not doubled)
|
||||
const network = byCategory[SpanCategory.Network].main;
|
||||
expect(network.cpu + network.wait).toEqual(60);
|
||||
expect(network.wait).toEqual(40);
|
||||
expect(byCategory[SpanCategory.Network].coverage).toEqual(60);
|
||||
|
||||
expect(byCategory[SpanCategory.Compute].main).toEqual({ cpu: 10, wait: 0 });
|
||||
// the task itself is innermost for 0-10, 70-80 and 90-100
|
||||
expect(byCategory[UNCATEGORIZED].main).toEqual({ cpu: 30, wait: 0 });
|
||||
|
||||
// the equal split: 30-50 is shared by a and b, 10-30 is a's alone, 50-70 is b's alone
|
||||
expect(analysis.attribution.get(task.children[0])!.cpu + analysis.attribution.get(task.children[0])!.wait).toEqual(30);
|
||||
expect(analysis.attribution.get(task.children[1])!.cpu + analysis.attribution.get(task.children[1])!.wait).toEqual(30);
|
||||
});
|
||||
|
||||
it('gives a synchronous span the whole segment even when async spans are in flight', () => {
|
||||
const task = span('task', 0, 100, {}, [
|
||||
// two async children that overlap: 10-50 and 30-70 cover 60ms, not 80ms
|
||||
span('a', 10, 50, { category: SpanCategory.Network }),
|
||||
span('b', 30, 70, { category: SpanCategory.Network }),
|
||||
span('c', 80, 90, { category: SpanCategory.Compute, sync: true })
|
||||
span('download', 0, 100, { category: SpanCategory.Network }),
|
||||
span('parse', 40, 60, { category: SpanCategory.Compute, sync: true })
|
||||
]);
|
||||
|
||||
const analysis = analyzeTraces([task]);
|
||||
const byCategory = indexByCategory(analysis);
|
||||
|
||||
expect(analysis.wall).toEqual(100);
|
||||
expect(byCategory[UNCATEGORIZED].selfTotal).toEqual(100 - 60 - 10);
|
||||
// self of leaves is their full duration, summed across the concurrent pair
|
||||
expect(byCategory[SpanCategory.Network].selfTotal).toEqual(40 + 40);
|
||||
// coverage de-duplicates the overlap
|
||||
expect(byCategory[SpanCategory.Network].coverage).toEqual(60);
|
||||
expect(byCategory[SpanCategory.Compute].selfTotal).toEqual(10);
|
||||
expect(byCategory[SpanCategory.Compute].selfSync).toEqual(10);
|
||||
expect(byCategory[SpanCategory.Network].selfSync).toEqual(0);
|
||||
|
||||
expect(analysis.tasks[0].byCategory[SpanCategory.Network]).toEqual(80);
|
||||
expect(analysis.tasks[0].byCategory[UNCATEGORIZED]).toEqual(30);
|
||||
expect(byCategory[SpanCategory.Compute].main).toEqual({ cpu: 20, wait: 0 });
|
||||
expect(byCategory[SpanCategory.Network].main).toEqual({ cpu: 80, wait: 0 });
|
||||
expect(byCategory[UNCATEGORIZED].main).toEqual({ cpu: 0, wait: 0 });
|
||||
});
|
||||
|
||||
it('clips children to the parent window and never reports negative self time', () => {
|
||||
// fire-and-forget child that outlives its parent
|
||||
const task = span('task', 0, 50, {}, [span('late', 40, 90, { category: SpanCategory.FsWrite })]);
|
||||
it('reports time with no span in flight as untraced', () => {
|
||||
const analysis = analyzeTraces([
|
||||
span('first', 0, 10),
|
||||
span('second', 30, 40)
|
||||
]);
|
||||
|
||||
const analysis = analyzeTraces([task]);
|
||||
const byCategory = indexByCategory(analysis);
|
||||
|
||||
expect(byCategory[UNCATEGORIZED].selfTotal).toEqual(40);
|
||||
expect(byCategory[SpanCategory.FsWrite].selfTotal).toEqual(50);
|
||||
expect(analysis.wall).toEqual(90);
|
||||
expect(mainThread(analysis).untraced).toEqual({ cpu: 20, wait: 0 });
|
||||
expect(analysis.wall).toEqual(40);
|
||||
});
|
||||
|
||||
it('skips unfinished spans but counts them', () => {
|
||||
@@ -68,29 +99,30 @@ describe('trace report analysis', () => {
|
||||
const analysis = analyzeTraces([task]);
|
||||
|
||||
expect(analysis.unfinishedSpans).toEqual(1);
|
||||
// the unfinished child does not eat into the parent's self time
|
||||
expect(analysis.categories.find(c => c.category === UNCATEGORIZED)!.selfTotal).toEqual(10);
|
||||
// the unfinished child does not steal the parent's time
|
||||
expect(indexByCategory(analysis)[UNCATEGORIZED].main).toEqual({ cpu: 10, wait: 0 });
|
||||
});
|
||||
|
||||
it('splits self time between the main thread and workers', () => {
|
||||
const task = span('task', 0, 100, {}, [
|
||||
span('offload', 0, 100, { category: SpanCategory.Worker }, [
|
||||
it('sweeps each thread on its own and keeps the main thread waiting on the worker', () => {
|
||||
const task = span('task', 0, 100, { idle: [0, 100] }, [
|
||||
span('offload', 0, 100, { category: SpanCategory.Worker, idle: [0, 100] }, [
|
||||
span('crunch', 20, 60, { category: SpanCategory.Compute, thread: 7, sync: true })
|
||||
])
|
||||
]);
|
||||
|
||||
const analysis = analyzeTraces([task]);
|
||||
const compute = analysis.categories.find(c => c.category === SpanCategory.Compute)!;
|
||||
const worker = analysis.categories.find(c => c.category === SpanCategory.Worker)!;
|
||||
const byCategory = indexByCategory(analysis);
|
||||
|
||||
expect(compute.selfOnWorkers).toEqual(40);
|
||||
expect(compute.selfOnMainThread).toEqual(0);
|
||||
expect(worker.selfOnMainThread).toEqual(60);
|
||||
// the child runs on another thread, so `offload` stays innermost on main for its full 100ms
|
||||
expect(byCategory[SpanCategory.Worker].main).toEqual({ cpu: 0, wait: 100 });
|
||||
expect(byCategory[SpanCategory.Compute].workers).toEqual({ cpu: 40, wait: 0 });
|
||||
expect(byCategory[SpanCategory.Compute].main).toEqual({ cpu: 0, wait: 0 });
|
||||
expect(analysis.threads.map(t => t.thread)).toEqual([0, 7]);
|
||||
});
|
||||
|
||||
it('shifts a trace produced on another thread onto the local clock', () => {
|
||||
// A worker whose clock started 1000ms after ours reports everything 1000ms early
|
||||
const workerTask = span('worker task', 0, 10, { timeOrigin: performance.timeOrigin + 1000 });
|
||||
const workerTask = span('worker task', 0, 10, { thread: 7, timeOrigin: performance.timeOrigin + 1000 });
|
||||
const mainTask = span('main task', 1000, 1010);
|
||||
|
||||
const analysis = analyzeTraces([workerTask, mainTask]);
|
||||
@@ -98,5 +130,7 @@ describe('trace report analysis', () => {
|
||||
expect(analysis.wallStart).toEqual(1000);
|
||||
expect(analysis.wallEnd).toEqual(1010);
|
||||
expect(analysis.tasks[0].node.start).toEqual(1000);
|
||||
// attribution is keyed by the shifted copy
|
||||
expect(analysis.attribution.get(analysis.traces[0])).toEqual({ cpu: 10, wait: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user