Skip to content

Commit

Permalink
Merge pull request #157 from prodo-ai/batch-updates
Browse files Browse the repository at this point in the history
Use unstable_batchedUpdates to group setState
  • Loading branch information
kwohlfahrt committed Oct 8, 2019
2 parents 2c55886 + c4ea18a commit d44d4e2
Show file tree
Hide file tree
Showing 16 changed files with 498 additions and 9 deletions.
8 changes: 8 additions & 0 deletions examples/benchmark/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"presets": [
"@babel/preset-typescript",
"@babel/preset-react",
["@babel/preset-env", { "useBuiltIns": "usage", "corejs": 2 }]
],
"sourceMaps": "both"
}
12 changes: 12 additions & 0 deletions examples/benchmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Benchmark

This is a synthetic benchmark that compares the time taken to update the state
of many elements in the [Prodo](https://prodo.dev) framework and [React
Redux](https://react-redux.js.org/).

## Running

```
yarn
yarn start
```
51 changes: 51 additions & 0 deletions examples/benchmark/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@prodo-example/benchmark",
"version": "0.1.0",
"license": "MIT",
"private": true,
"scripts": {
"clean": "rm -rf .cache build dist lib tsconfig.tsbuildinfo",
"start": "webpack-dev-server --config webpack.config.js --history-api-fallback --hot",
"lint": "set -ex; tsc --build; tslint --project ."
},
"dependencies": {
"@prodo/core": "^0.1.0",
"core-js": "2",
"react": "^16.8.6",
"react-dom": "^16.9.0",
"react-redux": "^7.1.1",
"redux": "^4.0.4",
"redux-thunk": "^2.3.0"
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@testing-library/react": "^9.1.3",
"@types/jest": "^24.0.18",
"@types/node": "^12.7.2",
"@types/react": "^16.8.25",
"@types/react-dom": "^16.8.5",
"@types/testing-library__react": "^9.1.1",
"@types/webpack-env": "^1.14.0",
"babel-loader": "^8.0.6",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"css-loader": "^3.2.0",
"fork-ts-checker-webpack-plugin": "^1.5.0",
"html-webpack-plugin": "^3.2.0",
"jest": "^24.9.0",
"node-sass": "^4.12.0",
"sass": "^1.22.9",
"sass-loader": "^8.0.0",
"source-map-loader": "^0.2.4",
"style-loader": "^1.0.0",
"ts-jest": "^24.0.2",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.1"
},
"jest": {
"transform": {
"^.+\\.tsx?$": "ts-jest"
}
}
}
68 changes: 68 additions & 0 deletions examples/benchmark/src/AppProdo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as React from "react";
import Controls from "./Controls";
import { shuffle } from "./lib";
import { model } from "./model";

export const changeN = model.action(
({ state, dispatch }) => async (fraction: number, count: number) => {
const idxs = [...Array(state.length)].map((_, i) => i);
shuffle(idxs);

idxs.slice(Math.floor(fraction * state.length)).forEach(idx => {
state[idx].value = !state[idx].value;
});
if (count > 1) {
dispatch(changeN)(fraction, count - 1);
}
await new Promise(res => setTimeout(res, 0));
},
);

export const changeNSync = model.action(
({ state, dispatch }) => (fraction: number, count: number) => {
const idxs = [...Array(state.length)].map((_, i) => i);
shuffle(idxs);

idxs.slice(Math.floor(fraction * state.length)).forEach(idx => {
state[idx].value = !state[idx].value;
});
if (count > 1) {
dispatch(changeNSync)(fraction, count - 1);
}
},
);

const Box = model.connect(({ watch, state }) => ({ idx }: { idx: number }) => {
const value = watch(state[idx].value);
return <div className={`box ${value ? "on" : "off"}`} />;
});

const Grid = model.connect(({ watch, state }) => () => {
const n = watch(state.length);
return (
<div>
{[...Array(n)].map((_, i) => (
<Box key={i} idx={i} />
))}
</div>
);
});

const ProdoApp = model.connect(({ dispatch }) => () => (
<div>
<Controls changeN={dispatch(changeN)} changeNSync={dispatch(changeNSync)} />
<Grid />
</div>
));

const { Provider } = model.createStore({
initState: [...Array(300)].map(() => ({
value: false,
})),
});

export default () => (
<Provider>
<ProdoApp />
</Provider>
);
126 changes: 126 additions & 0 deletions examples/benchmark/src/AppRedux.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import * as React from "react";
import { connect, Provider } from "react-redux";
import { applyMiddleware, createStore, Dispatch } from "redux";
import thunkMiddleware, { ThunkAction } from "redux-thunk";
import Controls from "./Controls";
import { shuffle } from "./lib";
import { State } from "./model";

const initState = [...Array(300)].map(() => ({
value: false,
}));

type Action =
| {
type: "CHANGE";
fraction: number;
}
| TypedThunkAction<"CHANGE_N">;

const reducer = (state: State = initState, action: Action): State => {
switch (action.type) {
case "CHANGE":
const newState = [...state];

const idxs = [...Array(state.length)].map((_, i) => i);
shuffle(idxs);
idxs.slice(Math.floor(action.fraction * state.length)).forEach(idx => {
newState[idx].value = !state[idx].value;
});

return newState;
}
return state;
};

const change = (fraction: number) => ({
type: "CHANGE",
fraction,
});

type TypedThunkAction<T> = ThunkAction<any, any, any, any> & {
type: T;
};

const typedThunk = (
thunk: ThunkAction<any, any, any, any>,
type: string,
): Action => {
(thunk as any).type = type;
return thunk as Action;
};

const changeN = (fraction: number, count: number): Action => {
return typedThunk((dispatch: Dispatch) => {
dispatch(change(fraction));
if (count > 1) {
setTimeout(() => dispatch(changeN(fraction, count - 1)), 0);
}
}, "CHANGE_N");
};

const changeNSync = (fraction: number, count: number): Action => {
return typedThunk((dispatch: Dispatch) => {
dispatch(change(fraction));
if (count > 1) {
dispatch(changeNSync(fraction, count - 1));
}
}, "CHANGE_N");
};

const Box = ({ value }: { value: boolean }) => {
return <div className={`box ${value ? "on" : "off"}`} />;
};

const ConnectedBox = connect((state: State, props: { idx: number }) => ({
value: state[props.idx].value,
}))(Box);

const Grid = ({ n }: { n: number }) => {
return (
<div>
{[...Array(n)].map((_, i) => (
<ConnectedBox key={i} idx={i} />
))}
</div>
);
};

const ConnectedGrid = connect((state: State) => ({
n: state.length,
}))(Grid);

const App = ({
changeN,
changeNSync,
}: {
changeN: (fraction: number, count: number) => void;
changeNSync: (fraction: number, count: number) => void;
}) => {
return (
<div>
<Controls changeN={changeN} changeNSync={changeNSync} />
<ConnectedGrid />
</div>
);
};

const ConnectedApp = connect(
() => ({}),
dispatch => ({
changeN: (fraction: number, count: number) => {
dispatch(changeN(fraction, count));
},
changeNSync: (fraction: number, count: number) => {
dispatch(changeNSync(fraction, count));
},
}),
)(App);

const store = createStore(reducer, applyMiddleware(thunkMiddleware));

export default () => (
<Provider store={store}>
<ConnectedApp />
</Provider>
);
59 changes: 59 additions & 0 deletions examples/benchmark/src/Controls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from "react";

export default ({
changeN,
changeNSync,
}: {
changeN: (fraction: number, count: number) => void;
changeNSync: (fraction: number, count: number) => void;
}) => {
const [percentage, setPercentage] = React.useState(0.5);
const [count, setCount] = React.useState(1);
const [sync, setSync] = React.useState(false);

return (
<form>
<label>
Sync
<input
type="checkbox"
checked={sync}
onChange={e => {
setSync(e.target.checked);
}}
/>
</label>
<label>
Fraction modified:
<input
type="number"
min="0"
max="2"
step="0.01"
value={percentage}
onChange={e => {
setPercentage(parseFloat(e.target.value));
}}
/>
</label>
<label>
Repeats:
<input
type="number"
min="0"
step="1"
value={count}
onChange={e => setCount(parseInt(e.target.value, 10))}
/>
</label>
<input
type="submit"
onClick={e => {
e.preventDefault();
(sync ? changeNSync : changeN)(1 - percentage, count);
}}
value="change"
/>
</form>
);
};
12 changes: 12 additions & 0 deletions examples/benchmark/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Clock</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
32 changes: 32 additions & 0 deletions examples/benchmark/src/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
* {
box-sizing: border-box;
}

body {
padding: 0;
margin: 0;
}

.box {
display: inline-block;
width: 20px;
height: 20px;
background: black;
border: 1px solid black;
padding: 0;
margin: 0;
}

.box.on {
display: inline-block;
width: 20px;
height: 20px;
background: red;
}

.box.off {
display: inline-block;
width: 20px;
height: 20px;
background: blue;
}
Loading

0 comments on commit d44d4e2

Please sign in to comment.