index.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. import { isCI } from 'ci-info';
  2. import { noop } from 'foxts/noop';
  3. import { basename, extname } from 'node:path';
  4. import process from 'node:process';
  5. import picocolors from 'picocolors';
  6. export const SPAN_STATUS_START = 0;
  7. export const SPAN_STATUS_END = 1;
  8. const spanTag = Symbol('span');
  9. export interface TraceResult {
  10. name: string,
  11. start: number,
  12. end: number,
  13. children: TraceResult[]
  14. }
  15. /** Pure data object — safe to transfer across Worker Thread boundaries. */
  16. export interface RawSpan {
  17. traceResult: TraceResult,
  18. status: typeof SPAN_STATUS_START | typeof SPAN_STATUS_END
  19. }
  20. export interface Span {
  21. [spanTag]: true,
  22. readonly rawSpan: RawSpan,
  23. readonly stop: (time?: number) => void,
  24. readonly traceChild: (name: string) => Span,
  25. readonly traceSyncFn: <T>(fn: (span: Span) => T) => T,
  26. readonly traceAsyncFn: <T>(fn: (span: Span) => T | Promise<T>) => Promise<T>,
  27. readonly tracePromise: <T>(promise: Promise<T>) => Promise<T>,
  28. readonly traceChildSync: <T>(name: string, fn: (span: Span) => T) => T,
  29. readonly traceChildAsync: <T>(name: string, fn: (span: Span) => Promise<T>) => Promise<T>,
  30. readonly traceChildPromise: <T>(name: string, promise: Promise<T>) => Promise<T>,
  31. readonly traceWorkerChild: <T>(name: string, factory: (rawSpan: RawSpan) => Promise<WorkerJobResult<T>>) => Promise<T>,
  32. readonly traceResult: TraceResult
  33. }
  34. /**
  35. * Wraps a serializable {@link RawSpan} with all span methods.
  36. * Use this on a worker thread after receiving a {@link RawSpan} (or {@link TraceResult})
  37. * transferred from another thread.
  38. */
  39. export function makeSpan(rawSpan: RawSpan): Span {
  40. const { traceResult } = rawSpan;
  41. const stop = (time?: number) => {
  42. if (rawSpan.status === SPAN_STATUS_END) {
  43. throw new Error(`span already stopped: ${traceResult.name}`);
  44. }
  45. traceResult.end = time ?? performance.now();
  46. rawSpan.status = SPAN_STATUS_END;
  47. };
  48. const traceChild = (name: string) => createSpan(name, traceResult);
  49. const span: Span = {
  50. [spanTag]: true,
  51. rawSpan,
  52. stop,
  53. traceChild,
  54. traceSyncFn<T>(fn: (span: Span) => T) {
  55. const res = fn(span);
  56. span.stop();
  57. return res;
  58. },
  59. async traceAsyncFn<T>(fn: (span: Span) => T | Promise<T>): Promise<T> {
  60. const res = await fn(span);
  61. span.stop();
  62. return res;
  63. },
  64. traceResult,
  65. async tracePromise<T>(promise: Promise<T>): Promise<T> {
  66. const res = await promise;
  67. span.stop();
  68. return res;
  69. },
  70. traceChildSync: <T>(name: string, fn: (span: Span) => T): T => traceChild(name).traceSyncFn(fn),
  71. traceChildAsync: <T>(name: string, fn: (span: Span) => T | Promise<T>): Promise<T> => traceChild(name).traceAsyncFn(fn),
  72. traceChildPromise: <T>(name: string, promise: Promise<T>): Promise<T> => traceChild(name).tracePromise(promise),
  73. async traceWorkerChild<T>(name: string, factory: (rawSpan: RawSpan) => Promise<WorkerJobResult<T>>): Promise<T> {
  74. const childSpan = traceChild(name);
  75. const { result, traceResult, workerTimeOrigin } = await factory(childSpan.rawSpan);
  76. mergeWorkerTrace(childSpan, traceResult, workerTimeOrigin);
  77. childSpan.stop();
  78. return result;
  79. }
  80. };
  81. // eslint-disable-next-line sukka/no-redundant-variable -- self reference
  82. return span;
  83. }
  84. export function createSpan(name: string, parentTraceResult?: TraceResult): Span {
  85. const rawSpan: RawSpan = {
  86. traceResult: {
  87. name,
  88. start: performance.now(),
  89. end: 0,
  90. children: []
  91. },
  92. status: SPAN_STATUS_START
  93. };
  94. parentTraceResult?.children.push(rawSpan.traceResult);
  95. return makeSpan(rawSpan);
  96. }
  97. export const dummySpan = createSpan('dummy');
  98. export function task(importMetaMain: boolean, importMetaPath: string) {
  99. return (fn: (span: Span, onCleanup: (cb: () => Promise<void> | void) => void) => Promise<unknown>, customName?: string) => {
  100. const taskName = customName ?? basename(importMetaPath, extname(importMetaPath));
  101. let cleanup: () => Promise<void> | void = noop;
  102. const onCleanup = (cb: () => void) => {
  103. cleanup = cb;
  104. };
  105. if (importMetaMain) {
  106. const innerSpan = createSpan(taskName);
  107. process.on('uncaughtException', (error) => {
  108. console.error('Uncaught exception:', error);
  109. process.exit(1);
  110. });
  111. process.on('unhandledRejection', (reason) => {
  112. console.error('Unhandled rejection:', reason);
  113. process.exit(1);
  114. });
  115. innerSpan.traceChildAsync('dummy', (childSpan) => fn(childSpan, onCleanup)).finally(() => {
  116. innerSpan.stop();
  117. printTraceResult(innerSpan.traceResult);
  118. process.nextTick(whyIsNodeRunning);
  119. process.nextTick(() => process.exit(0));
  120. });
  121. }
  122. let runSpan: Span;
  123. async function run(parentSpan?: Span | null): Promise<TraceResult> {
  124. if (parentSpan) {
  125. runSpan = parentSpan.traceChild(taskName);
  126. } else {
  127. runSpan = createSpan(taskName);
  128. }
  129. try {
  130. await fn(runSpan, onCleanup);
  131. } finally {
  132. runSpan.stop();
  133. cleanup();
  134. }
  135. return runSpan.traceResult;
  136. }
  137. return Object.assign(run, {
  138. getInternalTraceResult: () => runSpan.traceResult
  139. });
  140. };
  141. }
  142. export async function whyIsNodeRunning() {
  143. if (isCI && process.env.RUNNER_DEBUG === '1') {
  144. const mod = await import('why-is-node-running');
  145. return mod.default();
  146. }
  147. }
  148. // const isSpan = (obj: any): obj is Span => {
  149. // return typeof obj === 'object' && obj && spanTag in obj;
  150. // };
  151. // export const universalify = <A extends any[], R>(taskname: string, fn: (this: void, ...args: A) => R) => {
  152. // return (...args: A) => {
  153. // const lastArg = args[args.length - 1];
  154. // if (isSpan(lastArg)) {
  155. // return lastArg.traceChild(taskname).traceSyncFn(() => fn(...args));
  156. // }
  157. // return fn(...args);
  158. // };
  159. // };
  160. function adjustTraceTimestamps(trace: TraceResult, offset: number): TraceResult {
  161. return {
  162. name: trace.name,
  163. start: trace.start + offset,
  164. end: trace.end + offset,
  165. children: trace.children.map(child => adjustTraceTimestamps(child, offset))
  166. };
  167. }
  168. function mergeWorkerTrace(
  169. parentSpan: Span,
  170. workerTraceResult: TraceResult,
  171. workerTimeOrigin: number
  172. ): void {
  173. const offset = workerTimeOrigin - performance.timeOrigin;
  174. for (const child of workerTraceResult.children) {
  175. parentSpan.traceResult.children.push(adjustTraceTimestamps(child, offset));
  176. }
  177. }
  178. /** The envelope that a worker function returns so the main thread can recover both the result and the trace. */
  179. export interface WorkerJobResult<T> {
  180. result: T,
  181. traceResult: TraceResult,
  182. workerTimeOrigin: number
  183. }
  184. /**
  185. * Worker-side wrapper. Call this instead of manually constructing spans.
  186. *
  187. * - When `rawSpan` is provided (normal worker invocation from the main thread),
  188. * it is wrapped with {@link makeSpan} so all child spans are attached to the
  189. * caller's trace tree and can be recovered after the job finishes.
  190. * - When `rawSpan` is `undefined` (standalone / CLI invocation), a fresh
  191. * child span of {@link dummySpan} is used instead.
  192. *
  193. * The impl function receives a full {@link Span} and returns its result
  194. * normally; the wrapper packages everything into a {@link WorkerJobResult}.
  195. */
  196. export async function workerJob<T>(
  197. rawSpan: RawSpan | undefined,
  198. impl: (span: Span) => Promise<T>
  199. ): Promise<WorkerJobResult<T>> {
  200. const span = rawSpan == null ? dummySpan.traceChild('worker-standalone') : makeSpan(rawSpan);
  201. const result = await impl(span);
  202. return {
  203. result,
  204. traceResult: span.traceResult,
  205. workerTimeOrigin: performance.timeOrigin
  206. };
  207. }
  208. export function printTraceResult(traceResult: TraceResult) {
  209. printTree(
  210. traceResult,
  211. node => {
  212. if (node.end - node.start < 0) {
  213. return node.name;
  214. }
  215. return `${node.name} ${picocolors.bold(`${(node.end - node.start).toFixed(3)}ms`)}`;
  216. }
  217. );
  218. }
  219. function printTree(initialTree: TraceResult, printNode: (node: TraceResult, branch: string) => string) {
  220. function printBranch(tree: TraceResult, branch: string, isGraphHead: boolean, isChildOfLastBranch: boolean) {
  221. const children = tree.children;
  222. let branchHead = '';
  223. if (!isGraphHead) {
  224. branchHead = children.length > 0 ? '┬ ' : '─ ';
  225. }
  226. const toPrint = printNode(tree, `${branch}${branchHead}`);
  227. if (typeof toPrint === 'string') {
  228. console.log(`${branch}${branchHead}${toPrint}`);
  229. }
  230. let baseBranch = branch;
  231. if (!isGraphHead) {
  232. baseBranch = branch.slice(0, -2) + (isChildOfLastBranch ? ' ' : '│ ');
  233. }
  234. const nextBranch = `${baseBranch}├─`;
  235. const lastBranch = `${baseBranch}└─`;
  236. children.forEach((child, index) => {
  237. const last = children.length - 1 === index;
  238. printBranch(child, last ? lastBranch : nextBranch, false, last);
  239. });
  240. }
  241. printBranch(initialTree, '', true, false);
  242. }
  243. export function printStats(stats: TraceResult[]): void {
  244. const longestTaskName = Math.max(...stats.map(i => i.name.length));
  245. const realStart = Math.min(...stats.map(i => i.start));
  246. const realEnd = Math.max(...stats.map(i => i.end));
  247. const statsStep = ((realEnd - realStart) / 120) | 0;
  248. stats
  249. .sort((a, b) => a.start - b.start)
  250. .forEach(stat => {
  251. console.log(
  252. `[${stat.name}]${' '.repeat(longestTaskName - stat.name.length)}`,
  253. ' '.repeat(((stat.start - realStart) / statsStep) | 0),
  254. '='.repeat(Math.max(((stat.end - stat.start) / statsStep) | 0, 1))
  255. );
  256. });
  257. }