Add support for profiles w/ multiples processes & threads (#130)
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
This commit is contained in:
@@ -2,6 +2,13 @@
|
||||
|
||||
### Added
|
||||
|
||||
### Fixed
|
||||
|
||||
* 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
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "speedscope",
|
||||
"version": "0.5.1",
|
||||
"version": "0.6.0",
|
||||
"description": "",
|
||||
"repository": "jlfwong/speedscope",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -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"}]
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ Object {
|
||||
"totalWeight": 4,
|
||||
},
|
||||
],
|
||||
"name": "simple.txt",
|
||||
"stacks": Array [
|
||||
"a;b;c 2",
|
||||
"a;b;d 4",
|
||||
@@ -48,3 +49,7 @@ Object {
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importFromBGFlameGraph: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importFromBGFlameGraph: profileGroup.name 1`] = `"simple.txt"`;
|
||||
|
||||
@@ -49,6 +49,7 @@ Object {
|
||||
"totalWeight": 17557,
|
||||
},
|
||||
],
|
||||
"name": "simple.cpuprofile",
|
||||
"stacks": Array [
|
||||
"(anonymous) 11.57ms",
|
||||
"(anonymous);a;b;d 16.79ms",
|
||||
@@ -57,6 +58,10 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importFromChromeCPUProfile: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importFromChromeCPUProfile: profileGroup.name 1`] = `"simple.cpuprofile"`;
|
||||
|
||||
exports[`importFromChromeTimeline 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
@@ -97,6 +102,7 @@ Object {
|
||||
"totalWeight": 16987,
|
||||
},
|
||||
],
|
||||
"name": "simple-timeline.json",
|
||||
"stacks": Array [
|
||||
"(program) 10.16ms",
|
||||
" 1.99ms",
|
||||
@@ -255,3 +261,7 @@ Object {
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importFromChromeTimeline: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importFromChromeTimeline: profileGroup.name 1`] = `"simple-timeline.json"`;
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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"`;
|
||||
|
||||
@@ -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",
|
||||
@@ -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 [
|
||||
@@ -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",
|
||||
@@ -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 [
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
@@ -849,3 +850,7 @@ Object {
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importFromStackprof: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importFromStackprof: profileGroup.name 1`] = `"simple-stackprof.json"`;
|
||||
|
||||
@@ -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",
|
||||
@@ -258,3 +259,7 @@ Object {
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importFromV8ProfLog: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importFromV8ProfLog: profileGroup.name 1`] = `"simple.v8log.json"`;
|
||||
|
||||
+39
-29
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -83,7 +93,7 @@ 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
|
||||
@@ -91,7 +101,7 @@ async function _importProfile(fileName: string, contents: string): Promise<Profi
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+119
-50
@@ -1,7 +1,13 @@
|
||||
// This file contains methods to import data from OS X Instruments.app
|
||||
// https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/index.html
|
||||
|
||||
import {Profile, FrameInfo, CallTreeProfileBuilder, StackListProfileBuilder} from '../lib/profile'
|
||||
import {
|
||||
Profile,
|
||||
FrameInfo,
|
||||
CallTreeProfileBuilder,
|
||||
StackListProfileBuilder,
|
||||
ProfileGroup,
|
||||
} from '../lib/profile'
|
||||
import {sortBy, getOrThrow, getOrInsert, lastOf, getOrElse, zeroPad} from '../lib/utils'
|
||||
import * as pako from 'pako'
|
||||
import {ByteFormatter, TimeFormatter} from '../lib/value-formatters'
|
||||
@@ -382,92 +388,126 @@ interface SymbolInfo {
|
||||
addressToLine: Map<number, number>
|
||||
}
|
||||
|
||||
interface FormTemplateData {
|
||||
selectedRun: number
|
||||
instrument: string
|
||||
version: number
|
||||
interface FormTemplateRunData {
|
||||
number: number
|
||||
addressToFrameMap: Map<number, FrameInfo>
|
||||
}
|
||||
|
||||
interface FormTemplateData {
|
||||
version: number
|
||||
selectedRunNumber: number
|
||||
instrument: string
|
||||
runs: FormTemplateRunData[]
|
||||
}
|
||||
|
||||
async function readFormTemplate(tree: TraceDirectoryTree): Promise<FormTemplateData> {
|
||||
const formTemplate = getOrThrow(tree.files, 'form.template')
|
||||
const archive = readInstrumentsKeyedArchive(await readAsArrayBuffer(formTemplate))
|
||||
|
||||
const version = archive['com.apple.xray.owner.template.version']
|
||||
const selectedRun = archive['com.apple.xray.owner.template'].get('_selectedRunNumber')
|
||||
const selectedRunNumber = archive['com.apple.xray.owner.template'].get('_selectedRunNumber')
|
||||
let instrument = archive['$1']
|
||||
if ('stubInfoByUUID' in archive) {
|
||||
instrument = Array.from(archive['stubInfoByUUID'].keys())[0]
|
||||
}
|
||||
let allRunData = archive['com.apple.xray.run.data']
|
||||
const runData = getOrThrow<number, Map<any, any>>(
|
||||
allRunData.runData,
|
||||
allRunData.runNumbers.pop()!,
|
||||
)
|
||||
const allRunData = archive['com.apple.xray.run.data']
|
||||
|
||||
const symbolsByPid = getOrThrow<string, Map<number, {symbols: SymbolInfo[]}>>(
|
||||
runData,
|
||||
'symbolsByPid',
|
||||
)
|
||||
const runs: FormTemplateRunData[] = []
|
||||
for (let runNumber of allRunData.runNumbers) {
|
||||
const runData = getOrThrow<number, Map<any, any>>(allRunData.runData, runNumber)
|
||||
|
||||
const addressToFrameMap = new Map<number, FrameInfo>()
|
||||
const symbolsByPid = getOrThrow<string, Map<number, {symbols: SymbolInfo[]}>>(
|
||||
runData,
|
||||
'symbolsByPid',
|
||||
)
|
||||
|
||||
// TODO(jlfwong): Deal with profiles with conflicts addresses?
|
||||
for (let symbols of symbolsByPid.values()) {
|
||||
for (let symbol of symbols.symbols) {
|
||||
if (!symbol) continue
|
||||
const {sourcePath, symbolName, addressToLine} = symbol
|
||||
for (let address of addressToLine.keys()) {
|
||||
getOrInsert(addressToFrameMap, address, () => {
|
||||
const name = symbolName || `0x${zeroPad(address.toString(16), 16)}`
|
||||
const frame: FrameInfo = {
|
||||
key: `${sourcePath}:${name}`,
|
||||
name: name,
|
||||
}
|
||||
if (sourcePath) {
|
||||
frame.file = sourcePath
|
||||
}
|
||||
return frame
|
||||
})
|
||||
const addressToFrameMap = new Map<number, FrameInfo>()
|
||||
|
||||
// TODO(jlfwong): Deal with profiles with conflicting addresses?
|
||||
for (let symbols of symbolsByPid.values()) {
|
||||
for (let symbol of symbols.symbols) {
|
||||
if (!symbol) continue
|
||||
const {sourcePath, symbolName, addressToLine} = symbol
|
||||
for (let address of addressToLine.keys()) {
|
||||
getOrInsert(addressToFrameMap, address, () => {
|
||||
const name = symbolName || `0x${zeroPad(address.toString(16), 16)}`
|
||||
const frame: FrameInfo = {
|
||||
key: `${sourcePath}:${name}`,
|
||||
name: name,
|
||||
}
|
||||
if (sourcePath) {
|
||||
frame.file = sourcePath
|
||||
}
|
||||
return frame
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
runs.push({
|
||||
number: runNumber,
|
||||
addressToFrameMap,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
instrument,
|
||||
selectedRun,
|
||||
addressToFrameMap,
|
||||
selectedRunNumber,
|
||||
runs,
|
||||
}
|
||||
}
|
||||
|
||||
// Import from a .trace file saved from Mac Instruments.app
|
||||
export async function importFromInstrumentsTrace(
|
||||
entry: FileSystemDirectoryEntry,
|
||||
): Promise<Profile> {
|
||||
): Promise<ProfileGroup> {
|
||||
const tree = await extractDirectoryTree(entry)
|
||||
|
||||
const {version, selectedRun, instrument, addressToFrameMap} = await readFormTemplate(tree)
|
||||
const {version, runs, instrument, selectedRunNumber} = await readFormTemplate(tree)
|
||||
if (instrument !== 'com.apple.xray.instrument-type.coresampler2') {
|
||||
throw new Error(
|
||||
`The only supported instrument from .trace import is "com.apple.xray.instrument-type.coresampler2". Got ${instrument}`,
|
||||
)
|
||||
}
|
||||
console.log('version: ', version)
|
||||
console.log(`Importing time profile from run ${selectedRun}`)
|
||||
console.log(`Importing time profile`)
|
||||
|
||||
const core = getCoreDirForRun(tree, selectedRun)
|
||||
const profiles: Profile[] = []
|
||||
let indexToView = 0
|
||||
|
||||
for (let run of runs) {
|
||||
const {addressToFrameMap, number} = run
|
||||
const group = await importRunFromInstrumentsTrace({
|
||||
fileName: entry.name,
|
||||
tree,
|
||||
addressToFrameMap,
|
||||
runNumber: number,
|
||||
})
|
||||
|
||||
if (run.number === selectedRunNumber) {
|
||||
indexToView = profiles.length + group.indexToView
|
||||
}
|
||||
|
||||
profiles.push(...group.profiles)
|
||||
}
|
||||
|
||||
return {name: entry.name, indexToView, profiles}
|
||||
}
|
||||
|
||||
export async function importRunFromInstrumentsTrace(args: {
|
||||
fileName: string
|
||||
tree: TraceDirectoryTree
|
||||
addressToFrameMap: Map<number, FrameInfo>
|
||||
runNumber: number
|
||||
}): Promise<ProfileGroup> {
|
||||
const {fileName, tree, addressToFrameMap, runNumber} = args
|
||||
const core = getCoreDirForRun(tree, runNumber)
|
||||
let samples = await getRawSampleList(core)
|
||||
const arrays = await getIntegerArrays(samples, core)
|
||||
const backtraceIDtoStack = new Map<number, FrameInfo[]>()
|
||||
|
||||
const profile = new StackListProfileBuilder(lastOf(samples)!.timestamp)
|
||||
profile.setName(entry.name)
|
||||
|
||||
// For now, we can only display the flamechart for a single thread of execution,
|
||||
// So let's choose whichever thread had the most sample hits.
|
||||
//
|
||||
// TODO(jlfwong): Support displaying flamecharts for multiple threads.
|
||||
// We'll try to guess which thread is the main thread by assuming
|
||||
// it's the one with the most samples.
|
||||
const sampleCountByThreadID = new Map<number, number>()
|
||||
for (let sample of samples) {
|
||||
sampleCountByThreadID.set(
|
||||
@@ -476,9 +516,38 @@ export async function importFromInstrumentsTrace(
|
||||
)
|
||||
}
|
||||
const counts = Array.from(sampleCountByThreadID.entries())
|
||||
sortBy(counts, c => c[1])
|
||||
const mainThreadID = lastOf(counts)![0]
|
||||
samples = samples.filter(s => s.threadID === mainThreadID)
|
||||
sortBy(counts, c => -c[1])
|
||||
const threadIDs = counts.map(c => c[0])
|
||||
|
||||
return {
|
||||
name: fileName,
|
||||
indexToView: 0,
|
||||
profiles: threadIDs.map(threadID =>
|
||||
importThreadFromInstrumentsTrace({
|
||||
threadID,
|
||||
fileName,
|
||||
arrays,
|
||||
addressToFrameMap,
|
||||
samples,
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export function importThreadFromInstrumentsTrace(args: {
|
||||
fileName: string
|
||||
addressToFrameMap: Map<number, FrameInfo>
|
||||
threadID: number
|
||||
arrays: number[][]
|
||||
samples: Sample[]
|
||||
}): Profile {
|
||||
let {fileName, addressToFrameMap, arrays, threadID, samples} = args
|
||||
|
||||
const backtraceIDtoStack = new Map<number, FrameInfo[]>()
|
||||
samples = samples.filter(s => s.threadID === threadID)
|
||||
|
||||
const profile = new StackListProfileBuilder(lastOf(samples)!.timestamp)
|
||||
profile.setName(`${fileName} - thread ${threadID}`)
|
||||
|
||||
function appendRecursive(k: number, stack: FrameInfo[]) {
|
||||
const frame = addressToFrameMap.get(k)
|
||||
|
||||
@@ -40,6 +40,7 @@ Object {
|
||||
"totalWeight": 4,
|
||||
},
|
||||
],
|
||||
"name": "simple.txt",
|
||||
"stacks": Array [
|
||||
"a;b;c 2",
|
||||
"a;b;d 4",
|
||||
@@ -49,6 +50,10 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importSpeedscopeProfiles 0.0.1 evented profile: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importSpeedscopeProfiles 0.0.1 evented profile: profileGroup.name 1`] = `"simple.txt"`;
|
||||
|
||||
exports[`importSpeedscopeProfiles 0.1.2 sampled profile 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
@@ -89,6 +94,7 @@ Object {
|
||||
"totalWeight": 4,
|
||||
},
|
||||
],
|
||||
"name": "simple.speedscope.json",
|
||||
"stacks": Array [
|
||||
"a;b;c 2.00s",
|
||||
"a;b;d 4.00s",
|
||||
@@ -97,3 +103,111 @@ Object {
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importSpeedscopeProfiles 0.1.2 sampled profile: indexToView 1`] = `0`;
|
||||
|
||||
exports[`importSpeedscopeProfiles 0.1.2 sampled profile: profileGroup.name 1`] = `"simple.speedscope.json"`;
|
||||
|
||||
exports[`importSpeedscopeProfiles 0.6.0 multiple profiles 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": 0,
|
||||
"line": undefined,
|
||||
"name": "a",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 14,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": 1,
|
||||
"line": undefined,
|
||||
"name": "b",
|
||||
"selfWeight": 5,
|
||||
"totalWeight": 14,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": 2,
|
||||
"line": undefined,
|
||||
"name": "c",
|
||||
"selfWeight": 5,
|
||||
"totalWeight": 5,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": 3,
|
||||
"line": undefined,
|
||||
"name": "d",
|
||||
"selfWeight": 4,
|
||||
"totalWeight": 4,
|
||||
},
|
||||
],
|
||||
"name": "one",
|
||||
"stacks": Array [
|
||||
"a;b;c 2.00s",
|
||||
"a;b;d 4.00s",
|
||||
"a;b;c 3.00s",
|
||||
"a;b 5.00s",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importSpeedscopeProfiles 0.6.0 multiple profiles 2`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": 0,
|
||||
"line": undefined,
|
||||
"name": "a",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 14,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": 1,
|
||||
"line": undefined,
|
||||
"name": "b",
|
||||
"selfWeight": 5,
|
||||
"totalWeight": 14,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": 2,
|
||||
"line": undefined,
|
||||
"name": "c",
|
||||
"selfWeight": 5,
|
||||
"totalWeight": 5,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": undefined,
|
||||
"key": 3,
|
||||
"line": undefined,
|
||||
"name": "d",
|
||||
"selfWeight": 4,
|
||||
"totalWeight": 4,
|
||||
},
|
||||
],
|
||||
"name": "two",
|
||||
"stacks": Array [
|
||||
"a;b;c 2.00s",
|
||||
"a;b;d 4.00s",
|
||||
"a;b;c 3.00s",
|
||||
"a;b 5.00s",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importSpeedscopeProfiles 0.6.0 multiple profiles: indexToView 1`] = `1`;
|
||||
|
||||
exports[`importSpeedscopeProfiles 0.6.0 multiple profiles: profileGroup.name 1`] = `"Two Samples"`;
|
||||
|
||||
@@ -4,12 +4,36 @@ export namespace FileFormat {
|
||||
export type Profile = EventedProfile | SampledProfile
|
||||
|
||||
export interface File {
|
||||
version: string
|
||||
$schema: 'https://www.speedscope.app/file-format-schema.json'
|
||||
|
||||
// Data shared between profiles
|
||||
shared: {
|
||||
frames: Frame[]
|
||||
}
|
||||
|
||||
// List of profile definitions
|
||||
profiles: Profile[]
|
||||
|
||||
// The name of the contained profile group. If omitted, will use the name of
|
||||
// the file itself.
|
||||
// Added in 0.6.0
|
||||
name?: string
|
||||
|
||||
// The index into the `profiles` array that should be displayed upon file
|
||||
// load. If omitted, will default to displaying the first profile in the
|
||||
// file.
|
||||
//
|
||||
// Added in 0.6.0
|
||||
activeProfileIndex?: number
|
||||
|
||||
// The name of the the program which exported this profile. This isn't
|
||||
// consumed but can be helpful for debugging generated data by seeing what
|
||||
// was generating it! Recommended format is "name@version". e.g. when the
|
||||
// file was exported by speedscope v0.6.0 itself, it will be
|
||||
// "speedscope@0.6.0"
|
||||
//
|
||||
// Added in 0.6.0
|
||||
exporter?: string
|
||||
}
|
||||
|
||||
export interface Frame {
|
||||
|
||||
@@ -8,4 +8,8 @@ describe('importSpeedscopeProfiles', async () => {
|
||||
test('0.1.2 sampled profile', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/speedscope/0.1.2/simple-sampled.speedscope.json')
|
||||
})
|
||||
|
||||
test('0.6.0 multiple profiles', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/speedscope/0.6.0/two-sampled.speedscope.json')
|
||||
})
|
||||
})
|
||||
|
||||
+38
-25
@@ -5,29 +5,14 @@ import {
|
||||
CallTreeProfileBuilder,
|
||||
FrameInfo,
|
||||
StackListProfileBuilder,
|
||||
ProfileGroup,
|
||||
} from './profile'
|
||||
import {TimeFormatter, ByteFormatter, RawValueFormatter} from './value-formatters'
|
||||
import {FileFormat} from './file-format-spec'
|
||||
|
||||
export function exportProfile(profile: Profile): FileFormat.File {
|
||||
export function exportProfileGroup(profileGroup: ProfileGroup): FileFormat.File {
|
||||
const frames: FileFormat.Frame[] = []
|
||||
|
||||
const eventedProfile: FileFormat.EventedProfile = {
|
||||
type: FileFormat.ProfileType.EVENTED,
|
||||
name: profile.getName(),
|
||||
unit: profile.getWeightUnit(),
|
||||
startValue: 0,
|
||||
endValue: profile.getTotalWeight(),
|
||||
events: [],
|
||||
}
|
||||
|
||||
const file: FileFormat.File = {
|
||||
version: require('../../package.json').version,
|
||||
$schema: 'https://www.speedscope.app/file-format-schema.json',
|
||||
shared: {frames},
|
||||
profiles: [eventedProfile],
|
||||
}
|
||||
|
||||
const indexForFrame = new Map<Frame, number>()
|
||||
function getIndexForFrame(frame: Frame): number {
|
||||
let index = indexForFrame.get(frame)
|
||||
@@ -38,7 +23,6 @@ export function exportProfile(profile: Profile): FileFormat.File {
|
||||
if (frame.file != null) serializedFrame.file = frame.file
|
||||
if (frame.line != null) serializedFrame.line = frame.line
|
||||
if (frame.col != null) serializedFrame.col = frame.col
|
||||
|
||||
index = frames.length
|
||||
indexForFrame.set(frame, index)
|
||||
frames.push(serializedFrame)
|
||||
@@ -46,6 +30,31 @@ export function exportProfile(profile: Profile): FileFormat.File {
|
||||
return index
|
||||
}
|
||||
|
||||
const file: FileFormat.File = {
|
||||
exporter: `speedscope@${require('../../package.json').version}`,
|
||||
name: profileGroup.name,
|
||||
activeProfileIndex: profileGroup.indexToView,
|
||||
$schema: 'https://www.speedscope.app/file-format-schema.json',
|
||||
shared: {frames},
|
||||
profiles: [],
|
||||
}
|
||||
|
||||
for (let profile of profileGroup.profiles) {
|
||||
file.profiles.push(exportProfile(profile, getIndexForFrame))
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
function exportProfile(profile: Profile, getIndexForFrame: (frame: Frame) => number) {
|
||||
const eventedProfile: FileFormat.EventedProfile = {
|
||||
type: FileFormat.ProfileType.EVENTED,
|
||||
name: profile.getName(),
|
||||
unit: profile.getWeightUnit(),
|
||||
startValue: 0,
|
||||
endValue: profile.getTotalWeight(),
|
||||
events: [],
|
||||
}
|
||||
const openFrame = (node: CallTreeNode, value: number) => {
|
||||
eventedProfile.events.push({
|
||||
type: FileFormat.EventType.OPEN_FRAME,
|
||||
@@ -61,8 +70,7 @@ export function exportProfile(profile: Profile): FileFormat.File {
|
||||
})
|
||||
}
|
||||
profile.forEachCall(openFrame, closeFrame)
|
||||
|
||||
return file
|
||||
return eventedProfile
|
||||
}
|
||||
|
||||
function importSpeedscopeProfile(
|
||||
@@ -144,14 +152,19 @@ function importSpeedscopeProfile(
|
||||
}
|
||||
}
|
||||
|
||||
export function importSpeedscopeProfiles(serialized: FileFormat.File): Profile[] {
|
||||
return serialized.profiles.map(p => importSpeedscopeProfile(p, serialized.shared.frames))
|
||||
export function importSpeedscopeProfiles(serialized: FileFormat.File): ProfileGroup {
|
||||
return {
|
||||
name: serialized.name || serialized.profiles[0].name || 'profile',
|
||||
indexToView: serialized.activeProfileIndex || 0,
|
||||
profiles: serialized.profiles.map(p => importSpeedscopeProfile(p, serialized.shared.frames)),
|
||||
}
|
||||
}
|
||||
|
||||
export function saveToFile(profile: Profile): void {
|
||||
const blob = new Blob([JSON.stringify(exportProfile(profile))], {type: 'text/json'})
|
||||
export function saveToFile(profileGroup: ProfileGroup): void {
|
||||
const file = exportProfileGroup(profileGroup)
|
||||
const blob = new Blob([JSON.stringify(file)], {type: 'text/json'})
|
||||
|
||||
const nameWithoutExt = profile.getName().split('.')[0]!
|
||||
const nameWithoutExt = file.name ? file.name.split('.')[0]! : 'profile'
|
||||
const filename = `${nameWithoutExt.replace(/\W+/g, '_')}.speedscope.json`
|
||||
|
||||
console.log('Saving', filename)
|
||||
|
||||
@@ -103,6 +103,12 @@ export class CallTreeNode extends HasWeights {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProfileGroup {
|
||||
name: string
|
||||
indexToView: number
|
||||
profiles: Profile[]
|
||||
}
|
||||
|
||||
export class Profile {
|
||||
protected name: string = ''
|
||||
|
||||
|
||||
+37
-15
@@ -1,16 +1,18 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import {Profile, CallTreeNode, Frame} from './profile'
|
||||
import {importProfile} from '../import'
|
||||
import {exportProfile, importSpeedscopeProfiles} from './file-format'
|
||||
import {importProfileGroup} from '../import'
|
||||
import {exportProfileGroup, importSpeedscopeProfiles} from './file-format'
|
||||
|
||||
interface DumpedProfile {
|
||||
name: string
|
||||
stacks: string[]
|
||||
frames: Frame[]
|
||||
}
|
||||
|
||||
export function dumpProfile(profile: Profile): any {
|
||||
const dump: DumpedProfile = {
|
||||
name: profile.getName(),
|
||||
stacks: [],
|
||||
frames: [],
|
||||
}
|
||||
@@ -47,30 +49,50 @@ export function dumpProfile(profile: Profile): any {
|
||||
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()
|
||||
const profileGroup = await importProfileGroup(path.basename(filepath), input)
|
||||
if (profileGroup) {
|
||||
expect(profileGroup.name).toMatchSnapshot('profileGroup.name')
|
||||
expect(profileGroup.indexToView).toMatchSnapshot('indexToView')
|
||||
|
||||
for (let profile of profileGroup.profiles) {
|
||||
expect(dumpProfile(profile)).toMatchSnapshot()
|
||||
}
|
||||
} else {
|
||||
fail('Failed to extract profile')
|
||||
return
|
||||
}
|
||||
|
||||
const profileWithoutFilename = await importProfile('unknown', input)
|
||||
if (profileWithoutFilename) {
|
||||
profileWithoutFilename.setName(profile.getName())
|
||||
expect(exportProfile(profileWithoutFilename)).toEqual(exportProfile(profile))
|
||||
const profilesWithoutFilename = await importProfileGroup('unknown', input)
|
||||
if (profilesWithoutFilename) {
|
||||
expect(profilesWithoutFilename.profiles.length).toEqual(profileGroup.profiles.length)
|
||||
profilesWithoutFilename.name = profileGroup.name
|
||||
for (let i = 0; i < profileGroup.profiles.length; i++) {
|
||||
const a = profileGroup.profiles[i]
|
||||
const b = profilesWithoutFilename.profiles[i]
|
||||
b.setName(a.getName())
|
||||
}
|
||||
expect(exportProfileGroup(profileGroup)).toEqual(exportProfileGroup(profilesWithoutFilename))
|
||||
} else {
|
||||
fail('Failed to extract profile when filename was "unknown"')
|
||||
return
|
||||
}
|
||||
|
||||
const exported = exportProfile(profile)
|
||||
const reimported = importSpeedscopeProfiles(exported)[0]
|
||||
const exported = exportProfileGroup(profileGroup)
|
||||
const reimportedGroup = importSpeedscopeProfiles(exported)
|
||||
|
||||
expect(reimported.getName()).toEqual(profile.getName())
|
||||
expect(reimported.getTotalWeight()).toEqual(profile.getTotalWeight())
|
||||
expect(dumpProfile(reimported).stacks.join('\n')).toEqual(dumpProfile(profile).stacks.join('\n'))
|
||||
expect(reimportedGroup.name).toEqual(profileGroup.name)
|
||||
expect(reimportedGroup.profiles.length).toEqual(profileGroup.profiles.length)
|
||||
|
||||
const reexported = exportProfile(reimported)
|
||||
for (let i = 0; i < profileGroup.profiles.length; i++) {
|
||||
const profile = profileGroup.profiles[i]
|
||||
const reimported = reimportedGroup.profiles[i]
|
||||
|
||||
expect(reimported.getTotalWeight()).toEqual(profile.getTotalWeight())
|
||||
expect(dumpProfile(reimported).stacks.join('\n')).toEqual(
|
||||
dumpProfile(profile).stacks.join('\n'),
|
||||
)
|
||||
}
|
||||
|
||||
const reexported = exportProfileGroup(reimportedGroup)
|
||||
expect(exported).toEqual(reexported)
|
||||
}
|
||||
|
||||
+28
-6
@@ -37,6 +37,7 @@ export function actionCreator(type: string) {
|
||||
}
|
||||
|
||||
export type Reducer<T> = (state: T | undefined, action: Action<any>) => T
|
||||
export type ReducerWithActionType<T, A> = (state: T | undefined, action: Action<A>) => T
|
||||
|
||||
export function setter<T>(
|
||||
setterAction: ActionCreator<T>,
|
||||
@@ -51,18 +52,30 @@ export function setter<T>(
|
||||
}
|
||||
|
||||
export type Dispatch = redux.Dispatch<Action<any>>
|
||||
export type WithDispatch<T> = T & {dispatch: Dispatch}
|
||||
export type WithoutDispatch<T> = Pick<T, Exclude<keyof T, 'dispatch'>>
|
||||
|
||||
// We make this into a single function invocation instead of the connect(map, map)(Component)
|
||||
// syntax to make better use of type inference.
|
||||
export function createContainer<OwnProps, State, PropsFromState, ComponentType>(
|
||||
//
|
||||
// NOTE: The way this works right now is going to regenerate new setters on every
|
||||
// store update. To make this not the case, we'd have to have mapStateToProps actually
|
||||
// specified. This may be a performance issue in the future, but we're going to eat
|
||||
// this cost for now in exchange for simpler type inference.
|
||||
export function createContainer<OwnProps, State, ComponentProps, ComponentType>(
|
||||
component: {
|
||||
new (props: OwnProps & PropsFromState & {dispatch: Dispatch}): ComponentType
|
||||
new (props: ComponentProps): ComponentType
|
||||
},
|
||||
mapStateToProps: (state: State, ownProps: OwnProps) => PropsFromState,
|
||||
map: (state: State, dispatch: Dispatch, ownProps: OwnProps) => ComponentProps,
|
||||
): ComponentConstructor<OwnProps, {}> {
|
||||
return connect(mapStateToProps, (dispatch: Dispatch) => ({dispatch}))(component)
|
||||
const mapStateToProps = (state: State) => state
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({dispatch})
|
||||
const mergeProps = (
|
||||
stateProps: State,
|
||||
dispatchProps: {dispatch: Dispatch},
|
||||
ownProps: OwnProps,
|
||||
) => {
|
||||
return map(stateProps, dispatchProps.dispatch, ownProps)
|
||||
}
|
||||
return connect(mapStateToProps, mapDispatchToProps, mergeProps)(component)
|
||||
}
|
||||
|
||||
export type VoidState = {
|
||||
@@ -70,3 +83,12 @@ export type VoidState = {
|
||||
}
|
||||
|
||||
export abstract class StatelessComponent<P> extends Component<P, VoidState> {}
|
||||
|
||||
export function bindActionCreator<T>(
|
||||
dispatch: Dispatch,
|
||||
actionCreator: (payload: T) => Action<T>,
|
||||
): (t: T) => void {
|
||||
return (t: T) => {
|
||||
dispatch(actionCreator(t))
|
||||
}
|
||||
}
|
||||
|
||||
+2
-5
@@ -1,8 +1,7 @@
|
||||
import {h, render} from 'preact'
|
||||
import {createApplicationStore, ApplicationState} from './store'
|
||||
import {createApplicationStore} from './store'
|
||||
import {Provider} from 'preact-redux'
|
||||
import {createContainer} from './lib/typed-redux'
|
||||
import {Application} from './views/application'
|
||||
import {ApplicationContainer} from './views/application-container'
|
||||
|
||||
console.log(`speedscope v${require('../package.json').version}`)
|
||||
|
||||
@@ -19,8 +18,6 @@ const lastStore: any = (window as any)['store']
|
||||
const store = createApplicationStore(lastStore ? lastStore.getState() : {})
|
||||
;(window as any)['store'] = store
|
||||
|
||||
const ApplicationContainer = createContainer(Application, (state: ApplicationState) => state)
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ApplicationContainer />
|
||||
|
||||
+16
-15
@@ -1,21 +1,18 @@
|
||||
import {actionCreator} from '../lib/typed-redux'
|
||||
import {Profile, CallTreeNode, Frame} from '../lib/profile'
|
||||
import {CallTreeNode, Frame, ProfileGroup} from '../lib/profile'
|
||||
import {SortMethod} from '../views/profile-table-view'
|
||||
import {ViewMode} from '.'
|
||||
import {FlamechartID} from './flamechart-view-state'
|
||||
import {Rect, Vec2} from '../lib/math'
|
||||
import {HashParams} from '../lib/hash-params'
|
||||
import {actionCreatorWithIndex} from './profiles-state'
|
||||
|
||||
export namespace actions {
|
||||
// Set the top-level profile from which other data will be derived
|
||||
export const setProfile = actionCreator<Profile>('setProfile')
|
||||
// Set the top-level profile group from which other data will be derived
|
||||
export const setProfileGroup = actionCreator<ProfileGroup>('setProfileGroup')
|
||||
|
||||
// Set the profile currently being viewed
|
||||
export const setActiveProfile = actionCreator<Profile>('setActiveProfile')
|
||||
|
||||
export const setFrameToColorBucket = actionCreator<Map<string | number, number>>(
|
||||
'setFrameToColorBucket',
|
||||
)
|
||||
// Set the index into the profile group to view
|
||||
export const setProfileIndexToView = actionCreator<number>('setProfileIndexToView')
|
||||
|
||||
export const setGLCanvas = actionCreator<HTMLCanvasElement | null>('setGLCanvas')
|
||||
|
||||
@@ -41,28 +38,32 @@ export namespace actions {
|
||||
|
||||
export namespace sandwichView {
|
||||
// Set the table sorting method used for the sandwich view.
|
||||
export const setTableSortMethod = actionCreator<SortMethod>('sandwichView.setTableSortMethod')
|
||||
export const setTableSortMethod = actionCreatorWithIndex<SortMethod>(
|
||||
'sandwichView.setTableSortMethod',
|
||||
)
|
||||
|
||||
export const setSelectedFrame = actionCreator<Frame | null>('sandwichView.setSelectedFarmr')
|
||||
export const setSelectedFrame = actionCreatorWithIndex<Frame | null>(
|
||||
'sandwichView.setSelectedFarmr',
|
||||
)
|
||||
}
|
||||
|
||||
export namespace flamechart {
|
||||
export const setHoveredNode = actionCreator<{
|
||||
export const setHoveredNode = actionCreatorWithIndex<{
|
||||
id: FlamechartID
|
||||
hover: {node: CallTreeNode; event: MouseEvent} | null
|
||||
}>('flamechart.setHoveredNode')
|
||||
|
||||
export const setSelectedNode = actionCreator<{
|
||||
export const setSelectedNode = actionCreatorWithIndex<{
|
||||
id: FlamechartID
|
||||
selectedNode: CallTreeNode | null
|
||||
}>('flamechart.setSelectedNode')
|
||||
|
||||
export const setConfigSpaceViewportRect = actionCreator<{
|
||||
export const setConfigSpaceViewportRect = actionCreatorWithIndex<{
|
||||
id: FlamechartID
|
||||
configSpaceViewportRect: Rect
|
||||
}>('flamechart.setConfigSpaceViewportRect')
|
||||
|
||||
export const setLogicalSpaceViewportSize = actionCreator<{
|
||||
export const setLogicalSpaceViewportSize = actionCreatorWithIndex<{
|
||||
id: FlamechartID
|
||||
logicalSpaceViewportSize: Vec2
|
||||
}>('flamechart.setLogicalSpaceViewportSpace')
|
||||
|
||||
@@ -20,37 +20,38 @@ export interface FlamechartViewState {
|
||||
configSpaceViewportRect: Rect
|
||||
}
|
||||
|
||||
export function createFlamechartViewStateReducer(id: FlamechartID): Reducer<FlamechartViewState> {
|
||||
export function createFlamechartViewStateReducer(
|
||||
id: FlamechartID,
|
||||
profileIndex: number,
|
||||
): Reducer<FlamechartViewState> {
|
||||
let initialState: FlamechartViewState = {
|
||||
hover: null,
|
||||
selectedNode: null,
|
||||
configSpaceViewportRect: Rect.empty,
|
||||
logicalSpaceViewportSize: Vec2.zero,
|
||||
}
|
||||
function applies(action: {payload: {profileIndex: number; args: {id: FlamechartID}}}) {
|
||||
const {payload} = action
|
||||
return payload.args.id === id && payload.profileIndex === profileIndex
|
||||
}
|
||||
|
||||
return (state = initialState, action) => {
|
||||
if (actions.flamechart.setHoveredNode.matches(action) && action.payload.id === id) {
|
||||
const {hover} = action.payload
|
||||
if (actions.flamechart.setHoveredNode.matches(action) && applies(action)) {
|
||||
const {hover} = action.payload.args
|
||||
return {...state, hover}
|
||||
}
|
||||
if (actions.flamechart.setSelectedNode.matches(action) && action.payload.id === id) {
|
||||
const {selectedNode} = action.payload
|
||||
if (actions.flamechart.setSelectedNode.matches(action) && applies(action)) {
|
||||
const {selectedNode} = action.payload.args
|
||||
return {...state, selectedNode}
|
||||
}
|
||||
if (actions.flamechart.setConfigSpaceViewportRect.matches(action) && action.payload.id === id) {
|
||||
const {configSpaceViewportRect} = action.payload
|
||||
if (actions.flamechart.setConfigSpaceViewportRect.matches(action) && applies(action)) {
|
||||
const {configSpaceViewportRect} = action.payload.args
|
||||
return {...state, configSpaceViewportRect}
|
||||
}
|
||||
if (
|
||||
actions.flamechart.setLogicalSpaceViewportSize.matches(action) &&
|
||||
action.payload.id === id
|
||||
) {
|
||||
const {logicalSpaceViewportSize} = action.payload
|
||||
if (actions.flamechart.setLogicalSpaceViewportSize.matches(action) && applies(action)) {
|
||||
const {logicalSpaceViewportSize} = action.payload.args
|
||||
return {...state, logicalSpaceViewportSize}
|
||||
}
|
||||
if (actions.setProfile.matches(action)) {
|
||||
// If the profile changes, we should invalidate all of our state, since none of it still applies
|
||||
return initialState
|
||||
}
|
||||
if (actions.setViewMode.matches(action)) {
|
||||
// If we switch views, the hover information is no longer relevant
|
||||
return {...state, hover: null}
|
||||
|
||||
@@ -45,3 +45,25 @@ export const getProfileToView = memoizeByShallowEquality(
|
||||
return flattenRecursion ? profile.getProfileWithRecursionFlattened() : profile
|
||||
},
|
||||
)
|
||||
export const getFrameToColorBucket = memoizeByReference((profile: Profile): Map<
|
||||
string | number,
|
||||
number
|
||||
> => {
|
||||
document.title = `${profile.getName()} - speedscope`
|
||||
|
||||
const frames: Frame[] = []
|
||||
profile.forEachFrame(f => frames.push(f))
|
||||
function key(f: Frame) {
|
||||
return (f.file || '') + f.name
|
||||
}
|
||||
function compare(a: Frame, b: Frame) {
|
||||
return key(a) > key(b) ? 1 : -1
|
||||
}
|
||||
frames.sort(compare)
|
||||
const frameToColorBucket = new Map<string | number, number>()
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
frameToColorBucket.set(frames[i].key, Math.floor(255 * i / frames.length))
|
||||
}
|
||||
|
||||
return frameToColorBucket
|
||||
})
|
||||
|
||||
+3
-23
@@ -7,14 +7,8 @@ import {actions} from './actions'
|
||||
|
||||
import * as redux from 'redux'
|
||||
import {setter, Reducer} from '../lib/typed-redux'
|
||||
import {Profile} from '../lib/profile'
|
||||
import {
|
||||
createFlamechartViewStateReducer,
|
||||
FlamechartID,
|
||||
FlamechartViewState,
|
||||
} from './flamechart-view-state'
|
||||
import {SandwichViewState, sandwichView} from './sandwich-view-state'
|
||||
import {HashParams, getHashParams} from '../lib/hash-params'
|
||||
import {ProfileGroupState, profileGroup} from './profiles-state'
|
||||
|
||||
export const enum ViewMode {
|
||||
CHRONO_FLAME_CHART,
|
||||
@@ -23,9 +17,6 @@ export const enum ViewMode {
|
||||
}
|
||||
|
||||
export interface ApplicationState {
|
||||
profile: Profile | null
|
||||
frameToColorBucket: Map<string | number, number>
|
||||
|
||||
hashParams: HashParams
|
||||
|
||||
glCanvas: HTMLCanvasElement | null
|
||||
@@ -37,9 +28,7 @@ export interface ApplicationState {
|
||||
loading: boolean
|
||||
error: boolean
|
||||
|
||||
chronoView: FlamechartViewState
|
||||
leftHeavyView: FlamechartViewState
|
||||
sandwichView: SandwichViewState
|
||||
profileGroup: ProfileGroupState
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol
|
||||
@@ -57,11 +46,7 @@ export function createApplicationStore(
|
||||
const loading = canUseXHR && hashParams.profileURL != null
|
||||
|
||||
const reducer: Reducer<ApplicationState> = redux.combineReducers({
|
||||
profile: setter<Profile | null>(actions.setProfile, null),
|
||||
frameToColorBucket: setter<Map<string | number, number>>(
|
||||
actions.setFrameToColorBucket,
|
||||
new Map(),
|
||||
),
|
||||
profileGroup,
|
||||
|
||||
hashParams: setter<HashParams>(actions.setHashParams, hashParams),
|
||||
|
||||
@@ -74,11 +59,6 @@ export function createApplicationStore(
|
||||
dragActive: setter<boolean>(actions.setDragActive, false),
|
||||
loading: setter<boolean>(actions.setLoading, loading),
|
||||
error: setter<boolean>(actions.setError, false),
|
||||
|
||||
chronoView: createFlamechartViewStateReducer(FlamechartID.CHRONO),
|
||||
leftHeavyView: createFlamechartViewStateReducer(FlamechartID.LEFT_HEAVY),
|
||||
|
||||
sandwichView,
|
||||
})
|
||||
|
||||
return redux.createStore(reducer, initialState)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import {Profile} from '../lib/profile'
|
||||
import {
|
||||
FlamechartViewState,
|
||||
createFlamechartViewStateReducer,
|
||||
FlamechartID,
|
||||
} from './flamechart-view-state'
|
||||
import {SandwichViewState, createSandwichView} from './sandwich-view-state'
|
||||
import {Reducer, Action, actionCreator, setter} from '../lib/typed-redux'
|
||||
import {actions} from './actions'
|
||||
import {clamp} from '../lib/math'
|
||||
|
||||
export type ProfileGroupState = {
|
||||
name: string
|
||||
indexToView: number
|
||||
profiles: ProfileState[]
|
||||
} | null
|
||||
|
||||
export interface ProfileWithIndex {
|
||||
profile: Profile
|
||||
index: number
|
||||
}
|
||||
|
||||
export interface ProfileState {
|
||||
profile: Profile
|
||||
chronoViewState: FlamechartViewState
|
||||
leftHeavyViewState: FlamechartViewState
|
||||
sandwichViewState: SandwichViewState
|
||||
}
|
||||
|
||||
export function actionCreatorWithIndex<T>(name: string) {
|
||||
return actionCreator<{profileIndex: number; args: T}>(name)
|
||||
}
|
||||
|
||||
export function actionProfileIndex(action: Action<any>): number | null {
|
||||
const {payload} = action
|
||||
if (payload != null && typeof payload === 'object' && 'profileIndex' in payload) {
|
||||
return parseInt(payload.profileIndex, 0)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const profileGroup: Reducer<ProfileGroupState> = (state = null, action) => {
|
||||
if (actions.setProfileGroup.matches(action)) {
|
||||
const {indexToView, profiles, name} = action.payload
|
||||
return {
|
||||
indexToView,
|
||||
name,
|
||||
profiles: profiles.map((p, i) => {
|
||||
return {
|
||||
profile: p,
|
||||
frameToColorBucket: new Map(),
|
||||
chronoViewState: createFlamechartViewStateReducer(FlamechartID.CHRONO, i)(
|
||||
undefined,
|
||||
action,
|
||||
),
|
||||
leftHeavyViewState: createFlamechartViewStateReducer(FlamechartID.LEFT_HEAVY, i)(
|
||||
undefined,
|
||||
action,
|
||||
),
|
||||
sandwichViewState: createSandwichView(i)(undefined, action),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
if (state != null) {
|
||||
const {indexToView, profiles} = state
|
||||
|
||||
const nextIndexToView = clamp(
|
||||
setter(actions.setProfileIndexToView, 0)(indexToView, action),
|
||||
0,
|
||||
profiles.length - 1,
|
||||
)
|
||||
let nextProfiles = profiles
|
||||
|
||||
const profileIndexFromAction = actionProfileIndex(action)
|
||||
if (profileIndexFromAction != null) {
|
||||
nextProfiles = profiles.map((profileState, profileIndex) => {
|
||||
return {
|
||||
profile: profileState.profile,
|
||||
chronoViewState: createFlamechartViewStateReducer(FlamechartID.CHRONO, profileIndex)(
|
||||
profileState.chronoViewState,
|
||||
action,
|
||||
),
|
||||
leftHeavyViewState: createFlamechartViewStateReducer(
|
||||
FlamechartID.LEFT_HEAVY,
|
||||
profileIndex,
|
||||
)(profileState.leftHeavyViewState, action),
|
||||
sandwichViewState: createSandwichView(profileIndex)(
|
||||
profileState.sandwichViewState,
|
||||
action,
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (indexToView !== nextIndexToView || profiles !== nextProfiles) {
|
||||
return {
|
||||
...state,
|
||||
indexToView: nextIndexToView,
|
||||
profiles: nextProfiles,
|
||||
}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
@@ -24,63 +24,64 @@ const defaultSortMethod = {
|
||||
direction: SortDirection.DESCENDING,
|
||||
}
|
||||
|
||||
const calleesReducer = createFlamechartViewStateReducer(FlamechartID.SANDWICH_CALLEES)
|
||||
const invertedCallersReducer = createFlamechartViewStateReducer(
|
||||
FlamechartID.SANDWICH_INVERTED_CALLERS,
|
||||
)
|
||||
|
||||
export const sandwichView: Reducer<SandwichViewState> = (
|
||||
state = {tableSortMethod: defaultSortMethod, callerCallee: null},
|
||||
action,
|
||||
) => {
|
||||
if (actions.setProfile.matches(action)) {
|
||||
// When a new profile is dropped in, none of the selection state is going to make
|
||||
// sense any more.
|
||||
return {...state, callerCallee: null}
|
||||
export function createSandwichView(profileIndex: number): Reducer<SandwichViewState> {
|
||||
const calleesReducer = createFlamechartViewStateReducer(
|
||||
FlamechartID.SANDWICH_CALLEES,
|
||||
profileIndex,
|
||||
)
|
||||
const invertedCallersReducer = createFlamechartViewStateReducer(
|
||||
FlamechartID.SANDWICH_INVERTED_CALLERS,
|
||||
profileIndex,
|
||||
)
|
||||
function applies(action: {payload: {profileIndex: number}}) {
|
||||
const {payload} = action
|
||||
return payload.profileIndex === profileIndex
|
||||
}
|
||||
|
||||
const {callerCallee} = state
|
||||
if (callerCallee) {
|
||||
const {calleeFlamegraph, invertedCallerFlamegraph} = callerCallee
|
||||
const nextCalleeFlamegraph = calleesReducer(calleeFlamegraph, action)
|
||||
const nextInvertedCallerFlamegraph = invertedCallersReducer(invertedCallerFlamegraph, action)
|
||||
return (state = {tableSortMethod: defaultSortMethod, callerCallee: null}, action) => {
|
||||
const {callerCallee} = state
|
||||
if (callerCallee) {
|
||||
const {calleeFlamegraph, invertedCallerFlamegraph} = callerCallee
|
||||
const nextCalleeFlamegraph = calleesReducer(calleeFlamegraph, action)
|
||||
const nextInvertedCallerFlamegraph = invertedCallersReducer(invertedCallerFlamegraph, action)
|
||||
|
||||
if (
|
||||
nextCalleeFlamegraph !== calleeFlamegraph ||
|
||||
nextInvertedCallerFlamegraph !== invertedCallerFlamegraph
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
callerCallee: {
|
||||
...callerCallee,
|
||||
calleeFlamegraph: nextCalleeFlamegraph,
|
||||
invertedCallerFlamegraph: nextInvertedCallerFlamegraph,
|
||||
},
|
||||
if (
|
||||
nextCalleeFlamegraph !== calleeFlamegraph ||
|
||||
nextInvertedCallerFlamegraph !== invertedCallerFlamegraph
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
callerCallee: {
|
||||
...callerCallee,
|
||||
calleeFlamegraph: nextCalleeFlamegraph,
|
||||
invertedCallerFlamegraph: nextInvertedCallerFlamegraph,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.sandwichView.setTableSortMethod.matches(action)) {
|
||||
return {...state, tableSortMethod: action.payload}
|
||||
}
|
||||
if (actions.sandwichView.setTableSortMethod.matches(action) && applies(action)) {
|
||||
return {...state, tableSortMethod: action.payload.args}
|
||||
}
|
||||
|
||||
if (actions.sandwichView.setSelectedFrame.matches(action)) {
|
||||
if (action.payload == null) {
|
||||
return {
|
||||
...state,
|
||||
callerCallee: null,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...state,
|
||||
callerCallee: {
|
||||
selectedFrame: action.payload,
|
||||
calleeFlamegraph: calleesReducer(undefined, action),
|
||||
invertedCallerFlamegraph: invertedCallersReducer(undefined, action),
|
||||
},
|
||||
if (actions.sandwichView.setSelectedFrame.matches(action) && applies(action)) {
|
||||
if (action.payload.args == null) {
|
||||
return {
|
||||
...state,
|
||||
callerCallee: null,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...state,
|
||||
callerCallee: {
|
||||
selectedFrame: action.payload.args,
|
||||
calleeFlamegraph: calleesReducer(undefined, action),
|
||||
invertedCallerFlamegraph: invertedCallersReducer(undefined, action),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import {createContainer, Dispatch, bindActionCreator, ActionCreator} from '../lib/typed-redux'
|
||||
import {Application, ActiveProfileState} from './application'
|
||||
import {ApplicationState} from '../store'
|
||||
import {getProfileToView} from '../store/getters'
|
||||
import {actions} from '../store/actions'
|
||||
|
||||
export const ApplicationContainer = createContainer(
|
||||
Application,
|
||||
(state: ApplicationState, dispatch: Dispatch) => {
|
||||
const {flattenRecursion, profileGroup} = state
|
||||
|
||||
let activeProfileState: ActiveProfileState | null = null
|
||||
if (profileGroup) {
|
||||
if (profileGroup.profiles.length > profileGroup.indexToView) {
|
||||
const index = profileGroup.indexToView
|
||||
const profileState = profileGroup.profiles[index]
|
||||
activeProfileState = {
|
||||
...profileGroup.profiles[profileGroup.indexToView],
|
||||
profile: getProfileToView({profile: profileState.profile, flattenRecursion}),
|
||||
index: profileGroup.indexToView,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wrapActionCreator<T>(actionCreator: ActionCreator<T>): (t: T) => void {
|
||||
return bindActionCreator(dispatch, actionCreator)
|
||||
}
|
||||
|
||||
const setters = {
|
||||
setGLCanvas: wrapActionCreator(actions.setGLCanvas),
|
||||
setLoading: wrapActionCreator(actions.setLoading),
|
||||
setError: wrapActionCreator(actions.setError),
|
||||
setProfileGroup: wrapActionCreator(actions.setProfileGroup),
|
||||
setDragActive: wrapActionCreator(actions.setDragActive),
|
||||
setViewMode: wrapActionCreator(actions.setViewMode),
|
||||
setFlattenRecursion: wrapActionCreator(actions.setFlattenRecursion),
|
||||
setProfileIndexToView: wrapActionCreator(actions.setProfileIndexToView),
|
||||
}
|
||||
|
||||
return {
|
||||
activeProfileState,
|
||||
dispatch,
|
||||
...setters,
|
||||
...state,
|
||||
}
|
||||
},
|
||||
)
|
||||
+228
-135
@@ -2,22 +2,22 @@ import {h, Component} from 'preact'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {FileSystemDirectoryEntry} from '../import/file-system-entry'
|
||||
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {Profile, ProfileGroup} from '../lib/profile'
|
||||
import {FontFamily, FontSize, Colors, Sizes, Duration} from './style'
|
||||
import {importEmscriptenSymbolMap} from '../lib/emscripten'
|
||||
import {SandwichViewContainer} from './sandwich-view'
|
||||
import {saveToFile} from '../lib/file-format'
|
||||
import {ApplicationState, ViewMode, canUseXHR} from '../store'
|
||||
import {actions} from '../store/actions'
|
||||
import {Dispatch, StatelessComponent, WithDispatch} from '../lib/typed-redux'
|
||||
import {StatelessComponent} from '../lib/typed-redux'
|
||||
import {LeftHeavyFlamechartView, ChronoFlamechartView} from './flamechart-view-container'
|
||||
import {getProfileToView} from '../store/getters'
|
||||
import {SandwichViewState} from '../store/sandwich-view-state'
|
||||
import {FlamechartViewState} from '../store/flamechart-view-state'
|
||||
|
||||
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 importProfiles(fileName: string, contents: string): Promise<ProfileGroup | null> {
|
||||
return (await importModule).importProfileGroup(fileName, contents)
|
||||
}
|
||||
async function importFromFileSystemDirectoryEntry(entry: FileSystemDirectoryEntry) {
|
||||
return (await importModule).importFromFileSystemDirectoryEntry(entry)
|
||||
@@ -26,8 +26,7 @@ async function importFromFileSystemDirectoryEntry(entry: FileSystemDirectoryEntr
|
||||
declare function require(x: string): any
|
||||
const exampleProfileURL = require('../../sample/profiles/stackcollapse/perf-vertx-stacks-01-collapsed-all.txt')
|
||||
|
||||
interface ToolbarProps extends ApplicationState {
|
||||
setViewMode(order: ViewMode): void
|
||||
interface ToolbarProps extends ApplicationProps {
|
||||
browseForFile(): void
|
||||
saveFile(): void
|
||||
}
|
||||
@@ -45,7 +44,88 @@ export class Toolbar extends StatelessComponent<ToolbarProps> {
|
||||
this.props.setViewMode(ViewMode.SANDWICH_VIEW)
|
||||
}
|
||||
|
||||
render() {
|
||||
renderLeftContent() {
|
||||
if (!this.props.activeProfileState) return null
|
||||
|
||||
return (
|
||||
<div className={css(style.toolbarLeft)}>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
this.props.viewMode === ViewMode.CHRONO_FLAME_CHART && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={this.setTimeOrder}
|
||||
>
|
||||
<span className={css(style.emoji)}>🕰</span>Time Order
|
||||
</div>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
this.props.viewMode === ViewMode.LEFT_HEAVY_FLAME_GRAPH && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={this.setLeftHeavyOrder}
|
||||
>
|
||||
<span className={css(style.emoji)}>⬅️</span>Left Heavy
|
||||
</div>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
this.props.viewMode === ViewMode.SANDWICH_VIEW && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={this.setSandwichView}
|
||||
>
|
||||
<span className={css(style.emoji)}>🥪</span>Sandwich
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderCenterContent() {
|
||||
const {activeProfileState, profileGroup} = this.props
|
||||
if (activeProfileState && profileGroup) {
|
||||
const {index} = activeProfileState
|
||||
if (profileGroup.profiles.length === 1) {
|
||||
return activeProfileState.profile.getName()
|
||||
} else {
|
||||
function makeNavButton(content: string, disabled: boolean, onClick: () => void) {
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className={css(
|
||||
style.emoji,
|
||||
style.toolbarProfileNavButton,
|
||||
disabled && style.toolbarProfileNavButtonDisabled,
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const prevButton = makeNavButton('⬅️', index === 0, () =>
|
||||
this.props.setProfileIndexToView(index - 1),
|
||||
)
|
||||
const nextButton = makeNavButton('➡️', index >= profileGroup.profiles.length - 1, () =>
|
||||
this.props.setProfileIndexToView(index + 1),
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={css(style.toolbarCenter)}>
|
||||
{prevButton}
|
||||
{activeProfileState.profile.getName()}{' '}
|
||||
<span className={css(style.toolbarProfileIndex)}>
|
||||
({activeProfileState.index + 1}/{profileGroup.profiles.length})
|
||||
</span>
|
||||
{nextButton}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return '🔬speedscope'
|
||||
}
|
||||
|
||||
renderRightContent() {
|
||||
const importFile = (
|
||||
<div className={css(style.toolbarTab)} onClick={this.props.browseForFile}>
|
||||
<span className={css(style.emoji)}>⤵️</span>Import
|
||||
@@ -63,63 +143,32 @@ export class Toolbar extends StatelessComponent<ToolbarProps> {
|
||||
</div>
|
||||
)
|
||||
|
||||
if (!this.props.profile) {
|
||||
return (
|
||||
<div className={css(style.toolbar)}>
|
||||
🔬speedscope
|
||||
<div className={css(style.toolbarRight)}>
|
||||
{importFile}
|
||||
{help}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={css(style.toolbar)}>
|
||||
<div className={css(style.toolbarLeft)}>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
this.props.viewMode === ViewMode.CHRONO_FLAME_CHART && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={this.setTimeOrder}
|
||||
>
|
||||
<span className={css(style.emoji)}>🕰</span>Time Order
|
||||
</div>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
this.props.viewMode === ViewMode.LEFT_HEAVY_FLAME_GRAPH && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={this.setLeftHeavyOrder}
|
||||
>
|
||||
<span className={css(style.emoji)}>⬅️</span>Left Heavy
|
||||
</div>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
this.props.viewMode === ViewMode.SANDWICH_VIEW && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={this.setSandwichView}
|
||||
>
|
||||
<span className={css(style.emoji)}>🥪</span>Sandwich
|
||||
</div>
|
||||
</div>
|
||||
{this.props.profile.getName()}
|
||||
<div className={css(style.toolbarRight)}>
|
||||
<div className={css(style.toolbarRight)}>
|
||||
{this.props.activeProfileState && (
|
||||
<div className={css(style.toolbarTab)} onClick={this.props.saveFile}>
|
||||
<span className={css(style.emoji)}>⤴️</span>Export
|
||||
</div>
|
||||
{importFile}
|
||||
{help}
|
||||
</div>
|
||||
)}
|
||||
{importFile}
|
||||
{help}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={css(style.toolbar)}>
|
||||
{this.renderLeftContent()}
|
||||
{this.renderCenterContent()}
|
||||
{this.renderRightContent()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface GLCanvasProps {
|
||||
dispatch: Dispatch
|
||||
setGLCanvas: (canvas: HTMLCanvasElement | null) => void
|
||||
}
|
||||
export class GLCanvas extends Component<GLCanvasProps, void> {
|
||||
private canvas: HTMLCanvasElement | null = null
|
||||
@@ -131,7 +180,7 @@ export class GLCanvas extends Component<GLCanvasProps, void> {
|
||||
this.canvas = null
|
||||
}
|
||||
|
||||
this.props.dispatch(actions.setGLCanvas(this.canvas))
|
||||
this.props.setGLCanvas(this.canvas)
|
||||
}
|
||||
|
||||
private maybeResize() {
|
||||
@@ -168,64 +217,65 @@ export class GLCanvas extends Component<GLCanvasProps, void> {
|
||||
}
|
||||
}
|
||||
|
||||
export class Application extends StatelessComponent<WithDispatch<ApplicationState>> {
|
||||
async loadProfile(loader: () => Promise<Profile | null>) {
|
||||
this.props.dispatch(actions.setLoading(true))
|
||||
export interface ActiveProfileState {
|
||||
profile: Profile
|
||||
index: number
|
||||
chronoViewState: FlamechartViewState
|
||||
leftHeavyViewState: FlamechartViewState
|
||||
sandwichViewState: SandwichViewState
|
||||
}
|
||||
|
||||
export type ApplicationProps = ApplicationState & {
|
||||
setGLCanvas: (canvas: HTMLCanvasElement | null) => void
|
||||
setLoading: (loading: boolean) => void
|
||||
setError: (error: boolean) => void
|
||||
setProfileGroup: (profileGroup: ProfileGroup) => void
|
||||
setDragActive: (dragActive: boolean) => void
|
||||
setViewMode: (viewMode: ViewMode) => void
|
||||
setFlattenRecursion: (flattenRecursion: boolean) => void
|
||||
setProfileIndexToView: (profileIndex: number) => void
|
||||
activeProfileState: ActiveProfileState | null
|
||||
}
|
||||
|
||||
export class Application extends StatelessComponent<ApplicationProps> {
|
||||
private async loadProfile(loader: () => Promise<ProfileGroup | null>) {
|
||||
this.props.setLoading(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
if (!this.props.glCanvas) return
|
||||
|
||||
console.time('import')
|
||||
|
||||
let profile: Profile | null = null
|
||||
let profileGroup: ProfileGroup | null = null
|
||||
try {
|
||||
profile = await loader()
|
||||
profileGroup = await loader()
|
||||
} catch (e) {
|
||||
console.log('Failed to load format', e)
|
||||
this.props.dispatch(actions.setError(true))
|
||||
this.props.setError(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (profile == null) {
|
||||
if (profileGroup == null) {
|
||||
// TODO(jlfwong): Make this a nicer overlay
|
||||
alert('Unrecognized format! See documentation about supported formats.')
|
||||
this.props.dispatch(actions.setLoading(false))
|
||||
this.props.setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
await profile.demangle()
|
||||
for (let profile of profileGroup.profiles) {
|
||||
await profile.demangle()
|
||||
}
|
||||
|
||||
const title = this.props.hashParams.title || profile.getName()
|
||||
profile.setName(title)
|
||||
|
||||
await this.setActiveProfile(profile)
|
||||
for (let profile of profileGroup.profiles) {
|
||||
// TODO(jlfwong): Profile names vs. profile group names needs some thought
|
||||
const title = this.props.hashParams.title || profile.getName()
|
||||
profile.setName(title)
|
||||
}
|
||||
|
||||
console.timeEnd('import')
|
||||
this.props.dispatch(actions.setProfile(profile))
|
||||
this.props.dispatch(actions.setLoading(false))
|
||||
}
|
||||
|
||||
async setActiveProfile(profile: Profile) {
|
||||
if (!this.props.glCanvas) return
|
||||
|
||||
document.title = `${profile.getName()} - speedscope`
|
||||
|
||||
const frames: Frame[] = []
|
||||
profile.forEachFrame(f => frames.push(f))
|
||||
function key(f: Frame) {
|
||||
return (f.file || '') + f.name
|
||||
}
|
||||
function compare(a: Frame, b: Frame) {
|
||||
return key(a) > key(b) ? 1 : -1
|
||||
}
|
||||
frames.sort(compare)
|
||||
const frameToColorBucket = new Map<string | number, number>()
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
frameToColorBucket.set(frames[i].key, Math.floor(255 * i / frames.length))
|
||||
}
|
||||
|
||||
this.props.dispatch(actions.setActiveProfile(profile))
|
||||
this.props.dispatch(actions.setFrameToColorBucket(frameToColorBucket))
|
||||
this.props.setProfileGroup(profileGroup)
|
||||
this.props.setLoading(false)
|
||||
}
|
||||
|
||||
loadFromFile(file: File) {
|
||||
@@ -239,25 +289,31 @@ export class Application extends StatelessComponent<WithDispatch<ApplicationStat
|
||||
throw new Error('Expected ArrayBuffer')
|
||||
}
|
||||
|
||||
const profile = await importProfile(file.name, reader.result)
|
||||
if (profile) {
|
||||
if (!profile.getName()) {
|
||||
profile.setName(file.name)
|
||||
const profiles = await importProfiles(file.name, reader.result)
|
||||
if (profiles) {
|
||||
for (let profile of profiles.profiles) {
|
||||
if (!profile.getName()) {
|
||||
profile.setName(file.name)
|
||||
}
|
||||
}
|
||||
return profile
|
||||
return profiles
|
||||
}
|
||||
|
||||
if (this.props.profile) {
|
||||
if (this.props.profileGroup && this.props.activeProfileState) {
|
||||
// If a profile is already loaded, it's possible the file being imported is
|
||||
// a symbol map. If that's the case, we want to parse it, and apply the symbol
|
||||
// mapping to the already loaded profile. This can be use to take an opaque
|
||||
// profile and make it readable.
|
||||
const map = importEmscriptenSymbolMap(reader.result)
|
||||
if (map) {
|
||||
const {profile, index} = this.props.activeProfileState
|
||||
console.log('Importing as emscripten symbol map')
|
||||
let profile = this.props.profile
|
||||
profile.remapNames(name => map.get(name) || name)
|
||||
return profile
|
||||
return {
|
||||
name: this.props.profileGroup.name || 'profile',
|
||||
indexToView: index,
|
||||
profiles: [profile],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,16 +325,12 @@ export class Application extends StatelessComponent<WithDispatch<ApplicationStat
|
||||
this.loadProfile(async () => {
|
||||
const filename = 'perf-vertx-stacks-01-collapsed-all.txt'
|
||||
const data = await fetch(exampleProfileURL).then(resp => resp.text())
|
||||
const profile = await importProfile(filename, data)
|
||||
if (profile && !profile.getName()) {
|
||||
profile.setName(filename)
|
||||
}
|
||||
return profile
|
||||
return await importProfiles(filename, data)
|
||||
})
|
||||
}
|
||||
|
||||
onDrop = (ev: DragEvent) => {
|
||||
this.props.dispatch(actions.setDragActive(false))
|
||||
this.props.setDragActive(false)
|
||||
ev.preventDefault()
|
||||
|
||||
const firstItem = ev.dataTransfer.items[0]
|
||||
@@ -288,7 +340,9 @@ export class Application extends StatelessComponent<WithDispatch<ApplicationStat
|
||||
// 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 importFromFileSystemDirectoryEntry(webkitEntry))
|
||||
this.loadProfile(async () => {
|
||||
return await importFromFileSystemDirectoryEntry(webkitEntry)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -300,31 +354,47 @@ export class Application extends StatelessComponent<WithDispatch<ApplicationStat
|
||||
}
|
||||
|
||||
onDragOver = (ev: DragEvent) => {
|
||||
this.props.dispatch(actions.setDragActive(true))
|
||||
this.props.setDragActive(true)
|
||||
ev.preventDefault()
|
||||
}
|
||||
|
||||
onDragLeave = (ev: DragEvent) => {
|
||||
this.props.dispatch(actions.setDragActive(false))
|
||||
this.props.setDragActive(false)
|
||||
ev.preventDefault()
|
||||
}
|
||||
|
||||
onWindowKeyPress = async (ev: KeyboardEvent) => {
|
||||
if (ev.key === '1') {
|
||||
this.props.dispatch(actions.setViewMode(ViewMode.CHRONO_FLAME_CHART))
|
||||
this.props.setViewMode(ViewMode.CHRONO_FLAME_CHART)
|
||||
} else if (ev.key === '2') {
|
||||
this.props.dispatch(actions.setViewMode(ViewMode.LEFT_HEAVY_FLAME_GRAPH))
|
||||
this.props.setViewMode(ViewMode.LEFT_HEAVY_FLAME_GRAPH)
|
||||
} else if (ev.key === '3') {
|
||||
this.props.dispatch(actions.setViewMode(ViewMode.SANDWICH_VIEW))
|
||||
this.props.setViewMode(ViewMode.SANDWICH_VIEW)
|
||||
} else if (ev.key === 'r') {
|
||||
const {flattenRecursion} = this.props
|
||||
this.props.dispatch(actions.setFlattenRecursion(!flattenRecursion))
|
||||
this.props.setFlattenRecursion(!flattenRecursion)
|
||||
} else if (ev.key === 'n') {
|
||||
const {activeProfileState} = this.props
|
||||
if (activeProfileState) {
|
||||
this.props.setProfileIndexToView(activeProfileState.index + 1)
|
||||
}
|
||||
} else if (ev.key === 'p') {
|
||||
const {activeProfileState} = this.props
|
||||
if (activeProfileState) {
|
||||
this.props.setProfileIndexToView(activeProfileState.index - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private saveFile = () => {
|
||||
if (this.props.profile) {
|
||||
saveToFile(this.props.profile)
|
||||
if (this.props.profileGroup) {
|
||||
const {name, indexToView, profiles} = this.props.profileGroup
|
||||
const profileGroup: ProfileGroup = {
|
||||
name,
|
||||
indexToView,
|
||||
profiles: profiles.map(p => p.profile),
|
||||
}
|
||||
saveToFile(profileGroup)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,7 +422,9 @@ export class Application extends StatelessComponent<WithDispatch<ApplicationStat
|
||||
ev.stopPropagation()
|
||||
|
||||
const pasted = (ev as ClipboardEvent).clipboardData.getData('text')
|
||||
this.loadProfile(async () => importProfile('From Clipboard', pasted))
|
||||
this.loadProfile(async () => {
|
||||
return await importProfiles('From Clipboard', pasted)
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -382,7 +454,7 @@ export class Application extends StatelessComponent<WithDispatch<ApplicationStat
|
||||
if (filename.includes('/')) {
|
||||
filename = filename.slice(filename.lastIndexOf('/') + 1)
|
||||
}
|
||||
return await importProfile(filename, await response.text())
|
||||
return await importProfiles(filename, await response.text())
|
||||
})
|
||||
} else if (this.props.hashParams.localProfilePath) {
|
||||
// There isn't good cross-browser support for XHR of local files, even from
|
||||
@@ -391,7 +463,7 @@ export class Application extends StatelessComponent<WithDispatch<ApplicationStat
|
||||
;(window as any)['speedscope'] = {
|
||||
loadFileFromBase64: (filename: string, base64source: string) => {
|
||||
const source = atob(base64source)
|
||||
this.loadProfile(() => importProfile(filename, source))
|
||||
this.loadProfile(() => importProfiles(filename, source))
|
||||
},
|
||||
}
|
||||
|
||||
@@ -491,12 +563,8 @@ export class Application extends StatelessComponent<WithDispatch<ApplicationStat
|
||||
return <div className={css(style.loading)} />
|
||||
}
|
||||
|
||||
setViewMode = (viewMode: ViewMode) => {
|
||||
this.props.dispatch(actions.setViewMode(viewMode))
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const {viewMode, flattenRecursion, profile, error, loading, glCanvas} = this.props
|
||||
const {viewMode, activeProfileState, error, loading, glCanvas} = this.props
|
||||
|
||||
if (error) {
|
||||
return this.renderError()
|
||||
@@ -506,22 +574,21 @@ export class Application extends StatelessComponent<WithDispatch<ApplicationStat
|
||||
return this.renderLoadingBar()
|
||||
}
|
||||
|
||||
if (!profile || !glCanvas) {
|
||||
if (!activeProfileState || !glCanvas) {
|
||||
return this.renderLanding()
|
||||
}
|
||||
|
||||
const profileToView = getProfileToView({profile, flattenRecursion})
|
||||
|
||||
switch (viewMode) {
|
||||
case ViewMode.CHRONO_FLAME_CHART: {
|
||||
return <ChronoFlamechartView profile={profileToView} glCanvas={glCanvas} />
|
||||
return <ChronoFlamechartView activeProfileState={activeProfileState} glCanvas={glCanvas} />
|
||||
}
|
||||
case ViewMode.LEFT_HEAVY_FLAME_GRAPH: {
|
||||
return <LeftHeavyFlamechartView profile={profileToView} glCanvas={glCanvas} />
|
||||
return (
|
||||
<LeftHeavyFlamechartView activeProfileState={activeProfileState} glCanvas={glCanvas} />
|
||||
)
|
||||
}
|
||||
case ViewMode.SANDWICH_VIEW: {
|
||||
if (!this.props.profile) return null
|
||||
return <SandwichViewContainer />
|
||||
return <SandwichViewContainer activeProfileState={activeProfileState} glCanvas={glCanvas} />
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -534,12 +601,11 @@ export class Application extends StatelessComponent<WithDispatch<ApplicationStat
|
||||
onDragLeave={this.onDragLeave}
|
||||
className={css(style.root, this.props.dragActive && style.dragTargetRoot)}
|
||||
>
|
||||
<GLCanvas dispatch={this.props.dispatch} />
|
||||
<GLCanvas setGLCanvas={this.props.setGLCanvas} />
|
||||
<Toolbar
|
||||
setViewMode={this.setViewMode}
|
||||
saveFile={this.saveFile}
|
||||
browseForFile={this.browseForFile}
|
||||
{...this.props as ApplicationState}
|
||||
{...this.props as ApplicationProps}
|
||||
/>
|
||||
<div className={css(style.contentContainer)}>{this.renderContent()}</div>
|
||||
{this.props.dragActive && <div className={css(style.dragTarget)} />}
|
||||
@@ -671,6 +737,10 @@ const style = StyleSheet.create({
|
||||
marginRight: 2,
|
||||
textAlign: 'left',
|
||||
},
|
||||
toolbarCenter: {
|
||||
paddingTop: 1,
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
},
|
||||
toolbarRight: {
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
@@ -680,6 +750,29 @@ const style = StyleSheet.create({
|
||||
marginRight: 2,
|
||||
textAlign: 'right',
|
||||
},
|
||||
toolbarProfileIndex: {
|
||||
color: Colors.LIGHT_GRAY,
|
||||
},
|
||||
toolbarProfileNavButton: {
|
||||
opacity: 0.8,
|
||||
fontSize: FontSize.TITLE,
|
||||
lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`,
|
||||
':hover': {
|
||||
opacity: 1.0,
|
||||
},
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
marginLeft: '0.3em',
|
||||
marginRight: '0.3em',
|
||||
transition: `all ${Duration.HOVER_CHANGE} ease-in`,
|
||||
},
|
||||
toolbarProfileNavButtonDisabled: {
|
||||
opacity: 0.5,
|
||||
':hover': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
toolbarTab: {
|
||||
background: Colors.DARK_GRAY,
|
||||
marginTop: Sizes.SEPARATOR_HEIGHT,
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import {memoizeByShallowEquality} from '../lib/utils'
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {createMemoizedFlamechartRenderer} from './flamechart-view-container'
|
||||
import {createContainer} from '../lib/typed-redux'
|
||||
import {
|
||||
createMemoizedFlamechartRenderer,
|
||||
FlamechartViewContainerProps,
|
||||
createFlamechartSetters,
|
||||
} from './flamechart-view-container'
|
||||
import {createContainer, Dispatch} from '../lib/typed-redux'
|
||||
import {ApplicationState} from '../store'
|
||||
import {
|
||||
getCanvasContext,
|
||||
createGetColorBucketForFrame,
|
||||
createGetCSSColorForFrame,
|
||||
getFrameToColorBucket,
|
||||
} from '../store/getters'
|
||||
import {FlamechartID} from '../store/flamechart-view-state'
|
||||
import {FlamechartWrapper} from './flamechart-wrapper'
|
||||
@@ -43,14 +48,17 @@ const getCalleeFlamegraphRenderer = createMemoizedFlamechartRenderer()
|
||||
|
||||
export const CalleeFlamegraphView = createContainer(
|
||||
FlamechartWrapper,
|
||||
(state: ApplicationState) => {
|
||||
const {profile, flattenRecursion, glCanvas, frameToColorBucket, sandwichView} = state
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState} = ownProps
|
||||
const {index, profile, sandwichViewState} = activeProfileState
|
||||
const {flattenRecursion, glCanvas} = state
|
||||
if (!profile) throw new Error('profile missing')
|
||||
if (!glCanvas) throw new Error('glCanvas missing')
|
||||
const {callerCallee} = sandwichView
|
||||
const {callerCallee} = sandwichViewState
|
||||
if (!callerCallee) throw new Error('callerCallee missing')
|
||||
const {selectedFrame} = callerCallee
|
||||
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
@@ -62,12 +70,14 @@ export const CalleeFlamegraphView = createContainer(
|
||||
const flamechartRenderer = getCalleeFlamegraphRenderer({canvasContext, flamechart})
|
||||
|
||||
return {
|
||||
id: FlamechartID.SANDWICH_CALLEES,
|
||||
renderInverted: false,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...createFlamechartSetters(dispatch, FlamechartID.SANDWICH_CALLEES, index),
|
||||
// This overrides the setSelectedNode specified in createFlamechartSettesr
|
||||
setSelectedNode: () => {},
|
||||
...callerCallee.calleeFlamegraph,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,7 +8,6 @@ import {cachedMeasureTextWidth, ELLIPSIS, trimTextMid} from '../lib/text-utils'
|
||||
import {style} from './flamechart-style'
|
||||
import {h, Component} from 'preact'
|
||||
import {css} from 'aphrodite'
|
||||
import {FlamechartID} from '../store/flamechart-view-state'
|
||||
|
||||
interface FlamechartFrameLabel {
|
||||
configSpaceBounds: Rect
|
||||
@@ -34,7 +33,6 @@ interface FlamechartFrameLabel {
|
||||
* canvas primitives.
|
||||
*/
|
||||
export interface FlamechartPanZoomViewProps {
|
||||
id: FlamechartID
|
||||
flamechart: Flamechart
|
||||
canvasContext: CanvasContext
|
||||
flamechartRenderer: FlamechartRenderer
|
||||
|
||||
@@ -2,8 +2,8 @@ import {FlamechartID, FlamechartViewState} from '../store/flamechart-view-state'
|
||||
import {CanvasContext} from '../gl/canvas-context'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {FlamechartRenderer, FlamechartRendererOptions} from '../gl/flamechart-renderer'
|
||||
import {Dispatch, createContainer, WithoutDispatch} from '../lib/typed-redux'
|
||||
import {Frame, Profile} from '../lib/profile'
|
||||
import {Dispatch, createContainer, ActionCreator} from '../lib/typed-redux'
|
||||
import {Frame, Profile, CallTreeNode} from '../lib/profile'
|
||||
import {memoizeByShallowEquality} from '../lib/utils'
|
||||
import {ApplicationState} from '../store'
|
||||
import {FlamechartView} from './flamechart-view'
|
||||
@@ -12,17 +12,70 @@ import {
|
||||
createGetColorBucketForFrame,
|
||||
getCanvasContext,
|
||||
createGetCSSColorForFrame,
|
||||
getFrameToColorBucket,
|
||||
} from '../store/getters'
|
||||
import {ActiveProfileState} from './application'
|
||||
import {Vec2, Rect} from '../lib/math'
|
||||
import {actions} from '../store/actions'
|
||||
|
||||
interface FlamechartSetters {
|
||||
setLogicalSpaceViewportSize: (logicalSpaceViewportSize: Vec2) => void
|
||||
setConfigSpaceViewportRect: (configSpaceViewportRect: Rect) => void
|
||||
setNodeHover: (hover: {node: CallTreeNode; event: MouseEvent} | null) => void
|
||||
setSelectedNode: (node: CallTreeNode | null) => void
|
||||
}
|
||||
|
||||
interface WithFlamechartContext<T> {
|
||||
profileIndex: number
|
||||
args: {
|
||||
id: FlamechartID
|
||||
} & T
|
||||
}
|
||||
|
||||
export function createFlamechartSetters(
|
||||
dispatch: Dispatch,
|
||||
id: FlamechartID,
|
||||
profileIndex: number,
|
||||
): FlamechartSetters {
|
||||
function wrapActionCreator<T, U>(
|
||||
actionCreator: ActionCreator<WithFlamechartContext<U>>,
|
||||
map: (t: T) => U,
|
||||
): (t: T) => void {
|
||||
return (t: T) => {
|
||||
const args = Object.assign({}, map(t), {id})
|
||||
dispatch(actionCreator({profileIndex, args}))
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
setHoveredNode,
|
||||
setLogicalSpaceViewportSize,
|
||||
setConfigSpaceViewportRect,
|
||||
setSelectedNode,
|
||||
} = actions.flamechart
|
||||
|
||||
return {
|
||||
setNodeHover: wrapActionCreator(setHoveredNode, hover => ({hover})),
|
||||
setLogicalSpaceViewportSize: wrapActionCreator(
|
||||
setLogicalSpaceViewportSize,
|
||||
logicalSpaceViewportSize => ({logicalSpaceViewportSize}),
|
||||
),
|
||||
setConfigSpaceViewportRect: wrapActionCreator(
|
||||
setConfigSpaceViewportRect,
|
||||
configSpaceViewportRect => ({configSpaceViewportRect}),
|
||||
),
|
||||
setSelectedNode: wrapActionCreator(setSelectedNode, selectedNode => ({selectedNode})),
|
||||
}
|
||||
}
|
||||
|
||||
export type FlamechartViewProps = {
|
||||
id: FlamechartID
|
||||
canvasContext: CanvasContext
|
||||
flamechart: Flamechart
|
||||
flamechartRenderer: FlamechartRenderer
|
||||
renderInverted: boolean
|
||||
dispatch: Dispatch
|
||||
getCSSColorForFrame: (frame: Frame) => string
|
||||
} & FlamechartViewState
|
||||
} & FlamechartSetters &
|
||||
FlamechartViewState
|
||||
|
||||
export const getChronoViewFlamechart = memoizeByShallowEquality(
|
||||
({
|
||||
@@ -56,35 +109,39 @@ export const createMemoizedFlamechartRenderer = (options?: FlamechartRendererOpt
|
||||
|
||||
const getChronoViewFlamechartRenderer = createMemoizedFlamechartRenderer()
|
||||
|
||||
export const ChronoFlamechartView = createContainer<
|
||||
{profile: Profile; glCanvas: HTMLCanvasElement},
|
||||
ApplicationState,
|
||||
WithoutDispatch<FlamechartViewProps>,
|
||||
FlamechartView
|
||||
>(FlamechartView, (state, ownProps) => {
|
||||
const {profile, glCanvas} = ownProps
|
||||
const {frameToColorBucket, chronoView} = state
|
||||
export interface FlamechartViewContainerProps {
|
||||
activeProfileState: ActiveProfileState
|
||||
glCanvas: HTMLCanvasElement
|
||||
}
|
||||
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
export const ChronoFlamechartView = createContainer(
|
||||
FlamechartView,
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState, glCanvas} = ownProps
|
||||
const {index, profile, chronoViewState} = activeProfileState
|
||||
|
||||
const flamechart = getChronoViewFlamechart({profile, getColorBucketForFrame})
|
||||
const flamechartRenderer = getChronoViewFlamechartRenderer({
|
||||
canvasContext,
|
||||
flamechart,
|
||||
})
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
|
||||
return {
|
||||
id: FlamechartID.CHRONO,
|
||||
renderInverted: false,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...chronoView,
|
||||
}
|
||||
})
|
||||
const flamechart = getChronoViewFlamechart({profile, getColorBucketForFrame})
|
||||
const flamechartRenderer = getChronoViewFlamechartRenderer({
|
||||
canvasContext,
|
||||
flamechart,
|
||||
})
|
||||
|
||||
return {
|
||||
renderInverted: false,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...createFlamechartSetters(dispatch, FlamechartID.CHRONO, index),
|
||||
...chronoViewState,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const getLeftHeavyFlamechart = memoizeByShallowEquality(
|
||||
({
|
||||
@@ -105,32 +162,35 @@ export const getLeftHeavyFlamechart = memoizeByShallowEquality(
|
||||
|
||||
const getLeftHeavyFlamechartRenderer = createMemoizedFlamechartRenderer()
|
||||
|
||||
export const LeftHeavyFlamechartView = createContainer<
|
||||
{profile: Profile; glCanvas: HTMLCanvasElement},
|
||||
ApplicationState,
|
||||
WithoutDispatch<FlamechartViewProps>,
|
||||
FlamechartView
|
||||
>(FlamechartView, (state, ownProps) => {
|
||||
const {profile, glCanvas} = ownProps
|
||||
const {frameToColorBucket, leftHeavyView} = state
|
||||
export const LeftHeavyFlamechartView = createContainer(
|
||||
FlamechartView,
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState, glCanvas} = ownProps
|
||||
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const {index, profile, leftHeavyViewState} = activeProfileState
|
||||
|
||||
const flamechart = getLeftHeavyFlamechart({profile, getColorBucketForFrame})
|
||||
const flamechartRenderer = getLeftHeavyFlamechartRenderer({
|
||||
canvasContext,
|
||||
flamechart,
|
||||
})
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
|
||||
return {
|
||||
id: FlamechartID.LEFT_HEAVY,
|
||||
renderInverted: false,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...leftHeavyView,
|
||||
}
|
||||
})
|
||||
const flamechart = getLeftHeavyFlamechart({
|
||||
profile,
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = getLeftHeavyFlamechartRenderer({
|
||||
canvasContext,
|
||||
flamechart,
|
||||
})
|
||||
|
||||
return {
|
||||
renderInverted: false,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...createFlamechartSetters(dispatch, FlamechartID.LEFT_HEAVY, index),
|
||||
...leftHeavyViewState,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -12,7 +12,6 @@ import {Sizes, commonStyle} from './style'
|
||||
import {FlamechartDetailView} from './flamechart-detail-view'
|
||||
import {FlamechartPanZoomView} from './flamechart-pan-zoom-view'
|
||||
import {Hovertip} from './hovertip'
|
||||
import {actions} from '../store/actions'
|
||||
import {FlamechartViewProps} from './flamechart-view-container'
|
||||
import {StatelessComponent} from '../lib/typed-redux'
|
||||
|
||||
@@ -41,18 +40,11 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
|
||||
),
|
||||
)
|
||||
|
||||
this.props.dispatch(
|
||||
actions.flamechart.setConfigSpaceViewportRect({
|
||||
id: this.props.id,
|
||||
configSpaceViewportRect: new Rect(origin, viewportRect.size.withX(width)),
|
||||
}),
|
||||
)
|
||||
this.props.setConfigSpaceViewportRect(new Rect(origin, viewportRect.size.withX(width)))
|
||||
}
|
||||
|
||||
private setLogicalSpaceViewportSize = (logicalSpaceViewportSize: Vec2): void => {
|
||||
this.props.dispatch(
|
||||
actions.flamechart.setLogicalSpaceViewportSize({id: this.props.id, logicalSpaceViewportSize}),
|
||||
)
|
||||
this.props.setLogicalSpaceViewportSize(logicalSpaceViewportSize)
|
||||
}
|
||||
|
||||
private transformViewport = (transform: AffineTransform): void => {
|
||||
@@ -60,17 +52,12 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
|
||||
this.setConfigSpaceViewportRect(viewportRect)
|
||||
}
|
||||
|
||||
onNodeHover = (hover: {node: CallTreeNode; event: MouseEvent} | null) => {
|
||||
this.props.dispatch(
|
||||
actions.flamechart.setHoveredNode({
|
||||
id: this.props.id,
|
||||
hover,
|
||||
}),
|
||||
)
|
||||
private onNodeHover = (hover: {node: CallTreeNode; event: MouseEvent} | null) => {
|
||||
this.props.setNodeHover(hover)
|
||||
}
|
||||
|
||||
onNodeClick = (node: CallTreeNode | null) => {
|
||||
this.props.dispatch(actions.flamechart.setSelectedNode({id: this.props.id, selectedNode: node}))
|
||||
this.props.setSelectedNode(node)
|
||||
}
|
||||
|
||||
formatValue(weight: number) {
|
||||
@@ -116,7 +103,6 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
|
||||
/>
|
||||
<FlamechartPanZoomView
|
||||
canvasContext={this.props.canvasContext}
|
||||
id={this.props.id}
|
||||
flamechart={this.props.flamechart}
|
||||
flamechartRenderer={this.props.flamechartRenderer}
|
||||
renderInverted={false}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {Rect, AffineTransform, Vec2} from '../lib/math'
|
||||
import {FlamechartPanZoomView} from './flamechart-pan-zoom-view'
|
||||
import {noop, formatPercent} from '../lib/utils'
|
||||
import {Hovertip} from './hovertip'
|
||||
import {actions} from '../store/actions'
|
||||
import {FlamechartViewProps} from './flamechart-view-container'
|
||||
import {StatelessComponent} from '../lib/typed-redux'
|
||||
|
||||
@@ -24,17 +23,10 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
|
||||
return new Rect(origin, viewportRect.size.withX(width))
|
||||
}
|
||||
private setConfigSpaceViewportRect = (configSpaceViewportRect: Rect) => {
|
||||
this.props.dispatch(
|
||||
actions.flamechart.setConfigSpaceViewportRect({
|
||||
id: this.props.id,
|
||||
configSpaceViewportRect: this.clampViewportToFlamegraph(configSpaceViewportRect),
|
||||
}),
|
||||
)
|
||||
this.props.setConfigSpaceViewportRect(this.clampViewportToFlamegraph(configSpaceViewportRect))
|
||||
}
|
||||
private setLogicalSpaceViewportSize = (logicalSpaceViewportSize: Vec2): void => {
|
||||
this.props.dispatch(
|
||||
actions.flamechart.setLogicalSpaceViewportSize({id: this.props.id, logicalSpaceViewportSize}),
|
||||
)
|
||||
this.props.setLogicalSpaceViewportSize(logicalSpaceViewportSize)
|
||||
}
|
||||
|
||||
private transformViewport = (transform: AffineTransform) => {
|
||||
@@ -71,7 +63,7 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
|
||||
event: MouseEvent
|
||||
} | null,
|
||||
) => {
|
||||
this.props.dispatch(actions.flamechart.setHoveredNode({id: this.props.id, hover}))
|
||||
this.props.setNodeHover(hover)
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
@@ -80,7 +72,6 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
|
||||
ref={this.containerRef}
|
||||
>
|
||||
<FlamechartPanZoomView
|
||||
id={this.props.id}
|
||||
selectedNode={null}
|
||||
onNodeHover={this.setNodeHover}
|
||||
onNodeSelect={noop}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import {memoizeByShallowEquality} from '../lib/utils'
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {createMemoizedFlamechartRenderer} from './flamechart-view-container'
|
||||
import {createContainer} from '../lib/typed-redux'
|
||||
import {
|
||||
createMemoizedFlamechartRenderer,
|
||||
FlamechartViewContainerProps,
|
||||
createFlamechartSetters,
|
||||
} from './flamechart-view-container'
|
||||
import {createContainer, Dispatch} from '../lib/typed-redux'
|
||||
import {ApplicationState} from '../store'
|
||||
import {
|
||||
getCanvasContext,
|
||||
createGetColorBucketForFrame,
|
||||
createGetCSSColorForFrame,
|
||||
getProfileWithRecursionFlattened,
|
||||
getFrameToColorBucket,
|
||||
} from '../store/getters'
|
||||
import {FlamechartID} from '../store/flamechart-view-state'
|
||||
import {FlamechartWrapper} from './flamechart-wrapper'
|
||||
@@ -49,16 +54,19 @@ const getInvertedCallerFlamegraphRenderer = createMemoizedFlamechartRenderer({in
|
||||
|
||||
export const InvertedCallerFlamegraphView = createContainer(
|
||||
FlamechartWrapper,
|
||||
(state: ApplicationState) => {
|
||||
let {profile, flattenRecursion, glCanvas, frameToColorBucket, sandwichView} = state
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState} = ownProps
|
||||
let {profile, sandwichViewState, index} = activeProfileState
|
||||
let {flattenRecursion, glCanvas} = state
|
||||
if (!profile) throw new Error('profile missing')
|
||||
if (!glCanvas) throw new Error('glCanvas missing')
|
||||
const {callerCallee} = sandwichView
|
||||
const {callerCallee} = sandwichViewState
|
||||
if (!callerCallee) throw new Error('callerCallee missing')
|
||||
const {selectedFrame} = callerCallee
|
||||
|
||||
profile = flattenRecursion ? getProfileWithRecursionFlattened(profile) : profile
|
||||
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
@@ -74,12 +82,14 @@ export const InvertedCallerFlamegraphView = createContainer(
|
||||
const flamechartRenderer = getInvertedCallerFlamegraphRenderer({canvasContext, flamechart})
|
||||
|
||||
return {
|
||||
id: FlamechartID.SANDWICH_INVERTED_CALLERS,
|
||||
renderInverted: true,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...createFlamechartSetters(dispatch, FlamechartID.SANDWICH_INVERTED_CALLERS, index),
|
||||
// This overrides the setSelectedNode specified in createFlamechartSettesr
|
||||
setSelectedNode: () => {},
|
||||
...callerCallee.invertedCallerFlamegraph,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,7 +8,8 @@ import {ScrollableListView, ListItem} from './scrollable-list-view'
|
||||
import {actions} from '../store/actions'
|
||||
import {Dispatch, createContainer} from '../lib/typed-redux'
|
||||
import {ApplicationState} from '../store'
|
||||
import {createGetCSSColorForFrame} from '../store/getters'
|
||||
import {createGetCSSColorForFrame, getFrameToColorBucket} from '../store/getters'
|
||||
import {ActiveProfileState} from './application'
|
||||
|
||||
export enum SortField {
|
||||
SYMBOL_NAME,
|
||||
@@ -68,21 +69,15 @@ class SortIcon extends Component<SortIconProps, {}> {
|
||||
|
||||
interface ProfileTableViewProps {
|
||||
profile: Profile
|
||||
profileIndex: number
|
||||
selectedFrame: Frame | null
|
||||
getCSSColorForFrame: (frame: Frame) => string
|
||||
sortMethod: SortMethod
|
||||
dispatch: Dispatch
|
||||
setSelectedFrame: (frame: Frame | null) => void
|
||||
setSortMethod: (sortMethod: SortMethod) => void
|
||||
}
|
||||
|
||||
export class ProfileTableView extends Component<ProfileTableViewProps, void> {
|
||||
setSelectedFrame = (frame: Frame | null) => {
|
||||
this.props.dispatch(actions.sandwichView.setSelectedFrame(frame))
|
||||
}
|
||||
|
||||
setSortMethod = (method: SortMethod) => {
|
||||
this.props.dispatch(actions.sandwichView.setTableSortMethod(method))
|
||||
}
|
||||
|
||||
renderRow(frame: Frame, index: number) {
|
||||
const {profile, selectedFrame} = this.props
|
||||
|
||||
@@ -98,7 +93,7 @@ export class ProfileTableView extends Component<ProfileTableViewProps, void> {
|
||||
return (
|
||||
<tr
|
||||
key={`${index}`}
|
||||
onClick={this.setSelectedFrame.bind(null, frame)}
|
||||
onClick={this.props.setSelectedFrame.bind(null, frame)}
|
||||
className={css(
|
||||
style.tableRow,
|
||||
index % 2 == 0 && style.tableRowEven,
|
||||
@@ -128,7 +123,7 @@ export class ProfileTableView extends Component<ProfileTableViewProps, void> {
|
||||
|
||||
if (sortMethod.field == field) {
|
||||
// Toggle
|
||||
this.setSortMethod({
|
||||
this.props.setSortMethod({
|
||||
field,
|
||||
direction:
|
||||
sortMethod.direction === SortDirection.ASCENDING
|
||||
@@ -139,15 +134,15 @@ export class ProfileTableView extends Component<ProfileTableViewProps, void> {
|
||||
// Set a sane default
|
||||
switch (field) {
|
||||
case SortField.SYMBOL_NAME: {
|
||||
this.setSortMethod({field, direction: SortDirection.ASCENDING})
|
||||
this.props.setSortMethod({field, direction: SortDirection.ASCENDING})
|
||||
break
|
||||
}
|
||||
case SortField.SELF: {
|
||||
this.setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
this.props.setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
break
|
||||
}
|
||||
case SortField.TOTAL: {
|
||||
this.setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
this.props.setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -334,20 +329,37 @@ const style = StyleSheet.create({
|
||||
},
|
||||
})
|
||||
|
||||
interface ProfileTableViewContainerProps {
|
||||
activeProfileState: ActiveProfileState
|
||||
}
|
||||
|
||||
export const ProfileTableViewContainer = createContainer(
|
||||
ProfileTableView,
|
||||
(state: ApplicationState) => {
|
||||
const {profile, sandwichView, frameToColorBucket} = state
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: ProfileTableViewContainerProps) => {
|
||||
const {activeProfileState} = ownProps
|
||||
const {profile, sandwichViewState, index} = activeProfileState
|
||||
if (!profile) throw new Error('profile missing')
|
||||
const {tableSortMethod, callerCallee} = sandwichView
|
||||
const {tableSortMethod, callerCallee} = sandwichViewState
|
||||
const selectedFrame = callerCallee ? callerCallee.selectedFrame : null
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
|
||||
const setSelectedFrame = (selectedFrame: Frame | null) => {
|
||||
dispatch(actions.sandwichView.setSelectedFrame({profileIndex: index, args: selectedFrame}))
|
||||
}
|
||||
|
||||
const setSortMethod = (sortMethod: SortMethod) => {
|
||||
dispatch(actions.sandwichView.setTableSortMethod({profileIndex: index, args: sortMethod}))
|
||||
}
|
||||
|
||||
return {
|
||||
profile,
|
||||
profileIndex: activeProfileState.index,
|
||||
selectedFrame,
|
||||
getCSSColorForFrame,
|
||||
sortMethod: tableSortMethod,
|
||||
setSelectedFrame,
|
||||
setSortMethod,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -8,15 +8,19 @@ import {createContainer, Dispatch, StatelessComponent} from '../lib/typed-redux'
|
||||
import {ApplicationState} from '../store'
|
||||
import {InvertedCallerFlamegraphView} from './inverted-caller-flamegraph-view'
|
||||
import {CalleeFlamegraphView} from './callee-flamegraph-view'
|
||||
import {ActiveProfileState} from './application'
|
||||
|
||||
interface SandwichViewProps {
|
||||
selectedFrame: Frame | null
|
||||
dispatch: Dispatch
|
||||
profileIndex: number
|
||||
activeProfileState: ActiveProfileState
|
||||
setSelectedFrame: (selectedFrame: Frame | null) => void
|
||||
glCanvas: HTMLCanvasElement
|
||||
}
|
||||
|
||||
class SandwichView extends StatelessComponent<SandwichViewProps> {
|
||||
private setSelectedFrame = (selectedFrame: Frame | null) => {
|
||||
this.props.dispatch(actions.sandwichView.setSelectedFrame(selectedFrame))
|
||||
this.props.setSelectedFrame(selectedFrame)
|
||||
}
|
||||
|
||||
onWindowKeyPress = (ev: KeyboardEvent) => {
|
||||
@@ -43,14 +47,20 @@ class SandwichView extends StatelessComponent<SandwichViewProps> {
|
||||
<div className={css(style.flamechartLabelParent)}>
|
||||
<div className={css(style.flamechartLabel)}>Callers</div>
|
||||
</div>
|
||||
<InvertedCallerFlamegraphView />
|
||||
<InvertedCallerFlamegraphView
|
||||
glCanvas={this.props.glCanvas}
|
||||
activeProfileState={this.props.activeProfileState}
|
||||
/>
|
||||
</div>
|
||||
<div className={css(style.divider)} />
|
||||
<div className={css(commonStyle.hbox, style.panZoomViewWraper)}>
|
||||
<div className={css(style.flamechartLabelParent, style.flamechartLabelParentBottom)}>
|
||||
<div className={css(style.flamechartLabel, style.flamechartLabelBottom)}>Callees</div>
|
||||
</div>
|
||||
<CalleeFlamegraphView />
|
||||
<CalleeFlamegraphView
|
||||
glCanvas={this.props.glCanvas}
|
||||
activeProfileState={this.props.activeProfileState}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -59,7 +69,7 @@ class SandwichView extends StatelessComponent<SandwichViewProps> {
|
||||
return (
|
||||
<div className={css(commonStyle.hbox, commonStyle.fillY)}>
|
||||
<div className={css(style.tableView)}>
|
||||
<ProfileTableViewContainer />
|
||||
<ProfileTableViewContainer activeProfileState={this.props.activeProfileState} />
|
||||
</div>
|
||||
{flamegraphViews}
|
||||
</div>
|
||||
@@ -107,7 +117,33 @@ const style = StyleSheet.create({
|
||||
},
|
||||
})
|
||||
|
||||
export const SandwichViewContainer = createContainer(SandwichView, (state: ApplicationState) => {
|
||||
const {callerCallee} = state.sandwichView
|
||||
return {selectedFrame: callerCallee ? callerCallee.selectedFrame : null}
|
||||
})
|
||||
interface SandwichViewContainerProps {
|
||||
activeProfileState: ActiveProfileState
|
||||
glCanvas: HTMLCanvasElement
|
||||
}
|
||||
|
||||
export const SandwichViewContainer = createContainer(
|
||||
SandwichView,
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: SandwichViewContainerProps) => {
|
||||
const {activeProfileState, glCanvas} = ownProps
|
||||
const {sandwichViewState, index} = activeProfileState
|
||||
const {callerCallee} = sandwichViewState
|
||||
|
||||
const setSelectedFrame = (selectedFrame: Frame | null) => {
|
||||
dispatch(
|
||||
actions.sandwichView.setSelectedFrame({
|
||||
profileIndex: index,
|
||||
args: selectedFrame,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
activeProfileState: activeProfileState,
|
||||
glCanvas,
|
||||
setSelectedFrame,
|
||||
selectedFrame: callerCallee ? callerCallee.selectedFrame : null,
|
||||
profileIndex: index,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user