Skip to content

Commit 43bf473

Browse files
committed
Don't recompute old states when toggling actions
1 parent f757de4 commit 43bf473

File tree

2 files changed

+211
-121
lines changed

2 files changed

+211
-121
lines changed

src/instrument.js

Lines changed: 138 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,32 @@ function computeNextEntry(reducer, action, state, error) {
7777
}
7878

7979
/**
80-
* Runs the reducer on all actions to get a fresh computation log.
80+
* Runs the reducer on invalidated actions to get a fresh computation log.
8181
*/
82-
function recomputeStates(reducer, committedState, actionsById, stagedActionIds, skippedActionIds) {
83-
const computedStates = [];
84-
for (let i = 0; i < stagedActionIds.length; i++) {
82+
function recomputeStates(
83+
computedStates,
84+
minInvalidatedStateIndex,
85+
reducer,
86+
committedState,
87+
actionsById,
88+
stagedActionIds,
89+
skippedActionIds
90+
) {
91+
// Optimization: exit early and return the same reference
92+
// if we know nothing could have changed.
93+
if (
94+
minInvalidatedStateIndex >= computedStates.length &&
95+
computedStates.length === stagedActionIds.length
96+
) {
97+
return computedStates;
98+
}
99+
100+
const nextComputedStates = computedStates.slice(0, minInvalidatedStateIndex);
101+
for (let i = minInvalidatedStateIndex; i < stagedActionIds.length; i++) {
85102
const actionId = stagedActionIds[i];
86103
const action = actionsById[actionId].action;
87104

88-
const previousEntry = computedStates[i - 1];
105+
const previousEntry = nextComputedStates[i - 1];
89106
const previousState = previousEntry ? previousEntry.state : committedState;
90107
const previousError = previousEntry ? previousEntry.error : undefined;
91108

@@ -94,10 +111,10 @@ function recomputeStates(reducer, committedState, actionsById, stagedActionIds,
94111
previousEntry :
95112
computeNextEntry(reducer, action, previousState, previousError);
96113

97-
computedStates.push(entry);
114+
nextComputedStates.push(entry);
98115
}
99116

100-
return computedStates;
117+
return nextComputedStates;
101118
}
102119

103120
/**
@@ -121,14 +138,13 @@ function liftReducerWith(reducer, initialCommittedState, monitorReducer) {
121138
skippedActionIds: [],
122139
committedState: initialCommittedState,
123140
currentStateIndex: 0,
124-
computedStates: undefined
141+
computedStates: []
125142
};
126143

127144
/**
128145
* Manages how the history actions modify the history state.
129146
*/
130147
return (liftedState = initialLiftedState, liftedAction) => {
131-
let shouldRecomputeStates = true;
132148
let {
133149
monitorState,
134150
actionsById,
@@ -140,116 +156,125 @@ function liftReducerWith(reducer, initialCommittedState, monitorReducer) {
140156
computedStates
141157
} = liftedState;
142158

159+
// By default, agressively recompute every state whatever happens.
160+
// This has O(n) performance, so we'll override this to a sensible
161+
// value whenever we feel like we don't have to recompute the states.
162+
let minInvalidatedStateIndex = 0;
163+
143164
switch (liftedAction.type) {
144-
case ActionTypes.RESET:
145-
actionsById = {
146-
0: liftAction(INIT_ACTION)
147-
};
148-
nextActionId = 1;
149-
stagedActionIds = [0];
150-
skippedActionIds = [];
151-
committedState = initialCommittedState;
152-
currentStateIndex = 0;
153-
break;
154-
case ActionTypes.COMMIT:
155-
actionsById = {
156-
0: liftAction(INIT_ACTION)
157-
};
158-
nextActionId = 1;
159-
stagedActionIds = [0];
160-
skippedActionIds = [];
161-
committedState = computedStates[currentStateIndex].state;
162-
currentStateIndex = 0;
163-
break;
164-
case ActionTypes.ROLLBACK:
165-
actionsById = {
166-
0: liftAction(INIT_ACTION)
167-
};
168-
nextActionId = 1;
169-
stagedActionIds = [0];
170-
skippedActionIds = [];
171-
currentStateIndex = 0;
172-
break;
173-
case ActionTypes.TOGGLE_ACTION:
174-
const index = skippedActionIds.indexOf(liftedAction.id);
175-
if (index === -1) {
176-
skippedActionIds = [
177-
liftedAction.id,
178-
...skippedActionIds
179-
];
180-
} else {
181-
skippedActionIds = [
182-
...skippedActionIds.slice(0, index),
183-
...skippedActionIds.slice(index + 1)
184-
];
165+
case ActionTypes.RESET: {
166+
// Get back to the state the store was created with.
167+
actionsById = { 0: liftAction(INIT_ACTION) };
168+
nextActionId = 1;
169+
stagedActionIds = [0];
170+
skippedActionIds = [];
171+
committedState = initialCommittedState;
172+
currentStateIndex = 0;
173+
computedStates = [];
174+
break;
185175
}
186-
break;
187-
case ActionTypes.JUMP_TO_STATE:
188-
currentStateIndex = liftedAction.index;
189-
// Optimization: we know the history has not changed.
190-
shouldRecomputeStates = false;
191-
break;
192-
case ActionTypes.SWEEP:
193-
stagedActionIds = difference(stagedActionIds, skippedActionIds);
194-
skippedActionIds = [];
195-
currentStateIndex = Math.min(currentStateIndex, stagedActionIds.length - 1);
196-
break;
197-
case ActionTypes.PERFORM_ACTION:
198-
if (currentStateIndex === stagedActionIds.length - 1) {
199-
currentStateIndex++;
176+
case ActionTypes.COMMIT: {
177+
// Consider the last committed state the new starting point.
178+
// Squash any staged actions into a single committed state.
179+
actionsById = { 0: liftAction(INIT_ACTION) };
180+
nextActionId = 1;
181+
stagedActionIds = [0];
182+
skippedActionIds = [];
183+
committedState = computedStates[currentStateIndex].state;
184+
currentStateIndex = 0;
185+
computedStates = [];
186+
break;
187+
}
188+
case ActionTypes.ROLLBACK: {
189+
// Forget about any staged actions.
190+
// Start again from the last committed state.
191+
actionsById = { 0: liftAction(INIT_ACTION) };
192+
nextActionId = 1;
193+
stagedActionIds = [0];
194+
skippedActionIds = [];
195+
currentStateIndex = 0;
196+
computedStates = [];
197+
break;
198+
}
199+
case ActionTypes.TOGGLE_ACTION: {
200+
// Toggle whether an action with given ID is skipped.
201+
// Being skipped means it is a no-op during the computation.
202+
const { id: actionId } = liftedAction;
203+
const index = skippedActionIds.indexOf(actionId);
204+
if (index === -1) {
205+
skippedActionIds = [actionId, ...skippedActionIds];
206+
} else {
207+
skippedActionIds = skippedActionIds.filter(id => id !== actionId);
208+
}
209+
// Optimization: we know history before this action hasn't changed
210+
minInvalidatedStateIndex = stagedActionIds.indexOf(actionId);
211+
break;
212+
}
213+
case ActionTypes.JUMP_TO_STATE: {
214+
// Without recomputing anything, move the pointer that tell us
215+
// which state is considered the current one. Useful for sliders.
216+
currentStateIndex = liftedAction.index;
217+
// Optimization: we know the history has not changed.
218+
minInvalidatedStateIndex = Infinity;
219+
break;
220+
}
221+
case ActionTypes.SWEEP: {
222+
// Forget any actions that are currently being skipped.
223+
stagedActionIds = difference(stagedActionIds, skippedActionIds);
224+
skippedActionIds = [];
225+
currentStateIndex = Math.min(currentStateIndex, stagedActionIds.length - 1);
226+
break;
227+
}
228+
case ActionTypes.PERFORM_ACTION: {
229+
if (currentStateIndex === stagedActionIds.length - 1) {
230+
currentStateIndex++;
231+
}
232+
const actionId = nextActionId++;
233+
// Mutation! This is the hottest path, and we optimize on purpose.
234+
// It is safe because we set a new key in a cache dictionary.
235+
actionsById[actionId] = liftedAction;
236+
stagedActionIds = [...stagedActionIds, actionId];
237+
// Optimization: we know that only the new action needs computing.
238+
minInvalidatedStateIndex = stagedActionIds.length - 1;
239+
break;
240+
}
241+
case ActionTypes.IMPORT_STATE: {
242+
// Completely replace everything.
243+
({
244+
monitorState,
245+
actionsById,
246+
nextActionId,
247+
stagedActionIds,
248+
skippedActionIds,
249+
committedState,
250+
currentStateIndex,
251+
computedStates
252+
} = liftedAction.nextLiftedState);
253+
break;
254+
}
255+
case '@@redux/INIT': {
256+
// Always recompute states on hot reload and init.
257+
minInvalidatedStateIndex = 0;
258+
break;
259+
}
260+
default: {
261+
// If the action is not recognized, it's a monitor action.
262+
// Optimization: a monitor action can't change history.
263+
minInvalidatedStateIndex = Infinity;
264+
break;
200265
}
201-
202-
const actionId = nextActionId++;
203-
// Mutation! This is the hottest path, and we optimize on purpose.
204-
// It is safe because we set a new key in a cache dictionary.
205-
actionsById[actionId] = liftedAction;
206-
stagedActionIds = [...stagedActionIds, actionId];
207-
// Optimization: we know that the past has not changed.
208-
shouldRecomputeStates = false;
209-
// Instead of recomputing the states, append the next one.
210-
const previousEntry = computedStates[computedStates.length - 1];
211-
const nextEntry = computeNextEntry(
212-
reducer,
213-
liftedAction.action,
214-
previousEntry.state,
215-
previousEntry.error
216-
);
217-
computedStates = [...computedStates, nextEntry];
218-
break;
219-
case ActionTypes.IMPORT_STATE:
220-
({
221-
monitorState,
222-
actionsById,
223-
nextActionId,
224-
stagedActionIds,
225-
skippedActionIds,
226-
committedState,
227-
currentStateIndex,
228-
computedStates
229-
} = liftedAction.nextLiftedState);
230-
break;
231-
case '@@redux/INIT':
232-
// Always recompute states on hot reload and init.
233-
shouldRecomputeStates = true;
234-
break;
235-
default:
236-
// Optimization: a monitor action can't change history.
237-
shouldRecomputeStates = false;
238-
break;
239-
}
240-
241-
if (shouldRecomputeStates) {
242-
computedStates = recomputeStates(
243-
reducer,
244-
committedState,
245-
actionsById,
246-
stagedActionIds,
247-
skippedActionIds
248-
);
249266
}
250267

268+
computedStates = recomputeStates(
269+
computedStates,
270+
minInvalidatedStateIndex,
271+
reducer,
272+
committedState,
273+
actionsById,
274+
stagedActionIds,
275+
skippedActionIds
276+
);
251277
monitorState = monitorReducer(monitorState, liftedAction);
252-
253278
return {
254279
monitorState,
255280
actionsById,

0 commit comments

Comments
 (0)