Skip to content

Commit

Permalink
perf(ext/node): improve Buffer from string performance (#24567)
Browse files Browse the repository at this point in the history
Fixes #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
```

(cherry picked from commit 1ba88a7)
  • Loading branch information
littledivy authored and crowlKats committed Jul 31, 2024
1 parent 313b026 commit 98ecb56
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 33 deletions.
15 changes: 13 additions & 2 deletions ext/node/polyfills/_brotli.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ const {
TypedArrayPrototypeSlice,
TypedArrayPrototypeSubarray,
TypedArrayPrototypeGetByteLength,
TypedArrayPrototypeGetByteOffset,
DataViewPrototypeGetBuffer,
DataViewPrototypeGetByteLength,
DataViewPrototypeGetByteOffset,
TypedArrayPrototypeGetBuffer,
} = primordials;
const { isTypedArray, isDataView, close } = core;
Expand Down Expand Up @@ -38,9 +41,17 @@ const toU8 = (input) => {
}

if (isTypedArray(input)) {
return new Uint8Array(TypedArrayPrototypeGetBuffer(input));
return new Uint8Array(
TypedArrayPrototypeGetBuffer(input),
TypedArrayPrototypeGetByteOffset(input),
TypedArrayPrototypeGetByteLength(input),
);
} else if (isDataView(input)) {
return new Uint8Array(DataViewPrototypeGetBuffer(input));
return new Uint8Array(
DataViewPrototypeGetBuffer(input),
DataViewPrototypeGetByteOffset(input),
DataViewPrototypeGetByteLength(input),
);
}

return input;
Expand Down
2 changes: 1 addition & 1 deletion ext/node/polyfills/_http_outgoing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ export class OutgoingMessage extends Stream {
if (data instanceof Buffer) {
data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
}
if (data.buffer.byteLength > 0) {
if (data.byteLength > 0) {
this._bodyWriter.write(data).then(() => {
callback?.();
this.emit("drain");
Expand Down
102 changes: 81 additions & 21 deletions ext/node/polyfills/internal/buffer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
asciiToBytes,
base64ToBytes,
base64UrlToBytes,
base64Write,
bytesToAscii,
bytesToUtf16le,
hexToBytes,
Expand All @@ -42,6 +43,15 @@ import { Blob } from "ext:deno_web/09_file.js";

export { atob, Blob, btoa };

class FastBuffer extends Uint8Array {
// Using an explicit constructor here is necessary to avoid relying on
// `Array.prototype[Symbol.iterator]`, which can be mutated by users.
// eslint-disable-next-line no-useless-constructor
constructor(bufferOrLength, byteOffset, length) {
super(bufferOrLength, byteOffset, length);
}
}

const utf8Encoder = new TextEncoder();

// Temporary buffers to convert numbers.
Expand Down Expand Up @@ -72,6 +82,9 @@ export const constants = {
MAX_STRING_LENGTH: kStringMaxLength,
};

FastBuffer.prototype.constructor = Buffer;
Buffer.prototype = FastBuffer.prototype;

Object.defineProperty(Buffer.prototype, "parent", {
enumerable: true,
get: function () {
Expand All @@ -98,9 +111,7 @@ function createBuffer(length) {
'The value "' + length + '" is invalid for option "size"',
);
}
const buf = new Uint8Array(length);
Object.setPrototypeOf(buf, Buffer.prototype);
return buf;
return new FastBuffer(length);
}

export function Buffer(arg, encodingOrOffset, length) {
Expand All @@ -117,7 +128,32 @@ export function Buffer(arg, encodingOrOffset, length) {
return _from(arg, encodingOrOffset, length);
}

Buffer.poolSize = 8192;
Object.defineProperty(Buffer, Symbol.species, {
__proto__: null,
enumerable: false,
configurable: true,
get() {
return FastBuffer;
},
});

Buffer.poolSize = 8 * 1024;
let poolSize, poolOffset, allocPool;

function createPool() {
poolSize = Buffer.poolSize;
allocPool = new Uint8Array(poolSize).buffer;
poolOffset = 0;
}
createPool();

function alignPool() {
// Ensure aligned slices
if (poolOffset & 0x7) {
poolOffset |= 0x7;
poolOffset++;
}
}

function _from(value, encodingOrOffset, length) {
if (typeof value === "string") {
Expand Down Expand Up @@ -204,26 +240,44 @@ Buffer.allocUnsafeSlow = function allocUnsafeSlow(size) {
return _allocUnsafe(size);
};

function fromStringFast(string, ops) {
const length = ops.byteLength(string);
if (length >= (Buffer.poolSize >>> 1)) {
const data = ops.create(string);
Object.setPrototypeOf(data, Buffer.prototype);
return data;
}

if (length > (poolSize - poolOffset)) {
createPool();
}
let b = new FastBuffer(allocPool, poolOffset, length);
const actual = ops.write(b, string, 0, length);
if (actual != length) {
// byteLength() may overestimate. That's a rare case, though.
b = new FastBuffer(allocPool, poolOffset, actual);
}
poolOffset += actual;
alignPool();
return b;
}

function fromString(string, encoding) {
if (typeof encoding !== "string" || encoding === "") {
encoding = "utf8";
}
if (!Buffer.isEncoding(encoding)) {
throw new codes.ERR_UNKNOWN_ENCODING(encoding);
}
const length = byteLength(string, encoding) | 0;
let buf = createBuffer(length);
const actual = buf.write(string, encoding);
if (actual !== length) {
buf = buf.slice(0, actual);
const ops = getEncodingOps(encoding);
if (ops === undefined) {
throw new codes.ERR_UNKNOWN_ENCODING(encoding);
}
return buf;
return fromStringFast(string, ops);
}

function fromArrayLike(obj) {
const buf = new Uint8Array(obj);
Object.setPrototypeOf(buf, Buffer.prototype);
return buf;
return new FastBuffer(obj);
}

function fromObject(obj) {
Expand Down Expand Up @@ -260,7 +314,7 @@ Object.setPrototypeOf(SlowBuffer.prototype, Uint8Array.prototype);
Object.setPrototypeOf(SlowBuffer, Uint8Array);

Buffer.isBuffer = function isBuffer(b) {
return b != null && b._isBuffer === true && b !== Buffer.prototype;
return b instanceof Buffer;
};

Buffer.compare = function compare(a, b) {
Expand Down Expand Up @@ -664,12 +718,12 @@ Buffer.prototype.base64Slice = function base64Slice(
}
};

Buffer.prototype.base64Write = function base64Write(
Buffer.prototype.base64Write = function base64Write_(
string,
offset,
length,
) {
return blitBuffer(base64ToBytes(string), this, offset, length);
return base64Write(string, this, offset, length);
};

Buffer.prototype.base64urlSlice = function base64urlSlice(
Expand Down Expand Up @@ -737,8 +791,8 @@ Buffer.prototype.ucs2Write = function ucs2Write(string, offset, length) {
);
};

Buffer.prototype.utf8Slice = function utf8Slice(string, offset, length) {
return _utf8Slice(this, string, offset, length);
Buffer.prototype.utf8Slice = function utf8Slice(offset, length) {
return _utf8Slice(this, offset, length);
};

Buffer.prototype.utf8Write = function utf8Write(string, offset, length) {
Expand Down Expand Up @@ -831,9 +885,7 @@ function fromArrayBuffer(obj, byteOffset, length) {
}
}

const buffer = new Uint8Array(obj, byteOffset, length);
Object.setPrototypeOf(buffer, Buffer.prototype);
return buffer;
return new FastBuffer(obj, byteOffset, length);
}

function _base64Slice(buf, start, end) {
Expand Down Expand Up @@ -2105,6 +2157,7 @@ export const encodingOps = {
dir,
),
slice: (buf, start, end) => buf.asciiSlice(start, end),
create: (string) => asciiToBytes(string),
write: (buf, string, offset, len) => buf.asciiWrite(string, offset, len),
},
base64: {
Expand All @@ -2119,6 +2172,7 @@ export const encodingOps = {
encodingsMap.base64,
dir,
),
create: (string) => base64ToBytes(string),
slice: (buf, start, end) => buf.base64Slice(start, end),
write: (buf, string, offset, len) => buf.base64Write(string, offset, len),
},
Expand All @@ -2134,6 +2188,7 @@ export const encodingOps = {
encodingsMap.base64url,
dir,
),
create: (string) => base64UrlToBytes(string),
slice: (buf, start, end) => buf.base64urlSlice(start, end),
write: (buf, string, offset, len) =>
buf.base64urlWrite(string, offset, len),
Expand All @@ -2150,6 +2205,7 @@ export const encodingOps = {
encodingsMap.hex,
dir,
),
create: (string) => hexToBytes(string),
slice: (buf, start, end) => buf.hexSlice(start, end),
write: (buf, string, offset, len) => buf.hexWrite(string, offset, len),
},
Expand All @@ -2165,6 +2221,7 @@ export const encodingOps = {
encodingsMap.latin1,
dir,
),
create: (string) => asciiToBytes(string),
slice: (buf, start, end) => buf.latin1Slice(start, end),
write: (buf, string, offset, len) => buf.latin1Write(string, offset, len),
},
Expand All @@ -2180,6 +2237,7 @@ export const encodingOps = {
encodingsMap.utf16le,
dir,
),
create: (string) => utf16leToBytes(string),
slice: (buf, start, end) => buf.ucs2Slice(start, end),
write: (buf, string, offset, len) => buf.ucs2Write(string, offset, len),
},
Expand All @@ -2195,6 +2253,7 @@ export const encodingOps = {
encodingsMap.utf8,
dir,
),
create: (string) => utf8Encoder.encode(string),
slice: (buf, start, end) => buf.utf8Slice(start, end),
write: (buf, string, offset, len) => buf.utf8Write(string, offset, len),
},
Expand All @@ -2210,6 +2269,7 @@ export const encodingOps = {
encodingsMap.utf16le,
dir,
),
create: (string) => utf16leToBytes(string),
slice: (buf, start, end) => buf.ucs2Slice(start, end),
write: (buf, string, offset, len) => buf.ucs2Write(string, offset, len),
},
Expand Down
17 changes: 17 additions & 0 deletions ext/node/polyfills/internal_binding/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
forgivingBase64Decode,
forgivingBase64UrlDecode,
} from "ext:deno_web/00_infra.js";
import { op_base64_write } from "ext:core/ops";

export function asciiToBytes(str: string) {
const length = str.length;
Expand All @@ -27,6 +28,22 @@ export function base64ToBytes(str: string) {
}
}

export function base64Write(
str: string,
buffer: Uint8Array,
offset: number = 0,
length?: number,
): number {
length = length ?? buffer.byteLength - offset;
try {
return op_base64_write(str, buffer, offset, length);
} catch {
str = base64clean(str);
str = str.replaceAll("-", "+").replaceAll("_", "/");
return op_base64_write(str, buffer, offset, length);
}
}

const INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g;
function base64clean(str: string) {
// Node takes equal signs as end of the Base64 encoding
Expand Down
21 changes: 18 additions & 3 deletions ext/node/polyfills/string_decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,15 @@ const {
Symbol,
MathMin,
DataViewPrototypeGetBuffer,
DataViewPrototypeGetByteLength,
DataViewPrototypeGetByteOffset,
ObjectPrototypeIsPrototypeOf,
String,
TypedArrayPrototypeGetBuffer,
TypedArrayPrototypeGetByteLength,
TypedArrayPrototypeGetByteOffset,
StringPrototypeToLowerCase,
Uint8Array,
} = primordials;
const { isTypedArray } = core;

Expand Down Expand Up @@ -83,11 +88,21 @@ function normalizeBuffer(buf: Buffer) {
}
if (isBufferType(buf)) {
return buf;
} else if (isTypedArray(buf)) {
return Buffer.from(
new Uint8Array(
TypedArrayPrototypeGetBuffer(buf),
TypedArrayPrototypeGetByteOffset(buf),
TypedArrayPrototypeGetByteLength(buf),
),
);
} else {
return Buffer.from(
isTypedArray(buf)
? TypedArrayPrototypeGetBuffer(buf)
: DataViewPrototypeGetBuffer(buf),
new Uint8Array(
DataViewPrototypeGetBuffer(buf),
DataViewPrototypeGetByteOffset(buf),
DataViewPrototypeGetByteLength(buf),
),
);
}
}
Expand Down
Loading

0 comments on commit 98ecb56

Please sign in to comment.