Support importing from Firefox (#26)

This commit is contained in:
Jamie Wong 2018-04-17 18:50:53 -07:00 committed by GitHub
parent caba525465
commit 7ca4a41a6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 4352 additions and 1 deletions

View File

@ -13,6 +13,7 @@ import {Flamechart} from './flamechart'
import {FlamechartView} from './flamechart-view'
import {FontFamily, FontSize, Colors} from './style'
import {getHashParams, HashParams} from './hash-params'
import {importFromFirefox} from './import/firefox'
declare function require(x: string): any
const exampleProfileURL = require('./sample/perf-vertx-stacks-01-collapsed-all.txt')
@ -57,7 +58,10 @@ function importProfile(contents: string, fileName: string): Profile | null {
// Second pass: Try to guess what file format it is based on structure
try {
const parsed = JSON.parse(contents)
if (Array.isArray(parsed) && parsed[parsed.length - 1].name === 'CpuProfile') {
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) {

233
import/firefox.ts Normal file
View File

@ -0,0 +1,233 @@
import {Profile, FrameInfo, TimeFormatter} from '../profile'
import {getOrInsert, lastOf} from '../utils'
interface Allocations {
frames: any[]
sites: any[]
sizes: any[]
timestamps: any[]
}
interface Configuration {
allocationsMaxLogLength: number
allocationsSampleProbability: number
bufferSize: number
sampleFrequency: number
withAllocations: boolean
withMarkers: boolean
withMemory: boolean
withTicks: boolean
}
interface Lib {
arch: string
breakpadId: string
debugName: string
debugPath: string
end: any
name: string
offset: number
path: string
start: any
}
interface Meta {
abi: string
asyncstack: number
debug: number
gcpoison: number
interval: number
misc: string
oscpu: string
platform: string
processType: number
product: string
shutdownTime?: any
stackwalk: number
startTime: number
toolkit: string
version: number
}
interface PausedRange {
endTime: number
reason: string
startTime: number
}
type Frame = [number] | [number, number | null, number | null, number, number]
interface FrameTable {
data: Frame[]
/*
schema: {
location: 0
implementation: 1
optimizations: 2
line: 3
category: 4
}
*/
}
interface MarkerMeta {
category: string
interval: string
type: string
}
type Marker = [number, number] | [number, number, MarkerMeta]
interface Markers {
data: Marker[]
/*
schema: {
name: 0
time: 1
data: 2
}
*/
}
type Sample = [number, number, number] | [number, number, number, number, number]
interface Samples {
data: Sample[]
/*
schema: {
stack: 0
time: 1
responsiveness: 2
rss: 3
uss: 4
}
*/
}
export interface StackTable {
data: [number | null, number][]
/*
schema: {
prefix: 0
frame: 1
}
*/
}
export interface Thread {
frameTable: FrameTable
markers: Markers
name: string
pid: number
processType: string
registerTime: number
samples: Samples
stackTable: StackTable
stringTable: string[]
tid: number
unregisterTime?: any
}
export interface FirefoxCPUProfile {
libs: Lib[]
meta: Meta
pausedRanges: PausedRange[]
processes: any[]
threads: Thread[]
}
export interface FirefoxProfile {
allocations: Allocations
configuration: Configuration
duration: number
fileType: string
frames: any[]
label: string
markers: any[]
memory: any[]
profile: FirefoxCPUProfile
ticks: any[]
version: number
}
export function importFromFirefox(firefoxProfile: FirefoxProfile): Profile {
const cpuProfile = firefoxProfile.profile
const thread =
cpuProfile.threads.length === 1
? cpuProfile.threads[0]
: cpuProfile.threads.filter(t => t.name === 'GeckoMain')[0]
const frameIdToFrameInfo = new Map<number, FrameInfo>()
function extractStack(sample: Sample): FrameInfo[] {
let stackFrameId: number | null = sample[0]
const ret: number[] = []
while (stackFrameId != null) {
const nextStackFrame: [number | null, number] = thread.stackTable.data[stackFrameId]
const [nextStackId, frameId] = nextStackFrame
ret.push(frameId)
stackFrameId = nextStackId
}
ret.reverse()
return ret
.map(f => {
const frameData = thread.frameTable.data[f]
const location = thread.stringTable[frameData[0]]
const match = /(.*)\s+\((.*?):?(\d+)?\)$/.exec(location)
if (!match) return null
if (match[2].startsWith('resource:') || match[2] === 'self-hosted') {
// Ignore Firefox-internals stuff
return null
}
return getOrInsert(frameIdToFrameInfo, f, () => ({
key: location,
name: match[1]!,
file: match[2]!,
line: match[3] ? parseInt(match[3]) : undefined,
}))
})
.filter(f => f != null) as FrameInfo[]
}
const profile = new Profile(firefoxProfile.duration)
let prevStack: FrameInfo[] = []
for (let sample of thread.samples.data) {
const stack = extractStack(sample)
const value = sample[1]
// Find lowest common ancestor of the current stack and the previous one
let lca: FrameInfo | null = null
// This is O(n^2), but n should be relatively small here (stack height),
// so hopefully this isn't much of a problem
for (let i = stack.length - 1; i >= 0 && prevStack.indexOf(stack[i]) === -1; i--) {}
// Close frames that are no longer open
while (prevStack.length > 0 && lastOf(prevStack) != lca) {
const closingFrame = prevStack.pop()!
profile.leaveFrame(closingFrame, value)
}
// Open frames that are now becoming open
const toOpen: FrameInfo[] = []
for (let i = stack.length - 1; i >= 0 && stack[i] != lca; i--) {
toOpen.push(stack[i])
}
toOpen.reverse()
for (let frame of toOpen) {
profile.enterFrame(frame, value)
}
prevStack = stack
}
profile.setValueFormatter(new TimeFormatter('milliseconds'))
return profile
}

1
sample/firefox.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
<script src="./simple.js"></script>

32
sample/simple-firefox.js Normal file
View File

@ -0,0 +1,32 @@
function a() {
for (let i = 0; i < 1000; i++) {
b()
c()
}
}
function b() {
for (let i = 0; i < 10; i++) {
d()
}
}
function c() {
for (let i = 0; i < 10; i++) {
d()
}
}
function d() {
let prod = 1
for (let i = 1; i < 1000; i++) {
prod *= i
}
return prod
}
console.profile('a')
a()
setTimeout(() => {
console.profileEnd('a')
}, 0)

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

1
sample/simple.html Normal file
View File

@ -0,0 +1 @@
<script src="./simple.js"></script>