diff --git a/application.tsx b/application.tsx index 4153a76..7e33902 100644 --- a/application.tsx +++ b/application.tsx @@ -255,8 +255,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> { try { profile = await loader() } catch (e) { - alert('Failed to load format. See console for details') - console.log(e) + console.log('Failed to load format', e) this.setState({error: true}) return } @@ -264,6 +263,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> { if (profile == null) { // TODO(jlfwong): Make this a nicer overlay alert('Unrecognized format! See documentation about supported formats.') + await new Promise(resolve => this.setState({loading: false}, resolve)) return } @@ -343,40 +343,36 @@ export class Application extends ReloadableComponent<{}, ApplicationState> { } loadFromFile(file: File) { - this.loadProfile( - () => - new Promise((resolve, reject) => { - const reader = new FileReader() - reader.addEventListener('loadend', async () => { - const profile = await importProfile(file.name, reader.result) - if (profile) { - if (!profile.getName()) { - profile.setName(file.name) - } - resolve(profile) - return - } + this.loadProfile(async () => { + const reader = new FileReader() + const loadPromise = new Promise(resolve => reader.addEventListener('loadend', resolve)) + reader.readAsText(file) + await loadPromise - if (this.state.profile) { - // If a profile is already loaded, it's possible the file being imported is - // a symbol map. If that's the case, we want to parse it, and apply the symbol - // mapping to the already loaded profile. This can be use to take an opaque - // profile and make it readable. - const map = importAsmJsSymbolMap(reader.result) - if (map) { - console.log('Importing as asm.js symbol map') - let profile = this.state.profile - profile.remapNames(name => map.get(name) || name) - resolve(profile) - return - } - } + const profile = await importProfile(file.name, reader.result) + if (profile) { + if (!profile.getName()) { + profile.setName(file.name) + } + return profile + } - reject() - }) - reader.readAsText(file) - }), - ) + if (this.state.profile) { + // If a profile is already loaded, it's possible the file being imported is + // a symbol map. If that's the case, we want to parse it, and apply the symbol + // mapping to the already loaded profile. This can be use to take an opaque + // profile and make it readable. + const map = importAsmJsSymbolMap(reader.result) + if (map) { + console.log('Importing as asm.js symbol map') + let profile = this.state.profile + profile.remapNames(name => map.get(name) || name) + return profile + } + } + + return null + }) } loadExample = () => { diff --git a/import/chrome.ts b/import/chrome.ts index 820164a..dca5d6f 100644 --- a/import/chrome.ts +++ b/import/chrome.ts @@ -114,7 +114,13 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile { elapsed = 0 } - elapsed += chromeProfile.timeDeltas[i] + let timeDelta = chromeProfile.timeDeltas[i] + if (timeDelta < 0) { + console.warn('Substituting zero for unexpected time delta:', timeDelta, 'at index', i) + timeDelta = 0 + } + + elapsed += timeDelta lastNodeId = nodeId } if (!isNaN(lastNodeId)) { diff --git a/import/index.ts b/import/index.ts index 6ab6f11..2df9f83 100644 --- a/import/index.ts +++ b/import/index.ts @@ -8,65 +8,64 @@ import {importFromBGFlameGraph} from './bg-flamegraph' import {importFromFirefox} from './firefox' export async function importProfile(fileName: string, contents: string): Promise { + // First pass: Check known file format names to infer the file type + if (fileName.endsWith('.cpuprofile')) { + console.log('Importing as Chrome CPU Profile') + return importFromChromeCPUProfile(JSON.parse(contents)) + } else if (fileName.endsWith('.chrome.json') || /Profile-\d{8}T\d{6}/.exec(fileName)) { + console.log('Importing as Chrome Timeline') + return importFromChromeTimeline(JSON.parse(contents)) + } else if (fileName.endsWith('.stackprof.json')) { + console.log('Importing as stackprof profile') + return importFromStackprof(JSON.parse(contents)) + } else if (fileName.endsWith('.instruments.txt')) { + console.log('Importing as Instruments.app deep copy') + return importFromInstrumentsDeepCopy(contents) + } else if (fileName.endsWith('.collapsedstack.txt')) { + console.log('Importing as collapsed stack format') + return importFromBGFlameGraph(contents) + } + + // Second pass: Try to guess what file format it is based on structure + let parsed: any try { - // First pass: Check known file format names to infer the file type - if (fileName.endsWith('.cpuprofile')) { + parsed = JSON.parse(contents) + } catch (e) {} + if (parsed) { + if (parsed['systemHost'] && parsed['systemHost']['name'] == 'Firefox') { + console.log('Importing as Firefox profile') + return importFromFirefox(parsed) + } else if (Array.isArray(parsed) && parsed[parsed.length - 1].name === 'CpuProfile') { console.log('Importing as Chrome CPU Profile') - return importFromChromeCPUProfile(JSON.parse(contents)) - } else if (fileName.endsWith('.chrome.json') || /Profile-\d{8}T\d{6}/.exec(fileName)) { + return importFromChromeTimeline(parsed) + } else if ('nodes' in parsed && 'samples' in parsed && 'timeDeltas' in parsed) { console.log('Importing as Chrome Timeline') - return importFromChromeTimeline(JSON.parse(contents)) - } else if (fileName.endsWith('.stackprof.json')) { + return importFromChromeCPUProfile(parsed) + } else if ('mode' in parsed && 'frames' in parsed) { console.log('Importing as stackprof profile') - return importFromStackprof(JSON.parse(contents)) - } else if (fileName.endsWith('.instruments.txt')) { + return importFromStackprof(parsed) + } + } else { + // Format is not JSON + + // 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)) { console.log('Importing as Instruments.app deep copy') return importFromInstrumentsDeepCopy(contents) - } else if (fileName.endsWith('.collapsedstack.txt')) { + } + + // If every line ends with a space followed by a number, it's probably + // the collapsed stack format. + const lineCount = contents.split(/\n/).length + if (lineCount >= 1 && lineCount === contents.split(/ \d+\n/).length) { console.log('Importing as collapsed stack format') return importFromBGFlameGraph(contents) } - - // Second pass: Try to guess what file format it is based on structure - try { - const parsed = JSON.parse(contents) - if (parsed['systemHost'] && parsed['systemHost']['name'] == 'Firefox') { - console.log('Importing as Firefox profile') - return importFromFirefox(parsed) - } else if (Array.isArray(parsed) && parsed[parsed.length - 1].name === 'CpuProfile') { - console.log('Importing as Chrome CPU Profile') - return importFromChromeTimeline(parsed) - } else if ('nodes' in parsed && 'samples' in parsed && 'timeDeltas' in parsed) { - console.log('Importing as Chrome Timeline') - return importFromChromeCPUProfile(parsed) - } else if ('mode' in parsed && 'frames' in parsed) { - console.log('Importing as stackprof profile') - return importFromStackprof(parsed) - } - } catch (e) { - // Format is not JSON - - // 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)) { - console.log('Importing as Instruments.app deep copy') - return importFromInstrumentsDeepCopy(contents) - } - - // If every line ends with a space followed by a number, it's probably - // the collapsed stack format. - const lineCount = contents.split(/\n/).length - if (lineCount >= 1 && lineCount === contents.split(/ \d+\n/).length) { - console.log('Importing as collapsed stack format') - return importFromBGFlameGraph(contents) - } - } - - return null - } catch (e) { - console.error(e) - return null } + + // Unrecognized format + return null } export async function importFromFileSystemDirectoryEntry(entry: FileSystemDirectoryEntry) { diff --git a/profile.ts b/profile.ts index 2ded554..40f44bb 100644 --- a/profile.ts +++ b/profile.ts @@ -471,6 +471,11 @@ export class CallTreeProfileBuilder extends Profile { if (delta > 0) { this.samples.push(prevTop) this.weights.push(value - this.lastValue!) + } else if (delta < 0) { + throw new Error( + `Samples must be provided in increasing order of cumulative value. Last sample was ${this + .lastValue!}, this sample was ${value}`, + ) } } @@ -509,6 +514,11 @@ export class CallTreeProfileBuilder extends Profile { if (delta > 0) { this.samples.push(leavingStackTop!) this.weights.push(value - this.lastValue!) + } else if (delta < 0) { + throw new Error( + `Samples must be provided in increasing order of cumulative value. Last sample was ${this + .lastValue!}, this sample was ${value}`, + ) } } else { this.groupedOrderStack.pop()