Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7368e15396 | ||
|
|
1bbe3de944 | ||
|
|
8e09be7f7f | ||
|
|
5ee94ec092 | ||
|
|
4d23f1cdee | ||
|
|
fc9260ba1d | ||
|
|
182563dc7c | ||
|
|
e6d9994801 | ||
|
|
29c55c3921 | ||
|
|
0b1050034f | ||
|
|
a79beaa731 | ||
|
|
bca9c52ac9 | ||
|
|
970f1b2107 | ||
|
|
72c05fe6ba | ||
|
|
ee157c4139 | ||
|
|
94d0879417 | ||
|
|
608006232f | ||
|
|
11a3a67dce |
@@ -1,5 +1,20 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
### Fixed
|
||||
|
||||
## [0.5.0] - 2018-08-09
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix emscripten remapping when symbols contain dashes, like `527:i32s-div` [#129]
|
||||
* Improved firefox import speed and fixed bugs in it [#128]
|
||||
* Prevent non-contiguous blocks in the time ordered flamechart from appearing as a single node for selection [#123]
|
||||
* Prevent dragging from changing selection [#122]
|
||||
* Clamp zoom to prevent floating point issues [#121]
|
||||
* Preserve view state when switching tabs [#100]
|
||||
|
||||
## [0.4.0] - 2018-07-21
|
||||
|
||||
### Added
|
||||
|
||||
@@ -166,8 +166,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="./speedscope.tsx"></script>
|
||||
<script src="../src/speedscope.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+1
-1
@@ -52,7 +52,7 @@ async function main() {
|
||||
}
|
||||
|
||||
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
||||
console.log('v' + require('./package.json').version)
|
||||
console.log('v' + require('../package.json').version)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`importFromFirefox 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "file:///Users/jlfwong/code/speedscope/sample/simple.js",
|
||||
"key": "a (file:///Users/jlfwong/code/speedscope/sample/simple.js:1)",
|
||||
"line": 1,
|
||||
"name": "a",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 31.992820999992546,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "file:///Users/jlfwong/code/speedscope/sample/simple.js",
|
||||
"key": "b (file:///Users/jlfwong/code/speedscope/sample/simple.js:8)",
|
||||
"line": 8,
|
||||
"name": "b",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 20.971447000018088,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "file:///Users/jlfwong/code/speedscope/sample/simple.js",
|
||||
"key": "d (file:///Users/jlfwong/code/speedscope/sample/simple.js:20)",
|
||||
"line": 20,
|
||||
"name": "d",
|
||||
"selfWeight": 31.992820999992546,
|
||||
"totalWeight": 31.992820999992546,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "file:///Users/jlfwong/code/speedscope/sample/simple.js",
|
||||
"key": "c (file:///Users/jlfwong/code/speedscope/sample/simple.js:14)",
|
||||
"line": 14,
|
||||
"name": "c",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 11.021373999974458,
|
||||
},
|
||||
],
|
||||
"stacks": Array [
|
||||
"a;b;d 989.53µs",
|
||||
"a;c;d 1.02ms",
|
||||
"a;b;d 982.04µs",
|
||||
"a;c;d 1.02ms",
|
||||
"a;b;d 7.00ms",
|
||||
"a;c;d 2.00ms",
|
||||
"a;b;d 981.44µs",
|
||||
"a;c;d 2.00ms",
|
||||
"a;b;d 2.00ms",
|
||||
"a;c;d 3.02ms",
|
||||
"a;b;d 4.03ms",
|
||||
"a;c;d 1.96ms",
|
||||
"a;b;d 4.99ms",
|
||||
],
|
||||
}
|
||||
`;
|
||||
@@ -1,5 +0,0 @@
|
||||
import {checkProfileSnapshot} from '../test-utils'
|
||||
|
||||
test('importFromFirefox', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/Firefox/59/simple-firefox.json')
|
||||
})
|
||||
Generated
+40
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "speedscope",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -4555,7 +4555,8 @@
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz",
|
||||
"integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
@@ -4767,7 +4768,8 @@
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.8",
|
||||
"resolved": "",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz",
|
||||
"integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
@@ -5285,7 +5287,8 @@
|
||||
},
|
||||
"rc": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz",
|
||||
"integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
@@ -5397,7 +5400,8 @@
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.0.tgz",
|
||||
"integrity": "sha1-/yo+T9BEl1Vf7Zezmg/YL6+zozw=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
@@ -5443,7 +5447,8 @@
|
||||
},
|
||||
"stringstream": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "",
|
||||
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
|
||||
"integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
@@ -5493,7 +5498,8 @@
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz",
|
||||
"integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
@@ -10611,6 +10617,11 @@
|
||||
"integrity": "sha512-m34Ke8U32HyKRVzUOCAcaiIBLR2ye6syiuRclU5DxyixDPDFqdLbIElhERBrF6gDbPKQR+Vpv5bZ9CCbvN6pdQ==",
|
||||
"dev": true
|
||||
},
|
||||
"preact-redux": {
|
||||
"version": "github:jlfwong/preact-redux#a56dcc460f4993c8dd33b7c9caa5e4bde1fa72dd",
|
||||
"from": "github:jlfwong/preact-redux#a56dcc4",
|
||||
"dev": true
|
||||
},
|
||||
"prelude-ls": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
|
||||
@@ -11012,6 +11023,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"redux": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.0.tgz",
|
||||
"integrity": "sha512-NnnHF0h0WVE/hXyrB6OlX67LYRuaf/rJcbWvnHHEPCF/Xa/AZpwhs/20WyqzQae5x4SD2F9nPObgBh2rxAgLiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"symbol-observable": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"regenerate": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
|
||||
@@ -12728,6 +12749,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"symbol-observable": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
|
||||
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
|
||||
"dev": true
|
||||
},
|
||||
"symbol-tree": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz",
|
||||
@@ -13360,15 +13387,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"typescript": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.8.1.tgz",
|
||||
"integrity": "sha512-Ao/f6d/4EPLq0YwzsQz8iXflezpTkQzqAyenTiw4kCUGr1uPiFLC3+fZ+gMZz6eeI/qdRUqvC+HxIJzUAzEFdg==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.1.tgz",
|
||||
"integrity": "sha512-zQIMOmC+372pC/CCVLqnQ0zSBiY7HHodU7mpQdjiZddek4GMj31I3dUJ7gAs9o65X7mnRma6OokOkc6f9jjfBg==",
|
||||
"dev": true
|
||||
},
|
||||
"typescript-eslint-parser": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint-parser/-/typescript-eslint-parser-14.0.0.tgz",
|
||||
"integrity": "sha512-xDvhgZX08RRKSyz8oWXI/V9gjXelh4/a9jGFmmHnn/j/GMXKOQ6E6NAGVjQSvGBrjT/TMobKqsrxnBcVr4sfIw==",
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint-parser/-/typescript-eslint-parser-17.0.1.tgz",
|
||||
"integrity": "sha512-EPCOjxjnGqu9kQwH3CAwa1Jjty2Dn0FnehksVEmfZxXtkEK2Jerb2lnd/htsj1XooqEkKC5AzCaK+qOxHaBDBQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash.unescape": "4.0.1",
|
||||
|
||||
+14
-11
@@ -1,22 +1,23 @@
|
||||
{
|
||||
"name": "speedscope",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"description": "",
|
||||
"repository": "jlfwong/speedscope",
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
"speedscope": "./cli.js"
|
||||
"speedscope": "./bin/cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"deploy": "./deploy.sh",
|
||||
"prepack": "./build-release.sh",
|
||||
"prettier": "prettier --write './**/*.ts' './**/*.tsx'",
|
||||
"lint": "eslint './**/*.ts' './**/*.tsx'",
|
||||
"jest": "./test-setup.sh && jest",
|
||||
"deploy": "./scripts/deploy.sh",
|
||||
"prepack": "./scripts/build-release.sh",
|
||||
"prettier": "prettier --write 'src/**/*.ts' 'src/**/*.tsx'",
|
||||
"lint": "eslint 'src/**/*.ts' 'src/**/*.tsx'",
|
||||
"jest": "./scripts/test-setup.sh && jest",
|
||||
"coverage": "npm run jest -- --coverage && coveralls < coverage/lcov.info",
|
||||
"test": "tsc --noEmit && npm run lint && npm run coverage",
|
||||
"serve": "parcel index.html --open --no-autoinstall"
|
||||
"serve": "parcel assets/index.html --open --no-autoinstall"
|
||||
},
|
||||
"files": ["cli.js", "dist/release/**", "!*.map"],
|
||||
"files": ["bin/cli.js", "dist/release/**", "!*.map"],
|
||||
"browserslist": ["last 2 Chrome versions", "last 2 Firefox versions"],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
@@ -35,12 +36,14 @@
|
||||
"pako": "1.0.6",
|
||||
"parcel-bundler": "1.9.2",
|
||||
"preact": "8.2.7",
|
||||
"preact-redux": "jlfwong/preact-redux#a56dcc4",
|
||||
"prettier": "1.12.0",
|
||||
"quicktype": "15.0.45",
|
||||
"redux": "^4.0.0",
|
||||
"regl": "1.3.1",
|
||||
"ts-jest": "22.4.6",
|
||||
"typescript": "2.8.1",
|
||||
"typescript-eslint-parser": "14.0.0",
|
||||
"typescript": "3.0.1",
|
||||
"typescript-eslint-parser": "17.0.1",
|
||||
"uglify-es": "3.2.2"
|
||||
},
|
||||
"jest": {
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import {Component} from 'preact'
|
||||
|
||||
export interface SerializedComponent<S> {
|
||||
state: S
|
||||
serializedSubcomponents: {[key: string]: any}
|
||||
}
|
||||
|
||||
export abstract class ReloadableComponent<P, S> extends Component<P, S> {
|
||||
serialize(): SerializedComponent<S> {
|
||||
const serializedSubcomponents: {[key: string]: any} = Object.create(null)
|
||||
|
||||
const subcomponents = this.subcomponents()
|
||||
for (const key in subcomponents) {
|
||||
const val = subcomponents[key]
|
||||
if (val && val instanceof ReloadableComponent) {
|
||||
serializedSubcomponents[key] = val.serialize()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state: this.state,
|
||||
serializedSubcomponents,
|
||||
}
|
||||
}
|
||||
rehydrate(serialized: SerializedComponent<S>) {
|
||||
this.setState(serialized.state, () => {
|
||||
const subcomponents = this.subcomponents()
|
||||
for (const key in subcomponents) {
|
||||
const val = subcomponents[key]
|
||||
const data = serialized.serializedSubcomponents[key]
|
||||
if (data && val && val instanceof ReloadableComponent) {
|
||||
val.rehydrate(data)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
subcomponents(): {[key: string]: any} {
|
||||
return Object.create(null)
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
<script src="./recursion.js"></script>
|
||||
@@ -0,0 +1,41 @@
|
||||
function alpha(depth) {
|
||||
if (depth > 15 * Math.random()) {
|
||||
return
|
||||
}
|
||||
beta(depth + 1)
|
||||
delta()
|
||||
gamma()
|
||||
}
|
||||
|
||||
function beta(depth) {
|
||||
alpha(depth + 1)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
gamma()
|
||||
}
|
||||
}
|
||||
|
||||
function delta() {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
gamma()
|
||||
}
|
||||
}
|
||||
|
||||
function gamma() {
|
||||
let prod = 1
|
||||
for (let i = 1; i < 1000; i++) {
|
||||
prod *= i
|
||||
}
|
||||
return prod
|
||||
}
|
||||
|
||||
function main() {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
alpha(0)
|
||||
}
|
||||
}
|
||||
|
||||
console.profile('recursion')
|
||||
main()
|
||||
setTimeout(() => {
|
||||
console.profileEnd('recursion')
|
||||
}, 0)
|
||||
@@ -1,23 +1,23 @@
|
||||
function a() {
|
||||
function alpha() {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
b()
|
||||
c()
|
||||
beta()
|
||||
delta()
|
||||
}
|
||||
}
|
||||
|
||||
function b() {
|
||||
function beta() {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
d()
|
||||
gamma()
|
||||
}
|
||||
}
|
||||
|
||||
function c() {
|
||||
function delta() {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
d()
|
||||
gamma()
|
||||
}
|
||||
}
|
||||
|
||||
function d() {
|
||||
function gamma() {
|
||||
let prod = 1
|
||||
for (let i = 1; i < 1000; i++) {
|
||||
prod *= i
|
||||
@@ -25,10 +25,8 @@ function d() {
|
||||
return prod
|
||||
}
|
||||
|
||||
console.profile('a')
|
||||
a()
|
||||
console.profile('simple')
|
||||
alpha()
|
||||
setTimeout(() => {
|
||||
console.profileEnd('a')
|
||||
console.profileEnd('simple')
|
||||
}, 0)
|
||||
|
||||
window.addEventListener('click', a)
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
import {ReloadableComponent} from './reloadable'
|
||||
import {Profile, Frame, CallTreeNode} from './profile'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {SortMethod, ProfileTableView} from './profile-table-view'
|
||||
import {h} from 'preact'
|
||||
import {commonStyle, Sizes, Colors, FontSize} from './style'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
import {FlamechartRenderer, FlamechartRowAtlasKey} from './flamechart-renderer'
|
||||
import {Flamechart} from './flamechart'
|
||||
import {RowAtlas} from './row-atlas'
|
||||
import {Rect, AffineTransform, Vec2, clamp} from './math'
|
||||
import {FlamechartPanZoomView, FlamechartPanZoomViewProps} from './flamechart-pan-zoom-view'
|
||||
import {noop, formatPercent} from './utils'
|
||||
import {Hovertip} from './hovertip'
|
||||
|
||||
interface FlamechartWrapperProps {
|
||||
flamechart: Flamechart
|
||||
canvasContext: CanvasContext
|
||||
flamechartRenderer: FlamechartRenderer
|
||||
renderInverted: boolean
|
||||
}
|
||||
|
||||
interface FlamechartWrapperState {
|
||||
hover: {
|
||||
node: CallTreeNode
|
||||
event: MouseEvent
|
||||
} | null
|
||||
configSpaceViewportRect: Rect
|
||||
}
|
||||
|
||||
export class FlamechartWrapper extends ReloadableComponent<
|
||||
FlamechartWrapperProps,
|
||||
FlamechartWrapperState
|
||||
> {
|
||||
constructor(props: FlamechartWrapperProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
hover: null,
|
||||
configSpaceViewportRect: Rect.empty,
|
||||
}
|
||||
}
|
||||
|
||||
private clampViewportToFlamegraph(viewportRect: Rect, flamegraph: Flamechart, inverted: boolean) {
|
||||
const configSpaceSize = new Vec2(flamegraph.getTotalWeight(), flamegraph.getLayers().length)
|
||||
|
||||
const width = clamp(
|
||||
viewportRect.size.x,
|
||||
Math.min(configSpaceSize.x, 3 * flamegraph.getMinFrameWidth()),
|
||||
configSpaceSize.x,
|
||||
)
|
||||
|
||||
const size = viewportRect.size.withX(width)
|
||||
|
||||
const origin = Vec2.clamp(
|
||||
viewportRect.origin,
|
||||
new Vec2(0, inverted ? 0 : -1),
|
||||
Vec2.max(Vec2.zero, configSpaceSize.minus(size).plus(new Vec2(0, 1))),
|
||||
)
|
||||
|
||||
return new Rect(origin, viewportRect.size.withX(width))
|
||||
}
|
||||
|
||||
private setConfigSpaceViewportRect = (viewportRect: Rect) => {
|
||||
this.setState({
|
||||
configSpaceViewportRect: this.clampViewportToFlamegraph(
|
||||
viewportRect,
|
||||
this.props.flamechart,
|
||||
this.props.renderInverted,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
private transformViewport = (transform: AffineTransform) => {
|
||||
this.setConfigSpaceViewportRect(transform.transformRect(this.state.configSpaceViewportRect))
|
||||
}
|
||||
|
||||
private formatValue(weight: number) {
|
||||
const totalWeight = this.props.flamechart.getTotalWeight()
|
||||
const percent = 100 * weight / totalWeight
|
||||
const formattedPercent = formatPercent(percent)
|
||||
return `${this.props.flamechart.formatValue(weight)} (${formattedPercent})`
|
||||
}
|
||||
|
||||
private renderTooltip() {
|
||||
if (!this.container) return null
|
||||
|
||||
const {hover} = this.state
|
||||
if (!hover) return null
|
||||
const {width, height, left, top} = this.container.getBoundingClientRect()
|
||||
const offset = new Vec2(hover.event.clientX - left, hover.event.clientY - top)
|
||||
|
||||
return (
|
||||
<Hovertip containerSize={new Vec2(width, height)} offset={offset}>
|
||||
<span className={css(style.hoverCount)}>
|
||||
{this.formatValue(hover.node.getTotalWeight())}
|
||||
</span>{' '}
|
||||
{hover.node.frame.name}
|
||||
</Hovertip>
|
||||
)
|
||||
}
|
||||
|
||||
container: HTMLDivElement | null = null
|
||||
containerRef = (container?: Element) => {
|
||||
this.container = (container as HTMLDivElement) || null
|
||||
}
|
||||
|
||||
private setNodeHover = (hover: {node: CallTreeNode; event: MouseEvent} | null) => {
|
||||
this.setState({hover})
|
||||
}
|
||||
|
||||
render() {
|
||||
const props: FlamechartPanZoomViewProps = {
|
||||
...(this.props as FlamechartWrapperProps),
|
||||
selectedNode: null,
|
||||
onNodeHover: this.setNodeHover,
|
||||
onNodeSelect: noop,
|
||||
configSpaceViewportRect: this.state.configSpaceViewportRect,
|
||||
setConfigSpaceViewportRect: this.setConfigSpaceViewportRect,
|
||||
transformViewport: this.transformViewport,
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={css(commonStyle.fillY, commonStyle.fillX, commonStyle.vbox)}
|
||||
ref={this.containerRef}
|
||||
>
|
||||
<FlamechartPanZoomView {...props} />
|
||||
{this.renderTooltip()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface SandwichViewProps {
|
||||
profile: Profile
|
||||
flattenRecursion: boolean
|
||||
|
||||
// TODO(jlfwong): It's kind of awkward requiring both of these
|
||||
getColorBucketForFrame: (frame: Frame) => number
|
||||
getCSSColorForFrame: (frame: Frame) => string
|
||||
|
||||
sortMethod: SortMethod
|
||||
setSortMethod: (sortMethod: SortMethod) => void
|
||||
canvasContext: CanvasContext
|
||||
rowAtlas: RowAtlas<FlamechartRowAtlasKey>
|
||||
}
|
||||
|
||||
interface CallerCalleeState {
|
||||
selectedFrame: Frame
|
||||
|
||||
invertedCallerFlamegraph: Flamechart
|
||||
invertedCallerFlamegraphRenderer: FlamechartRenderer
|
||||
|
||||
calleeFlamegraph: Flamechart
|
||||
calleeFlamegraphRenderer: FlamechartRenderer
|
||||
}
|
||||
|
||||
interface SandwichViewState {
|
||||
callerCallee: CallerCalleeState | null
|
||||
}
|
||||
|
||||
export class SandwichView extends ReloadableComponent<SandwichViewProps, SandwichViewState> {
|
||||
constructor(props: SandwichViewProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
callerCallee: null,
|
||||
}
|
||||
}
|
||||
|
||||
private setSelectedFrame = (
|
||||
selectedFrame: Frame | null,
|
||||
props: SandwichViewProps = this.props,
|
||||
) => {
|
||||
const {profile, canvasContext, rowAtlas, getColorBucketForFrame, flattenRecursion} = props
|
||||
|
||||
if (!selectedFrame) {
|
||||
this.setState({callerCallee: null})
|
||||
return
|
||||
}
|
||||
|
||||
let invertedCallerProfile = profile.getInvertedProfileForCallersOf(selectedFrame)
|
||||
if (flattenRecursion) {
|
||||
invertedCallerProfile = invertedCallerProfile.getProfileWithRecursionFlattened()
|
||||
}
|
||||
|
||||
const invertedCallerFlamegraph = new Flamechart({
|
||||
getTotalWeight: invertedCallerProfile.getTotalNonIdleWeight.bind(invertedCallerProfile),
|
||||
forEachCall: invertedCallerProfile.forEachCallGrouped.bind(invertedCallerProfile),
|
||||
formatValue: invertedCallerProfile.formatValue.bind(invertedCallerProfile),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const invertedCallerFlamegraphRenderer = new FlamechartRenderer(
|
||||
canvasContext,
|
||||
rowAtlas,
|
||||
invertedCallerFlamegraph,
|
||||
{inverted: true},
|
||||
)
|
||||
|
||||
let calleeProfile = profile.getProfileForCalleesOf(selectedFrame)
|
||||
|
||||
if (flattenRecursion) {
|
||||
calleeProfile = calleeProfile.getProfileWithRecursionFlattened()
|
||||
}
|
||||
|
||||
const calleeFlamegraph = new Flamechart({
|
||||
getTotalWeight: calleeProfile.getTotalNonIdleWeight.bind(calleeProfile),
|
||||
forEachCall: calleeProfile.forEachCallGrouped.bind(calleeProfile),
|
||||
formatValue: calleeProfile.formatValue.bind(calleeProfile),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const calleeFlamegraphRenderer = new FlamechartRenderer(
|
||||
canvasContext,
|
||||
rowAtlas,
|
||||
calleeFlamegraph,
|
||||
)
|
||||
|
||||
this.setState({
|
||||
callerCallee: {
|
||||
selectedFrame,
|
||||
invertedCallerFlamegraph,
|
||||
invertedCallerFlamegraphRenderer,
|
||||
calleeFlamegraph,
|
||||
calleeFlamegraphRenderer,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onWindowKeyPress = (ev: KeyboardEvent) => {
|
||||
if (ev.key === 'Escape') {
|
||||
this.setState({callerCallee: null})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: SandwichViewProps) {
|
||||
if (this.props.flattenRecursion !== nextProps.flattenRecursion) {
|
||||
if (this.state.callerCallee) {
|
||||
this.setSelectedFrame(this.state.callerCallee.selectedFrame, nextProps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('keydown', this.onWindowKeyPress)
|
||||
}
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('keydown', this.onWindowKeyPress)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {canvasContext} = this.props
|
||||
const {callerCallee} = this.state
|
||||
|
||||
let selectedFrame: Frame | null = null
|
||||
let flamegraphViews: JSX.Element | null = null
|
||||
|
||||
if (callerCallee) {
|
||||
selectedFrame = callerCallee.selectedFrame
|
||||
flamegraphViews = (
|
||||
<div className={css(commonStyle.fillY, style.callersAndCallees, commonStyle.vbox)}>
|
||||
<div className={css(commonStyle.hbox, style.panZoomViewWraper)}>
|
||||
<div className={css(style.flamechartLabelParent)}>
|
||||
<div className={css(style.flamechartLabel)}>Callers</div>
|
||||
</div>
|
||||
<FlamechartWrapper
|
||||
flamechart={callerCallee.invertedCallerFlamegraph}
|
||||
canvasContext={canvasContext}
|
||||
flamechartRenderer={callerCallee.invertedCallerFlamegraphRenderer}
|
||||
renderInverted={true}
|
||||
/>
|
||||
</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>
|
||||
<FlamechartWrapper
|
||||
flamechart={callerCallee.calleeFlamegraph}
|
||||
canvasContext={canvasContext}
|
||||
flamechartRenderer={callerCallee.calleeFlamegraphRenderer}
|
||||
renderInverted={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css(commonStyle.hbox, commonStyle.fillY)}>
|
||||
<div className={css(style.tableView)}>
|
||||
<ProfileTableView
|
||||
selectedFrame={selectedFrame}
|
||||
setSelectedFrame={this.setSelectedFrame}
|
||||
profile={this.props.profile}
|
||||
getCSSColorForFrame={this.props.getCSSColorForFrame}
|
||||
sortMethod={this.props.sortMethod}
|
||||
setSortMethod={this.props.setSortMethod}
|
||||
/>
|
||||
</div>
|
||||
{flamegraphViews}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
tableView: {
|
||||
flex: 1,
|
||||
},
|
||||
panZoomViewWraper: {
|
||||
flex: 1,
|
||||
},
|
||||
flamechartLabelParent: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-start',
|
||||
fontSize: FontSize.TITLE,
|
||||
width: FontSize.TITLE * 1.2,
|
||||
borderRight: `1px solid ${Colors.LIGHT_GRAY}`,
|
||||
},
|
||||
flamechartLabelParentBottom: {
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
flamechartLabel: {
|
||||
transform: 'rotate(-90deg)',
|
||||
transformOrigin: '50% 50% 0',
|
||||
width: FontSize.TITLE * 1.2,
|
||||
flexShrink: 1,
|
||||
},
|
||||
flamechartLabelBottom: {
|
||||
transform: 'rotate(-90deg)',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
callersAndCallees: {
|
||||
flex: 1,
|
||||
borderLeft: `${Sizes.SEPARATOR_HEIGHT}px solid ${Colors.LIGHT_GRAY}`,
|
||||
},
|
||||
divider: {
|
||||
height: 2,
|
||||
background: Colors.LIGHT_GRAY,
|
||||
},
|
||||
hoverCount: {
|
||||
color: Colors.GREEN,
|
||||
},
|
||||
})
|
||||
@@ -16,7 +16,7 @@ date > "$OUTDIR"/release.txt
|
||||
git rev-parse HEAD >> "$OUTDIR"/release.txt
|
||||
|
||||
# Place a json schema for the file format into the build directory too
|
||||
node generate-file-format-schema-json.js > "$OUTDIR"/file-format-schema.json
|
||||
node scripts/generate-file-format-schema-json.js > "$OUTDIR"/file-format-schema.json
|
||||
|
||||
# Build the compiled assets
|
||||
node_modules/.bin/parcel build index.html --no-cache --out-dir "$OUTDIR" --public-url "./" --detailed-report
|
||||
node_modules/.bin/parcel build assets/index.html --no-cache --out-dir "$OUTDIR" --public-url "./" --detailed-report
|
||||
@@ -10,7 +10,7 @@ set -e
|
||||
OUTDIR=`pwd`/dist/release
|
||||
echo $OUTDIR
|
||||
|
||||
./build-release.sh
|
||||
./scripts/build-release.sh
|
||||
|
||||
# Create a shallow clone of the repository
|
||||
TMPDIR=`mktemp -d -t speedscope-release`
|
||||
@@ -3,7 +3,7 @@ const child_process = require('child_process')
|
||||
|
||||
// Convert the file-format-spec.ts file into a json schema file
|
||||
let jsonSchema = child_process.execSync(
|
||||
'node_modules/.bin/quicktype --lang schema ./file-format-spec.ts',
|
||||
'node_modules/.bin/quicktype --lang schema ./src/lib/file-format-spec.ts',
|
||||
{
|
||||
encoding: 'utf8',
|
||||
},
|
||||
@@ -1,26 +0,0 @@
|
||||
import {h, render} from 'preact'
|
||||
import {Application} from './application'
|
||||
|
||||
console.log(`speedscope v${require('./package.json').version}`)
|
||||
|
||||
let app: Application | null = null
|
||||
const retained = (window as any)['__retained__'] as any
|
||||
declare const module: any
|
||||
if (module.hot) {
|
||||
module.hot.dispose(() => {
|
||||
if (app) {
|
||||
;(window as any)['__retained__'] = app.serialize()
|
||||
}
|
||||
})
|
||||
module.hot.accept()
|
||||
}
|
||||
|
||||
function ref(instance: Application | null) {
|
||||
app = instance
|
||||
if (instance && retained) {
|
||||
console.log('rehydrating: ', retained)
|
||||
instance.rehydrate(retained)
|
||||
}
|
||||
}
|
||||
|
||||
render(<Application ref={ref} />, document.body, document.body.lastElementChild || undefined)
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
TextureRenderer,
|
||||
TextureRendererProps,
|
||||
} from './texture-cached-renderer'
|
||||
import {StatsPanel} from './stats'
|
||||
import {StatsPanel} from '../lib/stats'
|
||||
|
||||
import {Vec2, Rect} from './math'
|
||||
import {Vec2, Rect} from '../lib/math'
|
||||
import {
|
||||
FlamechartColorPassRenderer,
|
||||
FlamechartColorPassRenderProps,
|
||||
@@ -53,6 +53,7 @@ export class CanvasContext {
|
||||
}, version: ${this.gl.limits.version}`,
|
||||
)
|
||||
;(window as any)['CanvasContext'] = this
|
||||
|
||||
this.rectangleBatchRenderer = new RectangleBatchRenderer(this.gl)
|
||||
this.viewportRectangleRenderer = new ViewportRectangleRenderer(this.gl)
|
||||
this.textureRenderer = new TextureRenderer(this.gl)
|
||||
@@ -1,5 +1,5 @@
|
||||
import regl from 'regl'
|
||||
import {Vec2, Rect, AffineTransform} from './math'
|
||||
import {Vec2, Rect, AffineTransform} from '../lib/math'
|
||||
|
||||
export interface FlamechartColorPassRenderProps {
|
||||
rectInfoTexture: regl.Texture
|
||||
@@ -1,10 +1,10 @@
|
||||
import regl from 'regl'
|
||||
import {Flamechart} from './flamechart'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {RectangleBatch} from './rectangle-batch-renderer'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
import {Vec2, Rect, AffineTransform} from './math'
|
||||
import {Color} from './color'
|
||||
import {KeyedSet} from './utils'
|
||||
import {Vec2, Rect, AffineTransform} from '../lib/math'
|
||||
import {Color} from '../lib/color'
|
||||
import {KeyedSet} from '../lib/utils'
|
||||
import {RowAtlas} from './row-atlas'
|
||||
|
||||
const MAX_BATCH_SIZE = 10000
|
||||
@@ -121,7 +121,7 @@ export class FlamechartRowAtlasKey {
|
||||
}
|
||||
}
|
||||
|
||||
interface RendererOptions {
|
||||
export interface FlamechartRendererOptions {
|
||||
inverted: boolean
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export class FlamechartRenderer {
|
||||
private canvasContext: CanvasContext,
|
||||
private rowAtlas: RowAtlas<FlamechartRowAtlasKey>,
|
||||
private flamechart: Flamechart,
|
||||
private options: RendererOptions = {inverted: false},
|
||||
private options: FlamechartRendererOptions = {inverted: false},
|
||||
) {
|
||||
const nLayers = flamechart.getLayers().length
|
||||
for (let stackDepth = 0; stackDepth < nLayers; stackDepth++) {
|
||||
@@ -1,5 +1,5 @@
|
||||
import regl from 'regl'
|
||||
import {AffineTransform, Rect} from './math'
|
||||
import {AffineTransform, Rect} from '../lib/math'
|
||||
|
||||
export interface ViewportRectangleRendererProps {
|
||||
configSpaceToPhysicalViewSpace: AffineTransform
|
||||
@@ -1,6 +1,6 @@
|
||||
import regl from 'regl'
|
||||
import {Rect, Vec2, AffineTransform} from './math'
|
||||
import {Color} from './color'
|
||||
import {Rect, Vec2, AffineTransform} from '../lib/math'
|
||||
import {Color} from '../lib/color'
|
||||
|
||||
export class RectangleBatch {
|
||||
private rectCapacity = 1000
|
||||
@@ -1,9 +1,9 @@
|
||||
import regl from 'regl'
|
||||
import {LRUCache} from './lru-cache'
|
||||
import {LRUCache} from '../lib/lru-cache'
|
||||
import {RectangleBatch} from './rectangle-batch-renderer'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
import {Rect, Vec2} from './math'
|
||||
import {Color} from './color'
|
||||
import {Rect, Vec2} from '../lib/math'
|
||||
import {Color} from '../lib/color'
|
||||
|
||||
export class RowAtlas<K> {
|
||||
texture: regl.Texture
|
||||
@@ -1,5 +1,5 @@
|
||||
import regl from 'regl'
|
||||
import {Vec2, Rect, AffineTransform} from './math'
|
||||
import {Vec2, Rect, AffineTransform} from '../lib/math'
|
||||
|
||||
export interface TextureRendererProps {
|
||||
texture: regl.Texture
|
||||
@@ -0,0 +1,122 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`importFromFirefox 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "file:///Users/jlfwong/code/speedscope/sample/simple.js",
|
||||
"key": "a (file:///Users/jlfwong/code/speedscope/sample/simple.js:1)",
|
||||
"line": 1,
|
||||
"name": "a",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 31.992820999992546,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "file:///Users/jlfwong/code/speedscope/sample/simple.js",
|
||||
"key": "b (file:///Users/jlfwong/code/speedscope/sample/simple.js:8)",
|
||||
"line": 8,
|
||||
"name": "b",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 20.971447000018088,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "file:///Users/jlfwong/code/speedscope/sample/simple.js",
|
||||
"key": "d (file:///Users/jlfwong/code/speedscope/sample/simple.js:20)",
|
||||
"line": 20,
|
||||
"name": "d",
|
||||
"selfWeight": 31.992820999992546,
|
||||
"totalWeight": 31.992820999992546,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "file:///Users/jlfwong/code/speedscope/sample/simple.js",
|
||||
"key": "c (file:///Users/jlfwong/code/speedscope/sample/simple.js:14)",
|
||||
"line": 14,
|
||||
"name": "c",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 11.021373999974458,
|
||||
},
|
||||
],
|
||||
"stacks": Array [
|
||||
"a;b;d 989.53µs",
|
||||
"a;c;d 1.02ms",
|
||||
"a;b;d 982.04µs",
|
||||
"a;c;d 1.02ms",
|
||||
"a;b;d 7.00ms",
|
||||
"a;c;d 2.00ms",
|
||||
"a;b;d 981.44µs",
|
||||
"a;c;d 2.00ms",
|
||||
"a;b;d 2.00ms",
|
||||
"a;c;d 3.02ms",
|
||||
"a;b;d 4.03ms",
|
||||
"a;c;d 1.96ms",
|
||||
"a;b;d 4.99ms",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`importFromFirefox recursion 1`] = `
|
||||
Object {
|
||||
"frames": Array [
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "file:///Users/jlfwong/code/speedscope/sample/programs/javascript/recursion.js",
|
||||
"key": "main (file:///Users/jlfwong/code/speedscope/sample/programs/javascript/recursion.js:31)",
|
||||
"line": 31,
|
||||
"name": "main",
|
||||
"selfWeight": 1.1944820000207983,
|
||||
"totalWeight": 11.149112000013702,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "file:///Users/jlfwong/code/speedscope/sample/programs/javascript/recursion.js",
|
||||
"key": "alpha (file:///Users/jlfwong/code/speedscope/sample/programs/javascript/recursion.js:1)",
|
||||
"line": 1,
|
||||
"name": "alpha",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 9.954629999992903,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "file:///Users/jlfwong/code/speedscope/sample/programs/javascript/recursion.js",
|
||||
"key": "beta (file:///Users/jlfwong/code/speedscope/sample/programs/javascript/recursion.js:10)",
|
||||
"line": 10,
|
||||
"name": "beta",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 8.962158999987878,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "file:///Users/jlfwong/code/speedscope/sample/programs/javascript/recursion.js",
|
||||
"key": "delta (file:///Users/jlfwong/code/speedscope/sample/programs/javascript/recursion.js:17)",
|
||||
"line": 17,
|
||||
"name": "delta",
|
||||
"selfWeight": 0,
|
||||
"totalWeight": 6.859736999962479,
|
||||
},
|
||||
Frame {
|
||||
"col": undefined,
|
||||
"file": "file:///Users/jlfwong/code/speedscope/sample/programs/javascript/recursion.js",
|
||||
"key": "gamma (file:///Users/jlfwong/code/speedscope/sample/programs/javascript/recursion.js:23)",
|
||||
"line": 23,
|
||||
"name": "gamma",
|
||||
"selfWeight": 9.954629999992903,
|
||||
"totalWeight": 9.954629999992903,
|
||||
},
|
||||
],
|
||||
"stacks": Array [
|
||||
"main;alpha;beta;alpha;beta;alpha;beta;alpha;delta;gamma 998.89µs",
|
||||
"main 1.19ms",
|
||||
"main;alpha;beta;alpha;delta;gamma 1.83ms",
|
||||
"main;alpha;beta;alpha;beta;alpha;beta;gamma 1.16ms",
|
||||
"main;alpha;beta;alpha;delta;gamma 1.84ms",
|
||||
"main;alpha;beta;alpha;beta;alpha;beta;alpha;gamma 964.81µs",
|
||||
"main;alpha;beta;alpha;beta;alpha;beta;alpha;delta;gamma 1.19ms",
|
||||
"main;alpha;beta;gamma 965.25µs",
|
||||
"main;alpha;delta;gamma 992.47µs",
|
||||
],
|
||||
}
|
||||
`;
|
||||
@@ -1,4 +1,4 @@
|
||||
import {checkProfileSnapshot} from '../test-utils'
|
||||
import {checkProfileSnapshot} from '../lib/test-utils'
|
||||
|
||||
test('importFromBGFlameGraph', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/stackcollapse/simple.txt')
|
||||
@@ -1,6 +1,6 @@
|
||||
// https://github.com/brendangregg/FlameGraph#2-fold-stacks
|
||||
|
||||
import {Profile, FrameInfo, StackListProfileBuilder} from '../profile'
|
||||
import {Profile, FrameInfo, StackListProfileBuilder} from '../lib/profile'
|
||||
|
||||
interface BGSample {
|
||||
stack: FrameInfo[]
|
||||
@@ -1,4 +1,4 @@
|
||||
import {checkProfileSnapshot} from '../test-utils'
|
||||
import {checkProfileSnapshot} from '../lib/test-utils'
|
||||
|
||||
test('importFromChromeCPUProfile', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/Chrome/65/simple.cpuprofile')
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Profile, FrameInfo, CallTreeProfileBuilder} from '../profile'
|
||||
import {getOrInsert, lastOf} from '../utils'
|
||||
import {TimeFormatter} from '../value-formatters'
|
||||
import {Profile, FrameInfo, CallTreeProfileBuilder} from '../lib/profile'
|
||||
import {getOrInsert, lastOf} from '../lib/utils'
|
||||
import {TimeFormatter} from '../lib/value-formatters'
|
||||
|
||||
interface TimelineEvent {
|
||||
pid: number
|
||||
@@ -0,0 +1,9 @@
|
||||
import {checkProfileSnapshot} from '../lib/test-utils'
|
||||
|
||||
test('importFromFirefox', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/Firefox/59/simple-firefox.json')
|
||||
})
|
||||
|
||||
test('importFromFirefox recursion', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/Firefox/61/recursion.json')
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Profile, FrameInfo, CallTreeProfileBuilder} from '../profile'
|
||||
import {getOrInsert, lastOf} from '../utils'
|
||||
import {TimeFormatter} from '../value-formatters'
|
||||
import {Profile, FrameInfo, CallTreeProfileBuilder} from '../lib/profile'
|
||||
import {getOrInsert} from '../lib/utils'
|
||||
import {TimeFormatter} from '../lib/value-formatters'
|
||||
|
||||
interface Allocations {
|
||||
frames: any[]
|
||||
@@ -158,7 +158,7 @@ export function importFromFirefox(firefoxProfile: FirefoxProfile): Profile {
|
||||
? cpuProfile.threads[0]
|
||||
: cpuProfile.threads.filter(t => t.name === 'GeckoMain')[0]
|
||||
|
||||
const frameIdToFrameInfo = new Map<number, FrameInfo>()
|
||||
const frameKeyToFrameInfo = new Map<string, FrameInfo>()
|
||||
|
||||
function extractStack(sample: Sample): FrameInfo[] {
|
||||
let stackFrameId: number | null = sample[0]
|
||||
@@ -185,7 +185,7 @@ export function importFromFirefox(firefoxProfile: FirefoxProfile): Profile {
|
||||
return null
|
||||
}
|
||||
|
||||
return getOrInsert(frameIdToFrameInfo, f, () => ({
|
||||
return getOrInsert(frameKeyToFrameInfo, location, () => ({
|
||||
key: location,
|
||||
name: match[1]!,
|
||||
file: match[2]!,
|
||||
@@ -203,27 +203,22 @@ export function importFromFirefox(firefoxProfile: FirefoxProfile): Profile {
|
||||
const value = sample[1]
|
||||
|
||||
// Find lowest common ancestor of the current stack and the previous one
|
||||
let lca: FrameInfo | null = null
|
||||
let lcaIndex = -1
|
||||
|
||||
// This is O(n^2), but n should be relatively small here (stack height),
|
||||
// so hopefully this isn't much of a problem
|
||||
for (let i = stack.length - 1; i >= 0 && prevStack.indexOf(stack[i]) === -1; i--) {}
|
||||
for (let i = 0; i < Math.min(stack.length, prevStack.length); i++) {
|
||||
if (prevStack[i] !== stack[i]) {
|
||||
break
|
||||
}
|
||||
lcaIndex = i
|
||||
}
|
||||
|
||||
// Close frames that are no longer open
|
||||
while (prevStack.length > 0 && lastOf(prevStack) != lca) {
|
||||
const closingFrame = prevStack.pop()!
|
||||
profile.leaveFrame(closingFrame, value)
|
||||
for (let i = prevStack.length - 1; i > lcaIndex; i--) {
|
||||
profile.leaveFrame(prevStack[i], value)
|
||||
}
|
||||
|
||||
// Open frames that are now becoming open
|
||||
const toOpen: FrameInfo[] = []
|
||||
for (let i = stack.length - 1; i >= 0 && stack[i] != lca; i--) {
|
||||
toOpen.push(stack[i])
|
||||
}
|
||||
toOpen.reverse()
|
||||
|
||||
for (let frame of toOpen) {
|
||||
profile.enterFrame(frame, value)
|
||||
for (let i = lcaIndex + 1; i < stack.length; i++) {
|
||||
profile.enterFrame(stack[i], value)
|
||||
}
|
||||
|
||||
prevStack = stack
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Profile} from '../profile'
|
||||
import {Profile} from '../lib/profile'
|
||||
import {FileSystemDirectoryEntry} from './file-system-entry'
|
||||
|
||||
import {importFromChromeCPUProfile, importFromChromeTimeline} from './chrome'
|
||||
@@ -6,8 +6,8 @@ import {importFromStackprof} from './stackprof'
|
||||
import {importFromInstrumentsDeepCopy, importFromInstrumentsTrace} from './instruments'
|
||||
import {importFromBGFlameGraph} from './bg-flamegraph'
|
||||
import {importFromFirefox} from './firefox'
|
||||
import {importSpeedscopeProfiles} from '../file-format'
|
||||
import {FileFormat} from '../file-format-spec'
|
||||
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> {
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import {dumpProfile, checkProfileSnapshot} from '../test-utils'
|
||||
import {dumpProfile, checkProfileSnapshot} from '../lib/test-utils'
|
||||
|
||||
import * as JSZip from 'jszip'
|
||||
import {FileSystemEntry} from './file-system-entry'
|
||||
@@ -1,10 +1,10 @@
|
||||
// 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 '../profile'
|
||||
import {sortBy, getOrThrow, getOrInsert, lastOf, getOrElse, zeroPad} from '../utils'
|
||||
import {Profile, FrameInfo, CallTreeProfileBuilder, StackListProfileBuilder} from '../lib/profile'
|
||||
import {sortBy, getOrThrow, getOrInsert, lastOf, getOrElse, zeroPad} from '../lib/utils'
|
||||
import * as pako from 'pako'
|
||||
import {ByteFormatter, TimeFormatter} from '../value-formatters'
|
||||
import {ByteFormatter, TimeFormatter} from '../lib/value-formatters'
|
||||
import {FileSystemDirectoryEntry, FileSystemEntry, FileSystemFileEntry} from './file-system-entry'
|
||||
|
||||
function parseTSV<T>(contents: string): T[] {
|
||||
@@ -184,6 +184,9 @@ class MaybeCompressedFileReader {
|
||||
this.fileData = new Promise(resolve => {
|
||||
const reader = new FileReader()
|
||||
reader.addEventListener('loadend', () => {
|
||||
if (!(reader.result instanceof ArrayBuffer)) {
|
||||
throw new Error('Expected reader.result to be an instance of ArrayBuffer')
|
||||
}
|
||||
resolve(reader.result)
|
||||
})
|
||||
reader.readAsArrayBuffer(file)
|
||||
@@ -410,11 +413,11 @@ async function readFormTemplate(tree: TraceDirectoryTree): Promise<FormTemplateD
|
||||
const addressToFrameMap = new Map<number, FrameInfo>()
|
||||
|
||||
// TODO(jlfwong): Deal with profiles with conflicts addresses?
|
||||
for (let [_pid, symbols] of symbolsByPid.entries()) {
|
||||
for (let symbols of symbolsByPid.values()) {
|
||||
for (let symbol of symbols.symbols) {
|
||||
if (!symbol) continue
|
||||
const {sourcePath, symbolName, addressToLine} = symbol
|
||||
for (let [address, _line] of addressToLine) {
|
||||
for (let address of addressToLine.keys()) {
|
||||
getOrInsert(addressToFrameMap, address, () => {
|
||||
const name = symbolName || `0x${zeroPad(address.toString(16), 16)}`
|
||||
const frame: FrameInfo = {
|
||||
@@ -1,4 +1,4 @@
|
||||
import {checkProfileSnapshot} from '../test-utils'
|
||||
import {checkProfileSnapshot} from '../lib/test-utils'
|
||||
|
||||
test('importFromStackprof', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/stackprof/simple-stackprof.json')
|
||||
@@ -1,7 +1,7 @@
|
||||
// https://github.com/tmm1/stackprof
|
||||
|
||||
import {Profile, FrameInfo, StackListProfileBuilder} from '../profile'
|
||||
import {TimeFormatter} from '../value-formatters'
|
||||
import {Profile, FrameInfo, StackListProfileBuilder} from '../lib/profile'
|
||||
import {TimeFormatter} from '../lib/value-formatters'
|
||||
|
||||
interface StackprofFrame {
|
||||
name: string
|
||||
@@ -1,4 +1,4 @@
|
||||
import {checkProfileSnapshot} from '../test-utils'
|
||||
import {checkProfileSnapshot} from '../lib/test-utils'
|
||||
|
||||
test('importFromV8ProfLog', async () => {
|
||||
await checkProfileSnapshot('./sample/profiles/node/8.5.0/simple.v8log.json')
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Profile, FrameInfo, StackListProfileBuilder} from '../profile'
|
||||
import {getOrInsert, sortBy} from '../utils'
|
||||
import {TimeFormatter} from '../value-formatters'
|
||||
import {Profile, FrameInfo, StackListProfileBuilder} from '../lib/profile'
|
||||
import {getOrInsert, sortBy} from '../lib/utils'
|
||||
import {TimeFormatter} from '../lib/value-formatters'
|
||||
|
||||
// This imports profiles generated by a combination of the following commands:
|
||||
//
|
||||
@@ -0,0 +1,47 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CallTreeProfileBuilder separates non-contiguous: append order 1`] = `
|
||||
"((speedscope root):0:5
|
||||
(a:1:5
|
||||
(b:1:2
|
||||
(c:1:1)
|
||||
)
|
||||
(b:1:2
|
||||
(c:1:1)
|
||||
)
|
||||
)
|
||||
)"
|
||||
`;
|
||||
|
||||
exports[`CallTreeProfileBuilder separates non-contiguous: grouped 1`] = `
|
||||
"((speedscope root):0:5
|
||||
(a:1:5
|
||||
(b:2:4
|
||||
(c:2:2)
|
||||
)
|
||||
)
|
||||
)"
|
||||
`;
|
||||
|
||||
exports[`StackListProfileBuilder separates non-contiguous: append order 1`] = `
|
||||
"((speedscope root):4:0
|
||||
(a:1:5
|
||||
(b:1:2
|
||||
(c:1:1)
|
||||
)
|
||||
(b:1:2
|
||||
(c:1:1)
|
||||
)
|
||||
)
|
||||
)"
|
||||
`;
|
||||
|
||||
exports[`StackListProfileBuilder separates non-contiguous: grouped 1`] = `
|
||||
"((speedscope root):4:0
|
||||
(a:1:5
|
||||
(b:2:4
|
||||
(c:2:2)
|
||||
)
|
||||
)
|
||||
)"
|
||||
`;
|
||||
@@ -0,0 +1,29 @@
|
||||
import {Color} from './color'
|
||||
|
||||
describe('Color', () => {
|
||||
test('fromLumaChromaHue', () => {
|
||||
expect(Color.fromLumaChromaHue(0, 0, 0).toCSS()).toEqual('rgba(0, 0, 0, 1.00)')
|
||||
expect(Color.fromLumaChromaHue(1, 0, 0).toCSS()).toEqual('rgba(255, 255, 255, 1.00)')
|
||||
|
||||
expect(Color.fromLumaChromaHue(0.5, 0, 30).toCSS()).toEqual('rgba(128, 128, 128, 1.00)')
|
||||
expect(Color.fromLumaChromaHue(0.5, 0, 60).toCSS()).toEqual('rgba(128, 128, 128, 1.00)')
|
||||
|
||||
expect(Color.fromLumaChromaHue(0.5, 0.5, 0).toCSS()).toEqual('rgba(217, 89, 89, 1.00)')
|
||||
|
||||
expect(Color.fromLumaChromaHue(0.5, 1.0, 0).toCSS()).toEqual('rgba(255, 51, 51, 1.00)')
|
||||
expect(Color.fromLumaChromaHue(0.5, 1.0, 60).toCSS()).toEqual('rgba(156, 156, 0, 1.00)')
|
||||
expect(Color.fromLumaChromaHue(0.5, 1.0, 120).toCSS()).toEqual('rgba(0, 232, 0, 1.00)')
|
||||
expect(Color.fromLumaChromaHue(0.5, 1.0, 180).toCSS()).toEqual('rgba(0, 204, 204, 1.00)')
|
||||
expect(Color.fromLumaChromaHue(0.5, 1.0, 240).toCSS()).toEqual('rgba(99, 99, 255, 1.00)')
|
||||
expect(Color.fromLumaChromaHue(0.5, 1.0, 300).toCSS()).toEqual('rgba(255, 23, 255, 1.00)')
|
||||
expect(Color.fromLumaChromaHue(0.5, 1.0, 360).toCSS()).toEqual('rgba(255, 51, 51, 1.00)')
|
||||
})
|
||||
|
||||
test('toCSS', () => {
|
||||
expect(new Color().toCSS()).toEqual('rgba(0, 0, 0, 1.00)')
|
||||
expect(new Color(1, 0, 0, 1).toCSS()).toEqual('rgba(255, 0, 0, 1.00)')
|
||||
expect(new Color(0, 1, 0, 1).toCSS()).toEqual('rgba(0, 255, 0, 1.00)')
|
||||
expect(new Color(0, 0, 1, 1).toCSS()).toEqual('rgba(0, 0, 255, 1.00)')
|
||||
expect(new Color(0, 0, 1, 0.599).toCSS()).toEqual('rgba(0, 0, 255, 0.60)')
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,5 @@
|
||||
import {clamp} from './math'
|
||||
|
||||
export class Color {
|
||||
constructor(
|
||||
readonly r: number = 0,
|
||||
@@ -7,6 +9,9 @@ export class Color {
|
||||
) {}
|
||||
|
||||
static fromLumaChromaHue(L: number, C: number, H: number) {
|
||||
// 0 <= L <= 1
|
||||
// 0 <= C <= 1
|
||||
// 0 <= H <= 360
|
||||
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_luma/chroma/hue
|
||||
|
||||
const hPrime = H / 60
|
||||
@@ -26,7 +31,7 @@ export class Color {
|
||||
|
||||
const m = L - (0.3 * R1 + 0.59 * G1 + 0.11 * B1)
|
||||
|
||||
return new Color(R1 + m, G1 + m, B1 + m, 1.0)
|
||||
return new Color(clamp(R1 + m, 0, 1), clamp(G1 + m, 0, 1), clamp(B1 + m, 0, 1), 1.0)
|
||||
}
|
||||
|
||||
toCSS(): string {
|
||||
@@ -0,0 +1,13 @@
|
||||
import {demangleCpp} from './demangle-cpp'
|
||||
|
||||
test('demangleCpp', () => {
|
||||
expect(demangleCpp('a')).toBe('a')
|
||||
expect(demangleCpp('someUnobfuscatedFunction')).toBe('someUnobfuscatedFunction')
|
||||
expect(demangleCpp('__ZNK7Support6ColorFeqERKS0_')).toBe(
|
||||
'Support::ColorF::operator==(Support::ColorF const&) const',
|
||||
)
|
||||
// Running a second time to test the cache
|
||||
expect(demangleCpp('__ZNK7Support6ColorFeqERKS0_')).toBe(
|
||||
'Support::ColorF::operator==(Support::ColorF const&) const',
|
||||
)
|
||||
})
|
||||
File diff suppressed because one or more lines are too long
@@ -21,10 +21,11 @@ test('importEmscriptenSymbolMap', () => {
|
||||
'a:A',
|
||||
'b:B',
|
||||
'c:C',
|
||||
'd:D-D',
|
||||
'',
|
||||
].join('\n'),
|
||||
),
|
||||
).toEqual(new Map([['a', 'A'], ['b', 'B'], ['c', 'C']]))
|
||||
).toEqual(new Map([['a', 'A'], ['b', 'B'], ['c', 'C'], ['d', 'D-D']]))
|
||||
|
||||
// Valid symbol map with non-alpha characters
|
||||
expect(importEmscriptenSymbolMap('u6:__ZN8tinyxml210XMLCommentD0Ev\n')).toEqual(
|
||||
@@ -39,10 +40,16 @@ test('importEmscriptenSymbolMap', () => {
|
||||
'0:A',
|
||||
'1:B',
|
||||
'2:C',
|
||||
'3:D-D',
|
||||
].join('\n'),
|
||||
),
|
||||
).toEqual(
|
||||
new Map([['wasm-function[0]', 'A'], ['wasm-function[1]', 'B'], ['wasm-function[2]', 'C']]),
|
||||
new Map([
|
||||
['wasm-function[0]', 'A'],
|
||||
['wasm-function[1]', 'B'],
|
||||
['wasm-function[2]', 'C'],
|
||||
['wasm-function[3]', 'D-D'],
|
||||
]),
|
||||
)
|
||||
|
||||
// Invalid symbol map
|
||||
@@ -14,8 +14,8 @@ export function importEmscriptenSymbolMap(contents: string): EmscriptenSymbolMap
|
||||
if (!lines.length) return null
|
||||
|
||||
const map: EmscriptenSymbolMap = new Map()
|
||||
const intRegex = /^(\d+):([\$\w]+)$/
|
||||
const idRegex = /^([\$\w]+):([\$\w]+)$/
|
||||
const intRegex = /^(\d+):([\$\w-]+)$/
|
||||
const idRegex = /^([\$\w]+):([\$\w-]+)$/
|
||||
|
||||
for (const line of lines) {
|
||||
// Match lines like "103:__ZN8tinyxml210XMLCommentD0Ev"
|
||||
@@ -22,7 +22,7 @@ export function exportProfile(profile: Profile): FileFormat.File {
|
||||
}
|
||||
|
||||
const file: FileFormat.File = {
|
||||
version: require('./package.json').version,
|
||||
version: require('../../package.json').version,
|
||||
$schema: 'https://www.speedscope.app/file-format-schema.json',
|
||||
shared: {frames},
|
||||
profiles: [eventedProfile],
|
||||
@@ -1,6 +1,7 @@
|
||||
import {Frame, CallTreeNode} from './profile'
|
||||
|
||||
import {lastOf} from './utils'
|
||||
import {clamp} from './math'
|
||||
|
||||
export interface FlamechartFrame {
|
||||
node: CallTreeNode
|
||||
@@ -47,6 +48,48 @@ export class Flamechart {
|
||||
return this.source.formatValue(v)
|
||||
}
|
||||
|
||||
getClampedViewportWidth(viewportWidth: number) {
|
||||
const maxWidth = this.getTotalWeight()
|
||||
|
||||
// In order to avoid floating point error, we cap the maximum zoom. In
|
||||
// particular, it's important that at the maximum zoom level, the total
|
||||
// trace size + a viewport width is not equal to the trace size due to
|
||||
// floating point rounding.
|
||||
//
|
||||
// For instance, if the profile's total weight is 2^60, and the viewport
|
||||
// size is 1, trying to move one viewport width right will result in no
|
||||
// change because 2^60 + 1 = 2^60 in floating point arithmetic. JavaScript
|
||||
// numbers are 64 bit floats, and therefore have 53 mantissa bits. You can
|
||||
// see this for yourself in the console. Try:
|
||||
//
|
||||
// > Math.pow(2, 60) + 1 === Math.pow(2, 60)
|
||||
// true
|
||||
// > Math.pow(2, 53) + 1 === Math.pow(2, 53)
|
||||
// true
|
||||
// > Math.pow(2, 52) + 1 === Math.pow(2, 52)
|
||||
// false
|
||||
//
|
||||
// We use 2^40 as a cap instead, since we want to be able to make small
|
||||
// adjustments within a viewport width.
|
||||
//
|
||||
// For reference, this will still allow you to zoom until 1 nanosecond fills
|
||||
// the screen in a profile with a duration of over 18 minutes.
|
||||
//
|
||||
// > Math.pow(2, 40) / (60 * Math.pow(10, 9))
|
||||
// 18.325193796266667
|
||||
//
|
||||
const maxZoom = Math.pow(2, 40)
|
||||
|
||||
// In addition to capping zoom to avoid floating point error, we further cap
|
||||
// zoom to avoid letting you zoom in so that the smallest element more than
|
||||
// fills the screen, since that probably isn't useful. The final zoom cap is
|
||||
// determined by the minimum zoom of either 2^40x zoom or the necessary zoom
|
||||
// for the smallest frame to fill the screen three times.
|
||||
const minWidth = clamp(3 * this.getMinFrameWidth(), maxWidth / maxZoom, maxWidth)
|
||||
|
||||
return clamp(viewportWidth, minWidth, maxWidth)
|
||||
}
|
||||
|
||||
constructor(private source: FlamechartDataSource) {
|
||||
const stack: FlamechartFrame[] = []
|
||||
const openFrame = (node: CallTreeNode, value: number) => {
|
||||
@@ -0,0 +1,27 @@
|
||||
import {getHashParams} from './hash-params'
|
||||
|
||||
test('getHashParams', () => {
|
||||
expect(getHashParams('')).toEqual({})
|
||||
expect(getHashParams('#')).toEqual({})
|
||||
expect(getHashParams('#title=hello')).toEqual({title: 'hello'})
|
||||
expect(getHashParams('#localProfilePath=file:///tmp/file.js')).toEqual({
|
||||
localProfilePath: 'file:///tmp/file.js',
|
||||
})
|
||||
expect(
|
||||
getHashParams(
|
||||
'#profileURL=https://raw.githubusercontent.com/jlfwong/speedscope/master/sample/profiles/speedscope/0.1.2/simple-sampled.speedscope.json',
|
||||
),
|
||||
).toEqual({
|
||||
profileURL:
|
||||
'https://raw.githubusercontent.com/jlfwong/speedscope/master/sample/profiles/speedscope/0.1.2/simple-sampled.speedscope.json',
|
||||
})
|
||||
expect(getHashParams('#title=hello&localProfilePath=file:///tmp/file.js')).toEqual({
|
||||
title: 'hello',
|
||||
localProfilePath: 'file:///tmp/file.js',
|
||||
})
|
||||
expect(getHashParams('#title=hello%20world')).toEqual({
|
||||
title: 'hello world',
|
||||
})
|
||||
expect(getHashParams('#abc=bcd')).toEqual({})
|
||||
expect(getHashParams('garbage')).toEqual({})
|
||||
})
|
||||
@@ -4,22 +4,22 @@ export interface HashParams {
|
||||
localProfilePath?: string
|
||||
}
|
||||
|
||||
export function getHashParams(): HashParams {
|
||||
export function getHashParams(hashContents = window.location.hash): HashParams {
|
||||
try {
|
||||
const hashContents = window.location.hash
|
||||
if (!hashContents.startsWith('#')) {
|
||||
return {}
|
||||
}
|
||||
const components = hashContents.substr(1).split('&')
|
||||
const result: HashParams = {}
|
||||
for (const component of components) {
|
||||
const [key, value] = component.split('=')
|
||||
let [key, value] = component.split('=')
|
||||
value = decodeURIComponent(value)
|
||||
if (key === 'profileURL') {
|
||||
result.profileURL = decodeURIComponent(value)
|
||||
result.profileURL = value
|
||||
} else if (key === 'title') {
|
||||
result.title = decodeURIComponent(value)
|
||||
result.title = value
|
||||
} else if (key === 'localProfilePath') {
|
||||
result.localProfilePath = decodeURIComponent(value)
|
||||
result.localProfilePath = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -50,6 +50,30 @@ function toStackList(profile: Profile, grouped: boolean): string[] {
|
||||
return stackList
|
||||
}
|
||||
|
||||
function flatten<T>(ts: T[][]): T[] {
|
||||
let ret: T[] = []
|
||||
return ret.concat(...ts)
|
||||
}
|
||||
|
||||
function toTreeString(profile: Profile, grouped: boolean): string {
|
||||
function visit(node: CallTreeNode): string[] {
|
||||
const childLines = flatten(node.children.map(child => visit(child))).map(l => ` ${l}`)
|
||||
const nodeStr = `${node.frame.key}:${node.getSelfWeight()}:${node.getTotalWeight()}`
|
||||
|
||||
if (childLines.length > 0) {
|
||||
return [`(${nodeStr}`].concat(childLines).concat(')')
|
||||
} else {
|
||||
return [`(${nodeStr})`]
|
||||
}
|
||||
}
|
||||
|
||||
if (grouped) {
|
||||
return visit(profile.getGroupedCalltreeRoot()).join('\n')
|
||||
} else {
|
||||
return visit(profile.getAppendOrderCalltreeRoot()).join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
function verifyProfile(profile: Profile) {
|
||||
const allFrameKeys = new Set([fa, fb, fc, fd, fe].map(f => f.key))
|
||||
const framesInProfile = new Set<string | number>()
|
||||
@@ -133,6 +157,28 @@ test('StackListProfileBuilder', () => {
|
||||
verifyProfile(profile)
|
||||
})
|
||||
|
||||
test('StackListProfileBuilder separates non-contiguous', () => {
|
||||
const b = new StackListProfileBuilder()
|
||||
|
||||
const samples = [
|
||||
// prettier-ignore
|
||||
[fa, fb, fc],
|
||||
[fa, fb],
|
||||
[fa],
|
||||
[fa, fb],
|
||||
[fa, fb, fc],
|
||||
]
|
||||
|
||||
samples.forEach(stack => {
|
||||
b.appendSample(stack, 1)
|
||||
})
|
||||
b.appendSample([], 4)
|
||||
const profile = b.build()
|
||||
|
||||
expect(toTreeString(profile, true)).toMatchSnapshot('grouped')
|
||||
expect(toTreeString(profile, false)).toMatchSnapshot('append order')
|
||||
})
|
||||
|
||||
test('CallTreeProfileBuilder', () => {
|
||||
const b = new CallTreeProfileBuilder()
|
||||
|
||||
@@ -167,6 +213,30 @@ test('CallTreeProfileBuilder', () => {
|
||||
verifyProfile(profile)
|
||||
})
|
||||
|
||||
test('CallTreeProfileBuilder separates non-contiguous', () => {
|
||||
const b = new CallTreeProfileBuilder()
|
||||
|
||||
b.enterFrame(fa, 0)
|
||||
b.enterFrame(fb, 0)
|
||||
b.enterFrame(fc, 0)
|
||||
|
||||
b.leaveFrame(fc, 1)
|
||||
|
||||
b.leaveFrame(fb, 2)
|
||||
|
||||
b.enterFrame(fb, 3)
|
||||
|
||||
b.enterFrame(fc, 4)
|
||||
|
||||
b.leaveFrame(fc, 5)
|
||||
b.leaveFrame(fb, 5)
|
||||
b.leaveFrame(fa, 5)
|
||||
|
||||
const profile = b.build()
|
||||
expect(toTreeString(profile, true)).toMatchSnapshot('grouped')
|
||||
expect(toTreeString(profile, false)).toMatchSnapshot('append order')
|
||||
})
|
||||
|
||||
test('getInvertedProfileForCallersOf', () => {
|
||||
const b = new StackListProfileBuilder()
|
||||
|
||||
@@ -89,6 +89,15 @@ export class CallTreeNode extends HasWeights {
|
||||
return this.frame === Frame.root
|
||||
}
|
||||
|
||||
// If a node is "frozen", it means it should no longer be mutated.
|
||||
private frozen = false
|
||||
isFrozen() {
|
||||
return this.frozen
|
||||
}
|
||||
freeze() {
|
||||
this.frozen = true
|
||||
}
|
||||
|
||||
constructor(readonly frame: Frame, readonly parent: CallTreeNode | null) {
|
||||
super()
|
||||
}
|
||||
@@ -100,9 +109,17 @@ export class Profile {
|
||||
protected totalWeight: number
|
||||
|
||||
protected frames = new KeyedSet<Frame>()
|
||||
|
||||
protected appendOrderCalltreeRoot = new CallTreeNode(Frame.root, null)
|
||||
protected groupedCalltreeRoot = new CallTreeNode(Frame.root, null)
|
||||
|
||||
public getAppendOrderCalltreeRoot() {
|
||||
return this.appendOrderCalltreeRoot
|
||||
}
|
||||
public getGroupedCalltreeRoot() {
|
||||
return this.groupedCalltreeRoot
|
||||
}
|
||||
|
||||
// List of references to CallTreeNodes at the top of the
|
||||
// stack at the time of the sample.
|
||||
protected samples: CallTreeNode[] = []
|
||||
@@ -395,7 +412,7 @@ export class StackListProfileBuilder extends Profile {
|
||||
const last = useAppendOrder
|
||||
? lastOf(node.children)
|
||||
: node.children.find(c => c.frame === frame)
|
||||
if (last && last.frame == frame) {
|
||||
if (last && !last.isFrozen() && last.frame == frame) {
|
||||
node = last
|
||||
} else {
|
||||
const parent = node
|
||||
@@ -414,6 +431,12 @@ export class StackListProfileBuilder extends Profile {
|
||||
}
|
||||
node.addToSelfWeight(weight)
|
||||
|
||||
if (useAppendOrder) {
|
||||
for (let child of node.children) {
|
||||
child.freeze()
|
||||
}
|
||||
}
|
||||
|
||||
if (useAppendOrder) {
|
||||
node.frame.addToSelfWeight(weight)
|
||||
|
||||
@@ -493,7 +516,7 @@ export class CallTreeProfileBuilder extends Profile {
|
||||
? lastOf(prevTop.children)
|
||||
: prevTop.children.find(c => c.frame === frame)
|
||||
let node: CallTreeNode
|
||||
if (last && last.frame == frame) {
|
||||
if (last && !last.isFrozen() && last.frame == frame) {
|
||||
node = last
|
||||
} else {
|
||||
node = new CallTreeNode(frame, prevTop)
|
||||
@@ -520,10 +543,17 @@ export class CallTreeProfileBuilder extends Profile {
|
||||
|
||||
if (useAppendOrder) {
|
||||
const leavingStackTop = this.appendOrderStack.pop()
|
||||
const delta = value - this.lastValue!
|
||||
if (leavingStackTop == null) {
|
||||
throw new Error(`Trying to leave ${frame.key} when stack is empty`)
|
||||
}
|
||||
if (this.lastValue == null) {
|
||||
throw new Error(`Trying to leave a ${frame.key} before any have been entered`)
|
||||
}
|
||||
leavingStackTop.freeze()
|
||||
const delta = value - this.lastValue
|
||||
if (delta > 0) {
|
||||
this.samples.push(leavingStackTop!)
|
||||
this.weights.push(value - this.lastValue!)
|
||||
this.samples.push(leavingStackTop)
|
||||
this.weights.push(value - this.lastValue)
|
||||
} else if (delta < 0) {
|
||||
throw new Error(
|
||||
`Samples must be provided in increasing order of cumulative value. Last sample was ${this
|
||||
@@ -556,6 +586,11 @@ export class CallTreeProfileBuilder extends Profile {
|
||||
}
|
||||
|
||||
build(): Profile {
|
||||
// Each stack is expected to contain a single node which we initialize to be
|
||||
// the root node.
|
||||
if (this.appendOrderStack.length > 1 || this.groupedOrderStack.length > 1) {
|
||||
throw new Error('Tried to complete profile construction with a non-empty stack')
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import {Profile, CallTreeNode, Frame} from './profile'
|
||||
import {importProfile} from './import'
|
||||
import {importProfile} from '../import'
|
||||
import {exportProfile, importSpeedscopeProfiles} from './file-format'
|
||||
|
||||
interface DumpedProfile {
|
||||
@@ -0,0 +1,72 @@
|
||||
import {connect} from 'preact-redux'
|
||||
import * as redux from 'redux'
|
||||
import {ComponentConstructor, Component} from 'preact'
|
||||
|
||||
export interface Action<TPayload> extends redux.Action<string> {
|
||||
payload: TPayload
|
||||
}
|
||||
|
||||
export interface ActionCreator<TPayload> {
|
||||
// Returns an action with a non-empty payload
|
||||
(payload: TPayload): Action<TPayload>
|
||||
|
||||
// Returns an action with an empty payload ({})
|
||||
(): Action<TPayload>
|
||||
|
||||
matches(action: Action<any>): action is Action<TPayload>
|
||||
}
|
||||
|
||||
const usedActionTypes = new Set<string>()
|
||||
|
||||
export function actionCreator(type: string): ActionCreator<void>
|
||||
export function actionCreator<TPayload>(type: string): ActionCreator<TPayload>
|
||||
export function actionCreator(type: string) {
|
||||
if (usedActionTypes.has(type)) {
|
||||
throw new Error(`Cannot re-use action type name: ${type}`)
|
||||
}
|
||||
|
||||
const creator: any = (payload = {}) => {
|
||||
return {type, payload}
|
||||
}
|
||||
|
||||
creator.matches = (action: Action<any>) => {
|
||||
return action.type === type
|
||||
}
|
||||
|
||||
return creator
|
||||
}
|
||||
|
||||
export type Reducer<T> = (state: T | undefined, action: Action<any>) => T
|
||||
|
||||
export function setter<T>(
|
||||
setterAction: ActionCreator<T>,
|
||||
defaultVal: T,
|
||||
): (state: T | undefined, action: Action<any>) => T {
|
||||
return (state = defaultVal, action) => {
|
||||
if (setterAction.matches(action)) {
|
||||
return action.payload
|
||||
}
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
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>(
|
||||
component: {
|
||||
new (props: OwnProps & PropsFromState & {dispatch: Dispatch}): ComponentType
|
||||
},
|
||||
mapStateToProps: (state: State, ownProps: OwnProps) => PropsFromState,
|
||||
): ComponentConstructor<OwnProps, {}> {
|
||||
return connect(mapStateToProps, (dispatch: Dispatch) => ({dispatch}))(component)
|
||||
}
|
||||
|
||||
export type VoidState = {
|
||||
__dummyField: void
|
||||
}
|
||||
|
||||
export abstract class StatelessComponent<P> extends Component<P, VoidState> {}
|
||||
@@ -121,3 +121,50 @@ export function binarySearch(
|
||||
}
|
||||
|
||||
export function noop(...args: any[]) {}
|
||||
|
||||
function shallowEquals<T extends object>(a: T, b: T): boolean {
|
||||
for (let key in a) {
|
||||
if (a[key] !== b[key]) return false
|
||||
}
|
||||
for (let key in b) {
|
||||
if (a[key] !== b[key]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO(jlfwong): Write tests for this
|
||||
export function memoizeByShallowEquality<T extends object, U>(cb: (t: T) => U): (t: T) => U {
|
||||
let last: {args: T; result: U} | null = null
|
||||
return (args: T) => {
|
||||
let result: U
|
||||
if (last == null) {
|
||||
result = cb(args)
|
||||
last = {args, result}
|
||||
return result
|
||||
} else if (shallowEquals(last.args, args)) {
|
||||
return last.result
|
||||
} else {
|
||||
last.args = args
|
||||
last.result = cb(args)
|
||||
return last.result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function memoizeByReference<T, U>(cb: (t: T) => U): (t: T) => U {
|
||||
let last: {args: T; result: U} | null = null
|
||||
return (args: T) => {
|
||||
let result: U
|
||||
if (last == null) {
|
||||
result = cb(args)
|
||||
last = {args, result}
|
||||
return result
|
||||
} else if (last.args === args) {
|
||||
return last.result
|
||||
} else {
|
||||
last.args = args
|
||||
last.result = cb(args)
|
||||
return last.result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import {h, render} from 'preact'
|
||||
import {createApplicationStore, ApplicationState} from './store'
|
||||
import {Provider} from 'preact-redux'
|
||||
import {createContainer} from './lib/typed-redux'
|
||||
import {Application} from './views/application'
|
||||
|
||||
console.log(`speedscope v${require('../package.json').version}`)
|
||||
|
||||
declare const module: any
|
||||
if (module.hot) {
|
||||
module.hot.dispose(() => {
|
||||
// Force the old component go through teardown steps
|
||||
render(<div />, document.body, document.body.lastElementChild || undefined)
|
||||
})
|
||||
module.hot.accept()
|
||||
}
|
||||
|
||||
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 />
|
||||
</Provider>,
|
||||
document.body,
|
||||
document.body.lastElementChild || undefined,
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
import {actionCreator} from '../lib/typed-redux'
|
||||
import {Profile, CallTreeNode, Frame} 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'
|
||||
|
||||
export namespace actions {
|
||||
// Set the top-level profile from which other data will be derived
|
||||
export const setProfile = actionCreator<Profile>('setProfile')
|
||||
|
||||
// Set the profile currently being viewed
|
||||
export const setActiveProfile = actionCreator<Profile>('setActiveProfile')
|
||||
|
||||
export const setFrameToColorBucket = actionCreator<Map<string | number, number>>(
|
||||
'setFrameToColorBucket',
|
||||
)
|
||||
|
||||
export const setGLCanvas = actionCreator<HTMLCanvasElement | null>('setGLCanvas')
|
||||
|
||||
// Set which top-level view should be displayed
|
||||
export const setViewMode = actionCreator<ViewMode>('setViewMode')
|
||||
|
||||
// Set whether or not recursion should be flattened when viewing flamegraphs
|
||||
export const setFlattenRecursion = actionCreator<boolean>('setFlattenRecursion')
|
||||
|
||||
// Set whether a file drag is currently active. Used to indicate that the
|
||||
// application is a valid drop target.
|
||||
export const setDragActive = actionCreator<boolean>('setDragActive')
|
||||
|
||||
// Set whether the application is currently in a loading state. Used to
|
||||
// display a loading progress bar.
|
||||
export const setLoading = actionCreator<boolean>('setLoading')
|
||||
|
||||
// Set whether the application is in an errored state.
|
||||
export const setError = actionCreator<boolean>('setError')
|
||||
|
||||
// Set whether parameters defined by the URL encoded k=v pairs after the # in the URL
|
||||
export const setHashParams = actionCreator<HashParams>('setHashParams')
|
||||
|
||||
export namespace sandwichView {
|
||||
// Set the table sorting method used for the sandwich view.
|
||||
export const setTableSortMethod = actionCreator<SortMethod>('sandwichView.setTableSortMethod')
|
||||
|
||||
export const setSelectedFrame = actionCreator<Frame | null>('sandwichView.setSelectedFarmr')
|
||||
}
|
||||
|
||||
export namespace flamechart {
|
||||
export const setHoveredNode = actionCreator<{
|
||||
id: FlamechartID
|
||||
hover: {node: CallTreeNode; event: MouseEvent} | null
|
||||
}>('flamechart.setHoveredNode')
|
||||
|
||||
export const setSelectedNode = actionCreator<{
|
||||
id: FlamechartID
|
||||
selectedNode: CallTreeNode | null
|
||||
}>('flamechart.setSelectedNode')
|
||||
|
||||
export const setConfigSpaceViewportRect = actionCreator<{
|
||||
id: FlamechartID
|
||||
configSpaceViewportRect: Rect
|
||||
}>('flamechart.setConfigSpaceViewportRect')
|
||||
|
||||
export const setLogicalSpaceViewportSize = actionCreator<{
|
||||
id: FlamechartID
|
||||
logicalSpaceViewportSize: Vec2
|
||||
}>('flamechart.setLogicalSpaceViewportSpace')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import {CallTreeNode} from '../lib/profile'
|
||||
import {Rect, Vec2} from '../lib/math'
|
||||
import {Reducer} from '../lib/typed-redux'
|
||||
import {actions} from './actions'
|
||||
|
||||
export enum FlamechartID {
|
||||
LEFT_HEAVY = 'LEFT_HEAVY',
|
||||
CHRONO = 'CHRONO',
|
||||
SANDWICH_INVERTED_CALLERS = 'SANDWICH_INVERTED_CALLERS',
|
||||
SANDWICH_CALLEES = 'SANDWICH_CALLEES',
|
||||
}
|
||||
|
||||
export interface FlamechartViewState {
|
||||
hover: {
|
||||
node: CallTreeNode
|
||||
event: MouseEvent
|
||||
} | null
|
||||
selectedNode: CallTreeNode | null
|
||||
logicalSpaceViewportSize: Vec2
|
||||
configSpaceViewportRect: Rect
|
||||
}
|
||||
|
||||
export function createFlamechartViewStateReducer(id: FlamechartID): Reducer<FlamechartViewState> {
|
||||
let initialState: FlamechartViewState = {
|
||||
hover: null,
|
||||
selectedNode: null,
|
||||
configSpaceViewportRect: Rect.empty,
|
||||
logicalSpaceViewportSize: Vec2.zero,
|
||||
}
|
||||
return (state = initialState, action) => {
|
||||
if (actions.flamechart.setHoveredNode.matches(action) && action.payload.id === id) {
|
||||
const {hover} = action.payload
|
||||
return {...state, hover}
|
||||
}
|
||||
if (actions.flamechart.setSelectedNode.matches(action) && action.payload.id === id) {
|
||||
const {selectedNode} = action.payload
|
||||
return {...state, selectedNode}
|
||||
}
|
||||
if (actions.flamechart.setConfigSpaceViewportRect.matches(action) && action.payload.id === id) {
|
||||
const {configSpaceViewportRect} = action.payload
|
||||
return {...state, configSpaceViewportRect}
|
||||
}
|
||||
if (
|
||||
actions.flamechart.setLogicalSpaceViewportSize.matches(action) &&
|
||||
action.payload.id === id
|
||||
) {
|
||||
const {logicalSpaceViewportSize} = action.payload
|
||||
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}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import {Frame, Profile} from '../lib/profile'
|
||||
import {triangle, memoizeByReference, memoizeByShallowEquality} from '../lib/utils'
|
||||
import {RowAtlas} from '../gl/row-atlas'
|
||||
import {CanvasContext} from '../gl/canvas-context'
|
||||
import {Color} from '../lib/color'
|
||||
import {FlamechartRowAtlasKey} from '../gl/flamechart-renderer'
|
||||
|
||||
export const createGetColorBucketForFrame = memoizeByReference(
|
||||
(frameToColorBucket: Map<number | string, number>) => {
|
||||
return (frame: Frame): number => {
|
||||
return frameToColorBucket.get(frame.key) || 0
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const createGetCSSColorForFrame = memoizeByReference(
|
||||
(frameToColorBucket: Map<number | string, number>) => {
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
return (frame: Frame): string => {
|
||||
const t = getColorBucketForFrame(frame) / 255
|
||||
|
||||
const x = triangle(30.0 * t)
|
||||
const H = 360.0 * (0.9 * t)
|
||||
const C = 0.25 + 0.2 * x
|
||||
const L = 0.8 - 0.15 * x
|
||||
return Color.fromLumaChromaHue(L, C, H).toCSS()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const getCanvasContext = memoizeByReference((canvas: HTMLCanvasElement) => {
|
||||
return new CanvasContext(canvas)
|
||||
})
|
||||
|
||||
export const getRowAtlas = memoizeByReference((canvasContext: CanvasContext) => {
|
||||
return new RowAtlas<FlamechartRowAtlasKey>(canvasContext)
|
||||
})
|
||||
|
||||
export const getProfileWithRecursionFlattened = memoizeByReference((profile: Profile) =>
|
||||
profile.getProfileWithRecursionFlattened(),
|
||||
)
|
||||
|
||||
export const getProfileToView = memoizeByShallowEquality(
|
||||
({profile, flattenRecursion}: {profile: Profile; flattenRecursion: boolean}): Profile => {
|
||||
return flattenRecursion ? profile.getProfileWithRecursionFlattened() : profile
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
import {actions} from './actions'
|
||||
|
||||
/**
|
||||
* The root node of application state. We use redux (https://redux.js.org/)
|
||||
* as our state management solution.
|
||||
*/
|
||||
|
||||
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'
|
||||
|
||||
export const enum ViewMode {
|
||||
CHRONO_FLAME_CHART,
|
||||
LEFT_HEAVY_FLAME_GRAPH,
|
||||
SANDWICH_VIEW,
|
||||
}
|
||||
|
||||
export interface ApplicationState {
|
||||
profile: Profile | null
|
||||
frameToColorBucket: Map<string | number, number>
|
||||
|
||||
hashParams: HashParams
|
||||
|
||||
glCanvas: HTMLCanvasElement | null
|
||||
|
||||
flattenRecursion: boolean
|
||||
|
||||
viewMode: ViewMode
|
||||
dragActive: boolean
|
||||
loading: boolean
|
||||
error: boolean
|
||||
|
||||
chronoView: FlamechartViewState
|
||||
leftHeavyView: FlamechartViewState
|
||||
sandwichView: SandwichViewState
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol
|
||||
|
||||
// Speedscope is usable both from a local HTML file being served
|
||||
// from a file:// URL, and via websites. In the case of file:// URLs,
|
||||
// however, XHR will be unavailable to fetching files in adjacent directories.
|
||||
export const canUseXHR = protocol === 'http:' || protocol === 'https:'
|
||||
|
||||
export function createApplicationStore(
|
||||
initialState: Partial<ApplicationState>,
|
||||
): redux.Store<ApplicationState> {
|
||||
const hashParams = getHashParams()
|
||||
|
||||
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(),
|
||||
),
|
||||
|
||||
hashParams: setter<HashParams>(actions.setHashParams, hashParams),
|
||||
|
||||
flattenRecursion: setter<boolean>(actions.setFlattenRecursion, false),
|
||||
|
||||
viewMode: setter<ViewMode>(actions.setViewMode, ViewMode.CHRONO_FLAME_CHART),
|
||||
|
||||
glCanvas: setter<HTMLCanvasElement | null>(actions.setGLCanvas, null),
|
||||
|
||||
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,86 @@
|
||||
import {SortMethod, SortField, SortDirection} from '../views/profile-table-view'
|
||||
import {Frame} from '../lib/profile'
|
||||
import {
|
||||
FlamechartViewState,
|
||||
FlamechartID,
|
||||
createFlamechartViewStateReducer,
|
||||
} from './flamechart-view-state'
|
||||
import {Reducer} from '../lib/typed-redux'
|
||||
import {actions} from './actions'
|
||||
|
||||
export interface SandwichViewState {
|
||||
tableSortMethod: SortMethod
|
||||
callerCallee: CallerCalleeState | null
|
||||
}
|
||||
|
||||
export interface CallerCalleeState {
|
||||
selectedFrame: Frame
|
||||
invertedCallerFlamegraph: FlamechartViewState
|
||||
calleeFlamegraph: FlamechartViewState
|
||||
}
|
||||
|
||||
const defaultSortMethod = {
|
||||
field: SortField.SELF,
|
||||
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}
|
||||
}
|
||||
|
||||
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 (actions.sandwichView.setTableSortMethod.matches(action)) {
|
||||
return {...state, tableSortMethod: action.payload}
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
declare module 'preact-redux' {
|
||||
import {VNode, Component} from 'preact'
|
||||
import {Store} from 'redux'
|
||||
|
||||
// We just export the bare minimum here because we're going
|
||||
// to implement an API for readability convenience elsewhere
|
||||
export function connect(...args: any[]): any
|
||||
|
||||
export class Provider extends Component<{store: Store<any>}, {}> {
|
||||
render(): VNode
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,19 @@
|
||||
import {h} from 'preact'
|
||||
import {h, Component} from 'preact'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {ReloadableComponent, SerializedComponent} from './reloadable'
|
||||
import {FileSystemDirectoryEntry} from '../import/file-system-entry'
|
||||
|
||||
import {FileSystemDirectoryEntry} from './import/file-system-entry'
|
||||
|
||||
import {FlamechartRenderer, FlamechartRowAtlasKey} from './flamechart-renderer'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
|
||||
import {Profile, Frame} from './profile'
|
||||
import {Flamechart} from './flamechart'
|
||||
import {FlamechartView} from './flamechart-view'
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {FontFamily, FontSize, Colors, Sizes, Duration} from './style'
|
||||
import {getHashParams, HashParams} from './hash-params'
|
||||
import {SortMethod, SortField, SortDirection} from './profile-table-view'
|
||||
import {triangle} from './utils'
|
||||
import {Color} from './color'
|
||||
import {RowAtlas} from './row-atlas'
|
||||
import {importEmscriptenSymbolMap} from './emscripten'
|
||||
import {SandwichView} from './sandwich-view'
|
||||
import {saveToFile} from './file-format'
|
||||
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 {LeftHeavyFlamechartView, ChronoFlamechartView} from './flamechart-view-container'
|
||||
import {getProfileToView} from '../store/getters'
|
||||
|
||||
const importModule = import('./import')
|
||||
const importModule = import('../import')
|
||||
// Force eager loading of the module
|
||||
importModule.then(() => {})
|
||||
async function importProfile(fileName: string, contents: string): Promise<Profile | null> {
|
||||
@@ -30,36 +23,8 @@ async function importFromFileSystemDirectoryEntry(entry: FileSystemDirectoryEntr
|
||||
return (await importModule).importFromFileSystemDirectoryEntry(entry)
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol
|
||||
const canUseXHR = protocol === 'http:' || protocol === 'https:'
|
||||
|
||||
declare function require(x: string): any
|
||||
const exampleProfileURL = require('./sample/profiles/stackcollapse/perf-vertx-stacks-01-collapsed-all.txt')
|
||||
|
||||
const enum ViewMode {
|
||||
CHRONO_FLAME_CHART,
|
||||
LEFT_HEAVY_FLAME_GRAPH,
|
||||
SANDWICH_VIEW,
|
||||
}
|
||||
|
||||
interface ApplicationState {
|
||||
profile: Profile | null
|
||||
activeProfile: Profile | null
|
||||
flattenRecursion: boolean
|
||||
|
||||
chronoFlamechart: Flamechart | null
|
||||
chronoFlamechartRenderer: FlamechartRenderer | null
|
||||
|
||||
leftHeavyFlamegraph: Flamechart | null
|
||||
leftHeavyFlamegraphRenderer: FlamechartRenderer | null
|
||||
|
||||
tableSortMethod: SortMethod
|
||||
|
||||
viewMode: ViewMode
|
||||
dragActive: boolean
|
||||
loading: boolean
|
||||
error: boolean
|
||||
}
|
||||
const exampleProfileURL = require('../../sample/profiles/stackcollapse/perf-vertx-stacks-01-collapsed-all.txt')
|
||||
|
||||
interface ToolbarProps extends ApplicationState {
|
||||
setViewMode(order: ViewMode): void
|
||||
@@ -67,7 +32,7 @@ interface ToolbarProps extends ApplicationState {
|
||||
saveFile(): void
|
||||
}
|
||||
|
||||
export class Toolbar extends ReloadableComponent<ToolbarProps, void> {
|
||||
export class Toolbar extends StatelessComponent<ToolbarProps> {
|
||||
setTimeOrder = () => {
|
||||
this.props.setViewMode(ViewMode.CHRONO_FLAME_CHART)
|
||||
}
|
||||
@@ -154,21 +119,19 @@ export class Toolbar extends ReloadableComponent<ToolbarProps, void> {
|
||||
}
|
||||
|
||||
interface GLCanvasProps {
|
||||
setCanvasContext(canvasContext: CanvasContext | null): void
|
||||
dispatch: Dispatch
|
||||
}
|
||||
export class GLCanvas extends ReloadableComponent<GLCanvasProps, void> {
|
||||
export class GLCanvas extends Component<GLCanvasProps, void> {
|
||||
private canvas: HTMLCanvasElement | null = null
|
||||
private canvasContext: CanvasContext | null = null
|
||||
|
||||
private ref = (canvas?: Element) => {
|
||||
if (canvas instanceof HTMLCanvasElement) {
|
||||
this.canvas = canvas
|
||||
this.canvasContext = new CanvasContext(canvas)
|
||||
} else {
|
||||
this.canvas = null
|
||||
this.canvasContext = null
|
||||
}
|
||||
this.props.setCanvasContext(this.canvasContext)
|
||||
|
||||
this.props.dispatch(actions.setGLCanvas(this.canvas))
|
||||
}
|
||||
|
||||
private maybeResize() {
|
||||
@@ -200,76 +163,17 @@ export class GLCanvas extends ReloadableComponent<GLCanvasProps, void> {
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.onWindowResize)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <canvas className={css(style.glCanvasView)} ref={this.ref} width={1} height={1} />
|
||||
}
|
||||
}
|
||||
|
||||
export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
hashParams: HashParams
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.hashParams = getHashParams()
|
||||
this.state = {
|
||||
// Start out at a loading state if we know that we'll immediately be fetching a profile to
|
||||
// view.
|
||||
loading:
|
||||
(canUseXHR && this.hashParams.profileURL != null) ||
|
||||
this.hashParams.localProfilePath != null,
|
||||
dragActive: false,
|
||||
error: false,
|
||||
profile: null,
|
||||
activeProfile: null,
|
||||
flattenRecursion: false,
|
||||
|
||||
chronoFlamechart: null,
|
||||
chronoFlamechartRenderer: null,
|
||||
|
||||
leftHeavyFlamegraph: null,
|
||||
leftHeavyFlamegraphRenderer: null,
|
||||
|
||||
tableSortMethod: {
|
||||
field: SortField.SELF,
|
||||
direction: SortDirection.DESCENDING,
|
||||
},
|
||||
|
||||
viewMode: ViewMode.CHRONO_FLAME_CHART,
|
||||
}
|
||||
}
|
||||
|
||||
serialize() {
|
||||
const result = super.serialize()
|
||||
delete result.state.chronoFlamechartRenderer
|
||||
delete result.state.leftHeavyFlamegraphRenderer
|
||||
return result
|
||||
}
|
||||
|
||||
rehydrate(serialized: SerializedComponent<ApplicationState>) {
|
||||
super.rehydrate(serialized)
|
||||
const {chronoFlamechart, leftHeavyFlamegraph} = serialized.state
|
||||
if (this.canvasContext && this.rowAtlas && chronoFlamechart && leftHeavyFlamegraph) {
|
||||
this.setState({
|
||||
chronoFlamechartRenderer: new FlamechartRenderer(
|
||||
this.canvasContext,
|
||||
this.rowAtlas,
|
||||
chronoFlamechart,
|
||||
),
|
||||
leftHeavyFlamegraphRenderer: new FlamechartRenderer(
|
||||
this.canvasContext,
|
||||
this.rowAtlas,
|
||||
leftHeavyFlamegraph,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Application extends StatelessComponent<WithDispatch<ApplicationState>> {
|
||||
async loadProfile(loader: () => Promise<Profile | null>) {
|
||||
await new Promise(resolve => this.setState({loading: true}, resolve))
|
||||
this.props.dispatch(actions.setLoading(true))
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
if (!this.canvasContext || !this.rowAtlas) return
|
||||
if (!this.props.glCanvas) return
|
||||
|
||||
console.time('import')
|
||||
|
||||
@@ -278,30 +182,31 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
profile = await loader()
|
||||
} catch (e) {
|
||||
console.log('Failed to load format', e)
|
||||
this.setState({error: true})
|
||||
this.props.dispatch(actions.setError(true))
|
||||
return
|
||||
}
|
||||
|
||||
if (profile == null) {
|
||||
// TODO(jlfwong): Make this a nicer overlay
|
||||
alert('Unrecognized format! See documentation about supported formats.')
|
||||
await new Promise(resolve => this.setState({loading: false}, resolve))
|
||||
this.props.dispatch(actions.setLoading(false))
|
||||
return
|
||||
}
|
||||
|
||||
await profile.demangle()
|
||||
|
||||
const title = this.hashParams.title || profile.getName()
|
||||
const title = this.props.hashParams.title || profile.getName()
|
||||
profile.setName(title)
|
||||
|
||||
await this.setActiveProfile(profile)
|
||||
|
||||
console.timeEnd('import')
|
||||
this.setState({profile})
|
||||
this.props.dispatch(actions.setProfile(profile))
|
||||
this.props.dispatch(actions.setLoading(false))
|
||||
}
|
||||
|
||||
async setActiveProfile(profile: Profile) {
|
||||
if (!this.canvasContext || !this.rowAtlas) return
|
||||
if (!this.props.glCanvas) return
|
||||
|
||||
document.title = `${profile.getName()} - speedscope`
|
||||
|
||||
@@ -318,50 +223,9 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
frameToColorBucket.set(frames[i].key, Math.floor(255 * i / frames.length))
|
||||
}
|
||||
function getColorBucketForFrame(frame: Frame) {
|
||||
return frameToColorBucket.get(frame.key) || 0
|
||||
}
|
||||
|
||||
const chronoFlamechart = new Flamechart({
|
||||
getTotalWeight: profile.getTotalWeight.bind(profile),
|
||||
forEachCall: profile.forEachCall.bind(profile),
|
||||
formatValue: profile.formatValue.bind(profile),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const chronoFlamechartRenderer = new FlamechartRenderer(
|
||||
this.canvasContext,
|
||||
this.rowAtlas,
|
||||
chronoFlamechart,
|
||||
)
|
||||
|
||||
const leftHeavyFlamegraph = new Flamechart({
|
||||
getTotalWeight: profile.getTotalNonIdleWeight.bind(profile),
|
||||
forEachCall: profile.forEachCallGrouped.bind(profile),
|
||||
formatValue: profile.formatValue.bind(profile),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const leftHeavyFlamegraphRenderer = new FlamechartRenderer(
|
||||
this.canvasContext,
|
||||
this.rowAtlas,
|
||||
leftHeavyFlamegraph,
|
||||
)
|
||||
|
||||
await new Promise(resolve => {
|
||||
this.setState(
|
||||
{
|
||||
activeProfile: profile,
|
||||
|
||||
chronoFlamechart,
|
||||
chronoFlamechartRenderer,
|
||||
|
||||
leftHeavyFlamegraph,
|
||||
leftHeavyFlamegraphRenderer,
|
||||
|
||||
loading: false,
|
||||
},
|
||||
resolve,
|
||||
)
|
||||
})
|
||||
this.props.dispatch(actions.setActiveProfile(profile))
|
||||
this.props.dispatch(actions.setFrameToColorBucket(frameToColorBucket))
|
||||
}
|
||||
|
||||
loadFromFile(file: File) {
|
||||
@@ -371,6 +235,10 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
reader.readAsText(file)
|
||||
await loadPromise
|
||||
|
||||
if (typeof reader.result !== 'string') {
|
||||
throw new Error('Expected ArrayBuffer')
|
||||
}
|
||||
|
||||
const profile = await importProfile(file.name, reader.result)
|
||||
if (profile) {
|
||||
if (!profile.getName()) {
|
||||
@@ -379,7 +247,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
return profile
|
||||
}
|
||||
|
||||
if (this.state.profile) {
|
||||
if (this.props.profile) {
|
||||
// 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
|
||||
@@ -387,7 +255,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
const map = importEmscriptenSymbolMap(reader.result)
|
||||
if (map) {
|
||||
console.log('Importing as emscripten symbol map')
|
||||
let profile = this.state.profile
|
||||
let profile = this.props.profile
|
||||
profile.remapNames(name => map.get(name) || name)
|
||||
return profile
|
||||
}
|
||||
@@ -410,7 +278,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
}
|
||||
|
||||
onDrop = (ev: DragEvent) => {
|
||||
this.setState({dragActive: false})
|
||||
this.props.dispatch(actions.setDragActive(false))
|
||||
ev.preventDefault()
|
||||
|
||||
const firstItem = ev.dataTransfer.items[0]
|
||||
@@ -432,44 +300,31 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
}
|
||||
|
||||
onDragOver = (ev: DragEvent) => {
|
||||
this.setState({dragActive: true})
|
||||
this.props.dispatch(actions.setDragActive(true))
|
||||
ev.preventDefault()
|
||||
}
|
||||
|
||||
onDragLeave = (ev: DragEvent) => {
|
||||
this.setState({dragActive: false})
|
||||
this.props.dispatch(actions.setDragActive(false))
|
||||
ev.preventDefault()
|
||||
}
|
||||
|
||||
onWindowKeyPress = async (ev: KeyboardEvent) => {
|
||||
if (ev.key === '1') {
|
||||
this.setState({
|
||||
viewMode: ViewMode.CHRONO_FLAME_CHART,
|
||||
})
|
||||
this.props.dispatch(actions.setViewMode(ViewMode.CHRONO_FLAME_CHART))
|
||||
} else if (ev.key === '2') {
|
||||
this.setState({
|
||||
viewMode: ViewMode.LEFT_HEAVY_FLAME_GRAPH,
|
||||
})
|
||||
this.props.dispatch(actions.setViewMode(ViewMode.LEFT_HEAVY_FLAME_GRAPH))
|
||||
} else if (ev.key === '3') {
|
||||
this.setState({
|
||||
viewMode: ViewMode.SANDWICH_VIEW,
|
||||
})
|
||||
this.props.dispatch(actions.setViewMode(ViewMode.SANDWICH_VIEW))
|
||||
} else if (ev.key === 'r') {
|
||||
const {flattenRecursion, profile} = this.state
|
||||
if (!profile) return
|
||||
if (flattenRecursion) {
|
||||
await this.setActiveProfile(profile)
|
||||
this.setState({flattenRecursion: false})
|
||||
} else {
|
||||
await this.setActiveProfile(profile.getProfileWithRecursionFlattened())
|
||||
this.setState({flattenRecursion: true})
|
||||
}
|
||||
const {flattenRecursion} = this.props
|
||||
this.props.dispatch(actions.setFlattenRecursion(!flattenRecursion))
|
||||
}
|
||||
}
|
||||
|
||||
private saveFile = () => {
|
||||
if (this.state.profile) {
|
||||
saveToFile(this.state.profile)
|
||||
if (this.props.profile) {
|
||||
saveToFile(this.props.profile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,20 +369,22 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
}
|
||||
|
||||
async maybeLoadHashParamProfile() {
|
||||
if (this.hashParams.profileURL) {
|
||||
if (this.props.hashParams.profileURL) {
|
||||
if (!canUseXHR) {
|
||||
alert(`Cannot load a profile URL when loading from "${protocol}" URL protocol`)
|
||||
alert(
|
||||
`Cannot load a profile URL when loading from "${window.location.protocol}" URL protocol`,
|
||||
)
|
||||
return
|
||||
}
|
||||
this.loadProfile(async () => {
|
||||
const response = await fetch(this.hashParams.profileURL!)
|
||||
let filename = new URL(this.hashParams.profileURL!).pathname
|
||||
const response = await fetch(this.props.hashParams.profileURL!)
|
||||
let filename = new URL(this.props.hashParams.profileURL!).pathname
|
||||
if (filename.includes('/')) {
|
||||
filename = filename.slice(filename.lastIndexOf('/') + 1)
|
||||
}
|
||||
return await importProfile(filename, await response.text())
|
||||
})
|
||||
} else if (this.hashParams.localProfilePath) {
|
||||
} else if (this.props.hashParams.localProfilePath) {
|
||||
// There isn't good cross-browser support for XHR of local files, even from
|
||||
// other local files. To work around this restriction, we load the local profile
|
||||
// as a JavaScript file which will invoke a global function.
|
||||
@@ -539,19 +396,11 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = `file:///${this.hashParams.localProfilePath}`
|
||||
script.src = `file:///${this.props.hashParams.localProfilePath}`
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
}
|
||||
|
||||
flamechartView: FlamechartView | null = null
|
||||
flamechartRef = (view: FlamechartView | null) => (this.flamechartView = view)
|
||||
subcomponents() {
|
||||
return {
|
||||
flamechart: this.flamechartView,
|
||||
}
|
||||
}
|
||||
|
||||
onFileSelect = (ev: Event) => {
|
||||
const file = (ev.target as HTMLInputElement).files!.item(0)
|
||||
if (file) {
|
||||
@@ -643,105 +492,36 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
}
|
||||
|
||||
setViewMode = (viewMode: ViewMode) => {
|
||||
this.setState({viewMode})
|
||||
}
|
||||
|
||||
setTableSortMethod = (tableSortMethod: SortMethod) => {
|
||||
this.setState({tableSortMethod})
|
||||
}
|
||||
|
||||
private canvasContext: CanvasContext | null = null
|
||||
private rowAtlas: RowAtlas<FlamechartRowAtlasKey> | null = null
|
||||
private setCanvasContext = (canvasContext: CanvasContext | null) => {
|
||||
this.canvasContext = canvasContext
|
||||
if (canvasContext) {
|
||||
this.rowAtlas = new RowAtlas(canvasContext)
|
||||
} else {
|
||||
this.rowAtlas = null
|
||||
}
|
||||
}
|
||||
|
||||
getColorBucketForFrame = (frame: Frame): number => {
|
||||
const {chronoFlamechart} = this.state
|
||||
if (!chronoFlamechart) return 0
|
||||
return chronoFlamechart.getColorBucketForFrame(frame)
|
||||
}
|
||||
|
||||
getCSSColorForFrame = (frame: Frame): string => {
|
||||
const {chronoFlamechart} = this.state
|
||||
if (!chronoFlamechart) return '#FFFFFF'
|
||||
|
||||
const t = chronoFlamechart.getColorBucketForFrame(frame) / 255
|
||||
|
||||
const x = triangle(30.0 * t)
|
||||
const H = 360.0 * (0.9 * t)
|
||||
const C = 0.25 + 0.2 * x
|
||||
const L = 0.8 - 0.15 * x
|
||||
return Color.fromLumaChromaHue(L, C, H).toCSS()
|
||||
this.props.dispatch(actions.setViewMode(viewMode))
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const {viewMode} = this.state
|
||||
const {viewMode, flattenRecursion, profile, error, loading, glCanvas} = this.props
|
||||
|
||||
if (this.state.error) {
|
||||
if (error) {
|
||||
return this.renderError()
|
||||
}
|
||||
|
||||
if (this.state.loading) {
|
||||
if (loading) {
|
||||
return this.renderLoadingBar()
|
||||
}
|
||||
|
||||
if (!this.state.activeProfile) {
|
||||
if (!profile || !glCanvas) {
|
||||
return this.renderLanding()
|
||||
}
|
||||
|
||||
if (!this.canvasContext) {
|
||||
throw new Error('Missing canvas context')
|
||||
}
|
||||
const profileToView = getProfileToView({profile, flattenRecursion})
|
||||
|
||||
switch (viewMode) {
|
||||
case ViewMode.CHRONO_FLAME_CHART: {
|
||||
const {chronoFlamechart, chronoFlamechartRenderer} = this.state
|
||||
if (!chronoFlamechart || !chronoFlamechartRenderer)
|
||||
throw new Error('Missing dependencies for chrono flame chart')
|
||||
return (
|
||||
<FlamechartView
|
||||
canvasContext={this.canvasContext}
|
||||
flamechartRenderer={chronoFlamechartRenderer}
|
||||
ref={this.flamechartRef}
|
||||
flamechart={chronoFlamechart}
|
||||
getCSSColorForFrame={this.getCSSColorForFrame}
|
||||
/>
|
||||
)
|
||||
return <ChronoFlamechartView profile={profileToView} glCanvas={glCanvas} />
|
||||
}
|
||||
case ViewMode.LEFT_HEAVY_FLAME_GRAPH: {
|
||||
const {leftHeavyFlamegraph, leftHeavyFlamegraphRenderer} = this.state
|
||||
if (!leftHeavyFlamegraph || !leftHeavyFlamegraphRenderer)
|
||||
throw new Error('Missing dependencies for left heavy flame graph')
|
||||
return (
|
||||
<FlamechartView
|
||||
canvasContext={this.canvasContext}
|
||||
flamechartRenderer={leftHeavyFlamegraphRenderer}
|
||||
ref={this.flamechartRef}
|
||||
flamechart={leftHeavyFlamegraph}
|
||||
getCSSColorForFrame={this.getCSSColorForFrame}
|
||||
/>
|
||||
)
|
||||
return <LeftHeavyFlamechartView profile={profileToView} glCanvas={glCanvas} />
|
||||
}
|
||||
case ViewMode.SANDWICH_VIEW: {
|
||||
if (!this.rowAtlas || !this.state.profile) return null
|
||||
return (
|
||||
<SandwichView
|
||||
profile={this.state.profile}
|
||||
flattenRecursion={this.state.flattenRecursion}
|
||||
getColorBucketForFrame={this.getColorBucketForFrame}
|
||||
getCSSColorForFrame={this.getCSSColorForFrame}
|
||||
sortMethod={this.state.tableSortMethod}
|
||||
setSortMethod={this.setTableSortMethod}
|
||||
canvasContext={this.canvasContext}
|
||||
rowAtlas={this.rowAtlas}
|
||||
/>
|
||||
)
|
||||
if (!this.props.profile) return null
|
||||
return <SandwichViewContainer />
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -752,17 +532,17 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
onDrop={this.onDrop}
|
||||
onDragOver={this.onDragOver}
|
||||
onDragLeave={this.onDragLeave}
|
||||
className={css(style.root, this.state.dragActive && style.dragTargetRoot)}
|
||||
className={css(style.root, this.props.dragActive && style.dragTargetRoot)}
|
||||
>
|
||||
<GLCanvas setCanvasContext={this.setCanvasContext} />
|
||||
<GLCanvas dispatch={this.props.dispatch} />
|
||||
<Toolbar
|
||||
setViewMode={this.setViewMode}
|
||||
saveFile={this.saveFile}
|
||||
browseForFile={this.browseForFile}
|
||||
{...this.state}
|
||||
{...this.props as ApplicationState}
|
||||
/>
|
||||
<div className={css(style.contentContainer)}>{this.renderContent()}</div>
|
||||
{this.state.dragActive && <div className={css(style.dragTarget)} />}
|
||||
{this.props.dragActive && <div className={css(style.dragTarget)} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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 {ApplicationState} from '../store'
|
||||
import {
|
||||
getCanvasContext,
|
||||
createGetColorBucketForFrame,
|
||||
createGetCSSColorForFrame,
|
||||
} from '../store/getters'
|
||||
import {FlamechartID} from '../store/flamechart-view-state'
|
||||
import {FlamechartWrapper} from './flamechart-wrapper'
|
||||
|
||||
const getCalleeProfile = memoizeByShallowEquality<
|
||||
{
|
||||
profile: Profile
|
||||
frame: Frame
|
||||
flattenRecursion: boolean
|
||||
},
|
||||
Profile
|
||||
>(({profile, frame, flattenRecursion}) => {
|
||||
let p = profile.getProfileForCalleesOf(frame)
|
||||
return flattenRecursion ? p.getProfileWithRecursionFlattened() : p
|
||||
})
|
||||
|
||||
const getCalleeFlamegraph = memoizeByShallowEquality<
|
||||
{
|
||||
calleeProfile: Profile
|
||||
getColorBucketForFrame: (frame: Frame) => number
|
||||
},
|
||||
Flamechart
|
||||
>(({calleeProfile, getColorBucketForFrame}) => {
|
||||
return new Flamechart({
|
||||
getTotalWeight: calleeProfile.getTotalNonIdleWeight.bind(calleeProfile),
|
||||
forEachCall: calleeProfile.forEachCallGrouped.bind(calleeProfile),
|
||||
formatValue: calleeProfile.formatValue.bind(calleeProfile),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
})
|
||||
|
||||
const getCalleeFlamegraphRenderer = createMemoizedFlamechartRenderer()
|
||||
|
||||
export const CalleeFlamegraphView = createContainer(
|
||||
FlamechartWrapper,
|
||||
(state: ApplicationState) => {
|
||||
const {profile, flattenRecursion, glCanvas, frameToColorBucket, sandwichView} = state
|
||||
if (!profile) throw new Error('profile missing')
|
||||
if (!glCanvas) throw new Error('glCanvas missing')
|
||||
const {callerCallee} = sandwichView
|
||||
if (!callerCallee) throw new Error('callerCallee missing')
|
||||
const {selectedFrame} = callerCallee
|
||||
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
|
||||
const flamechart = getCalleeFlamegraph({
|
||||
calleeProfile: getCalleeProfile({profile, frame: selectedFrame, flattenRecursion}),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = getCalleeFlamegraphRenderer({canvasContext, flamechart})
|
||||
|
||||
return {
|
||||
id: FlamechartID.SANDWICH_CALLEES,
|
||||
renderInverted: false,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...callerCallee.calleeFlamegraph,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -1,11 +1,10 @@
|
||||
import {StyleDeclarationValue, css} from 'aphrodite'
|
||||
import {ReloadableComponent} from './reloadable'
|
||||
import {h} from 'preact'
|
||||
import {h, Component} from 'preact'
|
||||
import {style} from './flamechart-style'
|
||||
import {formatPercent} from './utils'
|
||||
import {Frame, CallTreeNode} from './profile'
|
||||
import {formatPercent} from '../lib/utils'
|
||||
import {Frame, CallTreeNode} from '../lib/profile'
|
||||
import {ColorChit} from './color-chit'
|
||||
import {Flamechart} from './flamechart'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
|
||||
interface StatisticsTableProps {
|
||||
title: string
|
||||
@@ -16,7 +15,7 @@ interface StatisticsTableProps {
|
||||
formatter: (v: number) => string
|
||||
}
|
||||
|
||||
class StatisticsTable extends ReloadableComponent<StatisticsTableProps, {}> {
|
||||
class StatisticsTable extends Component<StatisticsTableProps, {}> {
|
||||
render() {
|
||||
const total = this.props.formatter(this.props.selectedTotal)
|
||||
const self = this.props.formatter(this.props.selectedSelf)
|
||||
@@ -52,7 +51,7 @@ interface StackTraceViewProps {
|
||||
getFrameColor: (frame: Frame) => string
|
||||
node: CallTreeNode
|
||||
}
|
||||
class StackTraceView extends ReloadableComponent<StackTraceViewProps, {}> {
|
||||
class StackTraceView extends Component<StackTraceViewProps, {}> {
|
||||
render() {
|
||||
const rows: JSX.Element[] = []
|
||||
let node: CallTreeNode | null = this.props.node
|
||||
@@ -93,7 +92,7 @@ interface FlamechartDetailViewProps {
|
||||
selectedNode: CallTreeNode
|
||||
}
|
||||
|
||||
export class FlamechartDetailView extends ReloadableComponent<FlamechartDetailViewProps, {}> {
|
||||
export class FlamechartDetailView extends Component<FlamechartDetailViewProps, {}> {
|
||||
render() {
|
||||
const {flamechart, selectedNode} = this.props
|
||||
const {frame} = selectedNode
|
||||
@@ -1,13 +1,13 @@
|
||||
import {h, Component} from 'preact'
|
||||
import {css} from 'aphrodite'
|
||||
import {Flamechart} from './flamechart'
|
||||
import {Rect, Vec2, AffineTransform, clamp} from './math'
|
||||
import {FlamechartRenderer} from './flamechart-renderer'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {Rect, Vec2, AffineTransform, clamp} from '../lib/math'
|
||||
import {FlamechartRenderer} from '../gl/flamechart-renderer'
|
||||
import {style} from './flamechart-style'
|
||||
import {FontFamily, FontSize, Colors, Sizes, commonStyle} from './style'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
import {TextureCachedRenderer} from './texture-cached-renderer'
|
||||
import {cachedMeasureTextWidth} from './text-utils'
|
||||
import {CanvasContext} from '../gl/canvas-context'
|
||||
import {TextureCachedRenderer} from '../gl/texture-cached-renderer'
|
||||
import {cachedMeasureTextWidth} from '../lib/text-utils'
|
||||
|
||||
interface FlamechartMinimapViewProps {
|
||||
flamechart: Flamechart
|
||||
@@ -131,8 +131,6 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
const physicalViewSize = this.physicalViewSize()
|
||||
ctx.clearRect(0, 0, physicalViewSize.x, physicalViewSize.y)
|
||||
|
||||
this.resizeOverlayCanvasIfNeeded()
|
||||
|
||||
const configToPhysical = this.configSpaceToPhysicalViewSpace()
|
||||
|
||||
const left = 0
|
||||
@@ -239,6 +237,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
|
||||
private onBeforeFrame = () => {
|
||||
this.maybeClearInteractionLock()
|
||||
this.resizeOverlayCanvasIfNeeded()
|
||||
this.renderRects()
|
||||
this.renderOverlays()
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import {Rect, AffineTransform, Vec2, clamp} from './math'
|
||||
import {CallTreeNode} from './profile'
|
||||
import {Flamechart, FlamechartFrame} from './flamechart'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
import {FlamechartRenderer} from './flamechart-renderer'
|
||||
import {ReloadableComponent} from './reloadable'
|
||||
import {Rect, AffineTransform, Vec2, clamp} from '../lib/math'
|
||||
import {CallTreeNode} from '../lib/profile'
|
||||
import {Flamechart, FlamechartFrame} from '../lib/flamechart'
|
||||
import {CanvasContext} from '../gl/canvas-context'
|
||||
import {FlamechartRenderer} from '../gl/flamechart-renderer'
|
||||
import {Sizes, FontSize, Colors, FontFamily, commonStyle} from './style'
|
||||
import {cachedMeasureTextWidth, ELLIPSIS, trimTextMid} from './text-utils'
|
||||
import {cachedMeasureTextWidth, ELLIPSIS, trimTextMid} from '../lib/text-utils'
|
||||
import {style} from './flamechart-style'
|
||||
import {h} from 'preact'
|
||||
import {h, Component} from 'preact'
|
||||
import {css} from 'aphrodite'
|
||||
import {FlamechartID} from '../store/flamechart-view-state'
|
||||
|
||||
interface FlamechartFrameLabel {
|
||||
configSpaceBounds: Rect
|
||||
@@ -34,6 +34,7 @@ interface FlamechartFrameLabel {
|
||||
* canvas primitives.
|
||||
*/
|
||||
export interface FlamechartPanZoomViewProps {
|
||||
id: FlamechartID
|
||||
flamechart: Flamechart
|
||||
canvasContext: CanvasContext
|
||||
flamechartRenderer: FlamechartRenderer
|
||||
@@ -42,12 +43,16 @@ export interface FlamechartPanZoomViewProps {
|
||||
|
||||
onNodeHover: (hover: {node: CallTreeNode; event: MouseEvent} | null) => void
|
||||
onNodeSelect: (node: CallTreeNode | null) => void
|
||||
|
||||
configSpaceViewportRect: Rect
|
||||
transformViewport: (transform: AffineTransform) => void
|
||||
setConfigSpaceViewportRect: (rect: Rect) => void
|
||||
|
||||
logicalSpaceViewportSize: Vec2
|
||||
setLogicalSpaceViewportBounds: (size: Vec2) => void
|
||||
}
|
||||
|
||||
export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoomViewProps, {}> {
|
||||
export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps, {}> {
|
||||
private container: Element | null = null
|
||||
private containerRef = (element?: Element) => {
|
||||
this.container = element || null
|
||||
@@ -148,7 +153,6 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
private renderOverlays() {
|
||||
const ctx = this.overlayCtx
|
||||
if (!ctx) return
|
||||
this.resizeOverlayCanvasIfNeeded()
|
||||
if (this.props.configSpaceViewportRect.isEmpty()) return
|
||||
|
||||
const configToPhysical = this.configSpaceToPhysicalViewSpace()
|
||||
@@ -357,16 +361,16 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
}
|
||||
}
|
||||
|
||||
private lastBounds: ClientRect | null = null
|
||||
private updateConfigSpaceViewport() {
|
||||
if (!this.container) return
|
||||
const {logicalSpaceViewportSize} = this.props
|
||||
const bounds = this.container.getBoundingClientRect()
|
||||
const {width, height} = bounds
|
||||
|
||||
// Still initializing: don't resize yet
|
||||
if (width < 2 || height < 2) return
|
||||
|
||||
if (this.lastBounds == null) {
|
||||
if (this.props.configSpaceViewportRect.isEmpty()) {
|
||||
const configSpaceViewportHeight = height / this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT
|
||||
if (this.props.renderInverted) {
|
||||
this.setConfigSpaceViewportRect(
|
||||
@@ -380,18 +384,21 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
new Rect(new Vec2(0, -1), new Vec2(this.configSpaceSize().x, configSpaceViewportHeight)),
|
||||
)
|
||||
}
|
||||
} else if (this.lastBounds.width !== width || this.lastBounds.height !== height) {
|
||||
} else if (
|
||||
!logicalSpaceViewportSize.equals(Vec2.zero) &&
|
||||
(logicalSpaceViewportSize.x !== width || logicalSpaceViewportSize.y !== height)
|
||||
) {
|
||||
// Resize the viewport rectangle to match the window size aspect
|
||||
// ratio.
|
||||
this.setConfigSpaceViewportRect(
|
||||
this.props.configSpaceViewportRect.withSize(
|
||||
this.props.configSpaceViewportRect.size.timesPointwise(
|
||||
new Vec2(width / this.lastBounds.width, height / this.lastBounds.height),
|
||||
new Vec2(width / logicalSpaceViewportSize.x, height / logicalSpaceViewportSize.y),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
this.lastBounds = bounds
|
||||
this.props.setLogicalSpaceViewportBounds(new Vec2(width, height))
|
||||
}
|
||||
|
||||
onWindowResize = () => {
|
||||
@@ -442,6 +449,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
}
|
||||
|
||||
private onBeforeFrame = () => {
|
||||
this.resizeOverlayCanvasIfNeeded()
|
||||
this.renderRects()
|
||||
this.renderOverlays()
|
||||
this.maybeClearInteractionLock()
|
||||
@@ -484,8 +492,9 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
}
|
||||
|
||||
private lastDragPos: Vec2 | null = null
|
||||
private mouseDownPos: Vec2 | null = null
|
||||
private onMouseDown = (ev: MouseEvent) => {
|
||||
this.lastDragPos = new Vec2(ev.offsetX, ev.offsetY)
|
||||
this.mouseDownPos = this.lastDragPos = new Vec2(ev.offsetX, ev.offsetY)
|
||||
this.updateCursor()
|
||||
window.addEventListener('mouseup', this.onWindowMouseUp)
|
||||
}
|
||||
@@ -515,6 +524,16 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
}
|
||||
|
||||
private onClick = (ev: MouseEvent) => {
|
||||
const logicalMousePos = new Vec2(ev.offsetX, ev.offsetY)
|
||||
const mouseDownPos = this.mouseDownPos
|
||||
this.mouseDownPos = null
|
||||
|
||||
if (mouseDownPos && logicalMousePos.minus(mouseDownPos).length() > 5) {
|
||||
// If the cursor is more than 5 logical space pixels away from the mouse
|
||||
// down location, then don't interpret this event as a click.
|
||||
return
|
||||
}
|
||||
|
||||
if (this.hoveredLabel) {
|
||||
this.props.onNodeSelect(this.hoveredLabel.node)
|
||||
this.renderCanvas()
|
||||
@@ -663,7 +682,6 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
componentWillReceiveProps(nextProps: FlamechartPanZoomViewProps) {
|
||||
if (this.props.flamechart !== nextProps.flamechart) {
|
||||
this.hoveredLabel = null
|
||||
this.lastBounds = null
|
||||
this.renderCanvas()
|
||||
} else if (this.props.selectedNode !== nextProps.selectedNode) {
|
||||
this.renderCanvas()
|
||||
@@ -0,0 +1,136 @@
|
||||
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 {memoizeByShallowEquality} from '../lib/utils'
|
||||
import {ApplicationState} from '../store'
|
||||
import {FlamechartView} from './flamechart-view'
|
||||
import {
|
||||
getRowAtlas,
|
||||
createGetColorBucketForFrame,
|
||||
getCanvasContext,
|
||||
createGetCSSColorForFrame,
|
||||
} from '../store/getters'
|
||||
|
||||
export type FlamechartViewProps = {
|
||||
id: FlamechartID
|
||||
canvasContext: CanvasContext
|
||||
flamechart: Flamechart
|
||||
flamechartRenderer: FlamechartRenderer
|
||||
renderInverted: boolean
|
||||
dispatch: Dispatch
|
||||
getCSSColorForFrame: (frame: Frame) => string
|
||||
} & FlamechartViewState
|
||||
|
||||
export const getChronoViewFlamechart = memoizeByShallowEquality(
|
||||
({
|
||||
profile,
|
||||
getColorBucketForFrame,
|
||||
}: {
|
||||
profile: Profile
|
||||
getColorBucketForFrame: (frame: Frame) => number
|
||||
}): Flamechart => {
|
||||
return new Flamechart({
|
||||
getTotalWeight: profile.getTotalWeight.bind(profile),
|
||||
forEachCall: profile.forEachCall.bind(profile),
|
||||
formatValue: profile.formatValue.bind(profile),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
export const createMemoizedFlamechartRenderer = (options?: FlamechartRendererOptions) =>
|
||||
memoizeByShallowEquality(
|
||||
({
|
||||
canvasContext,
|
||||
flamechart,
|
||||
}: {
|
||||
canvasContext: CanvasContext
|
||||
flamechart: Flamechart
|
||||
}): FlamechartRenderer => {
|
||||
return new FlamechartRenderer(canvasContext, getRowAtlas(canvasContext), flamechart, options)
|
||||
},
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
|
||||
const flamechart = getChronoViewFlamechart({profile, getColorBucketForFrame})
|
||||
const flamechartRenderer = getChronoViewFlamechartRenderer({
|
||||
canvasContext,
|
||||
flamechart,
|
||||
})
|
||||
|
||||
return {
|
||||
id: FlamechartID.CHRONO,
|
||||
renderInverted: false,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...chronoView,
|
||||
}
|
||||
})
|
||||
|
||||
export const getLeftHeavyFlamechart = memoizeByShallowEquality(
|
||||
({
|
||||
profile,
|
||||
getColorBucketForFrame,
|
||||
}: {
|
||||
profile: Profile
|
||||
getColorBucketForFrame: (frame: Frame) => number
|
||||
}): Flamechart => {
|
||||
return new Flamechart({
|
||||
getTotalWeight: profile.getTotalNonIdleWeight.bind(profile),
|
||||
forEachCall: profile.forEachCallGrouped.bind(profile),
|
||||
formatValue: profile.formatValue.bind(profile),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
|
||||
const flamechart = getLeftHeavyFlamechart({profile, getColorBucketForFrame})
|
||||
const flamechartRenderer = getLeftHeavyFlamechartRenderer({
|
||||
canvasContext,
|
||||
flamechart,
|
||||
})
|
||||
|
||||
return {
|
||||
id: FlamechartID.LEFT_HEAVY,
|
||||
renderInverted: false,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...leftHeavyView,
|
||||
}
|
||||
})
|
||||
@@ -1,48 +1,22 @@
|
||||
import {h} from 'preact'
|
||||
import {css} from 'aphrodite'
|
||||
import {ReloadableComponent} from './reloadable'
|
||||
|
||||
import {CallTreeNode, Frame} from './profile'
|
||||
import {Flamechart} from './flamechart'
|
||||
import {CallTreeNode} from '../lib/profile'
|
||||
|
||||
import {Rect, Vec2, AffineTransform, clamp} from './math'
|
||||
import {formatPercent} from './utils'
|
||||
import {Rect, Vec2, AffineTransform} from '../lib/math'
|
||||
import {formatPercent} from '../lib/utils'
|
||||
import {FlamechartMinimapView} from './flamechart-minimap-view'
|
||||
|
||||
import {style} from './flamechart-style'
|
||||
import {Sizes, commonStyle} from './style'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
import {FlamechartRenderer} from './flamechart-renderer'
|
||||
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'
|
||||
|
||||
interface FlamechartViewProps {
|
||||
flamechart: Flamechart
|
||||
canvasContext: CanvasContext
|
||||
flamechartRenderer: FlamechartRenderer
|
||||
getCSSColorForFrame: (frame: Frame) => string
|
||||
}
|
||||
|
||||
interface FlamechartViewState {
|
||||
hover: {
|
||||
node: CallTreeNode
|
||||
event: MouseEvent
|
||||
} | null
|
||||
selectedNode: CallTreeNode | null
|
||||
configSpaceViewportRect: Rect
|
||||
}
|
||||
|
||||
export class FlamechartView extends ReloadableComponent<FlamechartViewProps, FlamechartViewState> {
|
||||
constructor() {
|
||||
super()
|
||||
this.state = {
|
||||
hover: null,
|
||||
selectedNode: null,
|
||||
configSpaceViewportRect: Rect.empty,
|
||||
}
|
||||
}
|
||||
|
||||
export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
|
||||
private configSpaceSize() {
|
||||
return new Vec2(
|
||||
this.props.flamechart.getTotalWeight(),
|
||||
@@ -55,12 +29,7 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
|
||||
const configSpaceSize = this.configSpaceSize()
|
||||
|
||||
const width = clamp(
|
||||
viewportRect.size.x,
|
||||
Math.min(configSpaceSize.x, 3 * this.props.flamechart.getMinFrameWidth()),
|
||||
configSpaceSize.x,
|
||||
)
|
||||
|
||||
const width = this.props.flamechart.getClampedViewportWidth(viewportRect.size.x)
|
||||
const size = viewportRect.size.withX(width)
|
||||
|
||||
const origin = Vec2.clamp(
|
||||
@@ -72,24 +41,36 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
),
|
||||
)
|
||||
|
||||
this.setState({
|
||||
configSpaceViewportRect: new Rect(origin, viewportRect.size.withX(width)),
|
||||
})
|
||||
this.props.dispatch(
|
||||
actions.flamechart.setConfigSpaceViewportRect({
|
||||
id: this.props.id,
|
||||
configSpaceViewportRect: new Rect(origin, viewportRect.size.withX(width)),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private setLogicalSpaceViewportSize = (logicalSpaceViewportSize: Vec2): void => {
|
||||
this.props.dispatch(
|
||||
actions.flamechart.setLogicalSpaceViewportSize({id: this.props.id, logicalSpaceViewportSize}),
|
||||
)
|
||||
}
|
||||
|
||||
private transformViewport = (transform: AffineTransform): void => {
|
||||
const viewportRect = transform.transformRect(this.state.configSpaceViewportRect)
|
||||
const viewportRect = transform.transformRect(this.props.configSpaceViewportRect)
|
||||
this.setConfigSpaceViewportRect(viewportRect)
|
||||
}
|
||||
|
||||
onNodeHover = (hover: {node: CallTreeNode; event: MouseEvent} | null) => {
|
||||
this.setState({hover})
|
||||
this.props.dispatch(
|
||||
actions.flamechart.setHoveredNode({
|
||||
id: this.props.id,
|
||||
hover,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
onNodeClick = (node: CallTreeNode | null) => {
|
||||
this.setState({
|
||||
selectedNode: node,
|
||||
})
|
||||
this.props.dispatch(actions.flamechart.setSelectedNode({id: this.props.id, selectedNode: node}))
|
||||
}
|
||||
|
||||
formatValue(weight: number) {
|
||||
@@ -102,7 +83,7 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
renderTooltip() {
|
||||
if (!this.container) return null
|
||||
|
||||
const {hover} = this.state
|
||||
const {hover} = this.props
|
||||
if (!hover) return null
|
||||
const {width, height, left, top} = this.container.getBoundingClientRect()
|
||||
const offset = new Vec2(hover.event.clientX - left, hover.event.clientY - top)
|
||||
@@ -122,21 +103,11 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
this.container = (container as HTMLDivElement) || null
|
||||
}
|
||||
|
||||
panZoomView: FlamechartPanZoomView | null = null
|
||||
panZoomRef = (view: FlamechartPanZoomView | null) => {
|
||||
this.panZoomView = view
|
||||
}
|
||||
subcomponents() {
|
||||
return {
|
||||
panZoom: this.panZoomView,
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={css(style.fill, commonStyle.vbox)} ref={this.containerRef}>
|
||||
<FlamechartMinimapView
|
||||
configSpaceViewportRect={this.state.configSpaceViewportRect}
|
||||
configSpaceViewportRect={this.props.configSpaceViewportRect}
|
||||
transformViewport={this.transformViewport}
|
||||
flamechart={this.props.flamechart}
|
||||
flamechartRenderer={this.props.flamechartRenderer}
|
||||
@@ -144,24 +115,26 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
setConfigSpaceViewportRect={this.setConfigSpaceViewportRect}
|
||||
/>
|
||||
<FlamechartPanZoomView
|
||||
ref={this.panZoomRef}
|
||||
canvasContext={this.props.canvasContext}
|
||||
id={this.props.id}
|
||||
flamechart={this.props.flamechart}
|
||||
flamechartRenderer={this.props.flamechartRenderer}
|
||||
renderInverted={false}
|
||||
onNodeHover={this.onNodeHover}
|
||||
onNodeSelect={this.onNodeClick}
|
||||
selectedNode={this.state.selectedNode}
|
||||
selectedNode={this.props.selectedNode}
|
||||
transformViewport={this.transformViewport}
|
||||
configSpaceViewportRect={this.state.configSpaceViewportRect}
|
||||
configSpaceViewportRect={this.props.configSpaceViewportRect}
|
||||
setConfigSpaceViewportRect={this.setConfigSpaceViewportRect}
|
||||
logicalSpaceViewportSize={this.props.logicalSpaceViewportSize}
|
||||
setLogicalSpaceViewportBounds={this.setLogicalSpaceViewportSize}
|
||||
/>
|
||||
{this.renderTooltip()}
|
||||
{this.state.selectedNode && (
|
||||
{this.props.selectedNode && (
|
||||
<FlamechartDetailView
|
||||
flamechart={this.props.flamechart}
|
||||
getCSSColorForFrame={this.props.getCSSColorForFrame}
|
||||
selectedNode={this.state.selectedNode}
|
||||
selectedNode={this.props.selectedNode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -0,0 +1,107 @@
|
||||
import {CallTreeNode} from '../lib/profile'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {h} from 'preact'
|
||||
import {commonStyle, Colors} from './style'
|
||||
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'
|
||||
|
||||
export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
|
||||
private clampViewportToFlamegraph(viewportRect: Rect) {
|
||||
const {flamechart, renderInverted} = this.props
|
||||
const configSpaceSize = new Vec2(flamechart.getTotalWeight(), flamechart.getLayers().length)
|
||||
const width = this.props.flamechart.getClampedViewportWidth(viewportRect.size.x)
|
||||
const size = viewportRect.size.withX(width)
|
||||
const origin = Vec2.clamp(
|
||||
viewportRect.origin,
|
||||
new Vec2(0, renderInverted ? 0 : -1),
|
||||
Vec2.max(Vec2.zero, configSpaceSize.minus(size).plus(new Vec2(0, 1))),
|
||||
)
|
||||
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),
|
||||
}),
|
||||
)
|
||||
}
|
||||
private setLogicalSpaceViewportSize = (logicalSpaceViewportSize: Vec2): void => {
|
||||
this.props.dispatch(
|
||||
actions.flamechart.setLogicalSpaceViewportSize({id: this.props.id, logicalSpaceViewportSize}),
|
||||
)
|
||||
}
|
||||
|
||||
private transformViewport = (transform: AffineTransform) => {
|
||||
this.setConfigSpaceViewportRect(transform.transformRect(this.props.configSpaceViewportRect))
|
||||
}
|
||||
private formatValue(weight: number) {
|
||||
const totalWeight = this.props.flamechart.getTotalWeight()
|
||||
const percent = 100 * weight / totalWeight
|
||||
const formattedPercent = formatPercent(percent)
|
||||
return `${this.props.flamechart.formatValue(weight)} (${formattedPercent})`
|
||||
}
|
||||
private renderTooltip() {
|
||||
if (!this.container) return null
|
||||
const {hover} = this.props
|
||||
if (!hover) return null
|
||||
const {width, height, left, top} = this.container.getBoundingClientRect()
|
||||
const offset = new Vec2(hover.event.clientX - left, hover.event.clientY - top)
|
||||
return (
|
||||
<Hovertip containerSize={new Vec2(width, height)} offset={offset}>
|
||||
<span className={css(style.hoverCount)}>
|
||||
{this.formatValue(hover.node.getTotalWeight())}
|
||||
</span>{' '}
|
||||
{hover.node.frame.name}
|
||||
</Hovertip>
|
||||
)
|
||||
}
|
||||
container: HTMLDivElement | null = null
|
||||
containerRef = (container?: Element) => {
|
||||
this.container = (container as HTMLDivElement) || null
|
||||
}
|
||||
private setNodeHover = (
|
||||
hover: {
|
||||
node: CallTreeNode
|
||||
event: MouseEvent
|
||||
} | null,
|
||||
) => {
|
||||
this.props.dispatch(actions.flamechart.setHoveredNode({id: this.props.id, hover}))
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className={css(commonStyle.fillY, commonStyle.fillX, commonStyle.vbox)}
|
||||
ref={this.containerRef}
|
||||
>
|
||||
<FlamechartPanZoomView
|
||||
id={this.props.id}
|
||||
selectedNode={null}
|
||||
onNodeHover={this.setNodeHover}
|
||||
onNodeSelect={noop}
|
||||
configSpaceViewportRect={this.props.configSpaceViewportRect}
|
||||
setConfigSpaceViewportRect={this.setConfigSpaceViewportRect}
|
||||
transformViewport={this.transformViewport}
|
||||
flamechart={this.props.flamechart}
|
||||
flamechartRenderer={this.props.flamechartRenderer}
|
||||
canvasContext={this.props.canvasContext}
|
||||
renderInverted={this.props.renderInverted}
|
||||
logicalSpaceViewportSize={this.props.logicalSpaceViewportSize}
|
||||
setLogicalSpaceViewportBounds={this.setLogicalSpaceViewportSize}
|
||||
/>
|
||||
{this.renderTooltip()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const style = StyleSheet.create({
|
||||
hoverCount: {
|
||||
color: Colors.GREEN,
|
||||
},
|
||||
})
|
||||
@@ -1,15 +1,14 @@
|
||||
import {ReloadableComponent} from './reloadable'
|
||||
import {Vec2} from './math'
|
||||
import {Vec2} from '../lib/math'
|
||||
import {Sizes, Colors, FontSize, FontFamily, ZIndex} from './style'
|
||||
import {css, StyleSheet} from 'aphrodite'
|
||||
import {h} from 'preact'
|
||||
import {h, Component} from 'preact'
|
||||
|
||||
interface HovertipProps {
|
||||
containerSize: Vec2
|
||||
offset: Vec2
|
||||
}
|
||||
|
||||
export class Hovertip extends ReloadableComponent<HovertipProps, {}> {
|
||||
export class Hovertip extends Component<HovertipProps, {}> {
|
||||
render() {
|
||||
const {containerSize, offset} = this.props
|
||||
const width = containerSize.x
|
||||
@@ -0,0 +1,86 @@
|
||||
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 {ApplicationState} from '../store'
|
||||
import {
|
||||
getCanvasContext,
|
||||
createGetColorBucketForFrame,
|
||||
createGetCSSColorForFrame,
|
||||
getProfileWithRecursionFlattened,
|
||||
} from '../store/getters'
|
||||
import {FlamechartID} from '../store/flamechart-view-state'
|
||||
import {FlamechartWrapper} from './flamechart-wrapper'
|
||||
|
||||
const getInvertedCallerProfile = memoizeByShallowEquality(
|
||||
({
|
||||
profile,
|
||||
frame,
|
||||
flattenRecursion,
|
||||
}: {
|
||||
profile: Profile
|
||||
frame: Frame
|
||||
flattenRecursion: boolean
|
||||
}): Profile => {
|
||||
let p = profile.getInvertedProfileForCallersOf(frame)
|
||||
return flattenRecursion ? p.getProfileWithRecursionFlattened() : p
|
||||
},
|
||||
)
|
||||
|
||||
const getInvertedCallerFlamegraph = memoizeByShallowEquality(
|
||||
({
|
||||
invertedCallerProfile,
|
||||
getColorBucketForFrame,
|
||||
}: {
|
||||
invertedCallerProfile: Profile
|
||||
getColorBucketForFrame: (frame: Frame) => number
|
||||
}): Flamechart => {
|
||||
return new Flamechart({
|
||||
getTotalWeight: invertedCallerProfile.getTotalNonIdleWeight.bind(invertedCallerProfile),
|
||||
forEachCall: invertedCallerProfile.forEachCallGrouped.bind(invertedCallerProfile),
|
||||
formatValue: invertedCallerProfile.formatValue.bind(invertedCallerProfile),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const getInvertedCallerFlamegraphRenderer = createMemoizedFlamechartRenderer({inverted: true})
|
||||
|
||||
export const InvertedCallerFlamegraphView = createContainer(
|
||||
FlamechartWrapper,
|
||||
(state: ApplicationState) => {
|
||||
let {profile, flattenRecursion, glCanvas, frameToColorBucket, sandwichView} = state
|
||||
if (!profile) throw new Error('profile missing')
|
||||
if (!glCanvas) throw new Error('glCanvas missing')
|
||||
const {callerCallee} = sandwichView
|
||||
if (!callerCallee) throw new Error('callerCallee missing')
|
||||
const {selectedFrame} = callerCallee
|
||||
|
||||
profile = flattenRecursion ? getProfileWithRecursionFlattened(profile) : profile
|
||||
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
|
||||
const flamechart = getInvertedCallerFlamegraph({
|
||||
invertedCallerProfile: getInvertedCallerProfile({
|
||||
profile,
|
||||
frame: selectedFrame,
|
||||
flattenRecursion,
|
||||
}),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = getInvertedCallerFlamegraphRenderer({canvasContext, flamechart})
|
||||
|
||||
return {
|
||||
id: FlamechartID.SANDWICH_INVERTED_CALLERS,
|
||||
renderInverted: true,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...callerCallee.invertedCallerFlamegraph,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -1,11 +1,14 @@
|
||||
import {h, Component} from 'preact'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {ReloadableComponent} from './reloadable'
|
||||
import {Profile, Frame} from './profile'
|
||||
import {sortBy, formatPercent} from './utils'
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {sortBy, formatPercent} from '../lib/utils'
|
||||
import {FontSize, Colors, Sizes, commonStyle} from './style'
|
||||
import {ColorChit} from './color-chit'
|
||||
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'
|
||||
|
||||
export enum SortField {
|
||||
SYMBOL_NAME,
|
||||
@@ -66,15 +69,18 @@ class SortIcon extends Component<SortIconProps, {}> {
|
||||
interface ProfileTableViewProps {
|
||||
profile: Profile
|
||||
selectedFrame: Frame | null
|
||||
setSelectedFrame: (frame: Frame | null) => void
|
||||
getCSSColorForFrame: (frame: Frame) => string
|
||||
sortMethod: SortMethod
|
||||
setSortMethod: (sortMethod: SortMethod) => void
|
||||
dispatch: Dispatch
|
||||
}
|
||||
|
||||
export class ProfileTableView extends ReloadableComponent<ProfileTableViewProps, void> {
|
||||
export class ProfileTableView extends Component<ProfileTableViewProps, void> {
|
||||
setSelectedFrame = (frame: Frame | null) => {
|
||||
this.props.setSelectedFrame(frame)
|
||||
this.props.dispatch(actions.sandwichView.setSelectedFrame(frame))
|
||||
}
|
||||
|
||||
setSortMethod = (method: SortMethod) => {
|
||||
this.props.dispatch(actions.sandwichView.setTableSortMethod(method))
|
||||
}
|
||||
|
||||
renderRow(frame: Frame, index: number) {
|
||||
@@ -122,7 +128,7 @@ export class ProfileTableView extends ReloadableComponent<ProfileTableViewProps,
|
||||
|
||||
if (sortMethod.field == field) {
|
||||
// Toggle
|
||||
this.props.setSortMethod({
|
||||
this.setSortMethod({
|
||||
field,
|
||||
direction:
|
||||
sortMethod.direction === SortDirection.ASCENDING
|
||||
@@ -133,22 +139,22 @@ export class ProfileTableView extends ReloadableComponent<ProfileTableViewProps,
|
||||
// Set a sane default
|
||||
switch (field) {
|
||||
case SortField.SYMBOL_NAME: {
|
||||
this.props.setSortMethod({field, direction: SortDirection.ASCENDING})
|
||||
this.setSortMethod({field, direction: SortDirection.ASCENDING})
|
||||
break
|
||||
}
|
||||
case SortField.SELF: {
|
||||
this.props.setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
this.setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
break
|
||||
}
|
||||
case SortField.TOTAL: {
|
||||
this.props.setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
this.setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
private getFrameList = (): Frame[] => {
|
||||
const {profile, sortMethod} = this.props
|
||||
|
||||
const frameList: Frame[] = []
|
||||
@@ -175,6 +181,26 @@ export class ProfileTableView extends ReloadableComponent<ProfileTableViewProps,
|
||||
frameList.reverse()
|
||||
}
|
||||
|
||||
return frameList
|
||||
}
|
||||
|
||||
private listView: ScrollableListView | null = null
|
||||
private listViewRef = (listView: ScrollableListView | null) => {
|
||||
if (listView === this.listView) return
|
||||
this.listView = listView
|
||||
|
||||
const {selectedFrame} = this.props
|
||||
if (!selectedFrame || !listView) return
|
||||
const index = this.getFrameList().indexOf(selectedFrame)
|
||||
if (index === -1) return
|
||||
listView.scrollIndexIntoView(index)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {sortMethod} = this.props
|
||||
|
||||
const frameList = this.getFrameList()
|
||||
|
||||
const renderItems = (firstIndex: number, lastIndex: number) => {
|
||||
const rows: JSX.Element[] = []
|
||||
|
||||
@@ -229,6 +255,8 @@ export class ProfileTableView extends ReloadableComponent<ProfileTableViewProps,
|
||||
</thead>
|
||||
</table>
|
||||
<ScrollableListView
|
||||
ref={this.listViewRef}
|
||||
axis={'y'}
|
||||
items={listItems}
|
||||
className={css(style.scrollView)}
|
||||
renderItems={renderItems}
|
||||
@@ -305,3 +333,21 @@ const style = StyleSheet.create({
|
||||
right: 0,
|
||||
},
|
||||
})
|
||||
|
||||
export const ProfileTableViewContainer = createContainer(
|
||||
ProfileTableView,
|
||||
(state: ApplicationState) => {
|
||||
const {profile, sandwichView, frameToColorBucket} = state
|
||||
if (!profile) throw new Error('profile missing')
|
||||
const {tableSortMethod, callerCallee} = sandwichView
|
||||
const selectedFrame = callerCallee ? callerCallee.selectedFrame : null
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
|
||||
return {
|
||||
profile,
|
||||
selectedFrame,
|
||||
getCSSColorForFrame,
|
||||
sortMethod: tableSortMethod,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,113 @@
|
||||
import {Frame} from '../lib/profile'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {ProfileTableViewContainer} from './profile-table-view'
|
||||
import {h} from 'preact'
|
||||
import {commonStyle, Sizes, Colors, FontSize} from './style'
|
||||
import {actions} from '../store/actions'
|
||||
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'
|
||||
|
||||
interface SandwichViewProps {
|
||||
selectedFrame: Frame | null
|
||||
dispatch: Dispatch
|
||||
}
|
||||
|
||||
class SandwichView extends StatelessComponent<SandwichViewProps> {
|
||||
private setSelectedFrame = (selectedFrame: Frame | null) => {
|
||||
this.props.dispatch(actions.sandwichView.setSelectedFrame(selectedFrame))
|
||||
}
|
||||
|
||||
onWindowKeyPress = (ev: KeyboardEvent) => {
|
||||
if (ev.key === 'Escape') {
|
||||
this.setSelectedFrame(null)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('keydown', this.onWindowKeyPress)
|
||||
}
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('keydown', this.onWindowKeyPress)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {selectedFrame} = this.props
|
||||
let flamegraphViews: JSX.Element | null = null
|
||||
|
||||
if (selectedFrame) {
|
||||
flamegraphViews = (
|
||||
<div className={css(commonStyle.fillY, style.callersAndCallees, commonStyle.vbox)}>
|
||||
<div className={css(commonStyle.hbox, style.panZoomViewWraper)}>
|
||||
<div className={css(style.flamechartLabelParent)}>
|
||||
<div className={css(style.flamechartLabel)}>Callers</div>
|
||||
</div>
|
||||
<InvertedCallerFlamegraphView />
|
||||
</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 />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css(commonStyle.hbox, commonStyle.fillY)}>
|
||||
<div className={css(style.tableView)}>
|
||||
<ProfileTableViewContainer />
|
||||
</div>
|
||||
{flamegraphViews}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
tableView: {
|
||||
flex: 1,
|
||||
},
|
||||
panZoomViewWraper: {
|
||||
flex: 1,
|
||||
},
|
||||
flamechartLabelParent: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-start',
|
||||
fontSize: FontSize.TITLE,
|
||||
width: FontSize.TITLE * 1.2,
|
||||
borderRight: `1px solid ${Colors.LIGHT_GRAY}`,
|
||||
},
|
||||
flamechartLabelParentBottom: {
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
flamechartLabel: {
|
||||
transform: 'rotate(-90deg)',
|
||||
transformOrigin: '50% 50% 0',
|
||||
width: FontSize.TITLE * 1.2,
|
||||
flexShrink: 1,
|
||||
},
|
||||
flamechartLabelBottom: {
|
||||
transform: 'rotate(-90deg)',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
callersAndCallees: {
|
||||
flex: 1,
|
||||
borderLeft: `${Sizes.SEPARATOR_HEIGHT}px solid ${Colors.LIGHT_GRAY}`,
|
||||
},
|
||||
divider: {
|
||||
height: 2,
|
||||
background: Colors.LIGHT_GRAY,
|
||||
},
|
||||
})
|
||||
|
||||
export const SandwichViewContainer = createContainer(SandwichView, (state: ApplicationState) => {
|
||||
const {callerCallee} = state.sandwichView
|
||||
return {selectedFrame: callerCallee ? callerCallee.selectedFrame : null}
|
||||
})
|
||||
@@ -1,8 +1,7 @@
|
||||
// A simple implementation of an efficient scrolling list view which
|
||||
// renders only items within the viewport + a couple extra items.
|
||||
|
||||
import {h} from 'preact'
|
||||
import {ReloadableComponent} from './reloadable'
|
||||
import {h, Component} from 'preact'
|
||||
|
||||
export interface ListItem {
|
||||
size: number
|
||||
@@ -10,6 +9,7 @@ export interface ListItem {
|
||||
|
||||
interface ScrollableListViewProps {
|
||||
items: ListItem[]
|
||||
axis: 'x' | 'y'
|
||||
renderItems: (firstVisibleIndex: number, lastVisibleIndex: number) => JSX.Element | JSX.Element[]
|
||||
className?: string
|
||||
}
|
||||
@@ -22,7 +22,7 @@ interface ScrollableListViewState {
|
||||
cachedTotalSize: number
|
||||
}
|
||||
|
||||
export class ScrollableListView extends ReloadableComponent<
|
||||
export class ScrollableListView extends Component<
|
||||
ScrollableListViewProps,
|
||||
ScrollableListViewState
|
||||
> {
|
||||
@@ -80,6 +80,22 @@ export class ScrollableListView extends ReloadableComponent<
|
||||
this.setState({invisiblePrefixSize, firstVisibleIndex, lastVisibleIndex})
|
||||
}
|
||||
|
||||
private pendingScroll = 0
|
||||
public scrollIndexIntoView(index: number) {
|
||||
this.pendingScroll = this.props.items.reduce((sum, cur, i) => {
|
||||
if (i >= index) return sum
|
||||
return sum + cur.size
|
||||
}, 0)
|
||||
}
|
||||
private applyPendingScroll() {
|
||||
if (!this.viewport) return
|
||||
|
||||
const leftOrTop = this.props.axis === 'y' ? 'top' : 'left'
|
||||
this.viewport.scrollTo({
|
||||
[leftOrTop]: this.pendingScroll,
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: ScrollableListViewProps) {
|
||||
if (this.props.items !== nextProps.items) {
|
||||
this.recomputeVisibleIndices(nextProps)
|
||||
@@ -87,6 +103,7 @@ export class ScrollableListView extends ReloadableComponent<
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.applyPendingScroll()
|
||||
this.recomputeVisibleIndices(this.props)
|
||||
window.addEventListener('resize', this.onWindowResize)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user