Skip to content

Commit

Permalink
Add support for profiles w/ multiples processes & threads (#130)
Browse files Browse the repository at this point in the history
More broadly, this just supports multiple profiles loaded into the editor in the same time, which supports import from profiles which are multithreaded by importing each thread as a different profile.

For now, the only two file formats that support multiprocess import are Instruments .trace files and speedscope's own file format

In the process of doing this, I refactored the container code considerably and extracted all the dispatch calls into containers rather than them being part of the non-container view code. This is nice because it means that views don't have to be aware of which Flamechart they are or which profile index is being operated upon.

Fixes #66 
Fixes #82 
Fixes #91
  • Loading branch information
jlfwong committed Aug 12, 2018
1 parent 7368e15 commit 0e2041c
Show file tree
Hide file tree
Showing 38 changed files with 1,231 additions and 507 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@

### Added

* Added support for multiple threads/processes [#130]
* Import all runs & threads from Instruments .trace files instead of just main thread from selected run [#130]

### Fixed

## [0.5.1] - 2018-08-09

### Fixed

* Fixed broken CLI

## [0.5.0] - 2018-08-09

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,5 @@ Once a profile has loaded, the main view is split into two: the top area is the
* `r`: Collapse recursion in the flamegraphs
* `Cmd+S`/`Ctrl+S` to save the current profile
* `Cmd+O`/`Ctrl+O` to open a new profile
* `n`: Go to next profile/thread if one is available
* `p`: Go to previous profile/thread if one is available
2 changes: 1 addition & 1 deletion bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ async function main() {
throw new Error('At most one argument expected')
}

let urlToOpen = 'file://' + path.resolve(__dirname, './dist/release/index.html')
let urlToOpen = 'file://' + path.resolve(__dirname, '../dist/release/index.html')

if (process.argv.length === 3) {
const relPath = process.argv[2]
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "speedscope",
"version": "0.5.0",
"version": "0.6.0",
"description": "",
"repository": "jlfwong/speedscope",
"main": "index.js",
Expand Down
29 changes: 29 additions & 0 deletions sample/profiles/speedscope/0.6.0/two-sampled.speedscope.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"exporter": "speedscope@0.6.0",
"$schema": "https://www.speedscope.app/file-format-schema.json",
"name": "Two Samples",
"activeProfileIndex": 1,
"profiles": [
{
"type": "sampled",
"name": "one",
"unit": "seconds",
"startValue": 0,
"endValue": 14,
"samples": [[0, 1, 2], [0, 1, 2], [0, 1, 3], [0, 1, 2], [0, 1]],
"weights": [1, 1, 4, 3, 5]
},
{
"type": "sampled",
"name": "two",
"unit": "seconds",
"startValue": 0,
"endValue": 14,
"samples": [[0, 1, 2], [0, 1, 2], [0, 1, 3], [0, 1, 2], [0, 1]],
"weights": [1, 1, 4, 3, 5]
}
],
"shared": {
"frames": [{"name": "a"}, {"name": "b"}, {"name": "c"}, {"name": "d"}]
}
}
5 changes: 5 additions & 0 deletions src/import/__snapshots__/bg-flamegraph.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Object {
"totalWeight": 4,
},
],
"name": "simple.txt",
"stacks": Array [
"a;b;c 2",
"a;b;d 4",
Expand All @@ -48,3 +49,7 @@ Object {
],
}
`;

exports[`importFromBGFlameGraph: indexToView 1`] = `0`;

exports[`importFromBGFlameGraph: profileGroup.name 1`] = `"simple.txt"`;
10 changes: 10 additions & 0 deletions src/import/__snapshots__/chrome.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Object {
"totalWeight": 17557,
},
],
"name": "simple.cpuprofile",
"stacks": Array [
"(anonymous) 11.57ms",
"(anonymous);a;b;d 16.79ms",
Expand All @@ -57,6 +58,10 @@ Object {
}
`;

exports[`importFromChromeCPUProfile: indexToView 1`] = `0`;

exports[`importFromChromeCPUProfile: profileGroup.name 1`] = `"simple.cpuprofile"`;

exports[`importFromChromeTimeline 1`] = `
Object {
"frames": Array [
Expand Down Expand Up @@ -97,6 +102,7 @@ Object {
"totalWeight": 16987,
},
],
"name": "simple-timeline.json",
"stacks": Array [
"(program) 10.16ms",
" 1.99ms",
Expand Down Expand Up @@ -255,3 +261,7 @@ Object {
],
}
`;

exports[`importFromChromeTimeline: indexToView 1`] = `0`;

exports[`importFromChromeTimeline: profileGroup.name 1`] = `"simple-timeline.json"`;
10 changes: 10 additions & 0 deletions src/import/__snapshots__/firefox.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Object {
"totalWeight": 11.021373999974458,
},
],
"name": "simple-firefox.json",
"stacks": Array [
"a;b;d 989.53µs",
"a;c;d 1.02ms",
Expand Down Expand Up @@ -107,6 +108,7 @@ Object {
"totalWeight": 9.954629999992903,
},
],
"name": "recursion.json",
"stacks": Array [
"main;alpha;beta;alpha;beta;alpha;beta;alpha;delta;gamma 998.89µs",
"main 1.19ms",
Expand All @@ -120,3 +122,11 @@ Object {
],
}
`;

exports[`importFromFirefox recursion: indexToView 1`] = `0`;

exports[`importFromFirefox recursion: profileGroup.name 1`] = `"recursion.json"`;

exports[`importFromFirefox: indexToView 1`] = `0`;

exports[`importFromFirefox: profileGroup.name 1`] = `"simple-firefox.json"`;
12 changes: 12 additions & 0 deletions src/import/__snapshots__/instruments.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Object {
"totalWeight": 74448896,
},
],
"name": "random-allocations-deep-copy.txt",
"stacks": Array [
"start;main;delta();alpha();leakMemory();malloc;malloc_zone_malloc 73.00 MB",
"start;main;delta();beta();leakMemory();malloc;malloc_zone_malloc 72.00 MB",
Expand All @@ -98,6 +99,10 @@ Object {
}
`;

exports[`importFromInstrumentsDeepCopy allocations profile: indexToView 1`] = `0`;

exports[`importFromInstrumentsDeepCopy allocations profile: profileGroup.name 1`] = `"random-allocations-deep-copy.txt"`;

exports[`importFromInstrumentsDeepCopy time profile 1`] = `
Object {
"frames": Array [
Expand Down Expand Up @@ -282,6 +287,7 @@ Object {
"totalWeight": 1,
},
],
"name": "simple-time-profile-deep-copy.txt",
"stacks": Array [
"start;main;delta();alpha();leakMemory();malloc;malloc_zone_malloc;szone_malloc_should_clear;small_malloc_from_free_list 4.00ms",
"start;main;delta();alpha();leakMemory();malloc;malloc_zone_malloc;szone_malloc_should_clear;allocate_pages_securely;mach_vm_map;_kernelrpc_mach_vm_map_trap 1.00ms",
Expand Down Expand Up @@ -326,6 +332,10 @@ Object {
}
`;

exports[`importFromInstrumentsDeepCopy time profile: indexToView 1`] = `0`;

exports[`importFromInstrumentsDeepCopy time profile: profileGroup.name 1`] = `"simple-time-profile-deep-copy.txt"`;

exports[`importFromInstrumentsTrace Instruments 8.3.3 1`] = `
Object {
"frames": Array [
Expand Down Expand Up @@ -681,6 +691,7 @@ Object {
"totalWeight": 1016403,
},
],
"name": "simple-time-profile.trace - thread 4",
"stacks": Array [
" 730.82ms",
"_dyld_start;dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*);dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*);dyld::link(ImageLoader*, bool, bool, ImageLoader::RPathChain const&, unsigned int);ImageLoader::link(ImageLoader::LinkContext const&, bool, bool, bool, ImageLoader::RPathChain const&, char const*);ImageLoader::recursiveBind(ImageLoader::LinkContext const&, bool, bool);ImageLoader::recursiveBind(ImageLoader::LinkContext const&, bool, bool);ImageLoader::recursiveBind(ImageLoader::LinkContext const&, bool, bool);ImageLoader::recursiveBind(ImageLoader::LinkContext const&, bool, bool);ImageLoader::recursiveBind(ImageLoader::LinkContext const&, bool, bool);ImageLoader::recursiveBind(ImageLoader::LinkContext const&, bool, bool);ImageLoader::recursiveBind(ImageLoader::LinkContext const&, bool, bool);ImageLoader::recursiveBind(ImageLoader::LinkContext const&, bool, bool);ImageLoader::recursiveBind(ImageLoader::LinkContext const&, bool, bool);ImageLoader::recursiveBind(ImageLoader::LinkContext const&, bool, bool);ImageLoader::recursiveBind(ImageLoader::LinkContext const&, bool, bool);ImageLoader::recursiveBind(ImageLoader::LinkContext const&, bool, bool);ImageLoaderMachOCompressed::doBind(ImageLoader::LinkContext const&, bool);ImageLoaderMachO::setupLazyPointerHandler(ImageLoader::LinkContext const&) 5.06ms",
Expand Down Expand Up @@ -4108,6 +4119,7 @@ Object {
"totalWeight": 1012057,
},
],
"name": "simple-time-profile.trace - thread 4",
"stacks": Array [
" 7.15ms",
"_dyld_start;dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*);dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*);dyld::initializeMainExecutable();ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&);ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&);ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&);ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&);ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&);ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&);ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&);0x0000000110c55a79;libSystem_initializer;_libc_initializer;__chk_init;_dyld_register_func_for_add_image;_dyld_func_lookup 9.02ms",
Expand Down
5 changes: 5 additions & 0 deletions src/import/__snapshots__/stackprof.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Object {
"totalWeight": 774706,
},
],
"name": "simple-stackprof.json",
"stacks": Array [
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d 6.11ms",
"<main>;<main>;block in <main>;Object#a;Object#a;Object#b;Object#b;Object#d;Object#d;(garbage collection) 1.02ms",
Expand Down Expand Up @@ -849,3 +850,7 @@ Object {
],
}
`;

exports[`importFromStackprof: indexToView 1`] = `0`;

exports[`importFromStackprof: profileGroup.name 1`] = `"simple-stackprof.json"`;
5 changes: 5 additions & 0 deletions src/import/__snapshots__/v8proflog.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ Object {
"totalWeight": 10154,
},
],
"name": "simple.v8log.json",
"stacks": Array [
"(anonymous);startup;setupGlobalVariables;NativeModule.require;NativeModule.compile;(anonymous);NativeModule.require;NativeModule.compile;(anonymous);(c++) v8::internal::Runtime_CreateArrayLiteral;(c++) v8::internal::JSFunction::EnsureHasInitialMap 29.38ms",
"(anonymous);startup;setupGlobalConsole;setupInspectorCommandLineAPI;NativeModule.require;NativeModule.compile;(anonymous);NativeModule.require;NativeModule.compile;(anonymous);(c++) v8::internal::Runtime_StoreIC_Miss;(c++) v8::internal::Map::RawCopy 22.89ms",
Expand All @@ -258,3 +259,7 @@ Object {
],
}
`;

exports[`importFromV8ProfLog: indexToView 1`] = `0`;

exports[`importFromV8ProfLog: profileGroup.name 1`] = `"simple.v8log.json"`;
68 changes: 39 additions & 29 deletions src/import/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Profile} from '../lib/profile'
import {Profile, ProfileGroup} from '../lib/profile'
import {FileSystemDirectoryEntry} from './file-system-entry'

import {importFromChromeCPUProfile, importFromChromeTimeline} from './chrome'
Expand All @@ -7,48 +7,58 @@ import {importFromInstrumentsDeepCopy, importFromInstrumentsTrace} from './instr
import {importFromBGFlameGraph} from './bg-flamegraph'
import {importFromFirefox} from './firefox'
import {importSpeedscopeProfiles} from '../lib/file-format'
import {FileFormat} from '../lib/file-format-spec'
import {importFromV8ProfLog} from './v8proflog'

export async function importProfile(fileName: string, contents: string): Promise<Profile | null> {
const profile = await _importProfile(fileName, contents)
if (profile && !profile.getName()) {
profile.setName(fileName)
export async function importProfileGroup(
fileName: string,
contents: string,
): Promise<ProfileGroup | null> {
const profileGroup = await _importProfileGroup(fileName, contents)
if (profileGroup) {
if (!profileGroup.name) {
profileGroup.name = fileName
}
for (let profile of profileGroup.profiles) {
if (profile && !profile.getName()) {
profile.setName(fileName)
}
}
return profileGroup
}
return profile
return null
}

function importSingleSpeedscopeProfile(serialized: FileFormat.File) {
const profiles = importSpeedscopeProfiles(serialized)
if (profiles.length === 0) {
throw new Error('Failed to extract any profiles from the imported speedscope profile')
}
return profiles[0]
function toGroup(profile: Profile | null): ProfileGroup | null {
if (!profile) return null
return {name: profile.getName(), indexToView: 0, profiles: [profile]}
}

async function _importProfile(fileName: string, contents: string): Promise<Profile | null> {
async function _importProfileGroup(
fileName: string,
contents: string,
): Promise<ProfileGroup | null> {
// First pass: Check known file format names to infer the file type
if (fileName.endsWith('.speedscope.json')) {
console.log('Importing as speedscope json file')
return importSingleSpeedscopeProfile(JSON.parse(contents))
return importSpeedscopeProfiles(JSON.parse(contents))
} else if (fileName.endsWith('.cpuprofile')) {
console.log('Importing as Chrome CPU Profile')
return importFromChromeCPUProfile(JSON.parse(contents))
return toGroup(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))
return toGroup(importFromChromeTimeline(JSON.parse(contents)))
} else if (fileName.endsWith('.stackprof.json')) {
console.log('Importing as stackprof profile')
return importFromStackprof(JSON.parse(contents))
return toGroup(importFromStackprof(JSON.parse(contents)))
} else if (fileName.endsWith('.instruments.txt')) {
console.log('Importing as Instruments.app deep copy')
return importFromInstrumentsDeepCopy(contents)
return toGroup(importFromInstrumentsDeepCopy(contents))
} else if (fileName.endsWith('.collapsedstack.txt')) {
console.log('Importing as collapsed stack format')
return importFromBGFlameGraph(contents)
return toGroup(importFromBGFlameGraph(contents))
} else if (fileName.endsWith('.v8log.json')) {
console.log('Importing as --prof-process v8 log')
return importFromV8ProfLog(JSON.parse(contents))
return toGroup(importFromV8ProfLog(JSON.parse(contents)))
}

// Second pass: Try to guess what file format it is based on structure
Expand All @@ -59,22 +69,22 @@ async function _importProfile(fileName: string, contents: string): Promise<Profi
if (parsed) {
if (parsed['$schema'] === 'https://www.speedscope.app/file-format-schema.json') {
console.log('Importing as speedscope json file')
return importSingleSpeedscopeProfile(parsed)
return importSpeedscopeProfiles(JSON.parse(contents))
} else if (parsed['systemHost'] && parsed['systemHost']['name'] == 'Firefox') {
console.log('Importing as Firefox profile')
return importFromFirefox(parsed)
return toGroup(importFromFirefox(parsed))
} else if (Array.isArray(parsed) && parsed[parsed.length - 1].name === 'CpuProfile') {
console.log('Importing as Chrome CPU Profile')
return importFromChromeTimeline(parsed)
return toGroup(importFromChromeTimeline(parsed))
} else if ('nodes' in parsed && 'samples' in parsed && 'timeDeltas' in parsed) {
console.log('Importing as Chrome Timeline')
return importFromChromeCPUProfile(parsed)
return toGroup(importFromChromeCPUProfile(parsed))
} else if ('mode' in parsed && 'frames' in parsed) {
console.log('Importing as stackprof profile')
return importFromStackprof(parsed)
return toGroup(importFromStackprof(parsed))
} else if ('code' in parsed && 'functions' in parsed && 'ticks' in parsed) {
console.log('Importing as --prof-process v8 log')
return importFromV8ProfLog(parsed)
return toGroup(importFromV8ProfLog(parsed))
}
} else {
// Format is not JSON
Expand All @@ -83,15 +93,15 @@ async function _importProfile(fileName: string, contents: string): Promise<Profi
// 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)
return toGroup(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 toGroup(importFromBGFlameGraph(contents))
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/import/instruments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ describe('importFromInstrumentsTrace', () => {
})
})
const root = new ZipBackedFileSystemEntry(zip, 'simple-time-profile.trace')
const profile = await importFromFileSystemDirectoryEntry(root)
const profileGroup = await importFromFileSystemDirectoryEntry(root)
const profile = profileGroup.profiles[profileGroup.indexToView]
expect(dumpProfile(profile)).toMatchSnapshot()
}

Expand Down
Loading

0 comments on commit 0e2041c

Please sign in to comment.