fork of hey-api/openapi-ts because I need some additional things
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}