Skip to content

Commit 012c924

Browse files
committed
perf(napi/parser, linter/plugins): speed up decoding strings in raw transfer (#21021)
Improve perf of deserializing strings in raw transfer. This PR combines several optimizations, which have been tested and benchmarked in https://github.com/overlookmotel/oxc-raw-str-bench. This PR implements the version "latin-slice-onebyte64" from that repo, which is the current winner. String deserialization is the main bottleneck in raw transfer, so speeding it up will likely make a large impact on deserialization overall. This work follows on from #20834 which produced a major speed-up in many files by making files which contain some non-ASCII characters take the fast path of slicing `sourceText` more often. This PR tackles the remainder - speeding up the fallback path where the fast path can't be taken. ## Optimizations The optimizations in this PR are: ### Latin1 When source is not 100% ASCII, decode source text from buffer as Latin1. A Latin1-decoded string represents each UTF-8 byte as a single Latin1 character, so it can be indexed into using UTF-8 offsets. So when we can't slice the string from `sourceText` because the UTF-8 and UTF-16 offsets differ (after any non-ASCII character), loop through the string's bytes and check if they're all ASCII. If they are, the string can be sliced from `sourceTextLatin` instead, with the original UTF-8 offsets. This is way faster than calling `textDecoder.decode`, as it avoids a call into C++. [Benchmarks show](https://github.com/overlookmotel/oxc-raw-str-bench/blob/4f96275efa9a35d5d27615abb27f21a137149cc0/README.md#apply30-vs-latin-vs-latin-source64) speed up of 55% on average, and up to 70% on some files. ### Latin1 decoding method It turns out that `new TextDecoder("latin1").decode(arr)` doesn't actually decode to Latin1! Per the WHATWG Encoding Standard, "latin1" is mapped to "windows-1252". The result is that with `TextDecoder("latin1")`: 1. `decode` is quite complicated, requiring a 2-pass scan of the bytes to determine if they're all ASCII, followed by a 2nd pass to do the actual `windows-1252` decoding. If the string *does* contain any non-ASCII characters (which it always does in our usecase), NodeJS implements the decoding in JS, not native code. Slow. 2. `decode` produces a 2-byte-per-char string (`TWO_BYTE` in V8), which takes more memory, and is slower for all operations on it e.g. string comparison, hashing for use as an object key etc. Instead, use `Buffer.prototype.latin1Slice` which: 1. Does a pure Latin1 decode, which is just a single `memcpy` call. 2. Produces a 1-byte-per-char string (`ONE_BYTE` in V8). `latin1Slice` involves a call into C++, but we only do it once per file, so this cost is tiny in context of deserializing the whole AST. ### Latin1 string slicing In the fast path, slice from the Latin1-decoded string, instead of `sourceText`. In the fast path, we know that all bytes of source comprising the string are ASCII, so no further checks are required. This makes no difference on benchmarks for `deserializeStr` itself, but it may have beneficial effects downstream for code (e.g. lint rules) which access strings in the AST, e.g. `Identifier` names. Because Latin1-decoded source text is `ONE_BYTE`-encoded, slices of it are too. In comparison, slices of `sourceText` may be `ONE_BYTE` or `TWO_BYTE`. If a file's source is pure ASCII, it'll be `ONE_BYTE`, if source contains any non-ASCII characters, it'll be `TWO_BYTE`. Files in a repo will likely be a mix of both, which makes strings returned from `deserializeStr` and placed in the AST a mix too. This in turn makes functions (e.g. lint rule visitors) polymorphic. V8 cannot optimize them as aggressively as if they see only `ONE_BYTE` strings. We cannot make sure that all strings returned by `deserializeStr` are `ONE_BYTE`. Some string may contain non-ASCII characters, and they *have* to be represented in `TWO_BYTE` form. But we can minimize it - now only strings which *themselves* contain non-ASCII characters are `TWO_BYTE`, whereas before they would be if the source text as a whole contains a single non-ASCII byte. Code which accesses `Identifier` names, for example will exclusively see `ONE_BYTE` strings and will be more heavily optimized, because Unicode `Identifier`s are rarer than hen's teeth in real-world code. ### Remove string-concatenation loop Previously strings which are outside of source text were assembled byte-by-byte in a loop via concatenation. Instead, check that all the bytes are ASCII first, copy them into an array and pass that array to `String.fromCharCode` with `fromCharCode.apply(null, array)`. To avoid allocating a fresh array every time, hold a stock of arrays for all string lengths that this path can require, and reuse them. This is a variation on the approach that #20883 took, but without the massive switch. This produces much tighter assembly, and avoids regressing the fast path due making `deserializeStr` a very large function. Despite the complexity, and multiple operations, [this is up to 3x faster](https://github.com/overlookmotel/oxc-raw-str-bench/blob/4f96275efa9a35d5d27615abb27f21a137149cc0/README.md#apply30-vs-switch30) than the switch approach, and gives an average 30% speed-up. ### Increase native call threshold The above optimizations make the slow path much faster. This shifts the tipping point at which it's faster to make a native call to `TextDecoder.decode` from 9 bytes to 64 bytes. Most strings now avoid the native call and stay in JS code which is heavily optimized by Turbofan. The tipping point of 64 is something of a guesstimate. Benchmarking shows its in the right ballpark, but we could finesse it, and probably squeeze out another couple of %. ## Credit The Latin1 string technique was cooked up by @joshuaisaact in overlookmotel/oxc-raw-str-bench#1. All credit to him for this masterstroke which cracks the whole problem!
1 parent 55e1e9b commit 012c924

10 files changed

Lines changed: 394 additions & 256 deletions

File tree

apps/oxlint/src-js/generated/deserialize.js

Lines changed: 43 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ let uint8,
88
uint32,
99
float64,
1010
sourceText,
11+
sourceTextLatin,
1112
sourceStartPos = 0,
1213
firstNonAsciiPos = 0,
1314
parent = null,
@@ -16,14 +17,18 @@ let uint8,
1617
const textDecoder = new TextDecoder("utf-8", { ignoreBOM: true }),
1718
decodeStr = textDecoder.decode.bind(textDecoder),
1819
{ fromCharCode } = String,
19-
NodeProto = Object.create(Object.prototype, {
20-
loc: {
21-
get() {
22-
return getLoc(this);
23-
},
24-
enumerable: true,
20+
{ latin1Slice } = Buffer.prototype,
21+
stringDecodeArrays = Array(65).fill(null);
22+
for (let i = 0; i <= 64; i++) stringDecodeArrays[i] = Array(i).fill(0);
23+
24+
const NodeProto = Object.create(Object.prototype, {
25+
loc: {
26+
get() {
27+
return getLoc(this);
2528
},
26-
});
29+
enumerable: true,
30+
},
31+
});
2732

2833
export function deserializeProgramOnly(
2934
buffer,
@@ -41,20 +46,23 @@ function deserializeWith(buffer, sourceTextInput, sourceByteLen, getLocInput, de
4146
uint32 = buffer.uint32;
4247
float64 = buffer.float64;
4348
sourceText = sourceTextInput;
44-
if (sourceText.length === sourceByteLen) firstNonAsciiPos = sourceStartPos + sourceByteLen;
45-
else {
49+
if (sourceText.length === sourceByteLen) {
50+
firstNonAsciiPos = sourceStartPos + sourceByteLen;
51+
sourceTextLatin = sourceText;
52+
} else {
4653
let i = sourceStartPos,
4754
sourceEndPos = sourceStartPos + sourceByteLen;
4855
for (; i < sourceEndPos && uint8[i] < 128; i++);
4956
firstNonAsciiPos = i;
57+
sourceTextLatin = latin1Slice.call(uint8, sourceStartPos, sourceEndPos);
5058
}
5159
getLoc = getLocInput;
5260
return deserialize(uint32[536870900]);
5361
}
5462

5563
export function resetBuffer() {
56-
// Clear buffer and source text string to allow them to be garbage collected
57-
uint8 = uint32 = float64 = sourceText = void 0;
64+
// Clear buffer and source text strings to allow them to be garbage collected
65+
uint8 = uint32 = float64 = sourceText = sourceTextLatin = void 0;
5866
}
5967

6068
function deserializeProgram(pos) {
@@ -5880,40 +5888,30 @@ function deserializeStr(pos) {
58805888
len = uint32[pos32 + 2];
58815889
if (len === 0) return "";
58825890
pos = uint32[pos32];
5883-
let end = pos + len;
5884-
// Note: Tried reducing this check to a single branch by making the comparison the equivalent of this Rust:
5885-
// `end.wrapping_sub(sourceStartPos) <= firstNonAsciiOffset`.
5886-
//
5887-
// The JS versions tried were:
5888-
// - `((end - sourceStartPos) >>> 0) <= firstNonAsciiOffset`
5889-
// - `((end - sourceStartPos) & 0x7FFF_FFFF) <= firstNonAsciiOffset`
5890-
// But it turned out that these are both slower by 5-10% on files which are all ASCII.
5891-
//
5892-
// `>>>` is slower as V8 can't assume result fits in an SMI (which is a 32-bit *signed* integer),
5893-
// as result could be greater or equal to `2 ** 31`. So it converts both the comparison's operands to `float64`s
5894-
// and does float compare (which is slower than integer compare).
5895-
//
5896-
// `& 0x7FFF_FFFF` is slower as it has a longer chain of data dependencies than the 2 independent
5897-
// branch comparisons.
5898-
//
5899-
// Both branches are very predictable, so 2 branches wins.
5900-
if (pos >= sourceStartPos && end <= firstNonAsciiPos)
5901-
return sourceText.substr(pos - sourceStartPos, len);
5902-
// Use `TextDecoder` for strings longer than 9 bytes.
5903-
// For shorter strings, the byte-by-byte loop below avoids native call overhead.
5904-
if (len > 9) return decodeStr(uint8.subarray(pos, end));
5905-
// Shorter strings decode by hand to avoid native call
5906-
let out = "",
5907-
c;
5908-
do {
5909-
c = uint8[pos++];
5910-
if (c < 128) out += fromCharCode(c);
5911-
else {
5912-
out += decodeStr(uint8.subarray(pos - 1, end));
5913-
break;
5914-
}
5915-
} while (pos < end);
5916-
return out;
5891+
let end = pos + len,
5892+
isInSourceRegion = pos >= sourceStartPos;
5893+
if (isInSourceRegion && end <= firstNonAsciiPos)
5894+
return sourceTextLatin.substr(pos - sourceStartPos, len);
5895+
// Use `TextDecoder` for strings longer than 64 bytes
5896+
if (len > 64) return decodeStr(uint8.subarray(pos, end));
5897+
// If string is in source region, use slice of `sourceTextLatin` if all ASCII
5898+
if (isInSourceRegion) {
5899+
// Check if all bytes are ASCII, use `TextDecoder` if not
5900+
for (let i = pos; i < end; i++) if (uint8[i] >= 128) return decodeStr(uint8.subarray(pos, end));
5901+
// String is all ASCII, so slice from `sourceTextLatin`
5902+
return sourceTextLatin.substr(pos - sourceStartPos, len);
5903+
}
5904+
// String is not in source region - use `fromCharCode.apply` with a temp array of correct length.
5905+
// Copy bytes into temp array.
5906+
// If any byte is non-ASCII, use `TextDecoder`.
5907+
let arr = stringDecodeArrays[len];
5908+
for (let i = 0; i < len; i++) {
5909+
let b = uint8[pos + i];
5910+
if (b >= 128) return decodeStr(uint8.subarray(pos, end));
5911+
arr[i] = b;
5912+
}
5913+
// Call `fromCharCode` with temp array
5914+
return fromCharCode.apply(null, arr);
59175915
}
59185916

59195917
function deserializeVecDirective(pos) {

napi/parser/src-js/generated/deserialize/js.js

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ let uint8,
55
uint32,
66
float64,
77
sourceText,
8+
sourceTextLatin,
9+
sourceEndPos = 0,
810
firstNonAsciiPos = 0;
911

1012
const textDecoder = new TextDecoder("utf-8", { ignoreBOM: true }),
1113
decodeStr = textDecoder.decode.bind(textDecoder),
12-
{ fromCharCode } = String;
14+
{ fromCharCode } = String,
15+
{ latin1Slice } = Buffer.prototype,
16+
stringDecodeArrays = Array(65).fill(null);
17+
for (let i = 0; i <= 64; i++) stringDecodeArrays[i] = Array(i).fill(0);
1318

1419
export function deserialize(buffer, sourceText, sourceByteLen) {
20+
sourceEndPos = sourceByteLen;
1521
let data = deserializeWith(buffer, sourceText, sourceByteLen, null, deserializeRawTransferData);
1622
resetBuffer();
1723
return data;
@@ -22,18 +28,21 @@ function deserializeWith(buffer, sourceTextInput, sourceByteLen, getLocInput, de
2228
uint32 = buffer.uint32;
2329
float64 = buffer.float64;
2430
sourceText = sourceTextInput;
25-
if (sourceText.length === sourceByteLen) firstNonAsciiPos = sourceByteLen;
26-
else {
31+
if (sourceText.length === sourceByteLen) {
32+
firstNonAsciiPos = sourceByteLen;
33+
sourceTextLatin = sourceText;
34+
} else {
2735
let i = 0;
2836
for (; i < sourceByteLen && uint8[i] < 128; i++);
2937
firstNonAsciiPos = i;
38+
sourceTextLatin = latin1Slice.call(uint8, 0, sourceByteLen);
3039
}
3140
return deserialize(uint32[536870900]);
3241
}
3342

3443
export function resetBuffer() {
35-
// Clear buffer and source text string to allow them to be garbage collected
36-
uint8 = uint32 = float64 = sourceText = void 0;
44+
// Clear buffer and source text strings to allow them to be garbage collected
45+
uint8 = uint32 = float64 = sourceText = sourceTextLatin = void 0;
3746
}
3847

3948
function deserializeProgram(pos) {
@@ -4547,22 +4556,26 @@ function deserializeStr(pos) {
45474556
if (len === 0) return "";
45484557
pos = uint32[pos32];
45494558
let end = pos + len;
4550-
if (end <= firstNonAsciiPos) return sourceText.substr(pos, len);
4551-
// Use `TextDecoder` for strings longer than 9 bytes.
4552-
// For shorter strings, the byte-by-byte loop below avoids native call overhead.
4553-
if (len > 9) return decodeStr(uint8.subarray(pos, end));
4554-
// Shorter strings decode by hand to avoid native call
4555-
let out = "",
4556-
c;
4557-
do {
4558-
c = uint8[pos++];
4559-
if (c < 128) out += fromCharCode(c);
4560-
else {
4561-
out += decodeStr(uint8.subarray(pos - 1, end));
4562-
break;
4563-
}
4564-
} while (pos < end);
4565-
return out;
4559+
if (end <= firstNonAsciiPos) return sourceTextLatin.substr(pos, len);
4560+
// Use `TextDecoder` for strings longer than 64 bytes
4561+
if (len > 64) return decodeStr(uint8.subarray(pos, end));
4562+
if (pos < sourceEndPos) {
4563+
// Check if all bytes are ASCII, use `TextDecoder` if not
4564+
for (let i = pos; i < end; i++) if (uint8[i] >= 128) return decodeStr(uint8.subarray(pos, end));
4565+
// String is all ASCII, so slice from `sourceTextLatin`
4566+
return sourceTextLatin.substr(pos, len);
4567+
}
4568+
// String is not in source region - use `fromCharCode.apply` with a temp array of correct length.
4569+
// Copy bytes into temp array.
4570+
// If any byte is non-ASCII, use `TextDecoder`.
4571+
let arr = stringDecodeArrays[len];
4572+
for (let i = 0; i < len; i++) {
4573+
let b = uint8[pos + i];
4574+
if (b >= 128) return decodeStr(uint8.subarray(pos, end));
4575+
arr[i] = b;
4576+
}
4577+
// Call `fromCharCode` with temp array
4578+
return fromCharCode.apply(null, arr);
45664579
}
45674580

45684581
function deserializeVecComment(pos) {

napi/parser/src-js/generated/deserialize/js_parent.js

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,20 @@ let uint8,
55
uint32,
66
float64,
77
sourceText,
8+
sourceTextLatin,
9+
sourceEndPos = 0,
810
firstNonAsciiPos = 0,
911
parent = null;
1012

1113
const textDecoder = new TextDecoder("utf-8", { ignoreBOM: true }),
1214
decodeStr = textDecoder.decode.bind(textDecoder),
13-
{ fromCharCode } = String;
15+
{ fromCharCode } = String,
16+
{ latin1Slice } = Buffer.prototype,
17+
stringDecodeArrays = Array(65).fill(null);
18+
for (let i = 0; i <= 64; i++) stringDecodeArrays[i] = Array(i).fill(0);
1419

1520
export function deserialize(buffer, sourceText, sourceByteLen) {
21+
sourceEndPos = sourceByteLen;
1622
let data = deserializeWith(buffer, sourceText, sourceByteLen, null, deserializeRawTransferData);
1723
resetBuffer();
1824
return data;
@@ -23,18 +29,21 @@ function deserializeWith(buffer, sourceTextInput, sourceByteLen, getLocInput, de
2329
uint32 = buffer.uint32;
2430
float64 = buffer.float64;
2531
sourceText = sourceTextInput;
26-
if (sourceText.length === sourceByteLen) firstNonAsciiPos = sourceByteLen;
27-
else {
32+
if (sourceText.length === sourceByteLen) {
33+
firstNonAsciiPos = sourceByteLen;
34+
sourceTextLatin = sourceText;
35+
} else {
2836
let i = 0;
2937
for (; i < sourceByteLen && uint8[i] < 128; i++);
3038
firstNonAsciiPos = i;
39+
sourceTextLatin = latin1Slice.call(uint8, 0, sourceByteLen);
3140
}
3241
return deserialize(uint32[536870900]);
3342
}
3443

3544
export function resetBuffer() {
36-
// Clear buffer and source text string to allow them to be garbage collected
37-
uint8 = uint32 = float64 = sourceText = void 0;
45+
// Clear buffer and source text strings to allow them to be garbage collected
46+
uint8 = uint32 = float64 = sourceText = sourceTextLatin = void 0;
3847
}
3948

4049
function deserializeProgram(pos) {
@@ -5078,22 +5087,26 @@ function deserializeStr(pos) {
50785087
if (len === 0) return "";
50795088
pos = uint32[pos32];
50805089
let end = pos + len;
5081-
if (end <= firstNonAsciiPos) return sourceText.substr(pos, len);
5082-
// Use `TextDecoder` for strings longer than 9 bytes.
5083-
// For shorter strings, the byte-by-byte loop below avoids native call overhead.
5084-
if (len > 9) return decodeStr(uint8.subarray(pos, end));
5085-
// Shorter strings decode by hand to avoid native call
5086-
let out = "",
5087-
c;
5088-
do {
5089-
c = uint8[pos++];
5090-
if (c < 128) out += fromCharCode(c);
5091-
else {
5092-
out += decodeStr(uint8.subarray(pos - 1, end));
5093-
break;
5094-
}
5095-
} while (pos < end);
5096-
return out;
5090+
if (end <= firstNonAsciiPos) return sourceTextLatin.substr(pos, len);
5091+
// Use `TextDecoder` for strings longer than 64 bytes
5092+
if (len > 64) return decodeStr(uint8.subarray(pos, end));
5093+
if (pos < sourceEndPos) {
5094+
// Check if all bytes are ASCII, use `TextDecoder` if not
5095+
for (let i = pos; i < end; i++) if (uint8[i] >= 128) return decodeStr(uint8.subarray(pos, end));
5096+
// String is all ASCII, so slice from `sourceTextLatin`
5097+
return sourceTextLatin.substr(pos, len);
5098+
}
5099+
// String is not in source region - use `fromCharCode.apply` with a temp array of correct length.
5100+
// Copy bytes into temp array.
5101+
// If any byte is non-ASCII, use `TextDecoder`.
5102+
let arr = stringDecodeArrays[len];
5103+
for (let i = 0; i < len; i++) {
5104+
let b = uint8[pos + i];
5105+
if (b >= 128) return decodeStr(uint8.subarray(pos, end));
5106+
arr[i] = b;
5107+
}
5108+
// Call `fromCharCode` with temp array
5109+
return fromCharCode.apply(null, arr);
50975110
}
50985111

50995112
function deserializeVecComment(pos) {

napi/parser/src-js/generated/deserialize/js_range.js

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ let uint8,
55
uint32,
66
float64,
77
sourceText,
8+
sourceTextLatin,
9+
sourceEndPos = 0,
810
firstNonAsciiPos = 0;
911

1012
const textDecoder = new TextDecoder("utf-8", { ignoreBOM: true }),
1113
decodeStr = textDecoder.decode.bind(textDecoder),
12-
{ fromCharCode } = String;
14+
{ fromCharCode } = String,
15+
{ latin1Slice } = Buffer.prototype,
16+
stringDecodeArrays = Array(65).fill(null);
17+
for (let i = 0; i <= 64; i++) stringDecodeArrays[i] = Array(i).fill(0);
1318

1419
export function deserialize(buffer, sourceText, sourceByteLen) {
20+
sourceEndPos = sourceByteLen;
1521
let data = deserializeWith(buffer, sourceText, sourceByteLen, null, deserializeRawTransferData);
1622
resetBuffer();
1723
return data;
@@ -22,18 +28,21 @@ function deserializeWith(buffer, sourceTextInput, sourceByteLen, getLocInput, de
2228
uint32 = buffer.uint32;
2329
float64 = buffer.float64;
2430
sourceText = sourceTextInput;
25-
if (sourceText.length === sourceByteLen) firstNonAsciiPos = sourceByteLen;
26-
else {
31+
if (sourceText.length === sourceByteLen) {
32+
firstNonAsciiPos = sourceByteLen;
33+
sourceTextLatin = sourceText;
34+
} else {
2735
let i = 0;
2836
for (; i < sourceByteLen && uint8[i] < 128; i++);
2937
firstNonAsciiPos = i;
38+
sourceTextLatin = latin1Slice.call(uint8, 0, sourceByteLen);
3039
}
3140
return deserialize(uint32[536870900]);
3241
}
3342

3443
export function resetBuffer() {
35-
// Clear buffer and source text string to allow them to be garbage collected
36-
uint8 = uint32 = float64 = sourceText = void 0;
44+
// Clear buffer and source text strings to allow them to be garbage collected
45+
uint8 = uint32 = float64 = sourceText = sourceTextLatin = void 0;
3746
}
3847

3948
function deserializeProgram(pos) {
@@ -5089,22 +5098,26 @@ function deserializeStr(pos) {
50895098
if (len === 0) return "";
50905099
pos = uint32[pos32];
50915100
let end = pos + len;
5092-
if (end <= firstNonAsciiPos) return sourceText.substr(pos, len);
5093-
// Use `TextDecoder` for strings longer than 9 bytes.
5094-
// For shorter strings, the byte-by-byte loop below avoids native call overhead.
5095-
if (len > 9) return decodeStr(uint8.subarray(pos, end));
5096-
// Shorter strings decode by hand to avoid native call
5097-
let out = "",
5098-
c;
5099-
do {
5100-
c = uint8[pos++];
5101-
if (c < 128) out += fromCharCode(c);
5102-
else {
5103-
out += decodeStr(uint8.subarray(pos - 1, end));
5104-
break;
5105-
}
5106-
} while (pos < end);
5107-
return out;
5101+
if (end <= firstNonAsciiPos) return sourceTextLatin.substr(pos, len);
5102+
// Use `TextDecoder` for strings longer than 64 bytes
5103+
if (len > 64) return decodeStr(uint8.subarray(pos, end));
5104+
if (pos < sourceEndPos) {
5105+
// Check if all bytes are ASCII, use `TextDecoder` if not
5106+
for (let i = pos; i < end; i++) if (uint8[i] >= 128) return decodeStr(uint8.subarray(pos, end));
5107+
// String is all ASCII, so slice from `sourceTextLatin`
5108+
return sourceTextLatin.substr(pos, len);
5109+
}
5110+
// String is not in source region - use `fromCharCode.apply` with a temp array of correct length.
5111+
// Copy bytes into temp array.
5112+
// If any byte is non-ASCII, use `TextDecoder`.
5113+
let arr = stringDecodeArrays[len];
5114+
for (let i = 0; i < len; i++) {
5115+
let b = uint8[pos + i];
5116+
if (b >= 128) return decodeStr(uint8.subarray(pos, end));
5117+
arr[i] = b;
5118+
}
5119+
// Call `fromCharCode` with temp array
5120+
return fromCharCode.apply(null, arr);
51085121
}
51095122

51105123
function deserializeVecComment(pos) {

0 commit comments

Comments
 (0)