diff --git a/benchmark/bench.ts b/benchmark/bench.ts index f517148..3986df9 100644 --- a/benchmark/bench.ts +++ b/benchmark/bench.ts @@ -16,7 +16,7 @@ import { import { Bench, hrtimeNow } from 'tinybench' import { compress as legacyCompress, uncompress as legacyUncompress } from 'legacy-snappy' -import { compress, uncompress, compressSync } from '../index.js' +import { compress, uncompress, compressSync, uncompressSync } from '../index.js' import { fileURLToPath } from 'node:url' const gzipAsync = promisify(gzip) @@ -33,6 +33,25 @@ const SNAPPY_COMPRESSED_FIXTURE = Buffer.from(compressSync(FIXTURE)) const GZIP_FIXTURE = gzipSync(FIXTURE) const DEFLATED_FIXTURE = deflateSync(FIXTURE) const BROTLI_COMPRESSED_FIXTURE = brotliCompressSync(FIXTURE) +const MEMORY_POOL = 50_000 + +const initiateMemoryPool = () => { + const pool = new Array(MEMORY_POOL) as Uint8Array[] + for (let i = 0; i < MEMORY_POOL; i++) { + pool[i] = new Uint8Array(FIXTURE.length) + } + + const response = { + currentBufferIndex: 0, + getAvailableBuffer() { + const buffer = pool[response.currentBufferIndex] + response.currentBufferIndex++ + return buffer! + }, + } + + return response +} const b = new Bench({ now: hrtimeNow, @@ -42,6 +61,10 @@ b.add('snappy-compress', () => { return compress(FIXTURE) }) +b.add('snappy-compress-sync', () => { + return compressSync(FIXTURE) +}) + b.add('snappy-v6-compress', () => { return compressV6(FIXTURE) }) @@ -65,10 +88,22 @@ console.table(b.table()) const bUncompress = new Bench({ now: hrtimeNow, }) +const outputPool = initiateMemoryPool() // new Uint8Array(FIXTURE.length) bUncompress.add('snappy-uncompress', () => { return uncompress(SNAPPY_COMPRESSED_FIXTURE) }) +bUncompress.add('snappy-alloc-uncompress', () => { + return uncompress(SNAPPY_COMPRESSED_FIXTURE, { output: outputPool.getAvailableBuffer() }) +}) +bUncompress.add('snappy-sync-uncompress', () => { + return uncompressSync(SNAPPY_COMPRESSED_FIXTURE) +}) +const outputPool2 = initiateMemoryPool() + +bUncompress.add('snappy-sync-alloc-uncompress', () => { + return uncompressSync(SNAPPY_COMPRESSED_FIXTURE, { output: outputPool2.getAvailableBuffer() }) +}) bUncompress.add('snappy-v6-uncompress', () => { // @ts-expect-error diff --git a/index.d.ts b/index.d.ts index 302f156..d935089 100644 --- a/index.d.ts +++ b/index.d.ts @@ -16,6 +16,7 @@ export interface DecOptions { * see https://www.electronjs.org/blog/v8-memory-cage and https://github.com/electron/electron/issues/35801#issuecomment-1261206333 */ copyOutputData?: boolean + output?: Uint8Array } export interface EncOptions { @@ -31,6 +32,16 @@ export interface EncOptions { copyOutputData?: boolean } -export declare function uncompress(input: string | Uint8Array, options?: DecOptions | undefined | null, signal?: AbortSignal | undefined | null): Promise +export declare function uncompress(input: string | Uint8Array, options?: DecOptions | undefined | null, signal?: AbortSignal | undefined | null): Promise +export declare function uncompress(input: string | Uint8Array, options: { asBuffer: false }): Promise; +export declare function uncompress(input: string | Uint8Array, options: { output: Uint8Array }): Promise; +export declare function uncompress(input: string | Uint8Array, options?: { asBuffer?: true }): Promise; +export declare function uncompress(input: string | Uint8Array, options?: DecOptions): Promise; + + +export declare function uncompressSync(input: undefined, options?: DecOptions | undefined | null): Buffer +export declare function uncompressSync(input: string | Uint8Array, options: { asBuffer: false }): string; +export declare function uncompressSync(input: string | Uint8Array, options: { output: Uint8Array }): number; +export declare function uncompressSync(input: string | Uint8Array, options?: { asBuffer?: true }): Buffer; +export declare function uncompressSync(input: string | Uint8Array, options?: DecOptions): string | Buffer | number; -export declare function uncompressSync(input: string | Uint8Array, options?: DecOptions | undefined | null): string | Buffer diff --git a/src/lib.rs b/src/lib.rs index 8bcb20e..aa1d6e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,7 @@ pub struct DecOptions { /// for compatibility with electron >= 21 \n /// see https://www.electronjs.org/blog/v8-memory-cage and https://github.com/electron/electron/issues/35801#issuecomment-1261206333 pub copy_output_data: Option, + pub output: Option, } #[napi(object)] @@ -74,31 +75,49 @@ pub struct Dec { #[napi] impl<'env> ScopedTask<'env> for Dec { - type Output = Vec; - type JsValue = Either>; + type Output = Either, u32>; + type JsValue = Either3, u32>; fn compute(&mut self) -> Result { + let input_data = match &self.data { + Either::A(ref s) => s.as_bytes(), + Either::B(b) => b.as_ref(), + }; + + if let Some(ref mut opts) = self.options { + if let Some(ref mut output_buffer) = opts.output { + let decompressed_len = self + .inner + .decompress(input_data, unsafe { output_buffer.as_mut() }) + .map_err(|err| Error::new(Status::GenericFailure, format!("{err}")))?; + + return Ok(Either::B(decompressed_len as u32)); + } + } self .inner - .decompress_vec(match self.data { - Either::A(ref s) => s.as_bytes(), - Either::B(ref b) => b.as_ref(), - }) + .decompress_vec(input_data) + .map(Either::A) .map_err(|e| Error::new(Status::GenericFailure, format!("{e}"))) } fn resolve(&mut self, env: &'env Env, output: Self::Output) -> Result { - let opt_ref = self.options.as_ref(); - if opt_ref.and_then(|o| o.as_buffer).unwrap_or(true) { - if opt_ref.and_then(|o| o.copy_output_data).unwrap_or(false) { - BufferSlice::copy_from(env, output).map(Either::B) - } else { - BufferSlice::from_data(env, output).map(Either::B) + match output { + Either::B(length) => Ok(Either3::C(length)), + Either::A(output) => { + let opt_ref = self.options.as_ref(); + if opt_ref.and_then(|o| o.as_buffer).unwrap_or(true) { + if opt_ref.and_then(|o| o.copy_output_data).unwrap_or(false) { + BufferSlice::copy_from(env, output).map(Either3::B) + } else { + BufferSlice::from_data(env, output).map(Either3::B) + } + } else { + Ok(Either3::A(String::from_utf8(output).map_err(|e| { + Error::new(Status::GenericFailure, format!("{e}")) + })?)) + } } - } else { - Ok(Either::A(String::from_utf8(output).map_err(|e| { - Error::new(Status::GenericFailure, format!("{e}")) - })?)) } } } @@ -144,39 +163,62 @@ pub fn compress( AsyncTask::with_optional_signal(encoder, signal) } -#[napi] +#[napi(ts_return_type = r#"Buffer +export declare function uncompressSync(input: string | Uint8Array, options: { asBuffer: false }): string; +export declare function uncompressSync(input: string | Uint8Array, options: { output: Uint8Array }): number; +export declare function uncompressSync(input: string | Uint8Array, options?: { asBuffer?: true }): Buffer; +export declare function uncompressSync(input: string | Uint8Array, options?: DecOptions): string | Buffer | number; +"#)] pub fn uncompress_sync<'env>( env: &'env Env, - input: Either, + #[napi(ts_arg_type = "undefined")] input: Either, options: Option, -) -> Result>> { +) -> Result, u32>> { let mut dec = Decoder::new(); + let input_data = match input { + Either::A(ref s) => s.as_bytes(), + Either::B(b) => b, + }; + + let as_buffer = options.as_ref().and_then(|o| o.as_buffer).unwrap_or(true); + let copy_output_data = options + .as_ref() + .and_then(|o| o.copy_output_data) + .unwrap_or(false); + + if let Some(mut opts) = options { + if let Some(ref mut output_buffer) = opts.output { + let decompressed_len = dec + .decompress(input_data, unsafe { output_buffer.as_mut() }) + .map_err(|err| Error::new(Status::GenericFailure, format!("{err}")))?; + + return Ok(Either3::C(decompressed_len as u32)); + } + } dec - .decompress_vec(match input { - Either::A(ref s) => s.as_bytes(), - Either::B(b) => b, - }) + .decompress_vec(input_data) .map_err(|err| Error::new(Status::GenericFailure, format!("{err}"))) .and_then(|output| { - if options.as_ref().and_then(|o| o.as_buffer).unwrap_or(true) { - if options - .as_ref() - .and_then(|o| o.copy_output_data) - .unwrap_or(false) - { - BufferSlice::copy_from(env, output).map(Either::B) + if as_buffer { + if copy_output_data { + BufferSlice::copy_from(env, output).map(Either3::B) } else { - BufferSlice::from_data(env, output).map(Either::B) + BufferSlice::from_data(env, output).map(Either3::B) } } else { - Ok(Either::A(String::from_utf8(output).map_err(|e| { + Ok(Either3::A(String::from_utf8(output).map_err(|e| { Error::new(Status::GenericFailure, format!("{e}")) })?)) } }) } -#[napi] +#[napi(ts_return_type = r#"Promise +export declare function uncompress(input: string | Uint8Array, options: { asBuffer: false }): Promise; +export declare function uncompress(input: string | Uint8Array, options: { output: Uint8Array }): Promise; +export declare function uncompress(input: string | Uint8Array, options?: { asBuffer?: true }): Promise; +export declare function uncompress(input: string | Uint8Array, options?: DecOptions): Promise; +"#)] pub fn uncompress( input: Either, options: Option,