1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-03 04:48:52 -05:00
denoland-deno/ext/node/polyfills/_brotli.js
Divy Srivastava 1ba88a7892
perf(ext/node): improve Buffer from string performance (#24567)
Fixes https://github.com/denoland/deno/issues/24323

- Use a Buffer pool for `fromString`
- Implement fast call base64 writes
- Direct from string `create` method for each encoding op

```
$ deno bench -A bench.mjs # 1.45.1+fee4d3a
cpu: Apple M1 Pro
runtime: deno 1.45.1+fee4d3a (aarch64-apple-darwin)

benchmark                time (avg)             (min … max)       p75       p99      p999
----------------------------------------------------------- -----------------------------
Buffer.from base64      550 ns/iter     (490 ns … 1'265 ns)    572 ns    606 ns  1'265 ns
Buffer#write base64     285 ns/iter       (259 ns … 371 ns)    307 ns    347 ns    360 ns

$ ~/gh/deno/target/release/deno bench -A bench.mjs # this PR
cpu: Apple M1 Pro
runtime: deno dev (aarch64-apple-darwin)

benchmark                time (avg)             (min … max)       p75       p99      p999
----------------------------------------------------------- -----------------------------
Buffer.from base64      151 ns/iter       (145 ns … 770 ns)    148 ns    184 ns    648 ns
Buffer#write base64   62.58 ns/iter     (60.79 ns … 157 ns)  61.65 ns  75.79 ns    141 ns

$ node bench.mjs # v22.4.0
cpu: Apple M1 Pro
runtime: node v22.4.0 (arm64-darwin)

benchmark                time (avg)             (min … max)       p75       p99      p999
----------------------------------------------------------- -----------------------------
Buffer.from base64      163 ns/iter     (96.92 ns … 375 ns)  99.45 ns    127 ns    220 ns
Buffer#write base64   75.48 ns/iter     (74.97 ns … 134 ns)  75.17 ns  81.83 ns  96.84 ns
```
2024-07-30 18:09:55 +05:30

207 lines
5.7 KiB
JavaScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { core, primordials } from "ext:core/mod.js";
const {
Uint8Array,
PromisePrototypeThen,
PromisePrototypeCatch,
ObjectValues,
TypedArrayPrototypeSlice,
TypedArrayPrototypeSubarray,
TypedArrayPrototypeGetByteLength,
TypedArrayPrototypeGetByteOffset,
DataViewPrototypeGetBuffer,
DataViewPrototypeGetByteLength,
DataViewPrototypeGetByteOffset,
TypedArrayPrototypeGetBuffer,
} = primordials;
const { isTypedArray, isDataView, close } = core;
import {
op_brotli_compress,
op_brotli_compress_async,
op_brotli_compress_stream,
op_brotli_compress_stream_end,
op_brotli_decompress,
op_brotli_decompress_async,
op_brotli_decompress_stream,
op_brotli_decompress_stream_end,
op_create_brotli_compress,
op_create_brotli_decompress,
} from "ext:core/ops";
import { zlib as constants } from "ext:deno_node/internal_binding/constants.ts";
import { TextEncoder } from "ext:deno_web/08_text_encoding.js";
import { Transform } from "node:stream";
import { Buffer } from "node:buffer";
const enc = new TextEncoder();
const toU8 = (input) => {
if (typeof input === "string") {
return enc.encode(input);
}
if (isTypedArray(input)) {
return new Uint8Array(
TypedArrayPrototypeGetBuffer(input),
TypedArrayPrototypeGetByteOffset(input),
TypedArrayPrototypeGetByteLength(input),
);
} else if (isDataView(input)) {
return new Uint8Array(
DataViewPrototypeGetBuffer(input),
DataViewPrototypeGetByteOffset(input),
DataViewPrototypeGetByteLength(input),
);
}
return input;
};
export function createBrotliCompress(options) {
return new BrotliCompress(options);
}
export function createBrotliDecompress(options) {
return new BrotliDecompress(options);
}
export class BrotliDecompress extends Transform {
#context;
// TODO(littledivy): use `options` argument
constructor(_options = {}) {
super({
// TODO(littledivy): use `encoding` argument
transform(chunk, _encoding, callback) {
const input = toU8(chunk);
const output = new Uint8Array(TypedArrayPrototypeGetByteLength(chunk));
const avail = op_brotli_decompress_stream(context, input, output);
// deno-lint-ignore prefer-primordials
this.push(TypedArrayPrototypeSlice(output, 0, avail));
callback();
},
flush(callback) {
const output = new Uint8Array(1024);
let avail;
while ((avail = op_brotli_decompress_stream_end(context, output)) > 0) {
// deno-lint-ignore prefer-primordials
this.push(TypedArrayPrototypeSlice(output, 0, avail));
}
close(context);
callback();
},
});
this.#context = op_create_brotli_decompress();
const context = this.#context;
}
}
export class BrotliCompress extends Transform {
#context;
constructor(options = {}) {
super({
// TODO(littledivy): use `encoding` argument
transform(chunk, _encoding, callback) {
const input = toU8(chunk);
const output = new Uint8Array(brotliMaxCompressedSize(input.length));
const written = op_brotli_compress_stream(context, input, output);
if (written > 0) {
// deno-lint-ignore prefer-primordials
this.push(TypedArrayPrototypeSlice(output, 0, written));
}
callback();
},
flush(callback) {
const output = new Uint8Array(1024);
let avail;
while ((avail = op_brotli_compress_stream_end(context, output)) > 0) {
// deno-lint-ignore prefer-primordials
this.push(TypedArrayPrototypeSlice(output, 0, avail));
}
close(context);
callback();
},
});
const params = ObjectValues(options?.params ?? {});
this.#context = op_create_brotli_compress(params);
const context = this.#context;
}
}
function oneOffCompressOptions(options) {
const quality = options?.params?.[constants.BROTLI_PARAM_QUALITY] ??
constants.BROTLI_DEFAULT_QUALITY;
const lgwin = options?.params?.[constants.BROTLI_PARAM_LGWIN] ??
constants.BROTLI_DEFAULT_WINDOW;
const mode = options?.params?.[constants.BROTLI_PARAM_MODE] ??
constants.BROTLI_MODE_GENERIC;
return {
quality,
lgwin,
mode,
};
}
function brotliMaxCompressedSize(input) {
if (input == 0) return 2;
// [window bits / empty metadata] + N * [uncompressed] + [last empty]
const numLargeBlocks = input >> 24;
const overhead = 2 + (4 * numLargeBlocks) + 3 + 1;
const result = input + overhead;
return result < input ? 0 : result;
}
export function brotliCompress(
input,
options,
callback,
) {
const buf = toU8(input);
if (typeof options === "function") {
callback = options;
options = {};
}
const { quality, lgwin, mode } = oneOffCompressOptions(options);
PromisePrototypeCatch(
PromisePrototypeThen(
op_brotli_compress_async(buf, quality, lgwin, mode),
(result) => callback(null, Buffer.from(result)),
),
(err) => callback(err),
);
}
export function brotliCompressSync(
input,
options,
) {
const buf = toU8(input);
const output = new Uint8Array(brotliMaxCompressedSize(buf.length));
const { quality, lgwin, mode } = oneOffCompressOptions(options);
const len = op_brotli_compress(buf, output, quality, lgwin, mode);
return Buffer.from(TypedArrayPrototypeSubarray(output, 0, len));
}
export function brotliDecompress(input) {
const buf = toU8(input);
return PromisePrototypeCatch(
PromisePrototypeThen(
op_brotli_decompress_async(buf),
(result) => callback(null, Buffer.from(result)),
),
(err) => callback(err),
);
}
export function brotliDecompressSync(input) {
return Buffer.from(op_brotli_decompress(toU8(input)));
}