index.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import path from 'path';
  2. import picocolors from 'picocolors';
  3. const SPAN_STATUS_START = 0;
  4. const SPAN_STATUS_END = 1;
  5. const NUM_OF_MS_IN_NANOSEC = 1_000_000;
  6. const spanTag = Symbol('span');
  7. export interface TraceResult {
  8. name: string,
  9. start: number,
  10. end: number,
  11. children: TraceResult[]
  12. }
  13. const rootTraceResult: TraceResult = {
  14. name: 'root',
  15. start: 0,
  16. end: 0,
  17. children: []
  18. };
  19. export interface Span {
  20. [spanTag]: true,
  21. readonly stop: (time?: number) => void,
  22. readonly traceChild: (name: string) => Span,
  23. readonly traceSyncFn: <T>(fn: (span: Span) => T) => T,
  24. readonly traceAsyncFn: <T>(fn: (span: Span) => T | Promise<T>) => Promise<T>,
  25. readonly tracePromise: <T>(promise: Promise<T>) => Promise<T>,
  26. readonly traceResult: TraceResult
  27. }
  28. export const createSpan = (name: string, parentTraceResult?: TraceResult): Span => {
  29. const start = Bun.nanoseconds();
  30. let curTraceResult: TraceResult;
  31. if (parentTraceResult == null) {
  32. curTraceResult = rootTraceResult;
  33. } else {
  34. curTraceResult = {
  35. name,
  36. start: start / NUM_OF_MS_IN_NANOSEC,
  37. end: 0,
  38. children: []
  39. };
  40. parentTraceResult.children.push(curTraceResult);
  41. }
  42. let status: typeof SPAN_STATUS_START | typeof SPAN_STATUS_END = SPAN_STATUS_START;
  43. const stop = (time?: number) => {
  44. if (status === SPAN_STATUS_END) {
  45. throw new Error(`span already stopped: ${name}`);
  46. }
  47. const end = time ?? Bun.nanoseconds();
  48. curTraceResult.end = end / NUM_OF_MS_IN_NANOSEC;
  49. status = SPAN_STATUS_END;
  50. };
  51. const traceChild = (name: string) => createSpan(name, curTraceResult);
  52. const span: Span = {
  53. [spanTag]: true,
  54. stop,
  55. traceChild,
  56. traceSyncFn<T>(fn: (span: Span) => T) {
  57. try {
  58. return fn(span);
  59. } finally {
  60. span.stop();
  61. }
  62. },
  63. async traceAsyncFn<T>(fn: (span: Span) => T | Promise<T>): Promise<T> {
  64. try {
  65. return await fn(span);
  66. } finally {
  67. span.stop();
  68. }
  69. },
  70. get traceResult() {
  71. return curTraceResult;
  72. },
  73. async tracePromise<T>(promise: Promise<T>): Promise<T> {
  74. try {
  75. return await promise;
  76. } finally {
  77. span.stop();
  78. }
  79. }
  80. };
  81. // eslint-disable-next-line sukka/no-redundant-variable -- self reference
  82. return span;
  83. };
  84. export const task = <T>(importMetaPath: string, fn: (span: Span) => T, customname?: string) => {
  85. const taskName = customname ?? path.basename(importMetaPath, path.extname(importMetaPath));
  86. return async (span?: Span) => {
  87. if (span) {
  88. return span.traceChild(taskName).traceAsyncFn(fn);
  89. }
  90. return fn(createSpan(taskName));
  91. };
  92. };
  93. const isSpan = (obj: any): obj is Span => {
  94. return typeof obj === 'object' && obj && spanTag in obj;
  95. };
  96. export const universalify = <A extends any[], R>(taskname: string, fn: (this: void, ...args: A) => R) => {
  97. return (...args: A) => {
  98. const lastArg = args[args.length - 1];
  99. if (isSpan(lastArg)) {
  100. return lastArg.traceChild(taskname).traceSyncFn(() => fn(...args));
  101. }
  102. return fn(...args);
  103. };
  104. };
  105. export const printTraceResult = (traceResult: TraceResult = rootTraceResult) => {
  106. printStats(traceResult.children);
  107. printTree(traceResult, node => `${node.name} ${picocolors.bold(`${(node.end - node.start).toFixed(3)}ms`)}`);
  108. };
  109. function printTree(initialTree: TraceResult, printNode: (node: TraceResult, branch: string) => string) {
  110. function printBranch(tree: TraceResult, branch: string) {
  111. const isGraphHead = branch.length === 0;
  112. const children = tree.children;
  113. let branchHead = '';
  114. if (!isGraphHead) {
  115. branchHead = children.length > 0 ? '┬ ' : '─ ';
  116. }
  117. const toPrint = printNode(tree, `${branch}${branchHead}`);
  118. if (typeof toPrint === 'string') {
  119. console.log(`${branch}${branchHead}${toPrint}`);
  120. }
  121. let baseBranch = branch;
  122. if (!isGraphHead) {
  123. const isChildOfLastBranch = branch.endsWith('└─');
  124. baseBranch = branch.slice(0, -2) + (isChildOfLastBranch ? ' ' : '│ ');
  125. }
  126. const nextBranch = `${baseBranch}├─`;
  127. const lastBranch = `${baseBranch}└─`;
  128. children.forEach((child, index) => {
  129. printBranch(child, children.length - 1 === index ? lastBranch : nextBranch);
  130. });
  131. }
  132. printBranch(initialTree, '');
  133. }
  134. function printStats(stats: TraceResult[]): void {
  135. stats.sort((a, b) => a.start - b.start);
  136. const longestTaskName = Math.max(...stats.map(i => i.name.length));
  137. const realStart = Math.min(...stats.map(i => i.start));
  138. const realEnd = Math.max(...stats.map(i => i.end));
  139. const statsStep = ((realEnd - realStart) / 160) | 0;
  140. stats.forEach(stat => {
  141. console.log(
  142. `[${stat.name}]${' '.repeat(longestTaskName - stat.name.length)}`,
  143. ' '.repeat(((stat.start - realStart) / statsStep) | 0),
  144. '='.repeat(Math.max(((stat.end - stat.start) / statsStep) | 0, 1))
  145. );
  146. });
  147. }