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:
Jamie Wong 2018-06-30 17:26:28 -07:00 committed by GitHub
parent 94f0875dc0
commit 4e32a73802
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 148 additions and 147 deletions

View File

@ -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
}
}

View File

@ -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')
})

View File

@ -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')
})

View 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
}

View File

@ -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
View 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)
}

View File

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

View File

@ -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,

View File

@ -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')
})

View File

@ -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')
}
}

View File

@ -1,10 +1,12 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "esnext",
"moduleResolution": "Node",
"esModuleInterop": true,
"strict": true,
"jsx": "react",
"noUnusedLocals": true,
"jsxFactory": "h",
"target": "es2015"
}
}
}