fork of hey-api/openapi-ts because I need some additional things
at feat/skip-token 208 lines 5.7 kB view raw
1import colors from 'ansi-colors'; 2 3interface LoggerEvent { 4 end?: PerformanceMark; 5 events: Array<LoggerEvent>; 6 id: string; // unique internal key 7 name: string; 8 start: PerformanceMark; 9} 10 11interface Severity { 12 color: colors.StyleFunction; 13 type: 'duration' | 'percentage'; 14} 15 16interface StoredEventResult { 17 position: ReadonlyArray<number>; 18} 19 20let loggerCounter = 0; 21const nameToId = (name: string) => `${name}-${loggerCounter++}`; 22const idEnd = (id: string) => `${id}-end`; 23const idLength = (id: string) => `${id}-length`; 24const idStart = (id: string) => `${id}-start`; 25 26const getSeverity = (duration: number, percentage: number): Severity | undefined => { 27 if (duration > 200) { 28 return { 29 color: colors.red, 30 type: 'duration', 31 }; 32 } 33 if (percentage > 30) { 34 return { 35 color: colors.red, 36 type: 'percentage', 37 }; 38 } 39 if (duration > 50) { 40 return { 41 color: colors.yellow, 42 type: 'duration', 43 }; 44 } 45 if (percentage > 10) { 46 return { 47 color: colors.yellow, 48 type: 'percentage', 49 }; 50 } 51 return; 52}; 53 54export class Logger { 55 private events: Array<LoggerEvent> = []; 56 57 private end(result: StoredEventResult): void { 58 let event: LoggerEvent | undefined; 59 let events = this.events; 60 for (const index of result.position) { 61 event = events[index]; 62 if (event?.events) { 63 events = event.events; 64 } 65 } 66 if (event && !event.end) { 67 event.end = performance.mark(idEnd(event.id)); 68 } 69 } 70 71 /** 72 * Recursively end all unended events in the event tree. 73 * This ensures all events have end marks before measuring. 74 */ 75 private endAllEvents(events: Array<LoggerEvent>): void { 76 for (const event of events) { 77 if (!event.end) { 78 event.end = performance.mark(idEnd(event.id)); 79 } 80 if (event.events.length > 0) { 81 this.endAllEvents(event.events); 82 } 83 } 84 } 85 86 report(print: boolean = true): PerformanceMeasure | undefined { 87 const firstEvent = this.events[0]; 88 if (!firstEvent) return; 89 90 // Ensure all events are ended before reporting 91 this.endAllEvents(this.events); 92 93 const lastEvent = this.events[this.events.length - 1]!; 94 const name = 'root'; 95 const id = nameToId(name); 96 97 try { 98 const measure = performance.measure( 99 idLength(id), 100 idStart(firstEvent.id), 101 idEnd(lastEvent.id), 102 ); 103 if (print) { 104 this.reportEvent({ 105 end: lastEvent.end, 106 events: this.events, 107 id, 108 indent: 0, 109 measure, 110 name, 111 start: firstEvent!.start, 112 }); 113 } 114 return measure; 115 } catch { 116 // If measuring fails (e.g., marks don't exist), silently skip reporting 117 // to avoid crashing the application 118 return; 119 } 120 } 121 122 private reportEvent({ 123 indent, 124 ...parent 125 }: LoggerEvent & { 126 indent: number; 127 measure: PerformanceMeasure; 128 }): void { 129 const color = !indent ? colors.cyan : colors.gray; 130 const lastIndex = parent.events.length - 1; 131 132 parent.events.forEach((event, index) => { 133 try { 134 const measure = performance.measure(idLength(event.id), idStart(event.id), idEnd(event.id)); 135 const duration = Math.ceil(measure.duration * 100) / 100; 136 const percentage = 137 Math.ceil((measure.duration / parent.measure.duration) * 100 * 100) / 100; 138 const severity = indent ? getSeverity(duration, percentage) : undefined; 139 140 let durationLabel = `${duration.toFixed(2).padStart(8)}ms`; 141 if (severity?.type === 'duration') { 142 durationLabel = severity.color(durationLabel); 143 } 144 145 const branch = index === lastIndex ? '└─ ' : '├─ '; 146 const prefix = !indent ? '' : '│ '.repeat(indent - 1) + branch; 147 const maxLength = 38 - prefix.length; 148 149 const percentageBranch = !indent ? '' : '↳ '; 150 const percentagePrefix = indent ? ' '.repeat(indent - 1) + percentageBranch : ''; 151 let percentageLabel = `${percentagePrefix}${percentage.toFixed(2)}%`; 152 if (severity?.type === 'percentage') { 153 percentageLabel = severity.color(percentageLabel); 154 } 155 const jobPrefix = colors.gray('[root] '); 156 console.log( 157 `${jobPrefix}${colors.gray(prefix)}${color( 158 `${event.name.padEnd(maxLength)} ${durationLabel} (${percentageLabel})`, 159 )}`, 160 ); 161 this.reportEvent({ ...event, indent: indent + 1, measure }); 162 } catch { 163 // If measuring fails (e.g., marks don't exist), silently skip this event 164 // to avoid crashing the application 165 } 166 }); 167 } 168 169 private start(id: string): PerformanceMark { 170 return performance.mark(idStart(id)); 171 } 172 173 private storeEvent({ 174 result, 175 ...event 176 }: Pick<LoggerEvent, 'events' | 'id' | 'name' | 'start'> & { 177 result: StoredEventResult; 178 }): void { 179 const lastEventIndex = event.events.length - 1; 180 const lastEvent = event.events[lastEventIndex]; 181 if (lastEvent && !lastEvent.end) { 182 result.position = [...result.position, lastEventIndex]; 183 this.storeEvent({ ...event, events: lastEvent.events, result }); 184 return; 185 } 186 const length = event.events.push({ ...event, events: [] }); 187 result.position = [...result.position, length - 1]; 188 } 189 190 timeEvent(name: string) { 191 const id = nameToId(name); 192 const start = this.start(id); 193 const event: LoggerEvent = { 194 events: this.events, 195 id, 196 name, 197 start, 198 }; 199 const result: StoredEventResult = { 200 position: [], 201 }; 202 this.storeEvent({ ...event, result }); 203 return { 204 mark: start, 205 timeEnd: () => this.end(result), 206 }; 207 } 208}