diff --git a/src/lib/typed-redux.ts b/src/lib/typed-redux.ts index 22ffaf9..d699ff9 100644 --- a/src/lib/typed-redux.ts +++ b/src/lib/typed-redux.ts @@ -56,10 +56,10 @@ export type Dispatch = redux.Dispatch> // We make this into a single function invocation instead of the connect(map, map)(Component) // syntax to make better use of type inference. // -// NOTE: The way this works right now is going to regenerate new setters on every -// store update. To make this not the case, we'd have to have mapStateToProps actually -// specified. This may be a performance issue in the future, but we're going to eat -// this cost for now in exchange for simpler type inference. +// 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( component: { new (props: ComponentProps): ComponentType diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index f760624..3c6e6bf 100644 --- a/src/lib/utils.test.ts +++ b/src/lib/utils.test.ts @@ -10,6 +10,9 @@ import { formatPercent, KeyedSet, binarySearch, + memoizeByReference, + memoizeByShallowEquality, + objectsHaveShallowEquality, } from './utils' test('sortBy', () => { @@ -109,3 +112,64 @@ test('binarySearch', () => { expect(lo).toBeLessThan(Math.E) expect(hi).toBeGreaterThan(Math.E) }) + +test('memoizeByReference', () => { + let hitCount = 0 + const identity = memoizeByReference((arg: number) => { + hitCount++ + return arg + }) + + expect(identity(3)).toBe(3) + expect(hitCount).toBe(1) + expect(identity(3)).toBe(3) + expect(hitCount).toBe(1) + + expect(identity(5)).toBe(5) + expect(hitCount).toBe(2) + + expect(identity(3)).toBe(3) + expect(hitCount).toBe(3) +}) + +test('memoizeByShallowEquality', () => { + let hitCount = 0 + const identity = memoizeByShallowEquality((arg: {a: number; b: number}) => { + hitCount++ + return arg + }) + + expect(identity({a: 1, b: 2})).toEqual({a: 1, b: 2}) + expect(hitCount).toBe(1) + expect(identity({a: 1, b: 2})).toEqual({a: 1, b: 2}) + expect(hitCount).toBe(1) + + expect(identity({a: 2, b: 2})).toEqual({a: 2, b: 2}) + expect(hitCount).toBe(2) + expect(identity({a: 2, b: 2})).toEqual({a: 2, b: 2}) + expect(hitCount).toBe(2) + + expect(identity({a: 2, b: 1})).toEqual({a: 2, b: 1}) + expect(hitCount).toBe(3) + expect(identity({a: 2, b: 1})).toEqual({a: 2, b: 1}) + expect(hitCount).toBe(3) + + expect(identity({a: 1, b: 2})).toEqual({a: 1, b: 2}) + expect(hitCount).toBe(4) +}) + +test('objectsHaveShallowEquality', () => { + expect(objectsHaveShallowEquality({}, {})).toBe(true) + expect(objectsHaveShallowEquality({a: 1, b: 2}, {a: 1, b: 2})).toBe(true) + + expect(objectsHaveShallowEquality({a: 1, b: 2}, {a: 1, b: 3})).toBe(false) + expect(objectsHaveShallowEquality({a: 1, b: 2}, {a: 2, b: 2})).toBe(false) + expect(objectsHaveShallowEquality({a: 1}, {a: 1, b: 2})).toBe(false) + expect(objectsHaveShallowEquality({a: 1, b: 2}, {b: 2})).toBe(false) + + expect(objectsHaveShallowEquality([], [])).toBe(true) + expect(objectsHaveShallowEquality([1, 2], [1, 2])).toBe(true) + + expect(objectsHaveShallowEquality([1], [1, 2])).toBe(false) + expect(objectsHaveShallowEquality([1, 2], [1])).toBe(false) +}) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 6aae95b..ccd77bc 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -122,7 +122,7 @@ export function binarySearch( export function noop(...args: any[]) {} -function shallowEquals(a: T, b: T): boolean { +export function objectsHaveShallowEquality(a: T, b: T): boolean { for (let key in a) { if (a[key] !== b[key]) return false } @@ -132,7 +132,6 @@ function shallowEquals(a: T, b: T): boolean { return true } -// TODO(jlfwong): Write tests for this export function memoizeByShallowEquality(cb: (t: T) => U): (t: T) => U { let last: {args: T; result: U} | null = null return (args: T) => { @@ -141,7 +140,7 @@ export function memoizeByShallowEquality(cb: (t: T) => U): result = cb(args) last = {args, result} return result - } else if (shallowEquals(last.args, args)) { + } else if (objectsHaveShallowEquality(last.args, args)) { return last.result } else { last.args = args diff --git a/src/store/actions.ts b/src/store/actions.ts index 760e6ff..0a655b5 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -38,9 +38,7 @@ export namespace actions { export namespace sandwichView { // Set the table sorting method used for the sandwich view. - export const setTableSortMethod = actionCreatorWithIndex( - 'sandwichView.setTableSortMethod', - ) + export const setTableSortMethod = actionCreator('sandwichView.setTableSortMethod') export const setSelectedFrame = actionCreatorWithIndex( 'sandwichView.setSelectedFarmr', diff --git a/src/store/flamechart-view-state.test.ts b/src/store/flamechart-view-state.test.ts new file mode 100644 index 0000000..895d15e --- /dev/null +++ b/src/store/flamechart-view-state.test.ts @@ -0,0 +1,105 @@ +import {storeTest, profileGroupTwoSampled} from './store-test-utils' +import {actions} from './actions' +import {Rect, Vec2} from '../lib/math' +import {FlamechartID} from './flamechart-view-state' +import {ViewMode} from '.' + +describe('flamechart view state', () => { + storeTest('setHoveredNode', ({getState, dispatch}) => { + dispatch(actions.setProfileGroup(profileGroupTwoSampled)) + expect(getState().profileGroup!.profiles[0].chronoViewState.hover).toEqual(null) + const {profileGroup} = getState() + const hover = { + node: profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0], + event: {} as MouseEvent, + } + dispatch( + actions.flamechart.setHoveredNode({ + profileIndex: 0, + args: { + id: FlamechartID.CHRONO, + hover, + }, + }), + ) + expect(getState().profileGroup!.profiles[0].chronoViewState.hover).toEqual(hover) + expect(getState().profileGroup!.profiles[0].leftHeavyViewState.hover).toEqual(null) + expect(getState().profileGroup!.profiles[1].chronoViewState.hover).toEqual(null) + + // Changing view mode should clear the hover state + dispatch(actions.setViewMode(ViewMode.LEFT_HEAVY_FLAME_GRAPH)) + expect(getState().profileGroup!.profiles[0].chronoViewState.hover).toEqual(null) + }) + + storeTest('setSelectedNode', ({getState, dispatch}) => { + dispatch(actions.setProfileGroup(profileGroupTwoSampled)) + expect(getState().profileGroup!.profiles[0].chronoViewState.selectedNode).toEqual(null) + const {profileGroup} = getState() + const selectedNode = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0] + dispatch( + actions.flamechart.setSelectedNode({ + profileIndex: 0, + args: { + id: FlamechartID.CHRONO, + selectedNode, + }, + }), + ) + expect(getState().profileGroup!.profiles[0].chronoViewState.selectedNode).toEqual(selectedNode) + expect(getState().profileGroup!.profiles[0].leftHeavyViewState.selectedNode).toEqual(null) + expect(getState().profileGroup!.profiles[1].chronoViewState.selectedNode).toEqual(null) + }) + + storeTest('setLogicalSpaceViewportSize', ({getState, dispatch}) => { + dispatch(actions.setProfileGroup(profileGroupTwoSampled)) + expect(getState().profileGroup!.profiles[0].chronoViewState.logicalSpaceViewportSize).toEqual( + Vec2.zero, + ) + const logicalSpaceViewportSize = new Vec2(3, 5) + dispatch( + actions.flamechart.setLogicalSpaceViewportSize({ + profileIndex: 0, + args: { + id: FlamechartID.CHRONO, + logicalSpaceViewportSize, + }, + }), + ) + expect(getState().profileGroup!.profiles[0].chronoViewState.logicalSpaceViewportSize).toEqual( + logicalSpaceViewportSize, + ) + + expect( + getState().profileGroup!.profiles[0].leftHeavyViewState.logicalSpaceViewportSize, + ).toEqual(Vec2.zero) + expect(getState().profileGroup!.profiles[1].chronoViewState.logicalSpaceViewportSize).toEqual( + Vec2.zero, + ) + }) + + storeTest('setConfigSpaceViewportRect', ({getState, dispatch}) => { + dispatch(actions.setProfileGroup(profileGroupTwoSampled)) + expect(getState().profileGroup!.profiles[0].chronoViewState.configSpaceViewportRect).toEqual( + Rect.empty, + ) + dispatch( + actions.flamechart.setConfigSpaceViewportRect({ + profileIndex: 0, + args: { + id: FlamechartID.CHRONO, + configSpaceViewportRect: new Rect(Vec2.zero, Vec2.unit), + }, + }), + ) + expect(getState().profileGroup!.profiles[0].chronoViewState.configSpaceViewportRect).toEqual( + new Rect(Vec2.zero, Vec2.unit), + ) + + expect(getState().profileGroup!.profiles[0].leftHeavyViewState.configSpaceViewportRect).toEqual( + Rect.empty, + ) + expect(getState().profileGroup!.profiles[1].chronoViewState.configSpaceViewportRect).toEqual( + Rect.empty, + ) + }) +}) diff --git a/src/store/getters.ts b/src/store/getters.ts index 19a87d0..0c62346 100644 --- a/src/store/getters.ts +++ b/src/store/getters.ts @@ -49,8 +49,6 @@ export const getFrameToColorBucket = memoizeByReference((profile: Profile): Map< string | number, number > => { - document.title = `${profile.getName()} - speedscope` - const frames: Frame[] = [] profile.forEachFrame(f => frames.push(f)) function key(f: Frame) { diff --git a/src/store/index.test.ts b/src/store/index.test.ts new file mode 100644 index 0000000..60e5de1 --- /dev/null +++ b/src/store/index.test.ts @@ -0,0 +1,51 @@ +import {ViewMode} from '.' +import {actions} from './actions' +import {storeTest} from './store-test-utils' +import {SortField, SortDirection} from '../views/profile-table-view' + +storeTest('flattenRecursion', ({getState, dispatch}) => { + expect(getState().flattenRecursion).toBe(false) + dispatch(actions.setFlattenRecursion(true)) + expect(getState().flattenRecursion).toBe(true) +}) + +storeTest('viewMode', ({getState, dispatch}) => { + expect(getState().viewMode).toBe(ViewMode.CHRONO_FLAME_CHART) + dispatch(actions.setViewMode(ViewMode.SANDWICH_VIEW)) + expect(getState().viewMode).toBe(ViewMode.SANDWICH_VIEW) +}) + +storeTest('dragActive', ({getState, dispatch}) => { + expect(getState().dragActive).toBe(false) + dispatch(actions.setDragActive(true)) + expect(getState().dragActive).toBe(true) +}) + +storeTest('loading', ({getState, dispatch}) => { + expect(getState().loading).toBe(false) + dispatch(actions.setLoading(true)) + expect(getState().loading).toBe(true) +}) + +storeTest('error', ({getState, dispatch}) => { + expect(getState().error).toBe(false) + dispatch(actions.setError(true)) + expect(getState().error).toBe(true) +}) + +storeTest('setTableSortMethod', ({getState, dispatch}) => { + expect(getState().tableSortMethod).toEqual({ + field: SortField.SELF, + direction: SortDirection.DESCENDING, + }) + dispatch( + actions.sandwichView.setTableSortMethod({ + field: SortField.SYMBOL_NAME, + direction: SortDirection.ASCENDING, + }), + ) + expect(getState().tableSortMethod).toEqual({ + field: SortField.SYMBOL_NAME, + direction: SortDirection.ASCENDING, + }) +}) diff --git a/src/store/index.ts b/src/store/index.ts index 7212918..58854eb 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -9,6 +9,7 @@ import * as redux from 'redux' 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' export const enum ViewMode { CHRONO_FLAME_CHART, @@ -28,6 +29,8 @@ export interface ApplicationState { loading: boolean error: boolean + tableSortMethod: SortMethod + profileGroup: ProfileGroupState } @@ -59,6 +62,11 @@ export function createApplicationStore( dragActive: setter(actions.setDragActive, false), loading: setter(actions.setLoading, loading), error: setter(actions.setError, false), + + tableSortMethod: setter(actions.sandwichView.setTableSortMethod, { + field: SortField.SELF, + direction: SortDirection.DESCENDING, + }), }) return redux.createStore(reducer, initialState) diff --git a/src/store/profiles-state.test.ts b/src/store/profiles-state.test.ts new file mode 100644 index 0000000..6be5e2f --- /dev/null +++ b/src/store/profiles-state.test.ts @@ -0,0 +1,36 @@ +import {storeTest, profileGroupTwoSampled} from './store-test-utils' +import {actions} from './actions' + +describe('profileGroup', () => { + storeTest('setProfileGroup', ({getState, dispatch}) => { + expect(getState().profileGroup).toBe(null) + dispatch(actions.setProfileGroup(profileGroupTwoSampled)) + const {profileGroup} = getState() + expect(profileGroup!.name).toBe(profileGroupTwoSampled.name) + expect(profileGroup!.indexToView).toBe(profileGroupTwoSampled.indexToView) + + for (let i = 0; i < profileGroupTwoSampled.profiles.length; i++) { + expect(profileGroup!.profiles[i].profile).toEqual(profileGroupTwoSampled.profiles[i]) + } + }) + + storeTest('setProfileIndexToView', ({getState, dispatch}) => { + dispatch(actions.setProfileGroup(profileGroupTwoSampled)) + expect(getState().profileGroup!.indexToView).toBe(1) + dispatch(actions.setProfileIndexToView(0)) + expect(getState()!.profileGroup!.indexToView).toBe(0) + dispatch(actions.setProfileIndexToView(-1)) + expect(getState()!.profileGroup!.indexToView).toBe(0) + dispatch(actions.setProfileIndexToView(2)) + expect(getState().profileGroup!.indexToView).toBe(1) + }) + + storeTest('preserve state', ({getState, dispatch}) => { + // When an unrelated action occurs, we should not change the identiy of the profileGroup + // property + dispatch(actions.setProfileGroup(profileGroupTwoSampled)) + const profileGroup = getState().profileGroup + dispatch(actions.setDragActive(true)) + expect(getState().profileGroup).toBe(profileGroup) + }) +}) diff --git a/src/store/profiles-state.ts b/src/store/profiles-state.ts index 2722bd9..9d2f579 100644 --- a/src/store/profiles-state.ts +++ b/src/store/profiles-state.ts @@ -5,9 +5,10 @@ import { FlamechartID, } from './flamechart-view-state' import {SandwichViewState, createSandwichView} from './sandwich-view-state' -import {Reducer, Action, actionCreator, setter} from '../lib/typed-redux' +import {Reducer, actionCreator, setter} from '../lib/typed-redux' import {actions} from './actions' import {clamp} from '../lib/math' +import {objectsHaveShallowEquality} from '../lib/utils' export type ProfileGroupState = { name: string @@ -31,12 +32,33 @@ export function actionCreatorWithIndex(name: string) { return actionCreator<{profileIndex: number; args: T}>(name) } -export function actionProfileIndex(action: Action): number | null { - const {payload} = action - if (payload != null && typeof payload === 'object' && 'profileIndex' in payload) { - return parseInt(payload.profileIndex, 0) - } else { - return null +function createProfileReducer(profile: Profile, index: number): Reducer { + const chronoViewStateReducer = createFlamechartViewStateReducer(FlamechartID.CHRONO, index) + const leftHeavyViewStateReducer = createFlamechartViewStateReducer(FlamechartID.LEFT_HEAVY, index) + const sandwichViewStateReducer = createSandwichView(index) + + return (state = undefined, action) => { + if (state === undefined) { + return { + profile, + chronoViewState: chronoViewStateReducer(undefined, action), + leftHeavyViewState: leftHeavyViewStateReducer(undefined, action), + sandwichViewState: sandwichViewStateReducer(undefined, action), + } + } + + const nextState = { + profile, + chronoViewState: chronoViewStateReducer(state.chronoViewState, action), + leftHeavyViewState: leftHeavyViewStateReducer(state.leftHeavyViewState, action), + sandwichViewState: sandwichViewStateReducer(state.sandwichViewState, action), + } + + if (objectsHaveShallowEquality(state, nextState)) { + return state + } + + return nextState } } @@ -47,19 +69,7 @@ export const profileGroup: Reducer = (state = null, action) = indexToView, name, profiles: profiles.map((p, i) => { - return { - profile: p, - frameToColorBucket: new Map(), - chronoViewState: createFlamechartViewStateReducer(FlamechartID.CHRONO, i)( - undefined, - action, - ), - leftHeavyViewState: createFlamechartViewStateReducer(FlamechartID.LEFT_HEAVY, i)( - undefined, - action, - ), - sandwichViewState: createSandwichView(i)(undefined, action), - } + return createProfileReducer(p, i)(undefined, action) }), } } @@ -72,35 +82,18 @@ export const profileGroup: Reducer = (state = null, action) = 0, profiles.length - 1, ) - let nextProfiles = profiles + const nextProfiles = profiles.map((profileState, profileIndex) => { + return createProfileReducer(profileState.profile, profileIndex)(profileState, action) + }) - const profileIndexFromAction = actionProfileIndex(action) - if (profileIndexFromAction != null) { - nextProfiles = profiles.map((profileState, profileIndex) => { - return { - profile: profileState.profile, - chronoViewState: createFlamechartViewStateReducer(FlamechartID.CHRONO, profileIndex)( - profileState.chronoViewState, - action, - ), - leftHeavyViewState: createFlamechartViewStateReducer( - FlamechartID.LEFT_HEAVY, - profileIndex, - )(profileState.leftHeavyViewState, action), - sandwichViewState: createSandwichView(profileIndex)( - profileState.sandwichViewState, - action, - ), - } - }) + if (indexToView === nextIndexToView && objectsHaveShallowEquality(profiles, nextProfiles)) { + return state } - if (indexToView !== nextIndexToView || profiles !== nextProfiles) { - return { - ...state, - indexToView: nextIndexToView, - profiles: nextProfiles, - } + return { + ...state, + indexToView: nextIndexToView, + profiles: nextProfiles, } } return state diff --git a/src/store/sandwich-view-state.test.ts b/src/store/sandwich-view-state.test.ts new file mode 100644 index 0000000..173e00f --- /dev/null +++ b/src/store/sandwich-view-state.test.ts @@ -0,0 +1,70 @@ +import {storeTest, profileGroupTwoSampled} from './store-test-utils' +import {actions} from './actions' +import {Vec2} from '../lib/math' +import {FlamechartID} from './flamechart-view-state' + +describe('sandwich view state', () => { + storeTest('setSelectedFrame', ({getState, dispatch}) => { + dispatch(actions.setProfileGroup(profileGroupTwoSampled)) + expect(getState().profileGroup!.profiles[0].sandwichViewState.callerCallee).toBe(null) + const {profileGroup} = getState() + const selectedFrame = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0] + .frame + dispatch(actions.sandwichView.setSelectedFrame({profileIndex: 0, args: selectedFrame})) + + expect(getState().profileGroup!.profiles[0].sandwichViewState.callerCallee!.selectedFrame).toBe( + selectedFrame, + ) + + // Other profiles selection state should not change + expect(getState().profileGroup!.profiles[1].sandwichViewState.callerCallee).toBe(null) + + // Changing selection when something is already selected should work + const selectedFrame2 = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot() + .children[0].children[0].frame + dispatch(actions.sandwichView.setSelectedFrame({profileIndex: 0, args: selectedFrame2})) + expect(getState().profileGroup!.profiles[0].sandwichViewState.callerCallee!.selectedFrame).toBe( + selectedFrame2, + ) + + // Clear selection + dispatch(actions.sandwichView.setSelectedFrame({profileIndex: 0, args: null})) + expect(getState().profileGroup!.profiles[0].sandwichViewState.callerCallee).toBe(null) + }) + + storeTest('contained flamegraphs receive updates', ({getState, dispatch}) => { + dispatch(actions.setProfileGroup(profileGroupTwoSampled)) + const {profileGroup} = getState() + const selectedFrame = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0] + .frame + dispatch(actions.sandwichView.setSelectedFrame({profileIndex: 0, args: selectedFrame})) + + expect( + getState().profileGroup!.profiles[0].sandwichViewState.callerCallee!.calleeFlamegraph + .logicalSpaceViewportSize, + ).toEqual(Vec2.zero) + dispatch( + actions.flamechart.setLogicalSpaceViewportSize({ + profileIndex: 0, + args: {id: FlamechartID.SANDWICH_CALLEES, logicalSpaceViewportSize: Vec2.unit}, + }), + ) + expect( + getState().profileGroup!.profiles[0].sandwichViewState.callerCallee!.calleeFlamegraph + .logicalSpaceViewportSize, + ).toEqual(Vec2.unit) + }) + + storeTest('preserve identity', ({getState, dispatch}) => { + dispatch(actions.setProfileGroup(profileGroupTwoSampled)) + const {profileGroup} = getState() + const selectedFrame = profileGroup!.profiles[0].profile.getAppendOrderCalltreeRoot().children[0] + .frame + dispatch(actions.sandwichView.setSelectedFrame({profileIndex: 0, args: selectedFrame})) + + // When an unrelated action is triggered, it should not change the identity of the sandwichViewState + const sandwichViewState = getState().profileGroup!.profiles[0].sandwichViewState + dispatch(actions.setLoading(true)) + expect(getState().profileGroup!.profiles[0].sandwichViewState).toBe(sandwichViewState) + }) +}) diff --git a/src/store/sandwich-view-state.ts b/src/store/sandwich-view-state.ts index c68bb17..1207a2a 100644 --- a/src/store/sandwich-view-state.ts +++ b/src/store/sandwich-view-state.ts @@ -1,4 +1,3 @@ -import {SortMethod, SortField, SortDirection} from '../views/profile-table-view' import {Frame} from '../lib/profile' import { FlamechartViewState, @@ -9,7 +8,6 @@ import {Reducer} from '../lib/typed-redux' import {actions} from './actions' export interface SandwichViewState { - tableSortMethod: SortMethod callerCallee: CallerCalleeState | null } @@ -19,11 +17,6 @@ export interface CallerCalleeState { calleeFlamegraph: FlamechartViewState } -const defaultSortMethod = { - field: SortField.SELF, - direction: SortDirection.DESCENDING, -} - export function createSandwichView(profileIndex: number): Reducer { const calleesReducer = createFlamechartViewStateReducer( FlamechartID.SANDWICH_CALLEES, @@ -38,32 +31,7 @@ export function createSandwichView(profileIndex: number): Reducer { - const {callerCallee} = state - if (callerCallee) { - const {calleeFlamegraph, invertedCallerFlamegraph} = callerCallee - const nextCalleeFlamegraph = calleesReducer(calleeFlamegraph, action) - const nextInvertedCallerFlamegraph = invertedCallersReducer(invertedCallerFlamegraph, action) - - if ( - nextCalleeFlamegraph !== calleeFlamegraph || - nextInvertedCallerFlamegraph !== invertedCallerFlamegraph - ) { - return { - ...state, - callerCallee: { - ...callerCallee, - calleeFlamegraph: nextCalleeFlamegraph, - invertedCallerFlamegraph: nextInvertedCallerFlamegraph, - }, - } - } - } - - if (actions.sandwichView.setTableSortMethod.matches(action) && applies(action)) { - return {...state, tableSortMethod: action.payload.args} - } - + return (state = {callerCallee: null}, action) => { if (actions.sandwichView.setSelectedFrame.matches(action) && applies(action)) { if (action.payload.args == null) { return { @@ -82,6 +50,29 @@ export function createSandwichView(profileIndex: number): Reducer) => void) { + const store = createApplicationStore({}) + test(name, () => { + cb(store) + }) +} + +const filepath = './sample/profiles/speedscope/0.6.0/two-sampled.speedscope.json' +const input = fs.readFileSync(filepath, 'utf8') +export const profileGroupTwoSampled = importSpeedscopeProfiles(JSON.parse(input)) diff --git a/src/views/application.tsx b/src/views/application.tsx index 4486b93..26abc6b 100644 --- a/src/views/application.tsx +++ b/src/views/application.tsx @@ -262,12 +262,19 @@ export class Application extends StatelessComponent { return } + if (this.props.hashParams.title) { + profileGroup = { + name: this.props.hashParams.title, + ...profileGroup, + } + } + document.title = `${profileGroup.name} - speedscope` + for (let profile of profileGroup.profiles) { await profile.demangle() } for (let profile of profileGroup.profiles) { - // TODO(jlfwong): Profile names vs. profile group names needs some thought const title = this.props.hashParams.title || profile.getName() profile.setName(title) } diff --git a/src/views/profile-table-view.tsx b/src/views/profile-table-view.tsx index 141396b..e292f91 100644 --- a/src/views/profile-table-view.tsx +++ b/src/views/profile-table-view.tsx @@ -339,7 +339,8 @@ export const ProfileTableViewContainer = createContainer( const {activeProfileState} = ownProps const {profile, sandwichViewState, index} = activeProfileState if (!profile) throw new Error('profile missing') - const {tableSortMethod, callerCallee} = sandwichViewState + const {tableSortMethod} = state + const {callerCallee} = sandwichViewState const selectedFrame = callerCallee ? callerCallee.selectedFrame : null const frameToColorBucket = getFrameToColorBucket(profile) const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket) @@ -349,7 +350,7 @@ export const ProfileTableViewContainer = createContainer( } const setSortMethod = (sortMethod: SortMethod) => { - dispatch(actions.sandwichView.setTableSortMethod({profileIndex: index, args: sortMethod})) + dispatch(actions.sandwichView.setTableSortMethod(sortMethod)) } return {