Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(ext/node): Optimise Buffer string operations #20158

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 24 additions & 11 deletions ext/node/polyfills/internal/buffer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ Buffer.prototype.base64urlWrite = function base64urlWrite(

Buffer.prototype.hexWrite = function hexWrite(string, offset, length) {
return blitBuffer(
hexToBytes(string, this.length - offset),
hexToBytes(string),
this,
offset,
length,
Expand Down Expand Up @@ -751,6 +751,9 @@ Buffer.prototype.utf8Write = function utf8Write(string, offset, length) {
};

Buffer.prototype.write = function write(string, offset, length, encoding) {
if (typeof string !== "string") {
throw new codes.ERR_INVALID_ARG_TYPE("argument", "string");
}
// Buffer#write(string);
if (offset === undefined) {
return this.utf8Write(string, 0, this.length);
Expand Down Expand Up @@ -1756,16 +1759,26 @@ function utf8ToBytes(string, units) {
return bytes;
}

function blitBuffer(src, dst, offset, byteLength) {
let i;
const length = byteLength === undefined ? src.length : byteLength;
for (i = 0; i < length; ++i) {
if (i + offset >= dst.length || i >= src.length) {
break;
}
dst[i + offset] = src[i];
}
return i;
function blitBuffer(src, dst, offset, byteLength = Infinity) {
// Establish the number of bytes to be written
const bytesToWrite = Math.min(
// If byte length is defined in the call, then it sets an upper bound,
// otherwise it is Infinity and is never chosen.
byteLength,
// The length of the source sets an upper bound being the source of data.
src.length,
// The length of the destination minus any offset into it sets an upper bound.
dst.length - offset,
);
if (bytesToWrite < src.length) {
// Resize the source buffer to the number of bytes we're about to write.
// This both makes sure that we're actually only writing what we're told to
// write but also prevents `Uint8Array#set` from throwing an error if the
// source is longer than the target.
src = src.subarray(0, length);
}
dst.set(src, offset);
return bytesToWrite;
}

function isInstance(obj, type) {
Expand Down
86 changes: 51 additions & 35 deletions ext/node/polyfills/internal_binding/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import {
} from "ext:deno_web/00_infra.js";

export function asciiToBytes(str: string) {
const byteArray = [];
for (let i = 0; i < str.length; ++i) {
byteArray.push(str.charCodeAt(i) & 255);
const length = str.length;
const byteArray = new Uint8Array(length);
for (let i = 0; i < length; ++i) {
byteArray[i] = str.charCodeAt(i) & 255;
}
return new Uint8Array(byteArray);
return byteArray;
}

export function base64ToBytes(str: string) {
Expand All @@ -25,16 +26,26 @@ export function base64ToBytes(str: string) {
const INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g;
function base64clean(str: string) {
// Node takes equal signs as end of the Base64 encoding
str = str.split("=")[0];
const eqIndex = str.indexOf("=");
str = eqIndex !== -1 ? str.substring(0, eqIndex).trimStart() : str.trim();
// Node strips out invalid characters like \n and \t from the string, std/base64 does not
str = str.trim().replace(INVALID_BASE64_RE, "");
str = str.replace(INVALID_BASE64_RE, "");
// Node converts strings with length < 2 to ''
if (str.length < 2) return "";
const length = str.length;
if (length < 2) return "";
// Node allows for non-padded base64 strings (missing trailing ===), std/base64 does not
while (str.length % 4 !== 0) {
str = str + "=";
switch (length % 4) {
case 0:
return str;
case 1:
return `${str}===`;
case 2:
return `${str}==`;
case 3:
return `${str}=`;
default:
throw new Error("Unexpected NaN value for string length");
}
return str;
}

export function base64UrlToBytes(str: string) {
Expand All @@ -44,49 +55,54 @@ export function base64UrlToBytes(str: string) {
}

export function hexToBytes(str: string) {
const byteArray = new Uint8Array(Math.floor((str || "").length / 2));
let i;
for (i = 0; i < byteArray.length; i++) {
const length = str.length >>> 1;
const byteArray = new Uint8Array(length);
let i: number;
for (i = 0; i < length; i++) {
const a = Number.parseInt(str[i * 2], 16);
const b = Number.parseInt(str[i * 2 + 1], 16);
if (Number.isNaN(a) && Number.isNaN(b)) {
break;
}
byteArray[i] = (a << 4) | b;
}
return new Uint8Array(
i === byteArray.length ? byteArray : byteArray.slice(0, i),
);
// Returning a buffer subarray is okay: This API's return value
// is never exposed to users and is only ever used for its length
// and the data within the subarray.
return i === length ? byteArray : byteArray.subarray(0, i);
}

export function utf16leToBytes(str: string, units: number) {
let c, hi, lo;
const byteArray = [];
for (let i = 0; i < str.length; ++i) {
if ((units -= 2) < 0) {
break;
}
c = str.charCodeAt(i);
hi = c >> 8;
lo = c % 256;
byteArray.push(lo);
byteArray.push(hi);
export function utf16leToBytes(str: string, units?: number) {
// If units is defined, round it to even values for 16 byte "steps"
// and use it as an upper bound value for our string byte array's length.
const length = Math.min(str.length * 2, units ? (units >>> 1) * 2 : Infinity);
const byteArray = new Uint8Array(length);
const view = new DataView(byteArray.buffer);
let i: number;
for (i = 0; i * 2 < length; i++) {
view.setUint16(i * 2, str.charCodeAt(i), true);
}
return new Uint8Array(byteArray);
// Returning a buffer subarray is okay: This API's return value
// is never exposed to users and is only ever used for its length
// and the data within the subarray.
return i * 2 === length ? byteArray : byteArray.subarray(0, i * 2);
}

export function bytesToAscii(bytes: Uint8Array) {
let ret = "";
for (let i = 0; i < bytes.length; ++i) {
ret += String.fromCharCode(bytes[i] & 127);
let res = "";
const length = bytes.byteLength;
for (let i = 0; i < length; ++i) {
res = `${res}${String.fromCharCode(bytes[i] & 127)}`;
}
return ret;
return res;
}

export function bytesToUtf16le(bytes: Uint8Array) {
let res = "";
for (let i = 0; i < bytes.length - 1; i += 2) {
res += String.fromCharCode(bytes[i] + bytes[i + 1] * 256);
const length = bytes.byteLength;
const view = new DataView(bytes.buffer, bytes.byteOffset, length);
for (let i = 0; i < length - 1; i += 2) {
res = `${res}${String.fromCharCode(view.getUint16(i, true))}`;
}
return res;
}
2 changes: 2 additions & 0 deletions ext/web/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ declare module "ext:deno_web/00_infra.js" {
};
function forgivingBase64Encode(data: Uint8Array): string;
function forgivingBase64Decode(data: string): Uint8Array;
function forgivingBase64UrlEncode(data: Uint8Array | string): string;
function forgivingBase64UrlDecode(data: string): Uint8Array;
function serializeJSValueToJSONString(value: unknown): string;
}

Expand Down