mirror of
https://github.com/jlfwong/speedscope.git
synced 2025-01-06 02:16:07 +03:00
Fix existing code splitting & code split profile importers into their own chunk (#79)
At some point in the past, the code splitting of demangle-cpp got broken. This fixes that and also code splits out all fo the different profile importers into their own module since they aren't needed for initial render of the page.
This commit is contained in:
parent
94f0875dc0
commit
4e32a73802
@ -2,16 +2,7 @@ import {h} from 'preact'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {ReloadableComponent, SerializedComponent} from './reloadable'
|
||||
|
||||
// TODO(jlfwong): Load these async, since none of them are required for initial render
|
||||
import {importFromBGFlameGraph} from './import/bg-flamegraph'
|
||||
import {importFromStackprof} from './import/stackprof'
|
||||
import {importFromChromeTimeline, importFromChromeCPUProfile} from './import/chrome'
|
||||
import {importFromFirefox} from './import/firefox'
|
||||
import {
|
||||
importFromInstrumentsDeepCopy,
|
||||
importFromInstrumentsTrace,
|
||||
FileSystemDirectoryEntry,
|
||||
} from './import/instruments'
|
||||
import {FileSystemDirectoryEntry} from './import/file-system-entry'
|
||||
|
||||
import {FlamechartRenderer, FlamechartRowAtlasKey} from './flamechart-renderer'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
@ -28,6 +19,16 @@ import {RowAtlas} from './row-atlas'
|
||||
import {importAsmJsSymbolMap} from './asm-js'
|
||||
import {SandwichView} from './sandwich-view'
|
||||
|
||||
const importModule = import('./import')
|
||||
// Force eager loading of the module
|
||||
importModule.then(() => {})
|
||||
async function importProfile(fileName: string, contents: string): Promise<Profile | null> {
|
||||
return (await importModule).importProfile(fileName, contents)
|
||||
}
|
||||
async function importFromFileSystemDirectoryEntry(entry: FileSystemDirectoryEntry) {
|
||||
return (await importModule).importFromFileSystemDirectoryEntry(entry)
|
||||
}
|
||||
|
||||
declare function require(x: string): any
|
||||
const exampleProfileURL = require('./sample/profiles/stackcollapse/perf-vertx-stacks-01-collapsed-all.txt')
|
||||
|
||||
@ -60,68 +61,6 @@ interface ToolbarProps extends ApplicationState {
|
||||
setViewMode(order: ViewMode): void
|
||||
}
|
||||
|
||||
function importProfile(fileName: string, contents: string): Profile | null {
|
||||
try {
|
||||
// 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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export class Toolbar extends ReloadableComponent<ToolbarProps, void> {
|
||||
setTimeOrder = () => {
|
||||
this.props.setViewMode(ViewMode.CHRONO_FLAME_CHART)
|
||||
@ -408,8 +347,8 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.addEventListener('loadend', () => {
|
||||
const profile = importProfile(file.name, reader.result)
|
||||
reader.addEventListener('loadend', async () => {
|
||||
const profile = await importProfile(file.name, reader.result)
|
||||
if (profile) {
|
||||
if (!profile.getName()) {
|
||||
profile.setName(file.name)
|
||||
@ -460,7 +399,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
// Instrument.app file format is actually a directory.
|
||||
if (webkitEntry.isDirectory && webkitEntry.name.endsWith('.trace')) {
|
||||
console.log('Importing as Instruments.app .trace file')
|
||||
this.loadProfile(async () => await importFromInstrumentsTrace(webkitEntry))
|
||||
this.loadProfile(async () => await importFromFileSystemDirectoryEntry(webkitEntry))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,5 @@
|
||||
import * as fs from 'fs'
|
||||
import {importFromBGFlameGraph} from './bg-flamegraph'
|
||||
import {dumpProfile} from '../test-utils'
|
||||
import {checkProfileSnapshot} from '../test-utils'
|
||||
|
||||
test('importFromBGFlameGraph', () => {
|
||||
const input = fs.readFileSync('./sample/profiles/stackcollapse/simple.txt', 'utf8')
|
||||
const profile = importFromBGFlameGraph(input)
|
||||
expect(dumpProfile(profile)).toMatchSnapshot()
|
||||
test('importFromBGFlameGraph', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple.txt')
|
||||
})
|
||||
|
@ -1,15 +1,9 @@
|
||||
import * as fs from 'fs'
|
||||
import {dumpProfile} from '../test-utils'
|
||||
import {importFromChromeCPUProfile, importFromChromeTimeline} from './chrome'
|
||||
import {checkProfileSnapshot} from '../test-utils'
|
||||
|
||||
test('importFromChromeCPUProfile', () => {
|
||||
const input = fs.readFileSync('./sample/profiles/Chrome/65/simple.cpuprofile', 'utf8')
|
||||
const profile = importFromChromeCPUProfile(JSON.parse(input))
|
||||
expect(dumpProfile(profile)).toMatchSnapshot()
|
||||
test('importFromChromeCPUProfile', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/Chrome/65/simple.cpuprofile')
|
||||
})
|
||||
|
||||
test('importFromChromeTimeline', () => {
|
||||
const input = fs.readFileSync('./sample/profiles/Chrome/65/simple-timeline.json', 'utf8')
|
||||
const profile = importFromChromeTimeline(JSON.parse(input))
|
||||
expect(dumpProfile(profile)).toMatchSnapshot()
|
||||
test('importFromChromeTimeline', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/Chrome/65/simple-timeline.json')
|
||||
})
|
||||
|
17
import/file-system-entry.ts
Normal file
17
import/file-system-entry.ts
Normal file
@ -0,0 +1,17 @@
|
||||
// The bits of this API that we care about. This is implemented by WebKitEntry
|
||||
// https://wicg.github.io/entries-api/#api-entry
|
||||
export interface FileSystemDirectoryReader {
|
||||
readEntries(cb: (entries: FileSystemEntry[]) => void, error: (err: Error) => void): void
|
||||
}
|
||||
export interface FileSystemEntry {
|
||||
readonly isFile: boolean
|
||||
readonly isDirectory: boolean
|
||||
readonly name: string
|
||||
readonly fullPath: string
|
||||
}
|
||||
export interface FileSystemDirectoryEntry extends FileSystemEntry {
|
||||
createReader(): FileSystemDirectoryReader
|
||||
}
|
||||
export interface FileSystemFileEntry extends FileSystemEntry {
|
||||
file(cb: (file: File) => void, errCb: (err: Error) => void): void
|
||||
}
|
@ -1,9 +1,5 @@
|
||||
import * as fs from 'fs'
|
||||
import {dumpProfile} from '../test-utils'
|
||||
import {importFromFirefox} from './firefox'
|
||||
import {checkProfileSnapshot} from '../test-utils'
|
||||
|
||||
test('importFromFirefox', () => {
|
||||
const input = fs.readFileSync('./sample/profiles/Firefox/59/simple-firefox.json', 'utf8')
|
||||
const profile = importFromFirefox(JSON.parse(input))
|
||||
expect(dumpProfile(profile)).toMatchSnapshot()
|
||||
test('importFromFirefox', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/Firefox/59/simple-firefox.json')
|
||||
})
|
||||
|
74
import/index.ts
Normal file
74
import/index.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import {Profile} from '../profile'
|
||||
import {FileSystemDirectoryEntry} from './file-system-entry'
|
||||
|
||||
import {importFromChromeCPUProfile, importFromChromeTimeline} from './chrome'
|
||||
import {importFromStackprof} from './stackprof'
|
||||
import {importFromInstrumentsDeepCopy, importFromInstrumentsTrace} from './instruments'
|
||||
import {importFromBGFlameGraph} from './bg-flamegraph'
|
||||
import {importFromFirefox} from './firefox'
|
||||
|
||||
export async function importProfile(fileName: string, contents: string): Promise<Profile | null> {
|
||||
try {
|
||||
// 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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export async function importFromFileSystemDirectoryEntry(entry: FileSystemDirectoryEntry) {
|
||||
return importFromInstrumentsTrace(entry)
|
||||
}
|
@ -1,31 +1,22 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import {dumpProfile} from '../test-utils'
|
||||
import {
|
||||
importFromInstrumentsDeepCopy,
|
||||
FileSystemEntry,
|
||||
importFromInstrumentsTrace,
|
||||
} from './instruments'
|
||||
import {dumpProfile, checkProfileSnapshot} from '../test-utils'
|
||||
|
||||
import * as JSZip from 'jszip'
|
||||
import {FileSystemEntry} from './file-system-entry'
|
||||
import {importFromFileSystemDirectoryEntry} from '.'
|
||||
|
||||
describe('importFromInstrumentsDeepCopy', () => {
|
||||
test('time profile', () => {
|
||||
const input = fs.readFileSync(
|
||||
test('time profile', async () => {
|
||||
await checkProfileSnapshot(
|
||||
'./sample/profiles/Instruments/7.3.1/simple-time-profile-deep-copy.txt',
|
||||
'utf8',
|
||||
)
|
||||
const profile = importFromInstrumentsDeepCopy(input)
|
||||
expect(dumpProfile(profile)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('allocations profile', () => {
|
||||
const input = fs.readFileSync(
|
||||
test('allocations profile', async () => {
|
||||
await checkProfileSnapshot(
|
||||
'./sample/profiles/Instruments/7.3.1/random-allocations-deep-copy.txt',
|
||||
'utf8',
|
||||
)
|
||||
const profile = importFromInstrumentsDeepCopy(input)
|
||||
expect(dumpProfile(profile)).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
@ -35,7 +26,7 @@ class ZipBackedFileSystemEntry implements FileSystemEntry {
|
||||
readonly name: string
|
||||
readonly fullPath: string
|
||||
|
||||
private zipDir: JSZip | null
|
||||
private zipDir: any | null
|
||||
private zipFile: JSZip.JSZipObject | null
|
||||
|
||||
constructor(private zip: JSZip, fullPath: string) {
|
||||
@ -76,7 +67,7 @@ class ZipBackedFileSystemEntry implements FileSystemEntry {
|
||||
readEntries: (cb: (entries: FileSystemEntry[]) => void, errCb: (error: Error) => void) => {
|
||||
if (!this.zipDir) return errCb(new Error('Failed to read folder entries'))
|
||||
const ret: FileSystemEntry[] = []
|
||||
this.zipDir.forEach((relativePath, file) => {
|
||||
this.zipDir.forEach((relativePath: string, file: {name: string}) => {
|
||||
if (relativePath.split('/').length === (relativePath.endsWith('/') ? 2 : 1)) {
|
||||
ret.push(new ZipBackedFileSystemEntry(this.zip, file.name))
|
||||
}
|
||||
@ -89,14 +80,14 @@ class ZipBackedFileSystemEntry implements FileSystemEntry {
|
||||
|
||||
describe('importFromInstrumentsTrace', () => {
|
||||
async function importFromTrace(tracePath: string) {
|
||||
const zip = await new Promise<JSZip>((resolve, reject) => {
|
||||
const zip = await new Promise<any>((resolve, reject) => {
|
||||
fs.readFile(tracePath, (err, data) => {
|
||||
if (err) return reject(err)
|
||||
JSZip.loadAsync(data).then(resolve)
|
||||
})
|
||||
})
|
||||
const root = new ZipBackedFileSystemEntry(zip, 'simple-time-profile.trace')
|
||||
const profile = await importFromInstrumentsTrace(root)
|
||||
const profile = await importFromFileSystemDirectoryEntry(root)
|
||||
expect(dumpProfile(profile)).toMatchSnapshot()
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import {Profile, FrameInfo, CallTreeProfileBuilder, StackListProfileBuilder} fro
|
||||
import {sortBy, getOrThrow, getOrInsert, lastOf, getOrElse, zeroPad} from '../utils'
|
||||
import * as pako from 'pako'
|
||||
import {ByteFormatter, TimeFormatter} from '../value-formatters'
|
||||
import {FileSystemDirectoryEntry, FileSystemEntry, FileSystemFileEntry} from './file-system-entry'
|
||||
|
||||
function parseTSV<T>(contents: string): T[] {
|
||||
const lines = contents.split('\n').map(l => l.split('\t'))
|
||||
@ -148,24 +149,6 @@ interface TraceDirectoryTree {
|
||||
subdirectories: Map<string, TraceDirectoryTree>
|
||||
}
|
||||
|
||||
// The bits of this API that we care about. This is implemented by WebKitEntry
|
||||
// https://wicg.github.io/entries-api/#api-entry
|
||||
export interface FileSystemDirectoryReader {
|
||||
readEntries(cb: (entries: FileSystemEntry[]) => void, error: (err: Error) => void): void
|
||||
}
|
||||
export interface FileSystemEntry {
|
||||
readonly isFile: boolean
|
||||
readonly isDirectory: boolean
|
||||
readonly name: string
|
||||
readonly fullPath: string
|
||||
}
|
||||
export interface FileSystemDirectoryEntry extends FileSystemEntry {
|
||||
createReader(): FileSystemDirectoryReader
|
||||
}
|
||||
export interface FileSystemFileEntry extends FileSystemEntry {
|
||||
file(cb: (file: File) => void, errCb: (err: Error) => void): void
|
||||
}
|
||||
|
||||
async function extractDirectoryTree(entry: FileSystemDirectoryEntry): Promise<TraceDirectoryTree> {
|
||||
const node: TraceDirectoryTree = {
|
||||
name: entry.name,
|
||||
|
@ -1,9 +1,5 @@
|
||||
import * as fs from 'fs'
|
||||
import {dumpProfile} from '../test-utils'
|
||||
import {importFromStackprof} from './stackprof'
|
||||
import {checkProfileSnapshot} from '../test-utils'
|
||||
|
||||
test('importFromStackprof', () => {
|
||||
const input = fs.readFileSync('./sample/profiles/stackprof/simple-stackprof.json', 'utf8')
|
||||
const profile = importFromStackprof(JSON.parse(input))
|
||||
expect(dumpProfile(profile)).toMatchSnapshot()
|
||||
test('importFromStackprof', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/stackprof/simple-stackprof.json')
|
||||
})
|
||||
|
@ -1,4 +1,7 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import {Profile, CallTreeNode, Frame} from './profile'
|
||||
import {importProfile} from './import'
|
||||
|
||||
interface DumpedProfile {
|
||||
stacks: string[]
|
||||
@ -39,3 +42,13 @@ export function dumpProfile(profile: Profile): any {
|
||||
|
||||
return dump
|
||||
}
|
||||
|
||||
export async function checkProfileSnapshot(filepath: string) {
|
||||
const input = fs.readFileSync(filepath, 'utf8')
|
||||
const profile = await importProfile(path.basename(filepath), input)
|
||||
if (profile) {
|
||||
expect(dumpProfile(profile)).toMatchSnapshot()
|
||||
} else {
|
||||
fail('Failed to extract profile')
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "Node",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"jsx": "react",
|
||||
"noUnusedLocals": true,
|
||||
"jsxFactory": "h",
|
||||
"target": "es2015"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user