From 13d0ce589df94690803a7d0b20446c5fd778ff1a Mon Sep 17 00:00:00 2001 From: Pixnop <77785313+Pixnop@users.noreply.github.com> Date: Sun, 30 Aug 2026 00:40:47 +0200 Subject: [PATCH] perf(inno): keep the native LZMA2 finish buffer instead of copying it The adapter's queue() copied every buffer the native decoder handed back. That is right for update(), which the library documents as returning a zero-copy view, but finish() resolves to the decoded tail of a decoder that is spent afterwards, so nothing can rewrite it. On a solid block that copy was a second copy of the whole block. --- src/ipc/workers/nativeLzma2.ts | 13 ++++++++++--- tests/ipc/nativeLzma2Adapter.test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/ipc/workers/nativeLzma2.ts b/src/ipc/workers/nativeLzma2.ts index 83523439..713998fc 100644 --- a/src/ipc/workers/nativeLzma2.ts +++ b/src/ipc/workers/nativeLzma2.ts @@ -89,9 +89,16 @@ class NativeLzma2Decoder implements Lzma2DecoderPort { } } - private queue(bytes: Uint8Array): void { + /** + * `owned` says the bytes are ours to keep. The library documents `update()` + * as handing back a zero-copy view, so that one is copied before a later call + * can write over it. `finish()` resolves to the decoded tail and leaves a + * spent decoder behind, so nothing can rewrite it, and on a solid block the + * copy would be a second copy of the entire block. + */ + private queue(bytes: Uint8Array, owned = false): void { if (bytes.length === 0) return - this.pending.push(Uint8Array.from(bytes)) + this.pending.push(owned ? bytes : Uint8Array.from(bytes)) this.pendingBytes += bytes.length } @@ -128,7 +135,7 @@ class NativeLzma2Decoder implements Lzma2DecoderPort { if (control === 0) { try { this.queue(this.decoder.update(Uint8Array.of(0))) - this.queue(await this.decoder.finish()) + this.queue(await this.decoder.finish(), true) } catch { throw new NativeLzma2Error("the native LZMA2 decoder rejected the stream") } diff --git a/tests/ipc/nativeLzma2Adapter.test.ts b/tests/ipc/nativeLzma2Adapter.test.ts index 75db172b..55b5cd05 100644 --- a/tests/ipc/nativeLzma2Adapter.test.ts +++ b/tests/ipc/nativeLzma2Adapter.test.ts @@ -48,6 +48,33 @@ describe("native LZMA2 adapter", () => { expect(received).toEqual(Uint8Array.from([0xa0, 0, 0, 0, 1, 0x11, 0x22])) }) + it("assembles output correctly when the decoder reuses its update buffer", async () => { + // The library returns update() output as a zero-copy view, so a later call + // may write over bytes the adapter is still holding. finish() hands back the + // tail of a spent decoder, which is why that one is queued without a copy. + const reused = new Uint8Array(4) + let call = 0 + class ReusingDecompressor { + update(): Uint8Array { + reused.fill(++call) + return reused + } + + async finish(): Promise { + reused.fill(0xff) + return Uint8Array.of(0xaa, 0xbb) + } + } + + const pieces: Uint8Array[] = [] + const factory = createNativeLzma2DecoderFactory(ReusingDecompressor) + const decoder = factory(0, (bytes) => pieces.push(bytes.slice())) + const input = inputOver(Uint8Array.from([0xa0, 0, 0, 0, 1, 0x11, 0x22, 0xa0, 0, 0, 0, 1, 0x33, 0x44, 0])) + while (!decoder.finished) await decoder.decodeChunk(input) + + expect(Buffer.concat(pieces.map((piece) => Buffer.from(piece)))).toEqual(Buffer.from([1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 0xaa, 0xbb])) + }) + it("drains buffered native output in bounded pieces", async () => { const expected = new Uint8Array(5 * 1024 * 1024 + 123) expected.fill(7)