Browsers have a limit on how big you can make strings. In Chrome on a 64 bit machine, this is around 512MB, which explains why in #340, a 600MB file fails to load. To work around this issue, we avoid making strings this large. To do so, we need two core changes: 1. Instead of sending large strings as the import mechanism to different file format importers, we introduce a new `TextFileContent` interface which exposes methods to get the lines in the file or the JSON representation. In the case of line splitting, we assume that no single line exceeds the 512MB limit. 2. We introduce a dependency on https://github.com/evanw/uint8array-json-parser to allow us to parse JSON files contained in `Uint8Array` objects To ensure that this code doesn't code rot without introducing 600MB test files or test file generation into the repository, we also re-run a small set of tests with a mocked maximum string size of 100 bytes. You can see that the chunked string representation code is getting executed via test coverage. Fixes #340
204 lines
8.0 KiB
TypeScript
204 lines
8.0 KiB
TypeScript
import {Profile, ProfileGroup} from '../lib/profile'
|
|
import {FileSystemDirectoryEntry} from './file-system-entry'
|
|
|
|
import {
|
|
importFromChromeCPUProfile,
|
|
importFromChromeTimeline,
|
|
isChromeTimeline,
|
|
importFromOldV8CPUProfile,
|
|
} from './chrome'
|
|
import {importFromStackprof} from './stackprof'
|
|
import {importFromInstrumentsDeepCopy, importFromInstrumentsTrace} from './instruments'
|
|
import {importFromBGFlameGraph} from './bg-flamegraph'
|
|
import {importFromFirefox} from './firefox'
|
|
import {importSpeedscopeProfiles} from '../lib/file-format'
|
|
import {importFromV8ProfLog} from './v8proflog'
|
|
import {importFromLinuxPerf} from './linux-tools-perf'
|
|
import {importFromHaskell} from './haskell'
|
|
import {importFromSafari} from './safari'
|
|
import {ProfileDataSource, TextProfileDataSource, MaybeCompressedDataReader} from './utils'
|
|
import {importAsPprofProfile} from './pprof'
|
|
import {decodeBase64} from '../lib/utils'
|
|
import {importFromChromeHeapProfile} from './v8heapalloc'
|
|
import {isTraceEventFormatted, importTraceEvents} from './trace-event'
|
|
import {importFromCallgrind} from './callgrind'
|
|
|
|
export async function importProfileGroupFromText(
|
|
fileName: string,
|
|
contents: string,
|
|
): Promise<ProfileGroup | null> {
|
|
return await importProfileGroup(new TextProfileDataSource(fileName, contents))
|
|
}
|
|
|
|
export async function importProfileGroupFromBase64(
|
|
fileName: string,
|
|
b64contents: string,
|
|
): Promise<ProfileGroup | null> {
|
|
return await importProfileGroup(
|
|
MaybeCompressedDataReader.fromArrayBuffer(fileName, decodeBase64(b64contents).buffer),
|
|
)
|
|
}
|
|
|
|
export async function importProfilesFromFile(file: File): Promise<ProfileGroup | null> {
|
|
return importProfileGroup(MaybeCompressedDataReader.fromFile(file))
|
|
}
|
|
|
|
export async function importProfilesFromArrayBuffer(
|
|
fileName: string,
|
|
buffer: ArrayBuffer,
|
|
): Promise<ProfileGroup | null> {
|
|
return importProfileGroup(MaybeCompressedDataReader.fromArrayBuffer(fileName, buffer))
|
|
}
|
|
|
|
async function importProfileGroup(dataSource: ProfileDataSource): Promise<ProfileGroup | null> {
|
|
const fileName = await dataSource.name()
|
|
|
|
const profileGroup = await _importProfileGroup(dataSource)
|
|
if (profileGroup) {
|
|
if (!profileGroup.name) {
|
|
profileGroup.name = fileName
|
|
}
|
|
for (let profile of profileGroup.profiles) {
|
|
if (profile && !profile.getName()) {
|
|
profile.setName(fileName)
|
|
}
|
|
}
|
|
return profileGroup
|
|
}
|
|
return null
|
|
}
|
|
|
|
function toGroup(profile: Profile | null): ProfileGroup | null {
|
|
if (!profile) return null
|
|
return {name: profile.getName(), indexToView: 0, profiles: [profile]}
|
|
}
|
|
|
|
async function _importProfileGroup(dataSource: ProfileDataSource): Promise<ProfileGroup | null> {
|
|
const fileName = await dataSource.name()
|
|
|
|
const buffer = await dataSource.readAsArrayBuffer()
|
|
|
|
{
|
|
const profile = importAsPprofProfile(buffer)
|
|
if (profile) {
|
|
console.log('Importing as protobuf encoded pprof file')
|
|
return toGroup(profile)
|
|
}
|
|
}
|
|
|
|
const contents = await dataSource.readAsText()
|
|
|
|
// First pass: Check known file format names to infer the file type
|
|
if (fileName.endsWith('.speedscope.json')) {
|
|
console.log('Importing as speedscope json file')
|
|
return importSpeedscopeProfiles(contents.parseAsJSON())
|
|
} else if (fileName.endsWith('.chrome.json') || /Profile-\d{8}T\d{6}/.exec(fileName)) {
|
|
console.log('Importing as Chrome Timeline')
|
|
return importFromChromeTimeline(contents.parseAsJSON(), fileName)
|
|
} else if (fileName.endsWith('.stackprof.json')) {
|
|
console.log('Importing as stackprof profile')
|
|
return toGroup(importFromStackprof(contents.parseAsJSON()))
|
|
} else if (fileName.endsWith('.instruments.txt')) {
|
|
console.log('Importing as Instruments.app deep copy')
|
|
return toGroup(importFromInstrumentsDeepCopy(contents))
|
|
} else if (fileName.endsWith('.linux-perf.txt')) {
|
|
console.log('Importing as output of linux perf script')
|
|
return importFromLinuxPerf(contents)
|
|
} else if (fileName.endsWith('.collapsedstack.txt')) {
|
|
console.log('Importing as collapsed stack format')
|
|
return toGroup(importFromBGFlameGraph(contents))
|
|
} else if (fileName.endsWith('.v8log.json')) {
|
|
console.log('Importing as --prof-process v8 log')
|
|
return toGroup(importFromV8ProfLog(contents.parseAsJSON()))
|
|
} else if (fileName.endsWith('.heapprofile')) {
|
|
console.log('Importing as Chrome Heap Profile')
|
|
return toGroup(importFromChromeHeapProfile(contents.parseAsJSON()))
|
|
} else if (fileName.endsWith('-recording.json')) {
|
|
console.log('Importing as Safari profile')
|
|
return toGroup(importFromSafari(contents.parseAsJSON()))
|
|
} else if (fileName.startsWith('callgrind.')) {
|
|
console.log('Importing as Callgrind profile')
|
|
return importFromCallgrind(contents, fileName)
|
|
}
|
|
|
|
// Second pass: Try to guess what file format it is based on structure
|
|
let parsed: any
|
|
try {
|
|
parsed = contents.parseAsJSON()
|
|
} catch (e) {}
|
|
if (parsed) {
|
|
if (parsed['$schema'] === 'https://www.speedscope.app/file-format-schema.json') {
|
|
console.log('Importing as speedscope json file')
|
|
return importSpeedscopeProfiles(parsed)
|
|
} else if (parsed['systemHost'] && parsed['systemHost']['name'] == 'Firefox') {
|
|
console.log('Importing as Firefox profile')
|
|
return toGroup(importFromFirefox(parsed))
|
|
} else if (isChromeTimeline(parsed)) {
|
|
console.log('Importing as Chrome Timeline')
|
|
return importFromChromeTimeline(parsed, fileName)
|
|
} else if ('nodes' in parsed && 'samples' in parsed && 'timeDeltas' in parsed) {
|
|
console.log('Importing as Chrome CPU Profile')
|
|
return toGroup(importFromChromeCPUProfile(parsed))
|
|
} else if (isTraceEventFormatted(parsed)) {
|
|
console.log('Importing as Trace Event Format profile')
|
|
return importTraceEvents(parsed)
|
|
} else if ('head' in parsed && 'samples' in parsed && 'timestamps' in parsed) {
|
|
console.log('Importing as Chrome CPU Profile (old format)')
|
|
return toGroup(importFromOldV8CPUProfile(parsed))
|
|
} else if ('mode' in parsed && 'frames' in parsed && 'raw_timestamp_deltas' in parsed) {
|
|
console.log('Importing as stackprof profile')
|
|
return toGroup(importFromStackprof(parsed))
|
|
} else if ('code' in parsed && 'functions' in parsed && 'ticks' in parsed) {
|
|
console.log('Importing as --prof-process v8 log')
|
|
return toGroup(importFromV8ProfLog(parsed))
|
|
} else if ('head' in parsed && 'selfSize' in parsed['head']) {
|
|
console.log('Importing as Chrome Heap Profile')
|
|
return toGroup(importFromChromeHeapProfile(parsed))
|
|
} else if ('rts_arguments' in parsed && 'initial_capabilities' in parsed) {
|
|
console.log('Importing as Haskell GHC JSON Profile')
|
|
return importFromHaskell(parsed)
|
|
} else if ('recording' in parsed && 'sampleStackTraces' in parsed.recording) {
|
|
console.log('Importing as Safari profile')
|
|
return toGroup(importFromSafari(parsed))
|
|
}
|
|
} else {
|
|
// Format is not JSON
|
|
|
|
// If the first line is "# callgrind format", it's probably in Callgrind
|
|
// Profile Format.
|
|
if (
|
|
/^# callgrind format/.exec(contents.firstChunk()) ||
|
|
(/^events:/m.exec(contents.firstChunk()) && /^fn=/m.exec(contents.firstChunk()))
|
|
) {
|
|
console.log('Importing as Callgrind profile')
|
|
return importFromCallgrind(contents, fileName)
|
|
}
|
|
|
|
// If the first line contains "Symbol Name", preceded by a tab, it's probably
|
|
// a deep copy from OS X Instruments.app
|
|
if (/^[\w \t\(\)]*\tSymbol Name/.exec(contents.firstChunk())) {
|
|
console.log('Importing as Instruments.app deep copy')
|
|
return toGroup(importFromInstrumentsDeepCopy(contents))
|
|
}
|
|
|
|
const fromLinuxPerf = importFromLinuxPerf(contents)
|
|
if (fromLinuxPerf) {
|
|
console.log('Importing from linux perf script output')
|
|
return fromLinuxPerf
|
|
}
|
|
|
|
const fromBGFlameGraph = importFromBGFlameGraph(contents)
|
|
if (fromBGFlameGraph) {
|
|
console.log('Importing as collapsed stack format')
|
|
return toGroup(fromBGFlameGraph)
|
|
}
|
|
}
|
|
|
|
// Unrecognized format
|
|
return null
|
|
}
|
|
|
|
export async function importFromFileSystemDirectoryEntry(entry: FileSystemDirectoryEntry) {
|
|
return importFromInstrumentsTrace(entry)
|
|
}
|