| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent f33dba7 commit 32bb197
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,36 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + | ||
| 3 | + // buf.write(string, 'utf8') for strings whose in-memory representation is | ||
| 4 | + // one-byte (Latin-1) or two-byte (UTF-16), which take different encoder paths. | ||
| 5 | + const common = require('../common.js'); | ||
| 6 | + const bench = common.createBenchmark(main, { | ||
| 7 | + chars: ['one-byte', 'two-byte', 'two-byte-astral', 'two-byte-lone-surrogate'], | ||
| 8 | + len: [16, 256, 2048, 65536], | ||
| 9 | + n: [5e5], | ||
| 10 | + }); | ||
| 11 | + | ||
| 12 | + function makeString(chars, len) { | ||
| 13 | + switch (chars) { | ||
| 14 | + case 'one-byte': | ||
| 15 | + return 'aé'.repeat(len / 2); | ||
| 16 | + case 'two-byte': | ||
| 17 | + return 'aé€日'.repeat(len / 4); | ||
| 18 | + case 'two-byte-astral': | ||
| 19 | + return 'aé€日\u{1F600}'.repeat(len / 6).padEnd(len, 'a'); | ||
| 20 | + case 'two-byte-lone-surrogate': | ||
| 21 | + return 'aé€日'.repeat(len / 4 - 1) + 'ab\ud800c'; | ||
| 22 | + default: | ||
| 23 | + throw new Error(chars); | ||
| 24 | + } | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + function main({ chars, len, n }) { | ||
| 28 | + const string = makeString(chars, len); | ||
| 29 | + const buf = Buffer.allocUnsafe(Buffer.byteLength(string)); | ||
| 30 | + if (len >= 65536) n = Math.floor(n / 32); | ||
| 31 | + bench.start(); | ||
| 32 | + for (let i = 0; i < n; ++i) { | ||
| 33 | + buf.write(string, 0, 'utf8'); | ||
| 34 | + } | ||
| 35 | + bench.end(n); | ||
| 36 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -310,9 +310,37 @@ size_t StringBytes::Write(Isolate* isolate, | |||
| 310 | 310 | input_view.length(), | |
| 311 | 311 | buf, | |
| 312 | 312 | buflen); | |
| 313 | - } else { | ||
| 313 | + } else if (input_view.length() <= 32) { | ||
| 314 | + // V8 is as fast for tiny strings (same threshold TextEncoder uses). | ||
| 314 | 315 | nbytes = str->WriteUtf8V2( | |
| 315 | 316 | isolate, buf, buflen, String::WriteFlags::kReplaceInvalidUtf8); | |
| 317 | + } else { | ||
| 318 | + // Use simdutf for two-byte strings as well whenever the UTF-8 form | ||
| 319 | + // is guaranteed to fit; truncating writes (which must stop at a | ||
| 320 | + // character boundary) keep going through V8 so that their output | ||
| 321 | + // stays byte-for-byte identical. | ||
| 322 | + const char16_t* data = | ||
| 323 | + reinterpret_cast<const char16_t*>(input_view.data16()); | ||
| 324 | + const size_t length = input_view.length(); | ||
| 325 | + MaybeStackBuffer<char16_t, 1024> well_formed; | ||
| 326 | + if (!simdutf::validate_utf16(data, length)) { | ||
| 327 | + // Unpaired surrogates: encode a copy in which each of them has been | ||
| 328 | + // replaced with U+FFFD, which is what kReplaceInvalidUtf8 produces. | ||
| 329 | + well_formed.AllocateSufficientStorage(length); | ||
| 330 | + simdutf::to_well_formed_utf16(data, length, well_formed.out()); | ||
| 331 | + data = well_formed.out(); | ||
| 332 | + } | ||
| 333 | + // A UTF-16 code unit never expands to more than 3 UTF-8 bytes, so | ||
| 334 | + // 3 * length is what StorageSize() hands most callers; only compute | ||
| 335 | + // the exact length when the buffer is smaller than that. | ||
| 336 | + if (buflen >= 3 * length || | ||
| 337 | + buflen >= simdutf::utf8_length_from_utf16(data, length)) { | ||
| 338 | + nbytes = simdutf::convert_utf16_to_utf8(data, length, buf); | ||
| 339 | + } else { | ||
| 340 | + // Does not fit: let V8 truncate at a character boundary. | ||
| 341 | + nbytes = str->WriteUtf8V2( | ||
| 342 | + isolate, buf, buflen, String::WriteFlags::kReplaceInvalidUtf8); | ||
| 343 | + } | ||
| 316 | 344 | } | |
| 317 | 345 | break; | |
| 318 | 346 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,128 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + // UTF-8 encoding of two-byte (UTF-16) JS strings through the Buffer write | ||
| 3 | + // paths (Buffer.from, Buffer#write, Buffer.byteLength) must: | ||
| 4 | + // - produce standard UTF-8 for well-formed input of any size, | ||
| 5 | + // - replace lone surrogates with U+FFFD (EF BF BD), | ||
| 6 | + // - never write a partial character when the target is too small, | ||
| 7 | + // independent of which internal fast path handles the string. | ||
| 8 | + require('../common'); | ||
| 9 | + const assert = require('assert'); | ||
| 10 | + | ||
| 11 | + // Reference encoder written out longhand so the test does not depend on the | ||
| 12 | + // implementation under test (TextEncoder shares code with it). | ||
| 13 | + function utf8Reference(str) { | ||
| 14 | + const out = []; | ||
| 15 | + for (let i = 0; i < str.length; i++) { | ||
| 16 | + let cp = str.charCodeAt(i); | ||
| 17 | + if (cp >= 0xd800 && cp <= 0xdbff) { | ||
| 18 | + const next = i + 1 < str.length ? str.charCodeAt(i + 1) : 0; | ||
| 19 | + if (next >= 0xdc00 && next <= 0xdfff) { | ||
| 20 | + cp = 0x10000 + ((cp - 0xd800) << 10) + (next - 0xdc00); | ||
| 21 | + i++; | ||
| 22 | + } else { | ||
| 23 | + cp = 0xfffd; | ||
| 24 | + } | ||
| 25 | + } else if (cp >= 0xdc00 && cp <= 0xdfff) { | ||
| 26 | + cp = 0xfffd; | ||
| 27 | + } | ||
| 28 | + if (cp < 0x80) { | ||
| 29 | + out.push(cp); | ||
| 30 | + } else if (cp < 0x800) { | ||
| 31 | + out.push(0xc0 | (cp >> 6), 0x80 | (cp & 0x3f)); | ||
| 32 | + } else if (cp < 0x10000) { | ||
| 33 | + out.push(0xe0 | (cp >> 12), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f)); | ||
| 34 | + } else { | ||
| 35 | + out.push(0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3f), | ||
| 36 | + 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f)); | ||
| 37 | + } | ||
| 38 | + } | ||
| 39 | + return Buffer.from(out); | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + function checkFull(str, label) { | ||
| 43 | + const expected = utf8Reference(str); | ||
| 44 | + assert.deepStrictEqual(Buffer.from(str, 'utf8'), expected, `${label}: Buffer.from`); | ||
| 45 | + assert.strictEqual(Buffer.byteLength(str, 'utf8'), expected.length, `${label}: byteLength`); | ||
| 46 | + // Exact-size target. | ||
| 47 | + const exact = Buffer.alloc(expected.length); | ||
| 48 | + assert.strictEqual(exact.write(str, 'utf8'), expected.length, `${label}: write exact`); | ||
| 49 | + assert.deepStrictEqual(exact, expected, `${label}: write exact bytes`); | ||
| 50 | + // Oversized target (3 bytes per code unit is what most internal callers allocate). | ||
| 51 | + const big = Buffer.alloc(str.length * 3 + 7, 0xaa); | ||
| 52 | + assert.strictEqual(big.write(str, 2, 'utf8'), expected.length, `${label}: write big`); | ||
| 53 | + assert.deepStrictEqual(big.subarray(2, 2 + expected.length), expected, `${label}: write big bytes`); | ||
| 54 | + assert.strictEqual(big[0], 0xaa); | ||
| 55 | + assert.strictEqual(big[2 + expected.length], 0xaa, `${label}: no overrun`); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + // Truncating writes must stop before the first character that does not fit. | ||
| 59 | + function checkTruncation(str, label) { | ||
| 60 | + const expected = utf8Reference(str); | ||
| 61 | + for (let size = 0; size <= Math.min(expected.length, 70); size++) { | ||
| 62 | + const target = Buffer.alloc(size + 1, 0xaa); | ||
| 63 | + const n = target.write(str, 0, size, 'utf8'); | ||
| 64 | + assert.ok(n <= size, `${label}: size=${size} wrote ${n}`); | ||
| 65 | + assert.deepStrictEqual(target.subarray(0, n), expected.subarray(0, n), `${label}: prefix size=${size}`); | ||
| 66 | + assert.strictEqual(target[size], 0xaa, `${label}: overrun size=${size}`); | ||
| 67 | + // What was written must be a whole number of characters: the next byte in | ||
| 68 | + // the reference (if any) has to be a lead byte, not a continuation byte. | ||
| 69 | + if (n < expected.length) { | ||
| 70 | + assert.notStrictEqual(expected[n] & 0xc0, 0x80, `${label}: split char at size=${size}`); | ||
| 71 | + // And it stopped only because the next character really did not fit. | ||
| 72 | + let next = n + 1; | ||
| 73 | + while (next < expected.length && (expected[next] & 0xc0) === 0x80) next++; | ||
| 74 | + assert.ok(next > size, `${label}: stopped early at size=${size} (n=${n}, next=${next})`); | ||
| 75 | + } | ||
| 76 | + } | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + // Force a two-byte representation even for ASCII/Latin-1 content by building | ||
| 80 | + // the string from a two-byte seed and slicing (V8 keeps the representation). | ||
| 81 | + function twoByte(str) { | ||
| 82 | + const s = ('\u{1F600}' + str).slice(2); | ||
| 83 | + assert.strictEqual(s, str); | ||
| 84 | + return s; | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + const samples = { | ||
| 88 | + ascii: 'The quick brown fox jumps over the lazy dog 0123456789', | ||
| 89 | + latin1: 'français élan über naïve façade ÿ', | ||
| 90 | + bmp: '日本語テキストとハングル한국어', | ||
| 91 | + astral: 'emoji \u{1F600}\u{1F4A9} math \u{1D49C} han \u{20BB7}', | ||
| 92 | + mixed: 'a é 日 \u{1F600} b ü 本 \u{1F4A9}', | ||
| 93 | + }; | ||
| 94 | + | ||
| 95 | + for (const [name, base] of Object.entries(samples)) { | ||
| 96 | + for (const repeat of [1, 3, 40, 700, 12000]) { | ||
| 97 | + const str = twoByte(base.repeat(repeat)); | ||
| 98 | + checkFull(str, `${name} x${repeat}`); | ||
| 99 | + } | ||
| 100 | + checkTruncation(twoByte(base.repeat(3)), `${name} truncation`); | ||
| 101 | + } | ||
| 102 | + | ||
| 103 | + // Lone surrogates in various positions and sizes -> U+FFFD, rest intact. | ||
| 104 | + const high = '\ud83d'; | ||
| 105 | + const low = '\ude00'; | ||
| 106 | + const surrogateCases = { | ||
| 107 | + 'lone high': `ab${high}cd`, | ||
| 108 | + 'lone low': `ab${low}cd`, | ||
| 109 | + 'reversed pair': `ab${low}${high}cd`, | ||
| 110 | + 'high at end': `abcd${high}`, | ||
| 111 | + 'low at start': `${low}abcd`, | ||
| 112 | + 'high high low': `${high}${high}${low}x`, | ||
| 113 | + 'pair then lone': `${high}${low}${high}`, | ||
| 114 | + 'only lone': high, | ||
| 115 | + }; | ||
| 116 | + for (const [name, base] of Object.entries(surrogateCases)) { | ||
| 117 | + for (const pad of ['', 'x'.repeat(50), 'é'.repeat(300), '日'.repeat(30000)]) { | ||
| 118 | + const str = pad + base + pad; | ||
| 119 | + checkFull(str, `${name} pad=${pad.length}`); | ||
| 120 | + } | ||
| 121 | + checkTruncation(base + 'zz', `${name} truncation`); | ||
| 122 | + } | ||
| 123 | + | ||
| 124 | + // Buffer.from of a large two-byte string equals TextEncoder output. | ||
| 125 | + { | ||
| 126 | + const str = twoByte(samples.mixed.repeat(50000)); | ||
| 127 | + assert.deepStrictEqual(Buffer.from(str), Buffer.from(new TextEncoder().encode(str))); | ||
| 128 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments