mirror of
https://github.com/jlfwong/speedscope.git
synced 2024-11-26 07:35:55 +03:00
Upgrade to Preact X, partially convert to using hooks (#267)
I'd like to try writing new components using hooks, and to do that I need to upgrade from preact 8 to preact X. For reasons that are... complicated, in order to upgrade without breaking part of my build process, I had to remove the dependency on `preact-redux` altogether. This led me to write my own implementation, and as part of that I realized I could remove `createContainer` in favour of some simple hooks that use redux. Before landing: - [x] Investigate performance issues in the sandwich views - [x] Investigate es-lint checks for exhaustive hook dependencies
This commit is contained in:
parent
ca1abfdd32
commit
351994972d
10
.eslintrc.js
10
.eslintrc.js
@ -1,13 +1,15 @@
|
||||
module.exports = {
|
||||
parser: 'typescript-eslint-parser',
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
plugins: ['prettier'],
|
||||
plugins: ['prettier', '@typescript-eslint', 'react-hooks'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
957
package-lock.json
generated
957
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@ -34,24 +34,28 @@
|
||||
"@types/jszip": "3.1.4",
|
||||
"@types/node": "14.0.1",
|
||||
"@types/pako": "1.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "2.33.0",
|
||||
"@typescript-eslint/parser": "2.33.0",
|
||||
"acorn": "7.2.0",
|
||||
"aphrodite": "2.1.0",
|
||||
"coveralls": "3.0.1",
|
||||
"eslint": "4.19.1",
|
||||
"eslint": "6.0.0",
|
||||
"eslint-plugin-prettier": "2.6.0",
|
||||
"eslint-plugin-react-hooks": "4.0.2",
|
||||
"jest": "24.3.0",
|
||||
"jsverify": "0.8.3",
|
||||
"jszip": "3.1.5",
|
||||
"pako": "1.0.6",
|
||||
"parcel-bundler": "1.12.4",
|
||||
"preact": "8.2.7",
|
||||
"preact-redux": "jlfwong/preact-redux#a56dcc4",
|
||||
"preact": "10.4.1",
|
||||
"prettier": "2.0.4",
|
||||
"protobufjs": "6.8.8",
|
||||
"typescript-json-schema": "0.42.0",
|
||||
"redux": "4.0.0",
|
||||
"react-redux": "^7.2.0",
|
||||
"redux": "^4.0.5",
|
||||
"ts-jest": "24.3.0",
|
||||
"typescript": "3.9.2",
|
||||
"typescript-eslint-parser": "17.0.1",
|
||||
"typescript-json-schema": "0.42.0",
|
||||
"uglify-es": "3.2.2"
|
||||
},
|
||||
"jest": {
|
||||
@ -75,6 +79,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"opn": "5.3.0"
|
||||
"opn": "5.3.0",
|
||||
"react": "^16.13.1"
|
||||
}
|
||||
}
|
||||
|
57
src/lib/preact-redux.tsx
Normal file
57
src/lib/preact-redux.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* As of Preact 10.x, they no longer have an officially supported preact-redux library.
|
||||
* It's possible to use react-redux with some hacks, but these hacks cause npm run pack
|
||||
* to error out because of (intentinoally) unmet peer dependencies.
|
||||
*
|
||||
* I could stack more hacks to fix this problem, but I'd rather just drop the dependency
|
||||
* and remove the need to do any dependency hacking by writing the very small part of
|
||||
* react-redux that I actually need myself.
|
||||
*/
|
||||
|
||||
import {h} from 'preact'
|
||||
import * as redux from 'redux'
|
||||
import {createContext, ComponentChildren} from 'preact'
|
||||
import {Dispatch, Action} from './typed-redux'
|
||||
import {useState, useContext, useCallback, useLayoutEffect} from 'preact/hooks'
|
||||
|
||||
const PreactRedux = createContext<redux.Store<any> | null>(null)
|
||||
|
||||
interface ProviderProps {
|
||||
store: redux.Store<any>
|
||||
children?: ComponentChildren
|
||||
}
|
||||
|
||||
export function Provider(props: ProviderProps) {
|
||||
return <PreactRedux.Provider value={props.store} children={props.children} />
|
||||
}
|
||||
|
||||
function useStore<T>(): redux.Store<T> {
|
||||
const store = useContext(PreactRedux)
|
||||
if (store == null) {
|
||||
throw new Error('Called useStore when no store exists in context')
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
export function useDispatch(): Dispatch {
|
||||
const store = useStore()
|
||||
return store.dispatch
|
||||
}
|
||||
|
||||
export function useActionCreator<T, U>(creator: (payload: T) => Action<U>): (t: T) => void {
|
||||
const dispatch = useDispatch()
|
||||
return useCallback((t: T) => dispatch(creator(t)), [dispatch, creator])
|
||||
}
|
||||
|
||||
export function useSelector<T, U>(selector: (t: T) => U): U {
|
||||
const store = useStore<T>()
|
||||
const [value, setValue] = useState(() => selector(store.getState()))
|
||||
|
||||
useLayoutEffect(() => {
|
||||
return store.subscribe(() => {
|
||||
setValue(selector(store.getState()))
|
||||
})
|
||||
}, [store, selector])
|
||||
|
||||
return value
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import {connect} from 'preact-redux'
|
||||
import * as redux from 'redux'
|
||||
import {ComponentConstructor, Component} from 'preact'
|
||||
import {Component} from 'preact'
|
||||
|
||||
export interface Action<TPayload> extends redux.Action<string> {
|
||||
payload: TPayload
|
||||
@ -53,31 +52,6 @@ export function setter<T>(
|
||||
|
||||
export type Dispatch = redux.Dispatch<Action<any>>
|
||||
|
||||
// We make this into a single function invocation instead of the connect(map, map)(Component)
|
||||
// syntax to make better use of type inference.
|
||||
//
|
||||
// NOTE: To avoid this returning objects which do not compare shallow equal, it's the
|
||||
// responsibility of the caller to ensure that the props returned by map compare shallow
|
||||
// equal. This most importantly mean memoizing functions which wrap dispatch to avoid
|
||||
// all callback props from being regenerated on every call.
|
||||
export function createContainer<OwnProps, State, ComponentProps, ComponentType>(
|
||||
component: {
|
||||
new (props: ComponentProps): ComponentType
|
||||
},
|
||||
map: (state: State, dispatch: Dispatch, ownProps: OwnProps) => ComponentProps,
|
||||
): ComponentConstructor<OwnProps, {}> {
|
||||
const mapStateToProps = (state: State) => state
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({dispatch})
|
||||
const mergeProps = (
|
||||
stateProps: State,
|
||||
dispatchProps: {dispatch: Dispatch},
|
||||
ownProps: OwnProps,
|
||||
) => {
|
||||
return map(stateProps, dispatchProps.dispatch, ownProps)
|
||||
}
|
||||
return connect(mapStateToProps, mapDispatchToProps, mergeProps)(component)
|
||||
}
|
||||
|
||||
export type VoidState = {
|
||||
__dummyField: void
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {h, render} from 'preact'
|
||||
import {createApplicationStore} from './store'
|
||||
import {Provider} from 'preact-redux'
|
||||
import {createAppStore} from './store'
|
||||
import {ApplicationContainer} from './views/application-container'
|
||||
import {Provider} from './lib/preact-redux'
|
||||
|
||||
console.log(`speedscope v${require('../package.json').version}`)
|
||||
|
||||
@ -15,7 +15,7 @@ if (module.hot) {
|
||||
}
|
||||
|
||||
const lastStore: any = (window as any)['store']
|
||||
const store = createApplicationStore(lastStore ? lastStore.getState() : {})
|
||||
const store = lastStore ? createAppStore(lastStore.getState()) : createAppStore()
|
||||
;(window as any)['store'] = store
|
||||
|
||||
render(
|
||||
|
@ -41,7 +41,7 @@ export namespace actions {
|
||||
export const setTableSortMethod = actionCreator<SortMethod>('sandwichView.setTableSortMethod')
|
||||
|
||||
export const setSelectedFrame = actionCreatorWithIndex<Frame | null>(
|
||||
'sandwichView.setSelectedFarmr',
|
||||
'sandwichView.setSelectedFrame',
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import {setter, Reducer} from '../lib/typed-redux'
|
||||
import {HashParams, getHashParams} from '../lib/hash-params'
|
||||
import {ProfileGroupState, profileGroup} from './profiles-state'
|
||||
import {SortMethod, SortField, SortDirection} from '../views/profile-table-view'
|
||||
import {useSelector} from '../lib/preact-redux'
|
||||
|
||||
export const enum ViewMode {
|
||||
CHRONO_FLAME_CHART,
|
||||
@ -41,9 +42,7 @@ const protocol = window.location.protocol
|
||||
// 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> {
|
||||
export function createAppStore(initialState?: ApplicationState): redux.Store<ApplicationState> {
|
||||
const hashParams = getHashParams()
|
||||
|
||||
const loading = canUseXHR && hashParams.profileURL != null
|
||||
@ -71,3 +70,7 @@ export function createApplicationStore(
|
||||
|
||||
return redux.createStore(reducer, initialState)
|
||||
}
|
||||
|
||||
export function useAppSelector<T>(selector: (t: ApplicationState) => T): T {
|
||||
return useSelector(selector)
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import * as fs from 'fs'
|
||||
|
||||
import {Store, AnyAction} from 'redux'
|
||||
import {ApplicationState, createApplicationStore} from '.'
|
||||
import {ApplicationState, createAppStore} from '.'
|
||||
import {importSpeedscopeProfiles} from '../lib/file-format'
|
||||
|
||||
export function storeTest(name: string, cb: (store: Store<ApplicationState, AnyAction>) => void) {
|
||||
const store = createApplicationStore({})
|
||||
const store = createAppStore()
|
||||
test(name, () => {
|
||||
cb(store)
|
||||
})
|
||||
|
12
src/typings/preact-redux.d.ts
vendored
12
src/typings/preact-redux.d.ts
vendored
@ -1,12 +0,0 @@
|
||||
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,63 +1,50 @@
|
||||
import {createContainer, Dispatch, bindActionCreator, ActionCreator} from '../lib/typed-redux'
|
||||
import {h} from 'preact'
|
||||
import {Application, ActiveProfileState} from './application'
|
||||
import {ApplicationState} from '../store'
|
||||
import {getProfileToView, getCanvasContext} from '../store/getters'
|
||||
import {actions} from '../store/actions'
|
||||
import {Graphics} from '../gl/graphics'
|
||||
import {useActionCreator} from '../lib/preact-redux'
|
||||
import {memo} from 'preact/compat'
|
||||
import {useCallback} from 'preact/hooks'
|
||||
import {useAppSelector} from '../store'
|
||||
|
||||
export const ApplicationContainer = createContainer(
|
||||
Application,
|
||||
(state: ApplicationState, dispatch: Dispatch) => {
|
||||
const {flattenRecursion, profileGroup} = state
|
||||
export const ApplicationContainer = memo(() => {
|
||||
const appState = useAppSelector(useCallback(state => state, []))
|
||||
const canvasContext = useAppSelector(
|
||||
useCallback(state => (state.glCanvas ? getCanvasContext(state.glCanvas) : null), []),
|
||||
)
|
||||
|
||||
let activeProfileState: ActiveProfileState | null = null
|
||||
if (profileGroup) {
|
||||
if (profileGroup.profiles.length > profileGroup.indexToView) {
|
||||
const index = profileGroup.indexToView
|
||||
const profileState = profileGroup.profiles[index]
|
||||
activeProfileState = {
|
||||
...profileGroup.profiles[profileGroup.indexToView],
|
||||
profile: getProfileToView({profile: profileState.profile, flattenRecursion}),
|
||||
index: profileGroup.indexToView,
|
||||
}
|
||||
const activeProfileState: ActiveProfileState | null = useAppSelector(
|
||||
useCallback(state => {
|
||||
const {profileGroup} = state
|
||||
if (!profileGroup) return null
|
||||
if (profileGroup.indexToView >= profileGroup.profiles.length) return null
|
||||
|
||||
const index = profileGroup.indexToView
|
||||
const profileState = profileGroup.profiles[index]
|
||||
return {
|
||||
...profileGroup.profiles[profileGroup.indexToView],
|
||||
profile: getProfileToView({
|
||||
profile: profileState.profile,
|
||||
flattenRecursion: state.flattenRecursion,
|
||||
}),
|
||||
index: profileGroup.indexToView,
|
||||
}
|
||||
}
|
||||
}, []),
|
||||
)
|
||||
|
||||
function wrapActionCreator<T>(actionCreator: ActionCreator<T>): (t: T) => void {
|
||||
return bindActionCreator(dispatch, actionCreator)
|
||||
}
|
||||
|
||||
// TODO(jlfwong): Cache this and resizeCanvas below to prevent re-renders
|
||||
// due to changing props.
|
||||
const setters = {
|
||||
setGLCanvas: wrapActionCreator(actions.setGLCanvas),
|
||||
setLoading: wrapActionCreator(actions.setLoading),
|
||||
setError: wrapActionCreator(actions.setError),
|
||||
setProfileGroup: wrapActionCreator(actions.setProfileGroup),
|
||||
setDragActive: wrapActionCreator(actions.setDragActive),
|
||||
setViewMode: wrapActionCreator(actions.setViewMode),
|
||||
setFlattenRecursion: wrapActionCreator(actions.setFlattenRecursion),
|
||||
setProfileIndexToView: wrapActionCreator(actions.setProfileIndexToView),
|
||||
}
|
||||
|
||||
return {
|
||||
activeProfileState,
|
||||
dispatch,
|
||||
canvasContext: state.glCanvas ? getCanvasContext(state.glCanvas) : null,
|
||||
resizeCanvas: (
|
||||
widthInPixels: number,
|
||||
heightInPixels: number,
|
||||
widthInAppUnits: number,
|
||||
heightInAppUnits: number,
|
||||
) => {
|
||||
if (state.glCanvas) {
|
||||
const gl = getCanvasContext(state.glCanvas).gl
|
||||
gl.resize(widthInPixels, heightInPixels, widthInAppUnits, heightInAppUnits)
|
||||
gl.clear(new Graphics.Color(1, 1, 1, 1))
|
||||
}
|
||||
},
|
||||
...setters,
|
||||
...state,
|
||||
}
|
||||
},
|
||||
)
|
||||
return (
|
||||
<Application
|
||||
activeProfileState={activeProfileState}
|
||||
canvasContext={canvasContext}
|
||||
setGLCanvas={useActionCreator(actions.setGLCanvas)}
|
||||
setLoading={useActionCreator(actions.setLoading)}
|
||||
setError={useActionCreator(actions.setError)}
|
||||
setProfileGroup={useActionCreator(actions.setProfileGroup)}
|
||||
setDragActive={useActionCreator(actions.setDragActive)}
|
||||
setViewMode={useActionCreator(actions.setViewMode)}
|
||||
setFlattenRecursion={useActionCreator(actions.setFlattenRecursion)}
|
||||
setProfileIndexToView={useActionCreator(actions.setProfileIndexToView)}
|
||||
{...appState}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {h, Component} from 'preact'
|
||||
import {h} from 'preact'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {FileSystemDirectoryEntry} from '../import/file-system-entry'
|
||||
|
||||
import {Profile, ProfileGroup} from '../lib/profile'
|
||||
import {FontFamily, FontSize, Colors, Sizes, Duration} from './style'
|
||||
import {FontFamily, FontSize, Colors, Duration} from './style'
|
||||
import {importEmscriptenSymbolMap} from '../lib/emscripten'
|
||||
import {SandwichViewContainer} from './sandwich-view'
|
||||
import {saveToFile} from '../lib/file-format'
|
||||
@ -14,6 +14,7 @@ import {SandwichViewState} from '../store/sandwich-view-state'
|
||||
import {FlamechartViewState} from '../store/flamechart-view-state'
|
||||
import {CanvasContext} from '../gl/canvas-context'
|
||||
import {Graphics} from '../gl/graphics'
|
||||
import {Toolbar} from './toolbar'
|
||||
|
||||
const importModule = import('../import')
|
||||
// Force eager loading of the module
|
||||
@ -50,155 +51,14 @@ async function importFromFileSystemDirectoryEntry(entry: FileSystemDirectoryEntr
|
||||
declare function require(x: string): any
|
||||
const exampleProfileURL = require('../../sample/profiles/stackcollapse/perf-vertx-stacks-01-collapsed-all.txt')
|
||||
|
||||
interface ToolbarProps extends ApplicationProps {
|
||||
browseForFile(): void
|
||||
saveFile(): void
|
||||
}
|
||||
|
||||
export class Toolbar extends StatelessComponent<ToolbarProps> {
|
||||
setTimeOrder = () => {
|
||||
this.props.setViewMode(ViewMode.CHRONO_FLAME_CHART)
|
||||
}
|
||||
|
||||
setLeftHeavyOrder = () => {
|
||||
this.props.setViewMode(ViewMode.LEFT_HEAVY_FLAME_GRAPH)
|
||||
}
|
||||
|
||||
setSandwichView = () => {
|
||||
this.props.setViewMode(ViewMode.SANDWICH_VIEW)
|
||||
}
|
||||
|
||||
renderLeftContent() {
|
||||
if (!this.props.activeProfileState) return null
|
||||
|
||||
return (
|
||||
<div className={css(style.toolbarLeft)}>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
this.props.viewMode === ViewMode.CHRONO_FLAME_CHART && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={this.setTimeOrder}
|
||||
>
|
||||
<span className={css(style.emoji)}>🕰</span>Time Order
|
||||
</div>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
this.props.viewMode === ViewMode.LEFT_HEAVY_FLAME_GRAPH && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={this.setLeftHeavyOrder}
|
||||
>
|
||||
<span className={css(style.emoji)}>⬅️</span>Left Heavy
|
||||
</div>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
this.props.viewMode === ViewMode.SANDWICH_VIEW && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={this.setSandwichView}
|
||||
>
|
||||
<span className={css(style.emoji)}>🥪</span>Sandwich
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderCenterContent() {
|
||||
const {activeProfileState, profileGroup} = this.props
|
||||
if (activeProfileState && profileGroup) {
|
||||
const {index} = activeProfileState
|
||||
if (profileGroup.profiles.length === 1) {
|
||||
return activeProfileState.profile.getName()
|
||||
} else {
|
||||
function makeNavButton(content: string, disabled: boolean, onClick: () => void) {
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className={css(
|
||||
style.emoji,
|
||||
style.toolbarProfileNavButton,
|
||||
disabled && style.toolbarProfileNavButtonDisabled,
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const prevButton = makeNavButton('⬅️', index === 0, () =>
|
||||
this.props.setProfileIndexToView(index - 1),
|
||||
)
|
||||
const nextButton = makeNavButton('➡️', index >= profileGroup.profiles.length - 1, () =>
|
||||
this.props.setProfileIndexToView(index + 1),
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={css(style.toolbarCenter)}>
|
||||
{prevButton}
|
||||
{activeProfileState.profile.getName()}{' '}
|
||||
<span className={css(style.toolbarProfileIndex)}>
|
||||
({activeProfileState.index + 1}/{profileGroup.profiles.length})
|
||||
</span>
|
||||
{nextButton}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return '🔬speedscope'
|
||||
}
|
||||
|
||||
renderRightContent() {
|
||||
const importFile = (
|
||||
<div className={css(style.toolbarTab)} onClick={this.props.browseForFile}>
|
||||
<span className={css(style.emoji)}>⤵️</span>Import
|
||||
</div>
|
||||
)
|
||||
const help = (
|
||||
<div className={css(style.toolbarTab)}>
|
||||
<a
|
||||
href="https://github.com/jlfwong/speedscope#usage"
|
||||
className={css(style.noLinkStyle)}
|
||||
target="_blank"
|
||||
>
|
||||
<span className={css(style.emoji)}>❓</span>Help
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={css(style.toolbarRight)}>
|
||||
{this.props.activeProfileState && (
|
||||
<div className={css(style.toolbarTab)} onClick={this.props.saveFile}>
|
||||
<span className={css(style.emoji)}>⤴️</span>Export
|
||||
</div>
|
||||
)}
|
||||
{importFile}
|
||||
{help}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={css(style.toolbar)}>
|
||||
{this.renderLeftContent()}
|
||||
{this.renderCenterContent()}
|
||||
{this.renderRightContent()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface GLCanvasProps {
|
||||
canvasContext: CanvasContext | null
|
||||
setGLCanvas: (canvas: HTMLCanvasElement | null) => void
|
||||
}
|
||||
export class GLCanvas extends Component<GLCanvasProps, void> {
|
||||
export class GLCanvas extends StatelessComponent<GLCanvasProps> {
|
||||
private canvas: HTMLCanvasElement | null = null
|
||||
|
||||
private ref = (canvas?: Element) => {
|
||||
private ref = (canvas: Element | null) => {
|
||||
if (canvas instanceof HTMLCanvasElement) {
|
||||
this.canvas = canvas
|
||||
} else {
|
||||
@ -209,7 +69,7 @@ export class GLCanvas extends Component<GLCanvasProps, void> {
|
||||
}
|
||||
|
||||
private container: HTMLElement | null = null
|
||||
private containerRef = (container?: Element) => {
|
||||
private containerRef = (container: Element | null) => {
|
||||
if (container instanceof HTMLElement) {
|
||||
this.container = container
|
||||
} else {
|
||||
@ -790,90 +650,4 @@ const style = StyleSheet.create({
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
toolbar: {
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
flexShrink: 0,
|
||||
background: Colors.BLACK,
|
||||
color: Colors.WHITE,
|
||||
textAlign: 'center',
|
||||
fontFamily: FontFamily.MONOSPACE,
|
||||
fontSize: FontSize.TITLE,
|
||||
lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`,
|
||||
userSelect: 'none',
|
||||
},
|
||||
toolbarLeft: {
|
||||
position: 'absolute',
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
top: 0,
|
||||
left: 0,
|
||||
marginRight: 2,
|
||||
textAlign: 'left',
|
||||
},
|
||||
toolbarCenter: {
|
||||
paddingTop: 1,
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
},
|
||||
toolbarRight: {
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
marginRight: 2,
|
||||
textAlign: 'right',
|
||||
},
|
||||
toolbarProfileIndex: {
|
||||
color: Colors.LIGHT_GRAY,
|
||||
},
|
||||
toolbarProfileNavButton: {
|
||||
opacity: 0.8,
|
||||
fontSize: FontSize.TITLE,
|
||||
lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`,
|
||||
':hover': {
|
||||
opacity: 1.0,
|
||||
},
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
marginLeft: '0.3em',
|
||||
marginRight: '0.3em',
|
||||
transition: `all ${Duration.HOVER_CHANGE} ease-in`,
|
||||
},
|
||||
toolbarProfileNavButtonDisabled: {
|
||||
opacity: 0.5,
|
||||
':hover': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
toolbarTab: {
|
||||
background: Colors.DARK_GRAY,
|
||||
marginTop: Sizes.SEPARATOR_HEIGHT,
|
||||
height: Sizes.TOOLBAR_TAB_HEIGHT,
|
||||
lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`,
|
||||
paddingLeft: 2,
|
||||
paddingRight: 8,
|
||||
display: 'inline-block',
|
||||
marginLeft: 2,
|
||||
transition: `all ${Duration.HOVER_CHANGE} ease-in`,
|
||||
':hover': {
|
||||
background: Colors.GRAY,
|
||||
},
|
||||
},
|
||||
toolbarTabActive: {
|
||||
background: Colors.BRIGHT_BLUE,
|
||||
':hover': {
|
||||
background: Colors.BRIGHT_BLUE,
|
||||
},
|
||||
},
|
||||
noLinkStyle: {
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
},
|
||||
emoji: {
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
paddingTop: '0px',
|
||||
marginRight: '0.3em',
|
||||
},
|
||||
})
|
||||
|
@ -1,84 +0,0 @@
|
||||
import {memoizeByShallowEquality} from '../lib/utils'
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {
|
||||
createMemoizedFlamechartRenderer,
|
||||
FlamechartViewContainerProps,
|
||||
createFlamechartSetters,
|
||||
} from './flamechart-view-container'
|
||||
import {createContainer, Dispatch} from '../lib/typed-redux'
|
||||
import {ApplicationState} from '../store'
|
||||
import {
|
||||
getCanvasContext,
|
||||
createGetColorBucketForFrame,
|
||||
createGetCSSColorForFrame,
|
||||
getFrameToColorBucket,
|
||||
} from '../store/getters'
|
||||
import {FlamechartID} from '../store/flamechart-view-state'
|
||||
import {FlamechartWrapper} from './flamechart-wrapper'
|
||||
|
||||
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, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState} = ownProps
|
||||
const {index, profile, sandwichViewState} = activeProfileState
|
||||
const {flattenRecursion, glCanvas} = state
|
||||
if (!profile) throw new Error('profile missing')
|
||||
if (!glCanvas) throw new Error('glCanvas missing')
|
||||
const {callerCallee} = sandwichViewState
|
||||
if (!callerCallee) throw new Error('callerCallee missing')
|
||||
const {selectedFrame} = callerCallee
|
||||
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
|
||||
const flamechart = getCalleeFlamegraph({
|
||||
calleeProfile: getCalleeProfile({profile, frame: selectedFrame, flattenRecursion}),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = getCalleeFlamegraphRenderer({canvasContext, flamechart})
|
||||
|
||||
return {
|
||||
renderInverted: false,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...createFlamechartSetters(dispatch, FlamechartID.SANDWICH_CALLEES, index),
|
||||
// This overrides the setSelectedNode specified in createFlamechartSettesr
|
||||
setSelectedNode: () => {},
|
||||
...callerCallee.calleeFlamegraph,
|
||||
}
|
||||
},
|
||||
)
|
87
src/views/callee-flamegraph-view.tsx
Normal file
87
src/views/callee-flamegraph-view.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import {memoizeByShallowEquality, noop} from '../lib/utils'
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {
|
||||
createMemoizedFlamechartRenderer,
|
||||
FlamechartViewContainerProps,
|
||||
useFlamechartSetters,
|
||||
} from './flamechart-view-container'
|
||||
import {
|
||||
getCanvasContext,
|
||||
createGetColorBucketForFrame,
|
||||
createGetCSSColorForFrame,
|
||||
getFrameToColorBucket,
|
||||
} from '../store/getters'
|
||||
import {FlamechartID} from '../store/flamechart-view-state'
|
||||
import {FlamechartWrapper} from './flamechart-wrapper'
|
||||
import {useAppSelector} from '../store'
|
||||
import {h} from 'preact'
|
||||
import {memo} from 'preact/compat'
|
||||
import {useCallback} from 'preact/hooks'
|
||||
|
||||
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 = memo((ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState} = ownProps
|
||||
const {index, profile, sandwichViewState} = activeProfileState
|
||||
const flattenRecursion = useAppSelector(useCallback(state => state.flattenRecursion, []))
|
||||
const glCanvas = useAppSelector(useCallback(state => state.glCanvas, []))
|
||||
|
||||
if (!profile) throw new Error('profile missing')
|
||||
if (!glCanvas) throw new Error('glCanvas missing')
|
||||
const {callerCallee} = sandwichViewState
|
||||
if (!callerCallee) throw new Error('callerCallee missing')
|
||||
const {selectedFrame} = callerCallee
|
||||
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
|
||||
const flamechart = getCalleeFlamegraph({
|
||||
calleeProfile: getCalleeProfile({profile, frame: selectedFrame, flattenRecursion}),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = getCalleeFlamegraphRenderer({canvasContext, flamechart})
|
||||
|
||||
return (
|
||||
<FlamechartWrapper
|
||||
renderInverted={false}
|
||||
flamechart={flamechart}
|
||||
flamechartRenderer={flamechartRenderer}
|
||||
canvasContext={canvasContext}
|
||||
getCSSColorForFrame={getCSSColorForFrame}
|
||||
{...useFlamechartSetters(FlamechartID.SANDWICH_CALLEES, index)}
|
||||
// This overrides the setSelectedNode specified in useFlamechartSettesr
|
||||
setSelectedNode={noop}
|
||||
{...callerCallee.calleeFlamegraph}
|
||||
/>
|
||||
)
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
import {h, Component} from 'preact'
|
||||
import {h} from 'preact'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {Colors, FontSize} from './style'
|
||||
|
||||
@ -6,10 +6,8 @@ interface ColorChitProps {
|
||||
color: string
|
||||
}
|
||||
|
||||
export class ColorChit extends Component<ColorChitProps, {}> {
|
||||
render() {
|
||||
return <span className={css(style.stackChit)} style={{backgroundColor: this.props.color}} />
|
||||
}
|
||||
export function ColorChit(props: ColorChitProps) {
|
||||
return <span className={css(style.stackChit)} style={{backgroundColor: props.color}} />
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {StyleDeclarationValue, css} from 'aphrodite'
|
||||
import {h, Component} from 'preact'
|
||||
import {h, Component, JSX} from 'preact'
|
||||
import {style} from './flamechart-style'
|
||||
import {formatPercent} from '../lib/utils'
|
||||
import {Frame, CallTreeNode} from '../lib/profile'
|
||||
|
@ -26,7 +26,7 @@ enum DraggingMode {
|
||||
|
||||
export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps, {}> {
|
||||
container: Element | null = null
|
||||
containerRef = (element?: Element) => {
|
||||
containerRef = (element: Element | null) => {
|
||||
this.container = element || null
|
||||
}
|
||||
|
||||
@ -394,7 +394,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
this.updateCursor(configSpaceMouse)
|
||||
}
|
||||
|
||||
private overlayCanvasRef = (element?: Element) => {
|
||||
private overlayCanvasRef = (element: Element | null) => {
|
||||
if (element) {
|
||||
this.overlayCanvas = element as HTMLCanvasElement
|
||||
this.overlayCtx = this.overlayCanvas.getContext('2d')
|
||||
|
@ -52,7 +52,7 @@ export interface FlamechartPanZoomViewProps {
|
||||
|
||||
export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps, {}> {
|
||||
private container: Element | null = null
|
||||
private containerRef = (element?: Element) => {
|
||||
private containerRef = (element: Element | null) => {
|
||||
this.container = element || null
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
|
||||
this.props.setConfigSpaceViewportRect(r)
|
||||
}
|
||||
|
||||
private overlayCanvasRef = (element?: Element) => {
|
||||
private overlayCanvasRef = (element: Element | null) => {
|
||||
if (element) {
|
||||
this.overlayCanvas = element as HTMLCanvasElement
|
||||
this.overlayCtx = this.overlayCanvas.getContext('2d')
|
||||
|
@ -1,11 +1,12 @@
|
||||
import {h} from 'preact'
|
||||
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, ActionCreator} from '../lib/typed-redux'
|
||||
import {ActionCreator} from '../lib/typed-redux'
|
||||
import {useActionCreator} from '../lib/preact-redux'
|
||||
import {Frame, Profile, CallTreeNode} from '../lib/profile'
|
||||
import {memoizeByShallowEquality} from '../lib/utils'
|
||||
import {ApplicationState} from '../store'
|
||||
import {FlamechartView} from './flamechart-view'
|
||||
import {
|
||||
getRowAtlas,
|
||||
@ -17,6 +18,8 @@ import {
|
||||
import {ActiveProfileState} from './application'
|
||||
import {Vec2, Rect} from '../lib/math'
|
||||
import {actions} from '../store/actions'
|
||||
import {memo} from 'preact/compat'
|
||||
import {useCallback} from 'preact/hooks'
|
||||
|
||||
interface FlamechartSetters {
|
||||
setLogicalSpaceViewportSize: (logicalSpaceViewportSize: Vec2) => void
|
||||
@ -32,19 +35,19 @@ interface WithFlamechartContext<T> {
|
||||
} & T
|
||||
}
|
||||
|
||||
export function createFlamechartSetters(
|
||||
dispatch: Dispatch,
|
||||
id: FlamechartID,
|
||||
profileIndex: number,
|
||||
): FlamechartSetters {
|
||||
function wrapActionCreator<T, U>(
|
||||
export function useFlamechartSetters(id: FlamechartID, profileIndex: number): FlamechartSetters {
|
||||
function useActionCreatorWithIndex<T, U>(
|
||||
actionCreator: ActionCreator<WithFlamechartContext<U>>,
|
||||
map: (t: T) => U,
|
||||
): (t: T) => void {
|
||||
return (t: T) => {
|
||||
const args = Object.assign({}, map(t), {id})
|
||||
dispatch(actionCreator({profileIndex, args}))
|
||||
}
|
||||
const callback = useCallback(
|
||||
(t: T) => {
|
||||
const args = Object.assign({}, map(t), {id})
|
||||
return actionCreator({profileIndex, args})
|
||||
},
|
||||
[actionCreator, map],
|
||||
)
|
||||
return useActionCreator(callback)
|
||||
}
|
||||
|
||||
const {
|
||||
@ -55,16 +58,22 @@ export function createFlamechartSetters(
|
||||
} = actions.flamechart
|
||||
|
||||
return {
|
||||
setNodeHover: wrapActionCreator(setHoveredNode, hover => ({hover})),
|
||||
setLogicalSpaceViewportSize: wrapActionCreator(
|
||||
setNodeHover: useActionCreatorWithIndex(
|
||||
setHoveredNode,
|
||||
useCallback(hover => ({hover}), []),
|
||||
),
|
||||
setLogicalSpaceViewportSize: useActionCreatorWithIndex(
|
||||
setLogicalSpaceViewportSize,
|
||||
logicalSpaceViewportSize => ({logicalSpaceViewportSize}),
|
||||
useCallback(logicalSpaceViewportSize => ({logicalSpaceViewportSize}), []),
|
||||
),
|
||||
setConfigSpaceViewportRect: wrapActionCreator(
|
||||
setConfigSpaceViewportRect: useActionCreatorWithIndex(
|
||||
setConfigSpaceViewportRect,
|
||||
configSpaceViewportRect => ({configSpaceViewportRect}),
|
||||
useCallback(configSpaceViewportRect => ({configSpaceViewportRect}), []),
|
||||
),
|
||||
setSelectedNode: useActionCreatorWithIndex(
|
||||
setSelectedNode,
|
||||
useCallback(selectedNode => ({selectedNode}), []),
|
||||
),
|
||||
setSelectedNode: wrapActionCreator(setSelectedNode, selectedNode => ({selectedNode})),
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,34 +130,33 @@ export interface FlamechartViewContainerProps {
|
||||
glCanvas: HTMLCanvasElement
|
||||
}
|
||||
|
||||
export const ChronoFlamechartView = createContainer(
|
||||
FlamechartView,
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState, glCanvas} = ownProps
|
||||
const {index, profile, chronoViewState} = activeProfileState
|
||||
export const ChronoFlamechartView = memo((props: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState, glCanvas} = props
|
||||
const {index, profile, chronoViewState} = activeProfileState
|
||||
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
|
||||
const flamechart = getChronoViewFlamechart({profile, getColorBucketForFrame})
|
||||
const flamechartRenderer = getChronoViewFlamechartRenderer({
|
||||
canvasContext,
|
||||
flamechart,
|
||||
})
|
||||
const flamechart = getChronoViewFlamechart({profile, getColorBucketForFrame})
|
||||
const flamechartRenderer = getChronoViewFlamechartRenderer({
|
||||
canvasContext,
|
||||
flamechart,
|
||||
})
|
||||
|
||||
return {
|
||||
renderInverted: false,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...createFlamechartSetters(dispatch, FlamechartID.CHRONO, index),
|
||||
...chronoViewState,
|
||||
}
|
||||
},
|
||||
)
|
||||
return (
|
||||
<FlamechartView
|
||||
renderInverted={false}
|
||||
flamechart={flamechart}
|
||||
flamechartRenderer={flamechartRenderer}
|
||||
canvasContext={canvasContext}
|
||||
getCSSColorForFrame={getCSSColorForFrame}
|
||||
{...useFlamechartSetters(FlamechartID.CHRONO, index)}
|
||||
{...chronoViewState}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export const getLeftHeavyFlamechart = memoizeByShallowEquality(
|
||||
({
|
||||
@ -169,35 +177,34 @@ export const getLeftHeavyFlamechart = memoizeByShallowEquality(
|
||||
|
||||
const getLeftHeavyFlamechartRenderer = createMemoizedFlamechartRenderer()
|
||||
|
||||
export const LeftHeavyFlamechartView = createContainer(
|
||||
FlamechartView,
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState, glCanvas} = ownProps
|
||||
export const LeftHeavyFlamechartView = memo((ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState, glCanvas} = ownProps
|
||||
|
||||
const {index, profile, leftHeavyViewState} = activeProfileState
|
||||
const {index, profile, leftHeavyViewState} = activeProfileState
|
||||
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
|
||||
const flamechart = getLeftHeavyFlamechart({
|
||||
profile,
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = getLeftHeavyFlamechartRenderer({
|
||||
canvasContext,
|
||||
flamechart,
|
||||
})
|
||||
const flamechart = getLeftHeavyFlamechart({
|
||||
profile,
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = getLeftHeavyFlamechartRenderer({
|
||||
canvasContext,
|
||||
flamechart,
|
||||
})
|
||||
|
||||
return {
|
||||
renderInverted: false,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...createFlamechartSetters(dispatch, FlamechartID.LEFT_HEAVY, index),
|
||||
...leftHeavyViewState,
|
||||
}
|
||||
},
|
||||
)
|
||||
return (
|
||||
<FlamechartView
|
||||
renderInverted={false}
|
||||
flamechart={flamechart}
|
||||
flamechartRenderer={flamechartRenderer}
|
||||
canvasContext={canvasContext}
|
||||
getCSSColorForFrame={getCSSColorForFrame}
|
||||
{...useFlamechartSetters(FlamechartID.LEFT_HEAVY, index)}
|
||||
{...leftHeavyViewState}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
@ -86,7 +86,7 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
|
||||
}
|
||||
|
||||
container: HTMLDivElement | null = null
|
||||
containerRef = (container?: Element) => {
|
||||
containerRef = (container: Element | null) => {
|
||||
this.container = (container as HTMLDivElement) || null
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
|
||||
)
|
||||
}
|
||||
container: HTMLDivElement | null = null
|
||||
containerRef = (container?: Element) => {
|
||||
containerRef = (container: Element | null) => {
|
||||
this.container = (container as HTMLDivElement) || null
|
||||
}
|
||||
private setNodeHover = (
|
||||
|
@ -14,12 +14,7 @@ export class Hovertip extends Component<HovertipProps, {}> {
|
||||
const width = containerSize.x
|
||||
const height = containerSize.y
|
||||
|
||||
const positionStyle: {
|
||||
left?: number
|
||||
right?: number
|
||||
top?: number
|
||||
bottom?: number
|
||||
} = {}
|
||||
const positionStyle: {[key: string]: number} = {}
|
||||
|
||||
const OFFSET_FROM_MOUSE = 7
|
||||
if (offset.x + OFFSET_FROM_MOUSE + Sizes.TOOLTIP_WIDTH_MAX < width) {
|
||||
|
@ -1,96 +0,0 @@
|
||||
import {memoizeByShallowEquality} from '../lib/utils'
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {
|
||||
createMemoizedFlamechartRenderer,
|
||||
FlamechartViewContainerProps,
|
||||
createFlamechartSetters,
|
||||
} from './flamechart-view-container'
|
||||
import {createContainer, Dispatch} from '../lib/typed-redux'
|
||||
import {ApplicationState} from '../store'
|
||||
import {
|
||||
getCanvasContext,
|
||||
createGetColorBucketForFrame,
|
||||
createGetCSSColorForFrame,
|
||||
getProfileWithRecursionFlattened,
|
||||
getFrameToColorBucket,
|
||||
} from '../store/getters'
|
||||
import {FlamechartID} from '../store/flamechart-view-state'
|
||||
import {FlamechartWrapper} from './flamechart-wrapper'
|
||||
|
||||
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, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState} = ownProps
|
||||
let {profile, sandwichViewState, index} = activeProfileState
|
||||
let {flattenRecursion, glCanvas} = state
|
||||
if (!profile) throw new Error('profile missing')
|
||||
if (!glCanvas) throw new Error('glCanvas missing')
|
||||
const {callerCallee} = sandwichViewState
|
||||
if (!callerCallee) throw new Error('callerCallee missing')
|
||||
const {selectedFrame} = callerCallee
|
||||
|
||||
profile = flattenRecursion ? getProfileWithRecursionFlattened(profile) : profile
|
||||
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
|
||||
const flamechart = getInvertedCallerFlamegraph({
|
||||
invertedCallerProfile: getInvertedCallerProfile({
|
||||
profile,
|
||||
frame: selectedFrame,
|
||||
flattenRecursion,
|
||||
}),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = getInvertedCallerFlamegraphRenderer({canvasContext, flamechart})
|
||||
|
||||
return {
|
||||
renderInverted: true,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
canvasContext,
|
||||
getCSSColorForFrame,
|
||||
...createFlamechartSetters(dispatch, FlamechartID.SANDWICH_INVERTED_CALLERS, index),
|
||||
// This overrides the setSelectedNode specified in createFlamechartSettesr
|
||||
setSelectedNode: () => {},
|
||||
...callerCallee.invertedCallerFlamegraph,
|
||||
}
|
||||
},
|
||||
)
|
99
src/views/inverted-caller-flamegraph-view.tsx
Normal file
99
src/views/inverted-caller-flamegraph-view.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import {memoizeByShallowEquality, noop} from '../lib/utils'
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {Flamechart} from '../lib/flamechart'
|
||||
import {
|
||||
createMemoizedFlamechartRenderer,
|
||||
FlamechartViewContainerProps,
|
||||
useFlamechartSetters,
|
||||
} from './flamechart-view-container'
|
||||
import {
|
||||
getCanvasContext,
|
||||
createGetColorBucketForFrame,
|
||||
createGetCSSColorForFrame,
|
||||
getProfileWithRecursionFlattened,
|
||||
getFrameToColorBucket,
|
||||
} from '../store/getters'
|
||||
import {FlamechartID} from '../store/flamechart-view-state'
|
||||
import {useAppSelector} from '../store'
|
||||
import {FlamechartWrapper} from './flamechart-wrapper'
|
||||
import {h} from 'preact'
|
||||
import {memo} from 'preact/compat'
|
||||
import {useCallback} from 'preact/hooks'
|
||||
|
||||
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 = memo((ownProps: FlamechartViewContainerProps) => {
|
||||
const {activeProfileState} = ownProps
|
||||
let {profile, sandwichViewState, index} = activeProfileState
|
||||
const flattenRecursion = useAppSelector(useCallback(state => state.flattenRecursion, []))
|
||||
const glCanvas = useAppSelector(useCallback(state => state.glCanvas, []))
|
||||
|
||||
if (!profile) throw new Error('profile missing')
|
||||
if (!glCanvas) throw new Error('glCanvas missing')
|
||||
const {callerCallee} = sandwichViewState
|
||||
if (!callerCallee) throw new Error('callerCallee missing')
|
||||
const {selectedFrame} = callerCallee
|
||||
|
||||
profile = flattenRecursion ? getProfileWithRecursionFlattened(profile) : profile
|
||||
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
const canvasContext = getCanvasContext(glCanvas)
|
||||
|
||||
const flamechart = getInvertedCallerFlamegraph({
|
||||
invertedCallerProfile: getInvertedCallerProfile({
|
||||
profile,
|
||||
frame: selectedFrame,
|
||||
flattenRecursion,
|
||||
}),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = getInvertedCallerFlamegraphRenderer({canvasContext, flamechart})
|
||||
|
||||
return (
|
||||
<FlamechartWrapper
|
||||
renderInverted={true}
|
||||
flamechart={flamechart}
|
||||
flamechartRenderer={flamechartRenderer}
|
||||
canvasContext={canvasContext}
|
||||
getCSSColorForFrame={getCSSColorForFrame}
|
||||
{...useFlamechartSetters(FlamechartID.SANDWICH_INVERTED_CALLERS, index)}
|
||||
// This overrides the setSelectedNode specified in useFlamechartSettesr
|
||||
setSelectedNode={noop}
|
||||
{...callerCallee.invertedCallerFlamegraph}
|
||||
/>
|
||||
)
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
import {h, Component} from 'preact'
|
||||
import {h, Component, JSX} from 'preact'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {Profile, Frame} from '../lib/profile'
|
||||
import {sortBy, formatPercent} from '../lib/utils'
|
||||
@ -6,10 +6,12 @@ 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, getFrameToColorBucket} from '../store/getters'
|
||||
import {ActiveProfileState} from './application'
|
||||
import {useActionCreator} from '../lib/preact-redux'
|
||||
import {useAppSelector} from '../store'
|
||||
import {memo} from 'preact/compat'
|
||||
import {useCallback, useMemo, useRef} from 'preact/hooks'
|
||||
|
||||
export enum SortField {
|
||||
SYMBOL_NAME,
|
||||
@ -31,14 +33,12 @@ interface HBarProps {
|
||||
perc: number
|
||||
}
|
||||
|
||||
class HBarDisplay extends Component<HBarProps, {}> {
|
||||
render() {
|
||||
return (
|
||||
<div className={css(style.hBarDisplay)}>
|
||||
<div className={css(style.hBarDisplayFilled)} style={{width: `${this.props.perc}%`}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
function HBarDisplay(props: HBarProps) {
|
||||
return (
|
||||
<div className={css(style.hBarDisplay)}>
|
||||
<div className={css(style.hBarDisplayFilled)} style={{width: `${props.perc}%`}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SortIconProps {
|
||||
@ -67,9 +67,54 @@ class SortIcon extends Component<SortIconProps, {}> {
|
||||
}
|
||||
}
|
||||
|
||||
interface ProfileTableRowViewProps {
|
||||
frame: Frame
|
||||
index: number
|
||||
profile: Profile
|
||||
selectedFrame: Frame | null
|
||||
setSelectedFrame: (f: Frame) => void
|
||||
getCSSColorForFrame: (frame: Frame) => string
|
||||
}
|
||||
|
||||
const ProfileTableRowView = (props: ProfileTableRowViewProps) => {
|
||||
const {frame, profile, index, selectedFrame, setSelectedFrame, getCSSColorForFrame} = props
|
||||
const totalWeight = frame.getTotalWeight()
|
||||
const selfWeight = frame.getSelfWeight()
|
||||
const totalPerc = (100.0 * totalWeight) / profile.getTotalNonIdleWeight()
|
||||
const selfPerc = (100.0 * selfWeight) / profile.getTotalNonIdleWeight()
|
||||
|
||||
const selected = frame === selectedFrame
|
||||
|
||||
// We intentionally use index rather than frame.key here as the tr key
|
||||
// in order to re-use rows when sorting rather than creating all new elements.
|
||||
return (
|
||||
<tr
|
||||
key={`${index}`}
|
||||
onClick={setSelectedFrame.bind(null, frame)}
|
||||
className={css(
|
||||
style.tableRow,
|
||||
index % 2 == 0 && style.tableRowEven,
|
||||
selected && style.tableRowSelected,
|
||||
)}
|
||||
>
|
||||
<td className={css(style.numericCell)}>
|
||||
{profile.formatValue(totalWeight)} ({formatPercent(totalPerc)})
|
||||
<HBarDisplay perc={totalPerc} />
|
||||
</td>
|
||||
<td className={css(style.numericCell)}>
|
||||
{profile.formatValue(selfWeight)} ({formatPercent(selfPerc)})
|
||||
<HBarDisplay perc={selfPerc} />
|
||||
</td>
|
||||
<td title={frame.file} className={css(style.textCell)}>
|
||||
<ColorChit color={getCSSColorForFrame(frame)} />
|
||||
{frame.name}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
interface ProfileTableViewProps {
|
||||
profile: Profile
|
||||
profileIndex: number
|
||||
selectedFrame: Frame | null
|
||||
getCSSColorForFrame: (frame: Frame) => string
|
||||
sortMethod: SortMethod
|
||||
@ -77,81 +122,51 @@ interface ProfileTableViewProps {
|
||||
setSortMethod: (sortMethod: SortMethod) => void
|
||||
}
|
||||
|
||||
export class ProfileTableView extends Component<ProfileTableViewProps, void> {
|
||||
renderRow(frame: Frame, index: number) {
|
||||
const {profile, selectedFrame} = this.props
|
||||
export const ProfileTableView = memo((props: ProfileTableViewProps) => {
|
||||
const {
|
||||
profile,
|
||||
sortMethod,
|
||||
setSortMethod,
|
||||
selectedFrame,
|
||||
setSelectedFrame,
|
||||
getCSSColorForFrame,
|
||||
} = props
|
||||
|
||||
const totalWeight = frame.getTotalWeight()
|
||||
const selfWeight = frame.getSelfWeight()
|
||||
const totalPerc = (100.0 * totalWeight) / profile.getTotalNonIdleWeight()
|
||||
const selfPerc = (100.0 * selfWeight) / profile.getTotalNonIdleWeight()
|
||||
const onSortClick = useCallback(
|
||||
(field: SortField, ev: MouseEvent) => {
|
||||
ev.preventDefault()
|
||||
|
||||
const selected = frame === selectedFrame
|
||||
|
||||
// We intentionally use index rather than frame.key here as the tr key
|
||||
// in order to re-use rows when sorting rather than creating all new elements.
|
||||
return (
|
||||
<tr
|
||||
key={`${index}`}
|
||||
onClick={this.props.setSelectedFrame.bind(null, frame)}
|
||||
className={css(
|
||||
style.tableRow,
|
||||
index % 2 == 0 && style.tableRowEven,
|
||||
selected && style.tableRowSelected,
|
||||
)}
|
||||
>
|
||||
<td className={css(style.numericCell)}>
|
||||
{profile.formatValue(totalWeight)} ({formatPercent(totalPerc)})
|
||||
<HBarDisplay perc={totalPerc} />
|
||||
</td>
|
||||
<td className={css(style.numericCell)}>
|
||||
{profile.formatValue(selfWeight)} ({formatPercent(selfPerc)})
|
||||
<HBarDisplay perc={selfPerc} />
|
||||
</td>
|
||||
<td title={frame.file} className={css(style.textCell)}>
|
||||
<ColorChit color={this.props.getCSSColorForFrame(frame)} />
|
||||
{frame.name}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
onSortClick = (field: SortField, ev: MouseEvent) => {
|
||||
ev.preventDefault()
|
||||
|
||||
const {sortMethod} = this.props
|
||||
|
||||
if (sortMethod.field == field) {
|
||||
// Toggle
|
||||
this.props.setSortMethod({
|
||||
field,
|
||||
direction:
|
||||
sortMethod.direction === SortDirection.ASCENDING
|
||||
? SortDirection.DESCENDING
|
||||
: SortDirection.ASCENDING,
|
||||
})
|
||||
} else {
|
||||
// Set a sane default
|
||||
switch (field) {
|
||||
case SortField.SYMBOL_NAME: {
|
||||
this.props.setSortMethod({field, direction: SortDirection.ASCENDING})
|
||||
break
|
||||
}
|
||||
case SortField.SELF: {
|
||||
this.props.setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
break
|
||||
}
|
||||
case SortField.TOTAL: {
|
||||
this.props.setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
break
|
||||
if (sortMethod.field == field) {
|
||||
// Toggle
|
||||
setSortMethod({
|
||||
field,
|
||||
direction:
|
||||
sortMethod.direction === SortDirection.ASCENDING
|
||||
? SortDirection.DESCENDING
|
||||
: SortDirection.ASCENDING,
|
||||
})
|
||||
} else {
|
||||
// Set a sane default
|
||||
switch (field) {
|
||||
case SortField.SYMBOL_NAME: {
|
||||
setSortMethod({field, direction: SortDirection.ASCENDING})
|
||||
break
|
||||
}
|
||||
case SortField.SELF: {
|
||||
setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
break
|
||||
}
|
||||
case SortField.TOTAL: {
|
||||
setSortMethod({field, direction: SortDirection.DESCENDING})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getFrameList = (): Frame[] => {
|
||||
const {profile, sortMethod} = this.props
|
||||
},
|
||||
[sortMethod, setSortMethod],
|
||||
)
|
||||
|
||||
const frameList = useMemo((): Frame[] => {
|
||||
const frameList: Frame[] = []
|
||||
|
||||
profile.forEachFrame(f => frameList.push(f))
|
||||
@ -177,89 +192,94 @@ export class ProfileTableView extends Component<ProfileTableViewProps, void> {
|
||||
}
|
||||
|
||||
return frameList
|
||||
}
|
||||
}, [profile, sortMethod])
|
||||
|
||||
private listView: ScrollableListView | null = null
|
||||
private listViewRef = (listView: ScrollableListView | null) => {
|
||||
if (listView === this.listView) return
|
||||
this.listView = listView
|
||||
const listViewRef = useRef<ScrollableListView | null>(null)
|
||||
const listViewCallback = useCallback(
|
||||
(listView: ScrollableListView | null) => {
|
||||
if (listView === listViewRef.current) return
|
||||
listViewRef.current = listView
|
||||
if (!selectedFrame || !listView) return
|
||||
const index = frameList.indexOf(selectedFrame)
|
||||
if (index === -1) return
|
||||
listView.scrollIndexIntoView(index)
|
||||
},
|
||||
[listViewRef, selectedFrame, frameList],
|
||||
)
|
||||
|
||||
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 renderItems = useCallback(
|
||||
(firstIndex: number, lastIndex: number) => {
|
||||
const rows: JSX.Element[] = []
|
||||
|
||||
for (let i = firstIndex; i <= lastIndex; i++) {
|
||||
rows.push(this.renderRow(frameList[i], i))
|
||||
rows.push(
|
||||
ProfileTableRowView({
|
||||
frame: frameList[i],
|
||||
index: i,
|
||||
profile: profile,
|
||||
selectedFrame: selectedFrame,
|
||||
setSelectedFrame: setSelectedFrame,
|
||||
getCSSColorForFrame: getCSSColorForFrame,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return <table className={css(style.tableView)}>{rows}</table>
|
||||
}
|
||||
},
|
||||
[frameList, profile, selectedFrame, setSelectedFrame, getCSSColorForFrame],
|
||||
)
|
||||
|
||||
const listItems: ListItem[] = frameList.map(f => ({size: Sizes.FRAME_HEIGHT}))
|
||||
const listItems: ListItem[] = frameList.map(f => ({size: Sizes.FRAME_HEIGHT}))
|
||||
|
||||
return (
|
||||
<div className={css(commonStyle.vbox, style.profileTableView)}>
|
||||
<table className={css(style.tableView)}>
|
||||
<thead className={css(style.tableHeader)}>
|
||||
<tr>
|
||||
<th
|
||||
className={css(style.numericCell)}
|
||||
onClick={ev => this.onSortClick(SortField.TOTAL, ev)}
|
||||
>
|
||||
<SortIcon
|
||||
activeDirection={
|
||||
sortMethod.field === SortField.TOTAL ? sortMethod.direction : null
|
||||
}
|
||||
/>
|
||||
Total
|
||||
</th>
|
||||
<th
|
||||
className={css(style.numericCell)}
|
||||
onClick={ev => this.onSortClick(SortField.SELF, ev)}
|
||||
>
|
||||
<SortIcon
|
||||
activeDirection={
|
||||
sortMethod.field === SortField.SELF ? sortMethod.direction : null
|
||||
}
|
||||
/>
|
||||
Self
|
||||
</th>
|
||||
<th
|
||||
className={css(style.textCell)}
|
||||
onClick={ev => this.onSortClick(SortField.SYMBOL_NAME, ev)}
|
||||
>
|
||||
<SortIcon
|
||||
activeDirection={
|
||||
sortMethod.field === SortField.SYMBOL_NAME ? sortMethod.direction : null
|
||||
}
|
||||
/>
|
||||
Symbol Name
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<ScrollableListView
|
||||
ref={this.listViewRef}
|
||||
axis={'y'}
|
||||
items={listItems}
|
||||
className={css(style.scrollView)}
|
||||
renderItems={renderItems}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
const onTotalClick = useCallback((ev: MouseEvent) => onSortClick(SortField.TOTAL, ev), [
|
||||
onSortClick,
|
||||
])
|
||||
const onSelfClick = useCallback((ev: MouseEvent) => onSortClick(SortField.SELF, ev), [
|
||||
onSortClick,
|
||||
])
|
||||
const onSymbolNameClick = useCallback(
|
||||
(ev: MouseEvent) => onSortClick(SortField.SYMBOL_NAME, ev),
|
||||
[onSortClick],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={css(commonStyle.vbox, style.profileTableView)}>
|
||||
<table className={css(style.tableView)}>
|
||||
<thead className={css(style.tableHeader)}>
|
||||
<tr>
|
||||
<th className={css(style.numericCell)} onClick={onTotalClick}>
|
||||
<SortIcon
|
||||
activeDirection={sortMethod.field === SortField.TOTAL ? sortMethod.direction : null}
|
||||
/>
|
||||
Total
|
||||
</th>
|
||||
<th className={css(style.numericCell)} onClick={onSelfClick}>
|
||||
<SortIcon
|
||||
activeDirection={sortMethod.field === SortField.SELF ? sortMethod.direction : null}
|
||||
/>
|
||||
Self
|
||||
</th>
|
||||
<th className={css(style.textCell)} onClick={onSymbolNameClick}>
|
||||
<SortIcon
|
||||
activeDirection={
|
||||
sortMethod.field === SortField.SYMBOL_NAME ? sortMethod.direction : null
|
||||
}
|
||||
/>
|
||||
Symbol Name
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<ScrollableListView
|
||||
ref={listViewCallback}
|
||||
axis={'y'}
|
||||
items={listItems}
|
||||
className={css(style.scrollView)}
|
||||
renderItems={renderItems}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const style = StyleSheet.create({
|
||||
profileTableView: {
|
||||
@ -333,34 +353,34 @@ interface ProfileTableViewContainerProps {
|
||||
activeProfileState: ActiveProfileState
|
||||
}
|
||||
|
||||
export const ProfileTableViewContainer = createContainer(
|
||||
ProfileTableView,
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: ProfileTableViewContainerProps) => {
|
||||
const {activeProfileState} = ownProps
|
||||
const {profile, sandwichViewState, index} = activeProfileState
|
||||
if (!profile) throw new Error('profile missing')
|
||||
const {tableSortMethod} = state
|
||||
const {callerCallee} = sandwichViewState
|
||||
const selectedFrame = callerCallee ? callerCallee.selectedFrame : null
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
export const ProfileTableViewContainer = memo((ownProps: ProfileTableViewContainerProps) => {
|
||||
const {activeProfileState} = ownProps
|
||||
const {profile, sandwichViewState, index} = activeProfileState
|
||||
if (!profile) throw new Error('profile missing')
|
||||
const tableSortMethod = useAppSelector(useCallback(state => state.tableSortMethod, []))
|
||||
const {callerCallee} = sandwichViewState
|
||||
const selectedFrame = callerCallee ? callerCallee.selectedFrame : null
|
||||
const frameToColorBucket = getFrameToColorBucket(profile)
|
||||
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
|
||||
|
||||
const setSelectedFrame = (selectedFrame: Frame | null) => {
|
||||
dispatch(actions.sandwichView.setSelectedFrame({profileIndex: index, args: selectedFrame}))
|
||||
}
|
||||
const setSelectedFrameWithIndex = useCallback(
|
||||
(selectedFrame: Frame | null) => {
|
||||
return actions.sandwichView.setSelectedFrame({profileIndex: index, args: selectedFrame})
|
||||
},
|
||||
[index],
|
||||
)
|
||||
|
||||
const setSortMethod = (sortMethod: SortMethod) => {
|
||||
dispatch(actions.sandwichView.setTableSortMethod(sortMethod))
|
||||
}
|
||||
const setSelectedFrame = useActionCreator(setSelectedFrameWithIndex)
|
||||
const setSortMethod = useActionCreator(actions.sandwichView.setTableSortMethod)
|
||||
|
||||
return {
|
||||
profile,
|
||||
profileIndex: activeProfileState.index,
|
||||
selectedFrame,
|
||||
getCSSColorForFrame,
|
||||
sortMethod: tableSortMethod,
|
||||
setSelectedFrame,
|
||||
setSortMethod,
|
||||
}
|
||||
},
|
||||
)
|
||||
return (
|
||||
<ProfileTableView
|
||||
profile={profile}
|
||||
selectedFrame={selectedFrame}
|
||||
getCSSColorForFrame={getCSSColorForFrame}
|
||||
sortMethod={tableSortMethod}
|
||||
setSelectedFrame={setSelectedFrame}
|
||||
setSortMethod={setSortMethod}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
@ -1,14 +1,16 @@
|
||||
import {Frame} from '../lib/profile'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {ProfileTableViewContainer} from './profile-table-view'
|
||||
import {h} from 'preact'
|
||||
import {h, JSX} from 'preact'
|
||||
import {memo} from 'preact/compat'
|
||||
import {useCallback} from 'preact/hooks'
|
||||
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 {StatelessComponent} from '../lib/typed-redux'
|
||||
import {InvertedCallerFlamegraphView} from './inverted-caller-flamegraph-view'
|
||||
import {CalleeFlamegraphView} from './callee-flamegraph-view'
|
||||
import {ActiveProfileState} from './application'
|
||||
import {useDispatch} from '../lib/preact-redux'
|
||||
|
||||
interface SandwichViewProps {
|
||||
selectedFrame: Frame | null
|
||||
@ -122,28 +124,30 @@ interface SandwichViewContainerProps {
|
||||
glCanvas: HTMLCanvasElement
|
||||
}
|
||||
|
||||
export const SandwichViewContainer = createContainer(
|
||||
SandwichView,
|
||||
(state: ApplicationState, dispatch: Dispatch, ownProps: SandwichViewContainerProps) => {
|
||||
const {activeProfileState, glCanvas} = ownProps
|
||||
const {sandwichViewState, index} = activeProfileState
|
||||
const {callerCallee} = sandwichViewState
|
||||
export const SandwichViewContainer = memo((ownProps: SandwichViewContainerProps) => {
|
||||
const {activeProfileState, glCanvas} = ownProps
|
||||
const {sandwichViewState, index} = activeProfileState
|
||||
const {callerCallee} = sandwichViewState
|
||||
|
||||
const setSelectedFrame = (selectedFrame: Frame | null) => {
|
||||
const dispatch = useDispatch()
|
||||
const setSelectedFrame = useCallback(
|
||||
(selectedFrame: Frame | null) => {
|
||||
dispatch(
|
||||
actions.sandwichView.setSelectedFrame({
|
||||
profileIndex: index,
|
||||
args: selectedFrame,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
activeProfileState: activeProfileState,
|
||||
glCanvas,
|
||||
setSelectedFrame,
|
||||
selectedFrame: callerCallee ? callerCallee.selectedFrame : null,
|
||||
profileIndex: index,
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
[dispatch, index],
|
||||
)
|
||||
return (
|
||||
<SandwichView
|
||||
activeProfileState={activeProfileState}
|
||||
glCanvas={glCanvas}
|
||||
setSelectedFrame={setSelectedFrame}
|
||||
selectedFrame={callerCallee ? callerCallee.selectedFrame : null}
|
||||
profileIndex={index}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
// A simple implementation of an efficient scrolling list view which
|
||||
// renders only items within the viewport + a couple extra items.
|
||||
|
||||
import {h, Component} from 'preact'
|
||||
import {h, Component, JSX} from 'preact'
|
||||
|
||||
export interface ListItem {
|
||||
size: number
|
||||
@ -38,7 +38,7 @@ export class ScrollableListView extends Component<
|
||||
}
|
||||
|
||||
private viewport: HTMLDivElement | null = null
|
||||
private viewportRef = (viewport?: Element) => {
|
||||
private viewportRef = (viewport: Element | null) => {
|
||||
this.viewport = (viewport as HTMLDivElement) || null
|
||||
}
|
||||
|
||||
|
230
src/views/toolbar.tsx
Normal file
230
src/views/toolbar.tsx
Normal file
@ -0,0 +1,230 @@
|
||||
import {ApplicationProps} from './application'
|
||||
import {ViewMode} from '../store'
|
||||
import {h, JSX, Fragment} from 'preact'
|
||||
import {useCallback} from 'preact/hooks'
|
||||
import {StyleSheet, css} from 'aphrodite'
|
||||
import {Sizes, Colors, FontFamily, FontSize, Duration} from './style'
|
||||
|
||||
interface ToolbarProps extends ApplicationProps {
|
||||
browseForFile(): void
|
||||
saveFile(): void
|
||||
}
|
||||
|
||||
function useSetViewMode(setViewMode: (viewMode: ViewMode) => void, viewMode: ViewMode) {
|
||||
return useCallback(() => setViewMode(viewMode), [setViewMode, viewMode])
|
||||
}
|
||||
|
||||
function ToolbarLeftContent(props: ToolbarProps) {
|
||||
const setChronoFlameChart = useSetViewMode(props.setViewMode, ViewMode.CHRONO_FLAME_CHART)
|
||||
const setLeftHeavyFlameGraph = useSetViewMode(props.setViewMode, ViewMode.LEFT_HEAVY_FLAME_GRAPH)
|
||||
const setSandwichView = useSetViewMode(props.setViewMode, ViewMode.SANDWICH_VIEW)
|
||||
|
||||
if (!props.activeProfileState) return null
|
||||
|
||||
return (
|
||||
<div className={css(style.toolbarLeft)}>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
props.viewMode === ViewMode.CHRONO_FLAME_CHART && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={setChronoFlameChart}
|
||||
>
|
||||
<span className={css(style.emoji)}>🕰</span>Time Order
|
||||
</div>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
props.viewMode === ViewMode.LEFT_HEAVY_FLAME_GRAPH && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={setLeftHeavyFlameGraph}
|
||||
>
|
||||
<span className={css(style.emoji)}>⬅️</span>Left Heavy
|
||||
</div>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
props.viewMode === ViewMode.SANDWICH_VIEW && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={setSandwichView}
|
||||
>
|
||||
<span className={css(style.emoji)}>🥪</span>Sandwich
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ToolbarCenterContent(props: ToolbarProps): JSX.Element {
|
||||
const {activeProfileState, profileGroup} = props
|
||||
if (activeProfileState && profileGroup) {
|
||||
const {index} = activeProfileState
|
||||
if (profileGroup.profiles.length === 1) {
|
||||
return <Fragment>{activeProfileState.profile.getName()}</Fragment>
|
||||
} else {
|
||||
function makeNavButton(content: string, disabled: boolean, onClick: () => void) {
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className={css(
|
||||
style.emoji,
|
||||
style.toolbarProfileNavButton,
|
||||
disabled && style.toolbarProfileNavButtonDisabled,
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const prevButton = makeNavButton('⬅️', index === 0, () =>
|
||||
props.setProfileIndexToView(index - 1),
|
||||
)
|
||||
const nextButton = makeNavButton('➡️', index >= profileGroup.profiles.length - 1, () =>
|
||||
props.setProfileIndexToView(index + 1),
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={css(style.toolbarCenter)}>
|
||||
{prevButton}
|
||||
{activeProfileState.profile.getName()}{' '}
|
||||
<span className={css(style.toolbarProfileIndex)}>
|
||||
({activeProfileState.index + 1}/{profileGroup.profiles.length})
|
||||
</span>
|
||||
{nextButton}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return <Fragment>{'🔬speedscope'}</Fragment>
|
||||
}
|
||||
|
||||
function ToolbarRightContent(props: ToolbarProps) {
|
||||
const importFile = (
|
||||
<div className={css(style.toolbarTab)} onClick={props.browseForFile}>
|
||||
<span className={css(style.emoji)}>⤵️</span>Import
|
||||
</div>
|
||||
)
|
||||
const help = (
|
||||
<div className={css(style.toolbarTab)}>
|
||||
<a
|
||||
href="https://github.com/jlfwong/speedscope#usage"
|
||||
className={css(style.noLinkStyle)}
|
||||
target="_blank"
|
||||
>
|
||||
<span className={css(style.emoji)}>❓</span>Help
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={css(style.toolbarRight)}>
|
||||
{props.activeProfileState && (
|
||||
<div className={css(style.toolbarTab)} onClick={props.saveFile}>
|
||||
<span className={css(style.emoji)}>⤴️</span>Export
|
||||
</div>
|
||||
)}
|
||||
{importFile}
|
||||
{help}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Toolbar(props: ToolbarProps) {
|
||||
return (
|
||||
<div className={css(style.toolbar)}>
|
||||
<ToolbarLeftContent {...props} />
|
||||
<ToolbarCenterContent {...props} />
|
||||
<ToolbarRightContent {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
toolbar: {
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
flexShrink: 0,
|
||||
background: Colors.BLACK,
|
||||
color: Colors.WHITE,
|
||||
textAlign: 'center',
|
||||
fontFamily: FontFamily.MONOSPACE,
|
||||
fontSize: FontSize.TITLE,
|
||||
lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`,
|
||||
userSelect: 'none',
|
||||
},
|
||||
toolbarLeft: {
|
||||
position: 'absolute',
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
top: 0,
|
||||
left: 0,
|
||||
marginRight: 2,
|
||||
textAlign: 'left',
|
||||
},
|
||||
toolbarCenter: {
|
||||
paddingTop: 1,
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
},
|
||||
toolbarRight: {
|
||||
height: Sizes.TOOLBAR_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
marginRight: 2,
|
||||
textAlign: 'right',
|
||||
},
|
||||
toolbarProfileIndex: {
|
||||
color: Colors.LIGHT_GRAY,
|
||||
},
|
||||
toolbarProfileNavButton: {
|
||||
opacity: 0.8,
|
||||
fontSize: FontSize.TITLE,
|
||||
lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`,
|
||||
':hover': {
|
||||
opacity: 1.0,
|
||||
},
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
marginLeft: '0.3em',
|
||||
marginRight: '0.3em',
|
||||
transition: `all ${Duration.HOVER_CHANGE} ease-in`,
|
||||
},
|
||||
toolbarProfileNavButtonDisabled: {
|
||||
opacity: 0.5,
|
||||
':hover': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
toolbarTab: {
|
||||
background: Colors.DARK_GRAY,
|
||||
marginTop: Sizes.SEPARATOR_HEIGHT,
|
||||
height: Sizes.TOOLBAR_TAB_HEIGHT,
|
||||
lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`,
|
||||
paddingLeft: 2,
|
||||
paddingRight: 8,
|
||||
display: 'inline-block',
|
||||
marginLeft: 2,
|
||||
transition: `all ${Duration.HOVER_CHANGE} ease-in`,
|
||||
':hover': {
|
||||
background: Colors.GRAY,
|
||||
},
|
||||
},
|
||||
toolbarTabActive: {
|
||||
background: Colors.BRIGHT_BLUE,
|
||||
':hover': {
|
||||
background: Colors.BRIGHT_BLUE,
|
||||
},
|
||||
},
|
||||
emoji: {
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
paddingTop: '0px',
|
||||
marginRight: '0.3em',
|
||||
},
|
||||
noLinkStyle: {
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
},
|
||||
})
|
Loading…
Reference in New Issue
Block a user