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:
@@ -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 = () => {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
10
profile.ts
10
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()
|
||||
|
||||
Reference in New Issue
Block a user