From df7eee7e522789cc6f558141260964ddbaa9f7a0 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 26 Jul 2026 23:14:57 +0200 Subject: [PATCH] fix(perf): exclude profiler flush from cancel latency --- .../app/workers/playlist-refresh.worker.ts | 30 ++++++++--------- .../worker-performance-cancellation.spec.ts | 33 +++++++++++++++++-- .../worker-performance-integration.spec.ts | 5 +-- 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/apps/electron-backend/src/app/workers/playlist-refresh.worker.ts b/apps/electron-backend/src/app/workers/playlist-refresh.worker.ts index 3bd0059ba..f6062de87 100644 --- a/apps/electron-backend/src/app/workers/playlist-refresh.worker.ts +++ b/apps/electron-backend/src/app/workers/playlist-refresh.worker.ts @@ -178,6 +178,20 @@ async function executeRefresh( checkpoint(payload); return playlist; + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + emitEvent(payload, { + status: 'cancelled', + phase: 'parsing', + }); + } else { + emitEvent(payload, { + status: 'error', + phase: payload.url ? 'fetching' : 'reading-file', + error: error instanceof Error ? error.message : String(error), + }); + } + throw error; } finally { releaseActiveRefresh(payload.operationId, activeRefresh); } @@ -219,22 +233,6 @@ parentPort.on( }); } else { const error = execution.error; - if (error instanceof Error && error.name === 'AbortError') { - emitEvent(payload, { - status: 'cancelled', - phase: 'parsing', - }); - } else { - emitEvent(payload, { - status: 'error', - phase: payload.url ? 'fetching' : 'reading-file', - error: - error instanceof Error - ? error.message - : String(error), - }); - } - postMessage({ type: 'response', success: false, diff --git a/apps/electron-backend/src/app/workers/worker-performance-cancellation.spec.ts b/apps/electron-backend/src/app/workers/worker-performance-cancellation.spec.ts index eef0171f3..4cdd550b7 100644 --- a/apps/electron-backend/src/app/workers/worker-performance-cancellation.spec.ts +++ b/apps/electron-backend/src/app/workers/worker-performance-cancellation.spec.ts @@ -50,7 +50,13 @@ function createParentPortHarness() { }; } -function mockPerformanceCapture(armGate: ArmGate): void { +function mockPerformanceCapture( + armGate: ArmGate, + failureGates?: { + readonly observed: ArmGate; + readonly profilingFinished: ArmGate; + } +): void { jest.doMock('./worker-performance-capture', () => ({ armWorkerPerformanceCapture: jest.fn(() => armGate.promise), executeWithWorkerPerformanceCapture: jest.fn( @@ -63,6 +69,8 @@ function mockPerformanceCapture(armGate: ArmGate): void { success: true, }; } catch (error) { + failureGates?.observed.release(); + await failureGates?.profilingFinished.promise; return { error, performance: undefined, @@ -86,12 +94,17 @@ describe('worker cancellation while performance capture arms', () => { it('cancels playlist work before file access when cancel arrives during arming', async () => { const armGate = createArmGate(); + const profilingFinished = createArmGate(); + const failureObserved = createArmGate(); const port = createParentPortHarness< PlaylistRefreshWorkerIncomingMessage, PlaylistRefreshWorkerMessage >(); const readFile = jest.fn().mockResolvedValue('#EXTM3U'); - mockPerformanceCapture(armGate); + mockPerformanceCapture(armGate, { + observed: failureObserved, + profilingFinished, + }); jest.doMock('worker_threads', () => ({ parentPort: port.parentPort, })); @@ -122,9 +135,25 @@ describe('worker cancellation while performance capture arms', () => { operationId: 'refresh-1', }); armGate.release(); + await failureObserved.promise; + + const cancelledBeforeProfilingFinished = + port.postMessage.mock.calls.some( + ([message]) => + message.type === 'event' && + message.event.status === 'cancelled' + ); + const responseBeforeProfilingFinished = + port.postMessage.mock.calls.some( + ([message]) => message.type === 'response' + ); + + profilingFinished.release(); await requestPromise; expect(readFile).not.toHaveBeenCalled(); + expect(cancelledBeforeProfilingFinished).toBe(true); + expect(responseBeforeProfilingFinished).toBe(false); expect(port.postMessage).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ diff --git a/apps/electron-backend/src/app/workers/worker-performance-integration.spec.ts b/apps/electron-backend/src/app/workers/worker-performance-integration.spec.ts index e13a11f29..6ecaac4bd 100644 --- a/apps/electron-backend/src/app/workers/worker-performance-integration.spec.ts +++ b/apps/electron-backend/src/app/workers/worker-performance-integration.spec.ts @@ -37,7 +37,7 @@ describe('worker performance integration order', () => { expect(handler).not.toContain('requestQueue'); }); - it('finishes playlist profiling before cancellation/error events and response serialization', () => { + it('keeps playlist response serialization after profiling completion', () => { const source = readWorkerSource('playlist-refresh.worker.ts'); const handler = source.slice(source.lastIndexOf('parentPort.on(')); const executionIndex = handler.indexOf( @@ -46,9 +46,6 @@ describe('worker performance integration order', () => { expect(executionIndex).toBeGreaterThanOrEqual(0); expect(handler).not.toContain('finishWorkerPerformanceCapture'); - expect(executionIndex).toBeLessThan( - handler.indexOf('emitEvent', executionIndex) - ); expect(executionIndex).toBeLessThan( handler.indexOf('serializeError', executionIndex) );