From 41473d3eeb194c62d80ba93d80e804d995c800f2 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Mon, 11 May 2026 14:13:41 -0400 Subject: [PATCH] Use @streamparser/json if the input is too large to fit in a V8 string. --- package.json | 1 + src/profile-logic/process-profile.ts | 73 +++++++++++++++++++++++++--- yarn.lock | 5 ++ 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 2cf3c17999..982151b1f4 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "@fluent/langneg": "^0.7.0", "@fluent/react": "^0.15.2", "@lezer/highlight": "^1.2.3", + "@streamparser/json": "^0.0.22", "@tgwf/co2": "^0.18.0", "array-move": "^3.0.1", "array-range": "^1.0.1", diff --git a/src/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts index 8a89feae17..d1f44222dc 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -1984,6 +1984,41 @@ function attemptToFixProcessedProfileThroughMutation( return profile; } +function decodeUtf8WithNiceError(bytes: Uint8Array): string { + try { + const textDecoder = new TextDecoder(undefined, { fatal: true }); + return textDecoder.decode(bytes); + } catch (e) { + console.error('Source exception:', e); + throw new Error( + 'The profile array buffer could not be parsed as a UTF-8 string.' + ); + } +} + +async function parseJSONFromBytes(bytes: Uint8Array): Promise { + const V8_STRING_MAX_SIZE = 512 * 1024 * 1024 - 24; // 512 MiB - 24 + if (bytes.byteLength < V8_STRING_MAX_SIZE) { + const jsonString = decodeUtf8WithNiceError(bytes); + return JSON.parse(jsonString); + } + + // The payload is too large to fit in a single string (in V8), so we can't decode + // it and call JSON.parse on it. Use a streaming JSON parser instead. This is + // much slower than native JSON.parse, so we only do it when necessary. + const { JSONParser } = await import('@streamparser/json'); + const parser = new JSONParser({ paths: ['$'] }); + let result: any; + parser.onValue = ({ value }) => { + result = value; + }; + parser.write(bytes); + if (!parser.isEnded) { + throw new Error('Input terminated before end of JSON'); + } + return result; +} + /** * Take some arbitrary profile file from some data source, and turn it into * the processed profile format. @@ -2034,20 +2069,42 @@ export async function unserializeProfileOfArbitraryFormat( await import('./import/simpleperf'); arbitraryFormat = convertSimpleperfTraceProfile(profileBytes); } else { - try { - const textDecoder = new TextDecoder(undefined, { fatal: true }); - arbitraryFormat = await textDecoder.decode(profileBytes); - } catch (e) { - console.error('Source exception:', e); - throw new Error( - 'The profile array buffer could not be parsed as a UTF-8 string.' + // Probably a string-based format. + // We don't want to materialize a string for the entire profileBytes + // here, in case we want to use the streaming JSON parser later. But + // to detect perf script + flamegraph, we need to look at some text, + // so let's decode the first 4096 bytes and detect the format based + // on the first one or two lines. + const CHARCODE_LINE_BREAK = 10; // '\n'.charCodeAt(0) + const firstPage = profileBytes.subarray(0, 4096); + const firstLineBreakPos = firstPage.indexOf(CHARCODE_LINE_BREAK); + const secondLineBreakPos = + firstLineBreakPos !== -1 + ? firstPage.indexOf(CHARCODE_LINE_BREAK, firstLineBreakPos + 1) + : -1; + const sniffEnd = + secondLineBreakPos !== -1 ? secondLineBreakPos : firstPage.byteLength; + // Non-fatal: the cut may fall inside a multi-byte UTF-8 sequence; + // we only need enough text to recognize the format. + const firstTwoLinesAsText = new TextDecoder().decode( + firstPage.subarray(0, sniffEnd) + ); + if (isPerfScriptFormat(firstTwoLinesAsText)) { + arbitraryFormat = convertPerfScriptProfile( + decodeUtf8WithNiceError(profileBytes) + ); + } else if (isFlameGraphFormat(firstTwoLinesAsText)) { + arbitraryFormat = convertFlameGraphProfile( + decodeUtf8WithNiceError(profileBytes) ); + } else { + // Try parsing as JSON. + arbitraryFormat = await parseJSONFromBytes(profileBytes); } } } if (typeof arbitraryFormat === 'string') { - // The profile could be JSON or the output from `perf script`. Try `perf script` first. if (isPerfScriptFormat(arbitraryFormat)) { arbitraryFormat = convertPerfScriptProfile(arbitraryFormat); } else if (isFlameGraphFormat(arbitraryFormat)) { diff --git a/yarn.lock b/yarn.lock index 8000649752..a07ca8e1ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2120,6 +2120,11 @@ dependencies: "@sinonjs/commons" "^3.0.1" +"@streamparser/json@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@streamparser/json/-/json-0.0.22.tgz#8ddcbcc8c3ca77aeadf80af47f54a64c8739a037" + integrity sha512-b6gTSBjJ8G8SuO3Gbbj+zXbVx8NSs1EbpbMKpzGLWMdkR+98McH9bEjSz3+0mPJf68c5nxa3CrJHp5EQNXM6zQ== + "@surma/rollup-plugin-off-main-thread@^2.2.3": version "2.2.3" resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"