Skip to content

Instantly share code, notes, and snippets.

@tappleby
Last active April 5, 2018 12:50
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tappleby/4b0b3e00e317a93e8048 to your computer and use it in GitHub Desktop.
Save tappleby/4b0b3e00e317a93e8048 to your computer and use it in GitHub Desktop.
Experimental middleware + reducer which tracks performance of all dispatched actions in https://github.com/gaearon/redux >= 1.0.0. Supports async actions which follow pattern outlined here https://github.com/gaearon/redux/issues/99#issuecomment-112212639
import { createStore } from 'redux';
import { performanceMiddleware, performanceReducer, performancePrintTable } from './redux/util/performance';
import thunkMiddleware from 'redux/lib/middleware/thunk';
import promiseMiddleware from './redux/middleware/promiseMiddleware';
import * as reducers from './reducers/index';
// Util functions.
function asyncAction(promise, request, success, failure) {
return { types: [request, success, failure], promise };
}
function requestSomething() {
const delay = parseInt(Math.random() * 50);
const rejectPromise = parseInt(Math.random() + 0.5);
return new Promise((resolve, reject) => {
setTimeout(rejectPromise ? reject : resolve, delay);
});
}
// Configure redux.
const middlewares = ({ dispatch, getState }) => [
thunkMiddleware({dispatch, getState}),
performanceMiddleware(),
promiseMiddleware()
];
const reducer = performanceReducer(reducers);
const redux = createStore(reducer, {}, middlewares);
// Dispatch sync action.
redux.dispatch({ type: SOME_ACTION });
// Dispatch some async actions.
const promises = [];
for (let i = 0; i < 2; i++) {
promises.push(redux.dispatch(asyncAction(
requestSomething(), SOMETHING_REQUEST, SOMETHING_SUCCESS, SOMETHING_FAILURE
)));
}
for (let i = 0; i < 3; i++) {
promises.push(redux.dispatch(asyncAction(
requestSomething(), FOO_REQUEST, FOO_SUCCESS, FOO_FAILURE
)));
}
Promise
.all(promises)
.then(() => performancePrintTable(redux.getState()));
// console output:
// Action type Avg time (ms) Total time (ms) Count
// "SOMETHING_REQUEST" 254.347 508.694 2
// "FOO_REQUEST" 250.638 751.916 3
// "SOME_ACTION" 3.530 3.530 1
import composeReducers from 'redux/lib/utils/composeReducers';
import uniqueId from 'lodash/utility/uniqueId';
import _map from 'lodash/collection/map';
export const defaultPerformance = window.performance || window.msPerformance || window.webkitPerformance;
const defaultStateKey = 'perf';
export function performanceMiddleware(performance = null) {
if (!performance) performance = defaultPerformance;
// return "noop" middleware if we have invalid performance object.
if (!performance || !performance.now) {
return next => action => next(action);
}
return next => action => {
const perfId = action.dispatchId || uniqueId('perf');
const perfTs = performance.now();
const { promise, types } = action;
let payload = {...action, perfId, perfTs};
if (promise && types) {
// Copy promise action types to new value for use in perf store.
payload.perfAsyncTypes = [].concat(types);
}
return next(payload);
}
}
export function performanceReducer(reducer = null, stateKey = defaultStateKey, performance = null) {
const perfReducer = _internalPerformanceReducer(performance);
if (!perfReducer) {
return reducer;
} else if (!reducer) {
return perfReducer;
}
const composedReducers = typeof reducer === 'function' ? reducer : composeReducers(reducer);
return function composedPerformanceReducer(state = {}, action = null) {
state = composedReducers(state, action);
state[stateKey] = perfReducer(state[stateKey], action);
return state;
}
}
export function performancePrintTable(reduxOrState, stateKey = defaultStateKey) {
const state = typeof reduxOrState.getState === 'function' ? reduxOrState.getState() : reduxOrState;
const actionStats = state[stateKey] ? state[stateKey].actions : null;
// Map action stats into summary count.
let summary = _map(actionStats || {}, (stats, type) => {
const { count, totalElapsed } = stats;
const avgElapsed = totalElapsed / count;
return { type, count, totalElapsed, avgElapsed };
});
// Sort by avg elapsed time.
summary.sort((a, b) => b.avgElapsed - a.avgElapsed );
// Display console table.
console.table(summary.map(stats => ({
"Action type": stats.type,
"Avg time (ms)": stats.avgElapsed,
"Total time (ms)": stats.totalElapsed,
"Count": stats.count
})));
}
function _internalPerformanceReducer(performance) {
if (!performance) performance = defaultPerformance;
// Exit out early if we have an invalid performance object.
if (!performance || !performance.now) {
return;
}
// Initial state + helpers.
const initialState = {actions: {}, pendingAsync: {}};
const trackAction = (state, type, elapsedTime, count = 1) => {
let perf = state.actions[type] = state.actions[type] || {totalElapsed: 0, count: 0};
perf.totalElapsed += elapsedTime;
perf.count += count;
return state;
};
return (state = initialState, action = null) => {
const { perfId, perfAsyncTypes } = action;
const endTime = performance.now();
let { perfTs, type } = action;
if (perfAsyncTypes) {
const [REQUEST, SUCCESS, FAILURE] = perfAsyncTypes;
switch (type) {
case REQUEST:
// Keep track of pending async actions
state.pendingAsync[perfId] = perfTs;
// Null out type so it doesnt get tracked in main perf store.
type = null;
break;
case SUCCESS:
case FAILURE:
// Async action completed, grab start time.
perfTs = state.pendingAsync[perfId];
// Track stats if pending time is valid.
type = perfTs ? REQUEST : null;
break;
}
}
if (perfId && type) {
const elapsed = endTime - perfTs;
state = trackAction(state, type, elapsed);
}
return state;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment