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:
Jamie Wong 2020-05-23 16:42:31 -07:00 committed by GitHub
parent ca1abfdd32
commit 351994972d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1436 additions and 1213 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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
}

View File

@ -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
}

View File

@ -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(

View File

@ -41,7 +41,7 @@ export namespace actions {
export const setTableSortMethod = actionCreator<SortMethod>('sandwichView.setTableSortMethod')
export const setSelectedFrame = actionCreatorWithIndex<Frame | null>(
'sandwichView.setSelectedFarmr',
'sandwichView.setSelectedFrame',
)
}

View File

@ -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)
}

View File

@ -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)
})

View File

@ -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
}
}

View File

@ -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}
/>
)
})

View File

@ -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',
},
})

View File

@ -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,
}
},
)

View 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}
/>
)
})

View File

@ -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({

View File

@ -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'

View File

@ -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')

View File

@ -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')

View File

@ -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}
/>
)
})

View File

@ -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
}

View File

@ -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 = (

View File

@ -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) {

View File

@ -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,
}
},
)

View 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}
/>
)
})

View File

@ -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}
/>
)
})

View File

@ -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}
/>
)
})

View File

@ -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
View 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',
},
})