Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
73 changes: 65 additions & 8 deletions src/profile-logic/process-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> {
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.
Expand Down Expand Up @@ -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)) {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading