From 4e32a73802f36246ff8bf225c48e9fe5745b1e9b Mon Sep 17 00:00:00 2001 From: Jamie Wong Date: Sat, 30 Jun 2018 17:26:28 -0700 Subject: [PATCH] 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. --- application.tsx | 89 ++++++------------------------------ import/bg-flamegraph.test.ts | 10 ++-- import/chrome.test.ts | 16 ++----- import/file-system-entry.ts | 17 +++++++ import/firefox.test.ts | 10 ++-- import/index.ts | 74 ++++++++++++++++++++++++++++++ import/instruments.test.ts | 31 +++++-------- import/instruments.ts | 19 +------- import/stackprof.test.ts | 10 ++-- test-utils.ts | 13 ++++++ tsconfig.json | 6 ++- 11 files changed, 148 insertions(+), 147 deletions(-) create mode 100644 import/file-system-entry.ts create mode 100644 import/index.ts diff --git a/application.tsx b/application.tsx index 52d7cc3..4153a76 100644 --- a/application.tsx +++ b/application.tsx @@ -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 { + 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 { 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 } } diff --git a/import/bg-flamegraph.test.ts b/import/bg-flamegraph.test.ts index 651375b..7d81c15 100644 --- a/import/bg-flamegraph.test.ts +++ b/import/bg-flamegraph.test.ts @@ -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') }) diff --git a/import/chrome.test.ts b/import/chrome.test.ts index ebb524c..4ae52bc 100644 --- a/import/chrome.test.ts +++ b/import/chrome.test.ts @@ -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') }) diff --git a/import/file-system-entry.ts b/import/file-system-entry.ts new file mode 100644 index 0000000..95138e1 --- /dev/null +++ b/import/file-system-entry.ts @@ -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 +} diff --git a/import/firefox.test.ts b/import/firefox.test.ts index 9f66975..984ad03 100644 --- a/import/firefox.test.ts +++ b/import/firefox.test.ts @@ -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') }) diff --git a/import/index.ts b/import/index.ts new file mode 100644 index 0000000..6ab6f11 --- /dev/null +++ b/import/index.ts @@ -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 { + 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) +} diff --git a/import/instruments.test.ts b/import/instruments.test.ts index 0859613..fe5a9f8 100644 --- a/import/instruments.test.ts +++ b/import/instruments.test.ts @@ -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((resolve, reject) => { + const zip = await new Promise((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() } diff --git a/import/instruments.ts b/import/instruments.ts index 8829af4..7ccf854 100644 --- a/import/instruments.ts +++ b/import/instruments.ts @@ -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(contents: string): T[] { const lines = contents.split('\n').map(l => l.split('\t')) @@ -148,24 +149,6 @@ interface TraceDirectoryTree { subdirectories: Map } -// 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 { const node: TraceDirectoryTree = { name: entry.name, diff --git a/import/stackprof.test.ts b/import/stackprof.test.ts index d3708c0..d862c68 100644 --- a/import/stackprof.test.ts +++ b/import/stackprof.test.ts @@ -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') }) diff --git a/test-utils.ts b/test-utils.ts index b23785e..c1e1283 100644 --- a/test-utils.ts +++ b/test-utils.ts @@ -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') + } +} diff --git a/tsconfig.json b/tsconfig.json index 96cd14b..69ec122 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,12 @@ { "compilerOptions": { - "module": "commonjs", + "module": "esnext", + "moduleResolution": "Node", + "esModuleInterop": true, "strict": true, "jsx": "react", "noUnusedLocals": true, "jsxFactory": "h", "target": "es2015" } -} \ No newline at end of file +}