lib: use utf8 fast path for streaming TextDecoder · nodejs/node@111b99e · GitHub
Skip to content

Commit 111b99e

Browse files
committed
lib: use utf8 fast path for streaming TextDecoder
1 parent e155415 commit 111b99e

3 files changed

Lines changed: 138 additions & 47 deletions

File tree

lib/internal/encoding.js

Lines changed: 53 additions & 6 deletions

lib/internal/encoding/util.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// From https://npmjs.com/package/@exodus/bytes
2+
// Copyright Exodus Movement. Licensed under MIT License.
3+
4+
'use strict';
5+
6+
const {
7+
Uint8Array,
8+
} = primordials;
9+
10+
11+
/**
12+
* Get a number of last bytes in an Uint8Array `u` ending at `len` that don't
13+
* form a codepoint yet, but can be a part of a single codepoint on more data.
14+
*
15+
* @param {Uint8Array} u Uint8Array of potentially UTF-8 bytes
16+
* @param {number} len Position to look behind from
17+
* @returns {number} Number of unfinished potentially valid UTF-8 bytes ending at position `len`
18+
*/
19+
function unfinishedBytesUtf8(u, len) {
20+
// 0-3
21+
let p = 0;
22+
while (p < 2 && p < len && (u[len - p - 1] & 0xc0) === 0x80) p++; // Go back 0-2 trailing bytes
23+
if (p === len) return 0; // no space for lead
24+
const l = u[len - p - 1];
25+
if (l < 0xc2 || l > 0xf4) return 0; // not a lead
26+
if (p === 0) return 1; // Nothing to recheck, we have only lead, return it. 2-byte must return here
27+
if (l < 0xe0 || (l < 0xf0 && p >= 2)) return 0; // 2-byte, or 3-byte or less and we already have 2 trailing
28+
const lower = l === 0xf0 ? 0x90 : l === 0xe0 ? 0xa0 : 0x80;
29+
const upper = l === 0xf4 ? 0x8f : l === 0xed ? 0x9f : 0xbf;
30+
const n = u[len - p];
31+
return n >= lower && n <= upper ? p + 1 : 0;
32+
}
33+
34+
/**
35+
* Merge prefix `chunk` with `u` and return new combined prefix.
36+
* For u.length < 3, fully consumes u and can return unfinished data,
37+
* otherwise returns a prefix with no unfinished bytes
38+
*
39+
* @param {Uint8Array} u Uint8Array of potentially UTF-8 bytes
40+
* @param {Uint8Array} chunk Prefix to prepend before `u`
41+
* @returns {Uint8Array} If u.length >= 3: an Uint8Array containing `chunk` and a portion of `u`
42+
* so that the result contains no unfinished UTF-8 codepoints. If u.length < 3: concat(chunk, u).
43+
*/
44+
function mergePrefixUtf8(u, chunk) {
45+
if (u.length === 0) return chunk;
46+
if (u.length < 3) {
47+
// No reason to bruteforce offsets, also it's possible this doesn't yet end the sequence
48+
const a = new Uint8Array(u.length + chunk.length);
49+
a.set(chunk);
50+
a.set(u, chunk.length);
51+
return a;
52+
}
53+
54+
// Slice off a small portion of u into prefix chunk so we can decode them separately without extending array size
55+
const t = new Uint8Array(chunk.length + 3); // We have 1-3 bytes and need 1-3 more bytes
56+
t.set(chunk);
57+
t.set(u.subarray(0, 3), chunk.length);
58+
59+
// Stop at the first offset where unfinished bytes reaches 0 or fits into u
60+
// If that doesn't happen (u too short), just concat chunk and u completely (above)
61+
for (let i = 1; i <= 3; i++) {
62+
const unfinished = unfinishedBytesUtf8(t, chunk.length + i); // 0-3
63+
if (unfinished <= i) {
64+
// Always reachable at 3, but we still need 'unfinished' value for it
65+
const add = i - unfinished; // 0-3
66+
return add > 0 ? t.subarray(0, chunk.length + add) : chunk;
67+
}
68+
}
69+
70+
// Unreachable
71+
}
72+
73+
module.exports = { unfinishedBytesUtf8, mergePrefixUtf8 };

test/parallel/test-whatwg-encoding-custom-textdecoder.js

Lines changed: 12 additions & 41 deletions

0 commit comments

Comments
 (0)