index.ts 6.5 KB

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