perf(core): add pure-ASCII fast path to text token estimation (#6551)

estimateTextTokens scanned every string char-by-char via charCodeAt to
classify ASCII vs non-ASCII code units. For pure-ASCII text (code,
English prose - the common case) a single regex scan using V8's
optimized string search replaces the JS loop, and the mixed-text path
now counts only non-ASCII units, deriving the ASCII count from the
length. The token formula is unchanged, so results are byte-identical
for every input; verified exhaustively over all 65536 single code units
plus 20k randomized mixed strings against the previous implementation.

Median of 6 solo benchmark runs over a deterministic mixed fixture set:
51.9ms -> 32.2ms (-38%, 1.61x).
This commit is contained in:
Dex 2026-07-09 00:39:02 +01:00 committed by GitHub
parent fbdaa52c52
commit e3a247f99e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 41 additions and 6 deletions

View file

@ -262,6 +262,37 @@ describe('TextTokenizer', () => {
});
});
describe('ASCII/non-ASCII boundary', () => {
it('should treat DEL (U+007F) as ASCII and U+0080 as non-ASCII', async () => {
// '\x7F' = 1 ASCII char: 1 / 4 = 0.25 -> ceil = 1
expect(await tokenizer.calculateTokens('\x7F')).toBe(1);
// '\u0080' = 1 non-ASCII char: 1 * 1.1 = 1.1 -> ceil = 2
expect(await tokenizer.calculateTokens('\u0080')).toBe(2);
});
it('should count pure-ASCII text of any length as ceil(length / 4)', async () => {
for (const len of [1, 3, 4, 5, 4096, 4097]) {
const text = 'a'.repeat(len);
expect(await tokenizer.calculateTokens(text)).toBe(Math.ceil(len / 4));
}
});
it('should stay consistent when a single non-ASCII char joins long ASCII text', async () => {
const ascii = 'x'.repeat(1000);
// 1000 / 4 = 250
expect(await tokenizer.calculateTokens(ascii)).toBe(250);
// 1000 / 4 + 1 * 1.1 = 251.1 -> ceil = 252, wherever the char sits
expect(await tokenizer.calculateTokens(ascii + '中')).toBe(252);
expect(await tokenizer.calculateTokens('中' + ascii)).toBe(252);
});
it('should count surrogate pairs as two non-ASCII units within mixed text', async () => {
const text = 'abcd🚀'; // 4 ASCII + 2 UTF-16 units
// 4 / 4 + 2 * 1.1 = 3.2 -> ceil = 4
expect(await tokenizer.calculateTokens(text)).toBe(4);
});
});
describe('large inputs', () => {
it('should handle very long text', async () => {
const longText = 'a'.repeat(200000); // 200k characters

View file

@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
const NON_ASCII_RE = /[\u0080-\uffff]/;
/**
* Text tokenizer for calculating text tokens using character-based estimation.
*
@ -19,17 +21,19 @@ export function estimateTextTokens(text: string): number {
return 0;
}
let asciiChars = 0;
let nonAsciiChars = 0;
// Fast path: pure-ASCII text (code, English prose). A single regex scan
// uses V8's optimized string search instead of a per-character JS loop.
if (!NON_ASCII_RE.test(text)) {
return Math.ceil(text.length / 4);
}
let nonAsciiChars = 0;
for (let i = 0; i < text.length; i++) {
const charCode = text.charCodeAt(i);
if (charCode < 128) {
asciiChars++;
} else {
if (text.charCodeAt(i) >= 128) {
nonAsciiChars++;
}
}
const asciiChars = text.length - nonAsciiChars;
const tokens = asciiChars / 4 + nonAsciiChars * 1.1;
return Math.ceil(tokens);