Fix negative import timeDeltas between samples in Chrome (#80)

I'm not sure why this happens, but I have multiple profiles coming from Chrome that have negative values in the exported `timeDelta` objects.

This PR also cleans up some of the error reporting logic to hopefully stop logging `undefined` to the console on import failure.

Fixes #70
This commit is contained in:
Jamie Wong
2018-06-30 19:54:59 -07:00
committed by GitHub
parent 4e32a73802
commit 16d02cf18f
4 changed files with 95 additions and 84 deletions

View File

@@ -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 = () => {

View File

@@ -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)) {

View File

@@ -8,65 +8,64 @@ import {importFromBGFlameGraph} from './bg-flamegraph'
import {importFromFirefox} from './firefox'
export async function importProfile(fileName: string, contents: string): Promise<Profile | null> {
// 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) {

View File

@@ -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()