Switch to redux for global state management (#100)
This should allow state to be more easily retained globally when switching views, and should make implementation of cross-view features like search easier too. It also removes the need for `ReloadableComponent` Fixes #78 Test Plan: This changes a lot of how the app works, and a lot of stuff that isn't currently covered by tests, so here's a rough manual test plan: 1. Loaded up http://localhost:1234/, click to load the example profile 2. Switch between views, see that viewport & selection position is now retained when switching views 3. See that clicking on nodes selects them in Time Order & Left Heavy views 4. See that hitting Cmd+S saves a profile 5. See that dropping a profile in works 6. See that dropping a profile + a symbol map works 7. See that visiting localhost:1234/#profileURL=https://raw.githubusercontent.com/jlfwong/speedscope/master/sample/profiles/speedscope/0.1.2/simple-sampled.speedscope.json works 8. See that hitting "r" to toggle recursion flattening works
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import {actionCreator} from './typed-redux'
|
||||
import {Profile, CallTreeNode, Frame} from '../profile'
|
||||
import {SortMethod} from '../profile-table-view'
|
||||
import {ViewMode} from '.'
|
||||
import {FlamechartID} from './flamechart-view-state'
|
||||
import {Rect, Vec2} from '../math'
|
||||
import {HashParams} from '../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 '../profile'
|
||||
import {Rect, Vec2} from '../math'
|
||||
import {Reducer} from './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 '../profile'
|
||||
import {triangle, memoizeByReference, memoizeByShallowEquality} from '../utils'
|
||||
import {RowAtlas} from '../row-atlas'
|
||||
import {CanvasContext} from '../canvas-context'
|
||||
import {Color} from '../color'
|
||||
import {FlamechartRowAtlasKey} from '../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 './typed-redux'
|
||||
import {Profile} from '../profile'
|
||||
import {
|
||||
createFlamechartViewStateReducer,
|
||||
FlamechartID,
|
||||
FlamechartViewState,
|
||||
} from './flamechart-view-state'
|
||||
import {SandwichViewState, sandwichView} from './sandwich-view-state'
|
||||
import {HashParams, getHashParams} from '../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 '../profile-table-view'
|
||||
import {Frame} from '../profile'
|
||||
import {
|
||||
FlamechartViewState,
|
||||
FlamechartID,
|
||||
createFlamechartViewStateReducer,
|
||||
} from './flamechart-view-state'
|
||||
import {Reducer} from './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
|
||||
}
|
||||
@@ -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> {}
|
||||
+57
-281
@@ -1,24 +1,17 @@
|
||||
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 {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 {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 {SandwichViewContainer} from './sandwich-view'
|
||||
import {saveToFile} from './file-format'
|
||||
import {ApplicationState, ViewMode, canUseXHR} from './app-state'
|
||||
import {actions} from './app-state/actions'
|
||||
import {Dispatch, StatelessComponent, WithDispatch} from './app-state/typed-redux'
|
||||
import {LeftHeavyFlamechartView, ChronoFlamechartView} from './flamechart-view-container'
|
||||
import {getProfileToView} from './app-state/getters'
|
||||
|
||||
const importModule = import('./import')
|
||||
// Force eager loading of the module
|
||||
@@ -30,44 +23,16 @@ 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
|
||||
}
|
||||
|
||||
interface ToolbarProps extends ApplicationState {
|
||||
setViewMode(order: ViewMode): void
|
||||
browseForFile(): void
|
||||
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) {
|
||||
@@ -379,7 +243,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 +251,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 +274,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 +296,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 +365,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 +392,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 +488,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 +528,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 './utils'
|
||||
import {Profile, Frame} from './profile'
|
||||
import {Flamechart} from './flamechart'
|
||||
import {createMemoizedFlamechartRenderer} from './flamechart-view-container'
|
||||
import {createContainer} from './app-state/typed-redux'
|
||||
import {ApplicationState} from './app-state'
|
||||
import {
|
||||
getCanvasContext,
|
||||
createGetColorBucketForFrame,
|
||||
createGetCSSColorForFrame,
|
||||
} from './app-state/getters'
|
||||
import {FlamechartID} from './app-state/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,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -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,6 +1,5 @@
|
||||
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'
|
||||
@@ -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
|
||||
|
||||
@@ -3,11 +3,10 @@ import {CallTreeNode} from './profile'
|
||||
import {Flamechart, FlamechartFrame} from './flamechart'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
import {FlamechartRenderer} from './flamechart-renderer'
|
||||
import {ReloadableComponent} from './reloadable'
|
||||
import {Sizes, FontSize, Colors, FontFamily, commonStyle} from './style'
|
||||
import {cachedMeasureTextWidth, ELLIPSIS, trimTextMid} from './text-utils'
|
||||
import {style} from './flamechart-style'
|
||||
import {h} from 'preact'
|
||||
import {h, Component} from 'preact'
|
||||
import {css} from 'aphrodite'
|
||||
|
||||
interface FlamechartFrameLabel {
|
||||
@@ -42,12 +41,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
|
||||
@@ -357,16 +360,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 +383,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 = () => {
|
||||
@@ -663,7 +669,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()
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import {FlamechartID, FlamechartViewState} from './app-state/flamechart-view-state'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
import {Flamechart} from './flamechart'
|
||||
import {FlamechartRenderer, FlamechartRendererOptions} from './flamechart-renderer'
|
||||
import {Dispatch, createContainer, WithoutDispatch} from './app-state/typed-redux'
|
||||
import {Frame, Profile} from './profile'
|
||||
import {memoizeByShallowEquality} from './utils'
|
||||
import {ApplicationState} from './app-state'
|
||||
import {FlamechartView} from './flamechart-view'
|
||||
import {
|
||||
getRowAtlas,
|
||||
createGetColorBucketForFrame,
|
||||
getCanvasContext,
|
||||
createGetCSSColorForFrame,
|
||||
} from './app-state/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,
|
||||
}
|
||||
})
|
||||
+35
-55
@@ -1,9 +1,7 @@
|
||||
import {h} from 'preact'
|
||||
import {h, Component} from 'preact'
|
||||
import {css} from 'aphrodite'
|
||||
import {ReloadableComponent} from './reloadable'
|
||||
|
||||
import {CallTreeNode, Frame} from './profile'
|
||||
import {Flamechart} from './flamechart'
|
||||
import {CallTreeNode} from './profile'
|
||||
|
||||
import {Rect, Vec2, AffineTransform, clamp} from './math'
|
||||
import {formatPercent} from './utils'
|
||||
@@ -11,38 +9,17 @@ 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 './app-state/actions'
|
||||
import {FlamechartViewProps} from './flamechart-view-container'
|
||||
|
||||
interface FlamechartViewProps {
|
||||
flamechart: Flamechart
|
||||
canvasContext: CanvasContext
|
||||
flamechartRenderer: FlamechartRenderer
|
||||
getCSSColorForFrame: (frame: Frame) => string
|
||||
interface EmptyState {
|
||||
__dummy: 1
|
||||
}
|
||||
|
||||
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 Component<FlamechartViewProps, EmptyState> {
|
||||
private configSpaceSize() {
|
||||
return new Vec2(
|
||||
this.props.flamechart.getTotalWeight(),
|
||||
@@ -72,24 +49,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 +91,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 +111,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 +123,25 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
setConfigSpaceViewportRect={this.setConfigSpaceViewportRect}
|
||||
/>
|
||||
<FlamechartPanZoomView
|
||||
ref={this.panZoomRef}
|
||||
canvasContext={this.props.canvasContext}
|
||||
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,110 @@
|
||||
import {CallTreeNode} from './profile'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {h} from 'preact'
|
||||
import {commonStyle, Colors} from './style'
|
||||
import {Rect, AffineTransform, Vec2, clamp} from './math'
|
||||
import {FlamechartPanZoomView} from './flamechart-pan-zoom-view'
|
||||
import {noop, formatPercent} from './utils'
|
||||
import {Hovertip} from './hovertip'
|
||||
import {actions} from './app-state/actions'
|
||||
import {FlamechartViewProps} from './flamechart-view-container'
|
||||
import {StatelessComponent} from './app-state/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 = clamp(
|
||||
viewportRect.size.x,
|
||||
Math.min(configSpaceSize.x, 3 * flamechart.getMinFrameWidth()),
|
||||
configSpaceSize.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
|
||||
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,
|
||||
},
|
||||
})
|
||||
+2
-3
@@ -1,15 +1,14 @@
|
||||
import {ReloadableComponent} from './reloadable'
|
||||
import {Vec2} from './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
|
||||
|
||||
@@ -166,7 +166,6 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="./speedscope.tsx"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import {memoizeByShallowEquality} from './utils'
|
||||
import {Profile, Frame} from './profile'
|
||||
import {Flamechart} from './flamechart'
|
||||
import {createMemoizedFlamechartRenderer} from './flamechart-view-container'
|
||||
import {createContainer} from './app-state/typed-redux'
|
||||
import {ApplicationState} from './app-state'
|
||||
import {
|
||||
getCanvasContext,
|
||||
createGetColorBucketForFrame,
|
||||
createGetCSSColorForFrame,
|
||||
getProfileWithRecursionFlattened,
|
||||
} from './app-state/getters'
|
||||
import {FlamechartID} from './app-state/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,
|
||||
}
|
||||
},
|
||||
)
|
||||
Generated
+34
-7
@@ -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",
|
||||
|
||||
+22
-4
@@ -16,8 +16,15 @@
|
||||
"test": "tsc --noEmit && npm run lint && npm run coverage",
|
||||
"serve": "parcel index.html --open --no-autoinstall"
|
||||
},
|
||||
"files": ["cli.js", "dist/release/**", "!*.map"],
|
||||
"browserslist": ["last 2 Chrome versions", "last 2 Firefox versions"],
|
||||
"files": [
|
||||
"cli.js",
|
||||
"dist/release/**",
|
||||
"!*.map"
|
||||
],
|
||||
"browserslist": [
|
||||
"last 2 Chrome versions",
|
||||
"last 2 Firefox versions"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
@@ -35,8 +42,10 @@
|
||||
"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",
|
||||
@@ -48,8 +57,17 @@
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
"testRegex": "\\.test\\.tsx?$",
|
||||
"collectCoverageFrom": ["**/*.{ts,tsx}", "!**/*.d.{ts,tsx}"],
|
||||
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json"]
|
||||
"collectCoverageFrom": [
|
||||
"**/*.{ts,tsx}",
|
||||
"!**/*.d.{ts,tsx}"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"jsx",
|
||||
"json"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"opn": "5.3.0"
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
+33
-9
@@ -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 {FontSize, Colors, Sizes, commonStyle} from './style'
|
||||
import {ColorChit} from './color-chit'
|
||||
import {ScrollableListView, ListItem} from './scrollable-list-view'
|
||||
import {actions} from './app-state/actions'
|
||||
import {Dispatch, createContainer} from './app-state/typed-redux'
|
||||
import {ApplicationState} from './app-state'
|
||||
import {createGetCSSColorForFrame} from './app-state/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,15 +139,15 @@ 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
|
||||
}
|
||||
}
|
||||
@@ -305,3 +311,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,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+23
-255
@@ -1,240 +1,27 @@
|
||||
import {ReloadableComponent} from './reloadable'
|
||||
import {Profile, Frame, CallTreeNode} from './profile'
|
||||
import {Frame} from './profile'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {SortMethod, ProfileTableView} from './profile-table-view'
|
||||
import {ProfileTableViewContainer} 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
import {actions} from './app-state/actions'
|
||||
import {createContainer, Dispatch, StatelessComponent} from './app-state/typed-redux'
|
||||
import {ApplicationState} from './app-state'
|
||||
import {InvertedCallerFlamegraphView} from './inverted-caller-flamegraph-view'
|
||||
import {CalleeFlamegraphView} from './callee-flamegraph-view'
|
||||
|
||||
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>
|
||||
selectedFrame: Frame | null
|
||||
dispatch: Dispatch
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
})
|
||||
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.setState({callerCallee: null})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: SandwichViewProps) {
|
||||
if (this.props.flattenRecursion !== nextProps.flattenRecursion) {
|
||||
if (this.state.callerCallee) {
|
||||
this.setSelectedFrame(this.state.callerCallee.selectedFrame, nextProps)
|
||||
}
|
||||
this.setSelectedFrame(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,38 +33,24 @@ export class SandwichView extends ReloadableComponent<SandwichViewProps, Sandwic
|
||||
}
|
||||
|
||||
render() {
|
||||
const {canvasContext} = this.props
|
||||
const {callerCallee} = this.state
|
||||
|
||||
let selectedFrame: Frame | null = null
|
||||
const {selectedFrame} = this.props
|
||||
let flamegraphViews: JSX.Element | null = null
|
||||
|
||||
if (callerCallee) {
|
||||
selectedFrame = callerCallee.selectedFrame
|
||||
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>
|
||||
<FlamechartWrapper
|
||||
flamechart={callerCallee.invertedCallerFlamegraph}
|
||||
canvasContext={canvasContext}
|
||||
flamechartRenderer={callerCallee.invertedCallerFlamegraphRenderer}
|
||||
renderInverted={true}
|
||||
/>
|
||||
<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>
|
||||
<FlamechartWrapper
|
||||
flamechart={callerCallee.calleeFlamegraph}
|
||||
canvasContext={canvasContext}
|
||||
flamechartRenderer={callerCallee.calleeFlamegraphRenderer}
|
||||
renderInverted={false}
|
||||
/>
|
||||
<CalleeFlamegraphView />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -286,14 +59,7 @@ export class SandwichView extends ReloadableComponent<SandwichViewProps, Sandwic
|
||||
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}
|
||||
/>
|
||||
<ProfileTableViewContainer />
|
||||
</div>
|
||||
{flamegraphViews}
|
||||
</div>
|
||||
@@ -339,7 +105,9 @@ const style = StyleSheet.create({
|
||||
height: 2,
|
||||
background: Colors.LIGHT_GRAY,
|
||||
},
|
||||
hoverCount: {
|
||||
color: Colors.GREEN,
|
||||
},
|
||||
})
|
||||
|
||||
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
|
||||
@@ -22,7 +21,7 @@ interface ScrollableListViewState {
|
||||
cachedTotalSize: number
|
||||
}
|
||||
|
||||
export class ScrollableListView extends ReloadableComponent<
|
||||
export class ScrollableListView extends Component<
|
||||
ScrollableListViewProps,
|
||||
ScrollableListViewState
|
||||
> {
|
||||
|
||||
+17
-13
@@ -1,26 +1,30 @@
|
||||
import {h, render} from 'preact'
|
||||
import {createApplicationStore, ApplicationState} from './app-state'
|
||||
import {Provider} from 'preact-redux'
|
||||
import {createContainer} from './app-state/typed-redux'
|
||||
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()
|
||||
}
|
||||
// Force the old component go through teardown steps
|
||||
render(<div />, document.body, document.body.lastElementChild || undefined)
|
||||
})
|
||||
module.hot.accept()
|
||||
}
|
||||
|
||||
function ref(instance: Application | null) {
|
||||
app = instance
|
||||
if (instance && retained) {
|
||||
console.log('rehydrating: ', retained)
|
||||
instance.rehydrate(retained)
|
||||
}
|
||||
}
|
||||
const lastStore: any = (window as any)['store']
|
||||
const store = createApplicationStore(lastStore ? lastStore.getState() : {})
|
||||
;(window as any)['store'] = store
|
||||
|
||||
render(<Application ref={ref} />, document.body, document.body.lastElementChild || undefined)
|
||||
const ApplicationContainer = createContainer(Application, (state: ApplicationState) => state)
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ApplicationContainer />
|
||||
</Provider>,
|
||||
document.body,
|
||||
document.body.lastElementChild || undefined,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user