index.ts 6.6 KB

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