| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- 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))
- );
- });
- }
|