index.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  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 stop: (time?: number) => void,
  23. readonly traceChild: (name: string) => Span,
  24. readonly traceSyncFn: <T>(fn: (span: Span) => T) => T,
  25. readonly traceAsyncFn: <T>(fn: (span: Span) => T | Promise<T>) => Promise<T>,
  26. readonly tracePromise: <T>(promise: Promise<T>) => Promise<T>,
  27. readonly traceChildSync: <T>(name: string, fn: (span: Span) => T) => T,
  28. readonly traceChildAsync: <T>(name: string, fn: (span: Span) => Promise<T>) => Promise<T>,
  29. readonly traceChildPromise: <T>(name: string, promise: Promise<T>) => Promise<T>,
  30. readonly traceResult: TraceResult
  31. }
  32. /**
  33. * Wraps a serializable {@link RawSpan} with all span methods.
  34. * Use this on a worker thread after receiving a {@link RawSpan} (or {@link TraceResult})
  35. * transferred from another thread.
  36. */
  37. export function makeSpan(rawSpan: RawSpan): Span {
  38. const { traceResult } = rawSpan;
  39. const stop = (time?: number) => {
  40. if (rawSpan.status === SPAN_STATUS_END) {
  41. throw new Error(`span already stopped: ${traceResult.name}`);
  42. }
  43. traceResult.end = time ?? performance.now();
  44. rawSpan.status = SPAN_STATUS_END;
  45. };
  46. const traceChild = (name: string) => createSpan(name, traceResult);
  47. const span: Span = {
  48. [spanTag]: true,
  49. stop,
  50. traceChild,
  51. traceSyncFn<T>(fn: (span: Span) => T) {
  52. const res = fn(span);
  53. span.stop();
  54. return res;
  55. },
  56. async traceAsyncFn<T>(fn: (span: Span) => T | Promise<T>): Promise<T> {
  57. const res = await fn(span);
  58. span.stop();
  59. return res;
  60. },
  61. traceResult,
  62. async tracePromise<T>(promise: Promise<T>): Promise<T> {
  63. const res = await promise;
  64. span.stop();
  65. return res;
  66. },
  67. traceChildSync: <T>(name: string, fn: (span: Span) => T): T => traceChild(name).traceSyncFn(fn),
  68. traceChildAsync: <T>(name: string, fn: (span: Span) => T | Promise<T>): Promise<T> => traceChild(name).traceAsyncFn(fn),
  69. traceChildPromise: <T>(name: string, promise: Promise<T>): Promise<T> => traceChild(name).tracePromise(promise)
  70. };
  71. // eslint-disable-next-line sukka/no-redundant-variable -- self reference
  72. return span;
  73. }
  74. export function createSpan(name: string, parentTraceResult?: TraceResult): Span {
  75. const rawSpan: RawSpan = {
  76. traceResult: {
  77. name,
  78. start: performance.now(),
  79. end: 0,
  80. children: []
  81. },
  82. status: SPAN_STATUS_START
  83. };
  84. parentTraceResult?.children.push(rawSpan.traceResult);
  85. return makeSpan(rawSpan);
  86. }
  87. export const dummySpan = createSpan('dummy');
  88. export function task(importMetaMain: boolean, importMetaPath: string) {
  89. return (fn: (span: Span, onCleanup: (cb: () => Promise<void> | void) => void) => Promise<unknown>, customName?: string) => {
  90. const taskName = customName ?? basename(importMetaPath, extname(importMetaPath));
  91. let cleanup: () => Promise<void> | void = noop;
  92. const onCleanup = (cb: () => void) => {
  93. cleanup = cb;
  94. };
  95. if (importMetaMain) {
  96. const innerSpan = createSpan(taskName);
  97. process.on('uncaughtException', (error) => {
  98. console.error('Uncaught exception:', error);
  99. process.exit(1);
  100. });
  101. process.on('unhandledRejection', (reason) => {
  102. console.error('Unhandled rejection:', reason);
  103. process.exit(1);
  104. });
  105. innerSpan.traceChildAsync('dummy', (childSpan) => fn(childSpan, onCleanup)).finally(() => {
  106. innerSpan.stop();
  107. printTraceResult(innerSpan.traceResult);
  108. process.nextTick(whyIsNodeRunning);
  109. process.nextTick(() => process.exit(0));
  110. });
  111. }
  112. let runSpan: Span;
  113. async function run(parentSpan?: Span | null): Promise<TraceResult> {
  114. if (parentSpan) {
  115. runSpan = parentSpan.traceChild(taskName);
  116. } else {
  117. runSpan = createSpan(taskName);
  118. }
  119. try {
  120. await fn(runSpan, onCleanup);
  121. } finally {
  122. runSpan.stop();
  123. cleanup();
  124. }
  125. return runSpan.traceResult;
  126. }
  127. return Object.assign(run, {
  128. getInternalTraceResult: () => runSpan.traceResult
  129. });
  130. };
  131. }
  132. export async function whyIsNodeRunning() {
  133. if (isCI && process.env.RUNNER_DEBUG === '1') {
  134. const mod = await import('why-is-node-running');
  135. return mod.default();
  136. }
  137. }
  138. // const isSpan = (obj: any): obj is Span => {
  139. // return typeof obj === 'object' && obj && spanTag in obj;
  140. // };
  141. // export const universalify = <A extends any[], R>(taskname: string, fn: (this: void, ...args: A) => R) => {
  142. // return (...args: A) => {
  143. // const lastArg = args[args.length - 1];
  144. // if (isSpan(lastArg)) {
  145. // return lastArg.traceChild(taskname).traceSyncFn(() => fn(...args));
  146. // }
  147. // return fn(...args);
  148. // };
  149. // };
  150. export function printTraceResult(traceResult: TraceResult) {
  151. printTree(
  152. traceResult,
  153. node => {
  154. if (node.end - node.start < 0) {
  155. return node.name;
  156. }
  157. return `${node.name} ${picocolors.bold(`${(node.end - node.start).toFixed(3)}ms`)}`;
  158. }
  159. );
  160. }
  161. function printTree(initialTree: TraceResult, printNode: (node: TraceResult, branch: string) => string) {
  162. function printBranch(tree: TraceResult, branch: string, isGraphHead: boolean, isChildOfLastBranch: boolean) {
  163. const children = tree.children;
  164. let branchHead = '';
  165. if (!isGraphHead) {
  166. branchHead = children.length > 0 ? '┬ ' : '─ ';
  167. }
  168. const toPrint = printNode(tree, `${branch}${branchHead}`);
  169. if (typeof toPrint === 'string') {
  170. console.log(`${branch}${branchHead}${toPrint}`);
  171. }
  172. let baseBranch = branch;
  173. if (!isGraphHead) {
  174. baseBranch = branch.slice(0, -2) + (isChildOfLastBranch ? ' ' : '│ ');
  175. }
  176. const nextBranch = `${baseBranch}├─`;
  177. const lastBranch = `${baseBranch}└─`;
  178. children.forEach((child, index) => {
  179. const last = children.length - 1 === index;
  180. printBranch(child, last ? lastBranch : nextBranch, false, last);
  181. });
  182. }
  183. printBranch(initialTree, '', true, false);
  184. }
  185. export function printStats(stats: TraceResult[]): void {
  186. const longestTaskName = Math.max(...stats.map(i => i.name.length));
  187. const realStart = Math.min(...stats.map(i => i.start));
  188. const realEnd = Math.max(...stats.map(i => i.end));
  189. const statsStep = ((realEnd - realStart) / 120) | 0;
  190. stats
  191. .sort((a, b) => a.start - b.start)
  192. .forEach(stat => {
  193. console.log(
  194. `[${stat.name}]${' '.repeat(longestTaskName - stat.name.length)}`,
  195. ' '.repeat(((stat.start - realStart) / statsStep) | 0),
  196. '='.repeat(Math.max(((stat.end - stat.start) / statsStep) | 0, 1))
  197. );
  198. });
  199. }