From 82e47704707038ef930fbca598b02840dfeb62ef Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 24 Jan 2024 21:46:32 -0500 Subject: [PATCH] perf(marshal): Replace more XS-expensive string operations Most notably, prefer `charAt(0) === ch` over `startsWith(ch)`. Ref #1982 Ref #2001 --- packages/marshal/src/encodePassable.js | 41 ++++++++++++----------- packages/marshal/src/encodeToSmallcaps.js | 8 ++--- packages/marshal/src/marshal.js | 4 +-- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/packages/marshal/src/encodePassable.js b/packages/marshal/src/encodePassable.js index 33909055f1..877c20d297 100644 --- a/packages/marshal/src/encodePassable.js +++ b/packages/marshal/src/encodePassable.js @@ -121,7 +121,7 @@ const encodeBinary64 = n => { * @returns {number} */ const decodeBinary64 = encoded => { - encoded.startsWith('f') || Fail`Encoded number expected: ${encoded}`; + encoded.charAt(0) === 'f' || Fail`Encoded number expected: ${encoded}`; let bits = BigInt(`0x${encoded.substring(1)}`); if (encoded[1] < '8') { bits ^= 0xffffffffffffffffn; @@ -182,37 +182,38 @@ const encodeBigInt = n => { } }; +const rBigIntPayload = /([0-9]+)(:([0-9]+$|)|)/s; + /** * @param {string} encoded * @returns {bigint} */ const decodeBigInt = encoded => { const typePrefix = encoded.charAt(0); // faster than encoded[0] - let rem = encoded.slice(1); typePrefix === 'p' || typePrefix === 'n' || Fail`Encoded bigint expected: ${encoded}`; - const lDigits = rem.search(/[0-9]/) + 1; - lDigits >= 1 || Fail`Digit count expected: ${encoded}`; - rem = rem.slice(lDigits - 1); - - rem.length >= lDigits || Fail`Complete digit count expected: ${encoded}`; - const snDigits = rem.slice(0, lDigits); - rem = rem.slice(lDigits); - /^[0-9]+$/.test(snDigits) || Fail`Decimal digit count expected: ${encoded}`; + const { + index: lDigits, + 1: snDigits, + 2: tail, + 3: digits, + } = encoded.match(rBigIntPayload) || Fail`Digit count expected: ${encoded}`; + + snDigits.length === lDigits || + Fail`Unary-prefixed decimal digit count expected: ${encoded}`; let nDigits = parseInt(snDigits, 10); if (typePrefix === 'n') { // TODO Assert to reject forbidden encodings // like "n0:" and "n00:…" and "n91:…" through "n99:…"? - nDigits = 10 ** lDigits - nDigits; + nDigits = 10 ** /** @type {number} */ (lDigits) - nDigits; } - rem.startsWith(':') || Fail`Separator expected: ${encoded}`; - rem = rem.slice(1); - rem.length === nDigits || + tail.charAt(0) === ':' || Fail`Separator expected: ${encoded}`; + digits.length === nDigits || Fail`Fixed-length digit sequence expected: ${encoded}`; - let n = BigInt(rem); + let n = BigInt(digits); if (typePrefix === 'n') { // TODO Assert to reject forbidden encodings // like "n9:0" and "n8:00" and "n8:91" through "n8:99"? @@ -292,7 +293,7 @@ const encodeRecord = (record, encodePassable) => { }; const decodeRecord = (encoded, decodePassable) => { - assert(encoded.startsWith('(')); + assert(encoded.charAt(0) === '('); // Skip the "(" inside `decodeArray` to avoid slow `substring` in XS. // https://github.com/endojs/endo/issues/1984 const unzippedEntries = decodeArray(encoded, decodePassable, 1); @@ -314,7 +315,7 @@ const encodeTagged = (tagged, encodePassable) => `:${encodeArray(harden([getTag(tagged), tagged.payload]), encodePassable)}`; const decodeTagged = (encoded, decodePassable) => { - assert(encoded.startsWith(':')); + assert(encoded.charAt(0) === ':'); // Skip the ":" inside `decodeArray` to avoid slow `substring` in XS. // https://github.com/endojs/endo/issues/1984 const taggedPayload = decodeArray(encoded, decodePassable, 1); @@ -378,19 +379,19 @@ export const makeEncodePassable = (encodeOptions = {}) => { } case 'remotable': { const result = encodeRemotable(passable, encodePassable); - result.startsWith('r') || + result.charAt(0) === 'r' || Fail`internal: Remotable encoding must start with "r": ${result}`; return result; } case 'error': { const result = encodeError(passable, encodePassable); - result.startsWith('!') || + result.charAt(0) === '!' || Fail`internal: Error encoding must start with "!": ${result}`; return result; } case 'promise': { const result = encodePromise(passable, encodePassable); - result.startsWith('?') || + result.charAt(0) === '?' || Fail`internal: Promise encoding must start with "?": ${result}`; return result; } diff --git a/packages/marshal/src/encodeToSmallcaps.js b/packages/marshal/src/encodeToSmallcaps.js index f96c51b841..0313f6c942 100644 --- a/packages/marshal/src/encodeToSmallcaps.js +++ b/packages/marshal/src/encodeToSmallcaps.js @@ -132,7 +132,7 @@ export const makeEncodeToSmallcaps = (encodeOptions = {}) => { // Assert that the #error property decodes to a string. const message = encoding['#error']; (typeof message === 'string' && - (!startsSpecial(message) || message.startsWith('!'))) || + (!startsSpecial(message) || message.charAt(0) === '!')) || Fail`internal: Error encoding must have string message: ${q(message)}`; }; @@ -241,7 +241,7 @@ export const makeEncodeToSmallcaps = (encodeOptions = {}) => { passable, encodeToSmallcapsRecur, ); - if (typeof result === 'string' && result.startsWith('$')) { + if (typeof result === 'string' && result.charAt(0) === '$') { return result; } // `throw` is noop since `Fail` throws. But linter confused @@ -252,7 +252,7 @@ export const makeEncodeToSmallcaps = (encodeOptions = {}) => { passable, encodeToSmallcapsRecur, ); - if (typeof result === 'string' && result.startsWith('&')) { + if (typeof result === 'string' && result.charAt(0) === '&') { return result; } throw Fail`internal: Promise encoding must start with "&": ${result}`; @@ -446,7 +446,7 @@ export const makeDecodeFromSmallcaps = (decodeOptions = {}) => { Fail`Property name ${q( encodedName, )} of ${encoding} must be a string`; - !encodedName.startsWith('#') || + encodedName.charAt(0) !== '#' || Fail`Unrecognized record type ${q(encodedName)}: ${encoding}`; const name = decodeFromSmallcaps(encodedName); typeof name === 'string' || diff --git a/packages/marshal/src/marshal.js b/packages/marshal/src/marshal.js index 181e28887d..f317325ea5 100644 --- a/packages/marshal/src/marshal.js +++ b/packages/marshal/src/marshal.js @@ -310,7 +310,7 @@ export const makeMarshal = ( * @returns {Remotable | Promise} */ return (stringEncoding, _decodeRecur) => { - assert(stringEncoding.startsWith(prefix)); + assert(stringEncoding.charAt(0) === prefix); // slots: $slotIndex.iface or $slotIndex const i = stringEncoding.indexOf('.'); const index = Number(stringEncoding.slice(1, i < 0 ? undefined : i)); @@ -350,7 +350,7 @@ export const makeMarshal = ( const { reviveFromCapData, reviveFromSmallcaps } = makeFullRevive(slots); let result; // JSON cannot begin with a '#', so this is an unambiguous signal. - if (body.startsWith('#')) { + if (body.charAt(0) === '#') { const smallcapsBody = body.slice(1); const encoding = harden(JSON.parse(smallcapsBody)); result = harden(reviveFromSmallcaps(encoding));