fork of hey-api/openapi-ts because I need some additional things

Merge pull request #2884 from hey-api/copilot/fix-performance-mark-issue

Fix performance mark crash when using DEBUG=true

authored by

Lubos and committed by
GitHub
c6de6843 f475ce71

+150 -47
+5
.changeset/pink-chefs-collect.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + fix: gracefully handle errors in debug reports
+67
packages/openapi-ts/src/utils/__tests__/logger.test.ts
··· 1 + import { describe, expect, it } from 'vitest'; 2 + 3 + import { Logger } from '../logger'; 4 + 5 + describe('Logger', () => { 6 + describe('report', () => { 7 + it('should handle unended events gracefully', () => { 8 + const logger = new Logger(); 9 + 10 + // Create an event but don't end it 11 + logger.timeEvent('test-event-1'); 12 + 13 + // Create another event and end it 14 + const event2 = logger.timeEvent('test-event-2'); 15 + event2.timeEnd(); 16 + 17 + // report() should not throw even though event1 was never ended 18 + expect(() => logger.report(false)).not.toThrow(); 19 + 20 + const measure = logger.report(false); 21 + expect(measure).toBeDefined(); 22 + expect(measure?.duration).toBeGreaterThanOrEqual(0); 23 + }); 24 + 25 + it('should handle nested unended events', () => { 26 + const logger = new Logger(); 27 + 28 + const parent = logger.timeEvent('parent'); 29 + logger.timeEvent('child1'); 30 + // Don't end child1 31 + 32 + const child2 = logger.timeEvent('child2'); 33 + child2.timeEnd(); 34 + 35 + parent.timeEnd(); 36 + 37 + // report() should not throw even though child1 was never ended 38 + expect(() => logger.report(false)).not.toThrow(); 39 + 40 + const measure = logger.report(false); 41 + expect(measure).toBeDefined(); 42 + }); 43 + 44 + it('should handle all events properly ended', () => { 45 + const logger = new Logger(); 46 + 47 + const event1 = logger.timeEvent('event1'); 48 + const event2 = logger.timeEvent('event2'); 49 + 50 + event1.timeEnd(); 51 + event2.timeEnd(); 52 + 53 + expect(() => logger.report(false)).not.toThrow(); 54 + 55 + const measure = logger.report(false); 56 + expect(measure).toBeDefined(); 57 + expect(measure?.duration).toBeGreaterThanOrEqual(0); 58 + }); 59 + 60 + it('should return undefined when no events exist', () => { 61 + const logger = new Logger(); 62 + 63 + const measure = logger.report(false); 64 + expect(measure).toBeUndefined(); 65 + }); 66 + }); 67 + });
+78 -47
packages/openapi-ts/src/utils/logger.ts
··· 71 71 } 72 72 } 73 73 74 + /** 75 + * Recursively end all unended events in the event tree. 76 + * This ensures all events have end marks before measuring. 77 + */ 78 + private endAllEvents(events: Array<LoggerEvent>): void { 79 + for (const event of events) { 80 + if (!event.end) { 81 + event.end = performance.mark(idEnd(event.id)); 82 + } 83 + if (event.events.length > 0) { 84 + this.endAllEvents(event.events); 85 + } 86 + } 87 + } 88 + 74 89 report(print: boolean = true): PerformanceMeasure | undefined { 75 90 const firstEvent = this.events[0]; 76 91 if (!firstEvent) return; 92 + 93 + // Ensure all events are ended before reporting 94 + this.endAllEvents(this.events); 95 + 77 96 const lastEvent = this.events[this.events.length - 1]!; 78 97 const name = 'root'; 79 98 const id = nameToId(name); 80 - const measure = performance.measure( 81 - idLength(id), 82 - idStart(firstEvent.id), 83 - idEnd(lastEvent.id), 84 - ); 85 - if (print) { 86 - this.reportEvent({ 87 - end: lastEvent.end, 88 - events: this.events, 89 - id, 90 - indent: 0, 91 - measure, 92 - name, 93 - start: firstEvent!.start, 94 - }); 99 + 100 + try { 101 + const measure = performance.measure( 102 + idLength(id), 103 + idStart(firstEvent.id), 104 + idEnd(lastEvent.id), 105 + ); 106 + if (print) { 107 + this.reportEvent({ 108 + end: lastEvent.end, 109 + events: this.events, 110 + id, 111 + indent: 0, 112 + measure, 113 + name, 114 + start: firstEvent!.start, 115 + }); 116 + } 117 + return measure; 118 + } catch { 119 + // If measuring fails (e.g., marks don't exist), silently skip reporting 120 + // to avoid crashing the application 121 + return; 95 122 } 96 - return measure; 97 123 } 98 124 99 125 private reportEvent({ ··· 107 133 const lastIndex = parent.events.length - 1; 108 134 109 135 parent.events.forEach((event, index) => { 110 - const measure = performance.measure( 111 - idLength(event.id), 112 - idStart(event.id), 113 - idEnd(event.id), 114 - ); 115 - const duration = Math.ceil(measure.duration * 100) / 100; 116 - const percentage = 117 - Math.ceil((measure.duration / parent.measure.duration) * 100 * 100) / 118 - 100; 119 - const severity = indent ? getSeverity(duration, percentage) : undefined; 136 + try { 137 + const measure = performance.measure( 138 + idLength(event.id), 139 + idStart(event.id), 140 + idEnd(event.id), 141 + ); 142 + const duration = Math.ceil(measure.duration * 100) / 100; 143 + const percentage = 144 + Math.ceil((measure.duration / parent.measure.duration) * 100 * 100) / 145 + 100; 146 + const severity = indent ? getSeverity(duration, percentage) : undefined; 120 147 121 - let durationLabel = `${duration.toFixed(2).padStart(8)}ms`; 122 - if (severity?.type === 'duration') { 123 - durationLabel = severity.color(durationLabel); 124 - } 148 + let durationLabel = `${duration.toFixed(2).padStart(8)}ms`; 149 + if (severity?.type === 'duration') { 150 + durationLabel = severity.color(durationLabel); 151 + } 125 152 126 - const branch = index === lastIndex ? '└─ ' : '├─ '; 127 - const prefix = !indent ? '' : '│ '.repeat(indent - 1) + branch; 128 - const maxLength = 38 - prefix.length; 153 + const branch = index === lastIndex ? '└─ ' : '├─ '; 154 + const prefix = !indent ? '' : '│ '.repeat(indent - 1) + branch; 155 + const maxLength = 38 - prefix.length; 129 156 130 - const percentageBranch = !indent ? '' : '↳ '; 131 - const percentagePrefix = indent 132 - ? ' '.repeat(indent - 1) + percentageBranch 133 - : ''; 134 - let percentageLabel = `${percentagePrefix}${percentage.toFixed(2)}%`; 135 - if (severity?.type === 'percentage') { 136 - percentageLabel = severity.color(percentageLabel); 157 + const percentageBranch = !indent ? '' : '↳ '; 158 + const percentagePrefix = indent 159 + ? ' '.repeat(indent - 1) + percentageBranch 160 + : ''; 161 + let percentageLabel = `${percentagePrefix}${percentage.toFixed(2)}%`; 162 + if (severity?.type === 'percentage') { 163 + percentageLabel = severity.color(percentageLabel); 164 + } 165 + const jobPrefix = colors.gray('[root] '); 166 + console.log( 167 + `${jobPrefix}${colors.gray(prefix)}${color( 168 + `${event.name.padEnd(maxLength)} ${durationLabel} (${percentageLabel})`, 169 + )}`, 170 + ); 171 + this.reportEvent({ ...event, indent: indent + 1, measure }); 172 + } catch { 173 + // If measuring fails (e.g., marks don't exist), silently skip this event 174 + // to avoid crashing the application 137 175 } 138 - const jobPrefix = colors.gray('[root] '); 139 - console.log( 140 - `${jobPrefix}${colors.gray(prefix)}${color( 141 - `${event.name.padEnd(maxLength)} ${durationLabel} (${percentageLabel})`, 142 - )}`, 143 - ); 144 - this.reportEvent({ ...event, indent: indent + 1, measure }); 145 176 }); 146 177 } 147 178