From 0f4b9da90f26856f7941298f3cf3c6641258eea4 Mon Sep 17 00:00:00 2001 From: chh-ay Date: Tue, 6 Oct 2026 15:33:04 +0700 Subject: [PATCH] perf(core): reduce JSON byte accounting overhead Batch bounded ASCII runs and avoid JSON serialization and descriptor allocation for finite numbers and own-property checks. This reduces large-clear undo by 7.16% outside the 4.62% A/A band, while CSV import and packed admission remain within 1%. --- .changeset/json-byte-counter.md | 5 +++++ packages/core/src/errors.ts | 25 ++++++++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 .changeset/json-byte-counter.md diff --git a/.changeset/json-byte-counter.md b/.changeset/json-byte-counter.md new file mode 100644 index 00000000..2c4b50f4 --- /dev/null +++ b/.changeset/json-byte-counter.md @@ -0,0 +1,5 @@ +--- +"@sheetwrite/core": patch +--- + +Reduce transaction admission work when counting encoded JSON bytes. Count plain ASCII runs together, encode finite number lengths without JSON serialization, and check own `toJSON` properties without allocating descriptors. Keep byte counts, resource limits, and invalid-input checks unchanged. diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts index ad24e78a..3a1c48f0 100644 --- a/packages/core/src/errors.ts +++ b/packages/core/src/errors.ts @@ -400,8 +400,22 @@ export function boundedJsonByteLength( }; const addString = (input: string): void => { add(2); - for (let index = 0; index < input.length; index++) { + let index = 0; + while (index < input.length) { const code = input.charCodeAt(index); + if (code >= 0x20 && code <= 0x7f && code !== 0x22 && code !== 0x5c) { + // Batch plain ASCII, but stop scanning at the first overflowing byte. + const runLimit = Math.min(input.length, index + Math.floor(limit - bytes) + 1); + let runEnd = index + 1; + while (runEnd < runLimit) { + const next = input.charCodeAt(runEnd); + if (next < 0x20 || next > 0x7f || next === 0x22 || next === 0x5c) break; + runEnd += 1; + } + add(runEnd - index); + index = runEnd; + continue; + } if ( code === 0x22 || code === 0x5c || @@ -414,8 +428,6 @@ export function boundedJsonByteLength( add(2); } else if (code < 0x20) { add(6); - } else if (code <= 0x7f) { - add(1); } else if (code <= 0x7ff) { add(2); } else if (code >= 0xd800 && code <= 0xdbff) { @@ -431,6 +443,7 @@ export function boundedJsonByteLength( } else { add(3); } + index += 1; } }; const visit = (input: unknown): void => { @@ -450,7 +463,8 @@ export function boundedJsonByteLength( if (!Number.isFinite(input)) { throw new JsonByteLengthError("invalid", "JSON numbers must be finite"); } - add(JSON.stringify(input).length); + // JSON encodes a finite number as its ToString, so no JSON machinery is needed. + add(String(input).length); return; } if (typeof input !== "object") { @@ -491,7 +505,8 @@ export function boundedJsonByteLength( throw new JsonByteLengthError("invalid", "JSON values must contain only plain objects"); } const record = input as Record; - if (Object.getOwnPropertyDescriptor(record, "toJSON")) { + // Any own property named toJSON is rejected, whatever its descriptor says. + if (Object.hasOwn(record, "toJSON")) { throw new JsonByteLengthError("invalid", "JSON values cannot define toJSON"); } add(2);