Files
Surge_by_SukkaW/Build/trace/index.ts
2026-03-31 20:53:36 +08:00

238 lines
7.1 KiB
TypeScript

import { isCI } from 'ci-info';
import { noop } from 'foxts/noop';
import { basename, extname } from 'node:path';
import process from 'node:process';
import picocolors from 'picocolors';
export const SPAN_STATUS_START = 0;
export const SPAN_STATUS_END = 1;
const spanTag = Symbol('span');
export interface TraceResult {
name: string,
start: number,
end: number,
children: TraceResult[]
}
/** Pure data object — safe to transfer across Worker Thread boundaries. */
export interface RawSpan {
traceResult: TraceResult,
status: typeof SPAN_STATUS_START | typeof SPAN_STATUS_END
}
export interface Span {
[spanTag]: true,
readonly stop: (time?: number) => void,
readonly traceChild: (name: string) => Span,
readonly traceSyncFn: <T>(fn: (span: Span) => T) => T,
readonly traceAsyncFn: <T>(fn: (span: Span) => T | Promise<T>) => Promise<T>,
readonly tracePromise: <T>(promise: Promise<T>) => Promise<T>,
readonly traceChildSync: <T>(name: string, fn: (span: Span) => T) => T,
readonly traceChildAsync: <T>(name: string, fn: (span: Span) => Promise<T>) => Promise<T>,
readonly traceChildPromise: <T>(name: string, promise: Promise<T>) => Promise<T>,
readonly traceResult: TraceResult
}
/**
* Wraps a serializable {@link RawSpan} with all span methods.
* Use this on a worker thread after receiving a {@link RawSpan} (or {@link TraceResult})
* transferred from another thread.
*/
export function makeSpan(rawSpan: RawSpan): Span {
const { traceResult } = rawSpan;
const stop = (time?: number) => {
if (rawSpan.status === SPAN_STATUS_END) {
throw new Error(`span already stopped: ${traceResult.name}`);
}
traceResult.end = time ?? performance.now();
rawSpan.status = SPAN_STATUS_END;
};
const traceChild = (name: string) => createSpan(name, traceResult);
const span: Span = {
[spanTag]: true,
stop,
traceChild,
traceSyncFn<T>(fn: (span: Span) => T) {
const res = fn(span);
span.stop();
return res;
},
async traceAsyncFn<T>(fn: (span: Span) => T | Promise<T>): Promise<T> {
const res = await fn(span);
span.stop();
return res;
},
traceResult,
async tracePromise<T>(promise: Promise<T>): Promise<T> {
const res = await promise;
span.stop();
return res;
},
traceChildSync: <T>(name: string, fn: (span: Span) => T): T => traceChild(name).traceSyncFn(fn),
traceChildAsync: <T>(name: string, fn: (span: Span) => T | Promise<T>): Promise<T> => traceChild(name).traceAsyncFn(fn),
traceChildPromise: <T>(name: string, promise: Promise<T>): Promise<T> => traceChild(name).tracePromise(promise)
};
// eslint-disable-next-line sukka/no-redundant-variable -- self reference
return span;
}
export function createSpan(name: string, parentTraceResult?: TraceResult): Span {
const rawSpan: RawSpan = {
traceResult: {
name,
start: performance.now(),
end: 0,
children: []
},
status: SPAN_STATUS_START
};
parentTraceResult?.children.push(rawSpan.traceResult);
return makeSpan(rawSpan);
}
export const dummySpan = createSpan('dummy');
export function task(importMetaMain: boolean, importMetaPath: string) {
return (fn: (span: Span, onCleanup: (cb: () => Promise<void> | void) => void) => Promise<unknown>, customName?: string) => {
const taskName = customName ?? basename(importMetaPath, extname(importMetaPath));
let cleanup: () => Promise<void> | void = noop;
const onCleanup = (cb: () => void) => {
cleanup = cb;
};
if (importMetaMain) {
const innerSpan = createSpan(taskName);
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection:', reason);
process.exit(1);
});
innerSpan.traceChildAsync('dummy', (childSpan) => fn(childSpan, onCleanup)).finally(() => {
innerSpan.stop();
printTraceResult(innerSpan.traceResult);
process.nextTick(whyIsNodeRunning);
process.nextTick(() => process.exit(0));
});
}
let runSpan: Span;
async function run(parentSpan?: Span | null): Promise<TraceResult> {
if (parentSpan) {
runSpan = parentSpan.traceChild(taskName);
} else {
runSpan = createSpan(taskName);
}
try {
await fn(runSpan, onCleanup);
} finally {
runSpan.stop();
cleanup();
}
return runSpan.traceResult;
}
return Object.assign(run, {
getInternalTraceResult: () => runSpan.traceResult
});
};
}
export async function whyIsNodeRunning() {
if (isCI && process.env.RUNNER_DEBUG === '1') {
const mod = await import('why-is-node-running');
return mod.default();
}
}
// const isSpan = (obj: any): obj is Span => {
// return typeof obj === 'object' && obj && spanTag in obj;
// };
// export const universalify = <A extends any[], R>(taskname: string, fn: (this: void, ...args: A) => R) => {
// return (...args: A) => {
// const lastArg = args[args.length - 1];
// if (isSpan(lastArg)) {
// return lastArg.traceChild(taskname).traceSyncFn(() => fn(...args));
// }
// return fn(...args);
// };
// };
export function printTraceResult(traceResult: TraceResult) {
printTree(
traceResult,
node => {
if (node.end - node.start < 0) {
return node.name;
}
return `${node.name} ${picocolors.bold(`${(node.end - node.start).toFixed(3)}ms`)}`;
}
);
}
function printTree(initialTree: TraceResult, printNode: (node: TraceResult, branch: string) => string) {
function printBranch(tree: TraceResult, branch: string, isGraphHead: boolean, isChildOfLastBranch: boolean) {
const children = tree.children;
let branchHead = '';
if (!isGraphHead) {
branchHead = children.length > 0 ? '┬ ' : '─ ';
}
const toPrint = printNode(tree, `${branch}${branchHead}`);
if (typeof toPrint === 'string') {
console.log(`${branch}${branchHead}${toPrint}`);
}
let baseBranch = branch;
if (!isGraphHead) {
baseBranch = branch.slice(0, -2) + (isChildOfLastBranch ? ' ' : '│ ');
}
const nextBranch = `${baseBranch}├─`;
const lastBranch = `${baseBranch}└─`;
children.forEach((child, index) => {
const last = children.length - 1 === index;
printBranch(child, last ? lastBranch : nextBranch, false, last);
});
}
printBranch(initialTree, '', true, false);
}
export function printStats(stats: TraceResult[]): void {
const longestTaskName = Math.max(...stats.map(i => i.name.length));
const realStart = Math.min(...stats.map(i => i.start));
const realEnd = Math.max(...stats.map(i => i.end));
const statsStep = ((realEnd - realStart) / 120) | 0;
stats
.sort((a, b) => a.start - b.start)
.forEach(stat => {
console.log(
`[${stat.name}]${' '.repeat(longestTaskName - stat.name.length)}`,
' '.repeat(((stat.start - realStart) / statsStep) | 0),
'='.repeat(Math.max(((stat.end - stat.start) / statsStep) | 0, 1))
);
});
}