Skip to content

Commit 50705bc

Browse files
committed
performance optimizations
1 parent d4e60b5 commit 50705bc

File tree

15 files changed

+175
-106
lines changed

15 files changed

+175
-106
lines changed

__tests__/makeProxyTest.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { createMakeProxyFunction } from "../src/trackObjectUse"
2+
3+
describe("basic proxy behavior", () => {
4+
it("if a proxied object is proxied again, both proxies will be active, rather than the second proxy simply overriding the first", () => {
5+
const handler1 = {
6+
get(target, propKey) {
7+
return "proxied get"
8+
}
9+
}
10+
11+
const handler2 = {
12+
get(target, propKey) {
13+
return `${target[propKey]} is wrapped with an outer proxy`
14+
}
15+
}
16+
const proxy = new Proxy({ test: 1 }, handler1)
17+
const wrappedProxy = new Proxy(proxy, handler2)
18+
expect(proxy.test).toBe("proxied get")
19+
expect(wrappedProxy.test).toBe("proxied get is wrapped with an outer proxy")
20+
})
21+
it("there can be a special hidden key that just returns the unproxied target to avoid this proxy nesting", () => {
22+
const handler1 = {
23+
get(target, propKey) {
24+
if (propKey === "__initialVal") return target
25+
if (target.__initialVal !== undefined) return target.__initialVal[propKey]
26+
return "proxied get"
27+
}
28+
}
29+
30+
const handler2 = {
31+
get(target, propKey) {
32+
const value =
33+
target.__initialVal !== undefined ? target.__initialVal[propKey] : target[propKey]
34+
return `${value} is wrapped with an outer proxy`
35+
}
36+
}
37+
const proxy = new Proxy({ test: 1 }, handler1)
38+
const wrappedProxy = new Proxy(proxy, handler2)
39+
expect(proxy.test).toBe("proxied get")
40+
expect(proxy.__initialVal.test).toBe(1)
41+
expect(wrappedProxy.test).toBe("1 is wrapped with an outer proxy")
42+
})
43+
it("JSON.parse(JSON.stringify(obj)) will return a non-proxied object from a proxied object, after calling the proxy's get methods", () => {
44+
const handler1 = {
45+
get: jest.fn(function(target, propKey) {
46+
debugger
47+
return "proxied get"
48+
})
49+
}
50+
51+
const proxy = new Proxy({ test: 1 }, handler1)
52+
const copied = JSON.parse(JSON.stringify(proxy))
53+
expect(handler1.get.mock.calls.length).toBe(2)
54+
expect(copied.test).toBe("proxied get")
55+
expect(handler1.get.mock.calls.length).toBe(2)
56+
})
57+
})

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "",
55
"main": "dist/index.js",
66
"scripts": {
7-
"test": "jest",
7+
"test": "NODE_ENV=test jest",
88
"build": "NODE_ENV=production ./node_modules/.bin/babel src -d dist --ignore node_modules,src/lib; cp -r src/lib dist/",
99
"prepublish": "npm run build"
1010
},
@@ -14,9 +14,8 @@
1414
"deep-object-diff": "^1.0.4",
1515
"fs": "^0.0.1-security",
1616
"install": "^0.10.4",
17-
"lodash.clone": "^4.5.0",
17+
"lodash.debounce": "^4.0.8",
1818
"lodash.isequal": "^4.5.0",
19-
"lodash.throttle": "^4.1.1",
2019
"prop-types": "^15.6.0",
2120
"react": "^16.2.0",
2221
"react-json-tree": "^0.11.0",

src/generateReduxReport.js

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,16 @@ import { diff } from "deep-object-diff"
22
import StackTrace from "stacktrace-js"
33
import { isObjectOrArray } from "./utility"
44
import { createMakeProxyFunction } from "./trackObjectUse"
5-
import cloneDeep from "lodash.clone"
6-
import throttle from "lodash.throttle"
7-
import "./lib/browser-source-map-support"
5+
import debounce from "lodash.debounce"
86

97
// we need source maps for the stack traces
108
// or else we won't know whether to ignore object access
119
// from non-local code (e.g node_modules, browser extensions...)
1210
// this takes the stack trace file name from e.g. fileName: "http://localhost:3001/static/js/bundle.js",
1311
// to "http://localhost:3000/Users/alexholachek/Desktop/work/redux-usage-report/todomvc-example/src/containers/App.js
1412
// this raises an error during jest tests so limit to development
15-
//
1613
if (process.env.NODE_ENV === "development") {
14+
require("./lib/browser-source-map-support")
1715
sourceMapSupport.install() // eslint-disable-line
1816
}
1917

@@ -34,7 +32,9 @@ function replaceUndefinedWithNull(obj) {
3432

3533
let globalObjectCache
3634

37-
const shouldSkipProxy = (target, propKey) => {
35+
const shouldSkipProxy = () => {
36+
if (global.reduxReport.__inProgress || global.reduxReport.__reducerInProgress) return true
37+
3838
// this is kind of hacky, but webpack dev server serves non-local files
3939
// that look like this: `webpack:///./~/react-redux/lib/components/connect.js `
4040
// whereas local files look like this: webpack:///./containers/TodoApp.js
@@ -46,17 +46,12 @@ const shouldSkipProxy = (target, propKey) => {
4646

4747
const initiatingFuncNotLocal =
4848
!!initiatingFunc &&
49+
initiatingFunc.fileName &&
4950
(initiatingFunc.fileName.match(/\.\/~\/|\/node_modules\//) ||
5051
initiatingFunc.fileName.match(/extension:\/\//))
5152

52-
if (
53-
!!initiatingFuncNotLocal ||
54-
!target.hasOwnProperty(propKey) ||
55-
global.reduxReport.__inProgress ||
56-
global.reduxReport.__reducerInProgress
57-
) {
58-
return true
59-
}
53+
if (!!initiatingFuncNotLocal) return true
54+
6055
return false
6156
}
6257

@@ -66,7 +61,7 @@ function generateReduxReport(global, rootReducer) {
6661
accessedState: {},
6762
state: {},
6863
setOnChangeCallback(cb) {
69-
global.reduxReport.onChangeCallback = throttle(cb, 1000)
64+
global.reduxReport.onChangeCallback = debounce(cb, 25)
7065
},
7166
removeOnChangeCallback() {
7267
global.reduxReport.onChangeCallback = undefined
@@ -81,8 +76,8 @@ function generateReduxReport(global, rootReducer) {
8176
},
8277
generate() {
8378
global.reduxReport.__inProgress = true
84-
const used = cloneDeep(this.accessedState)
85-
const stateCopy = cloneDeep(this.state)
79+
const used = JSON.parse(JSON.stringify(this.accessedState))
80+
const stateCopy = JSON.parse(JSON.stringify(this.state))
8681
const unused = diff(stateCopy, used)
8782
replaceUndefinedWithNull(unused)
8883
const report = {
@@ -99,7 +94,8 @@ function generateReduxReport(global, rootReducer) {
9994
shouldSkipProxy,
10095
accessedProperties: global.reduxReport.accessedState,
10196
getBreakpoint: () => global.localStorage && global.localStorage.getItem(localStorageKey),
102-
onChange: (...args) => global.reduxReport.onChangeCallback && global.reduxReport.onChangeCallback(...args)
97+
onChange: stateLocation =>
98+
global.reduxReport.onChangeCallback && global.reduxReport.onChangeCallback(stateLocation)
10399
})
104100

105101
return (prevState, action) => {

src/monitor/index.js

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ class ReduxUsageMonitor extends Component {
8585

8686
componentDidMount() {
8787
this.updateReport()
88-
window.reduxReport.setOnChangeCallback(this.updateReport)
88+
// not sure why this bind is necessary
89+
window.reduxReport.setOnChangeCallback(this.updateReport.bind(this))
8990
}
9091

9192
componentWillUnmount() {
@@ -94,22 +95,12 @@ class ReduxUsageMonitor extends Component {
9495

9596
updateReport = () => {
9697
const report = window.reduxReport.generate()
97-
if (!isEqual(this.state.used, report.used)) {
98-
this.setState(() => ({
99-
used: report.used,
100-
stateCopy: report.stateCopy
101-
}))
102-
}
103-
}
104-
105-
componentDidUpdate = (prevProps, prevState) => {
106-
if (prevProps.computedStates.length !== this.props.computedStates.length) {
107-
const report = window.reduxReport.generate()
108-
this.setState(() => ({
109-
used: report.used,
110-
stateCopy: report.stateCopy
111-
}))
112-
}
98+
if (isEqual(report.used, this.state.used) && isEqual(report.stateCopy, this.state.stateCopy))
99+
return
100+
this.setState({
101+
used: report.used,
102+
stateCopy: report.stateCopy
103+
})
113104
}
114105

115106
setBreakpoint = breakpointPath => {

src/trackObjectUse.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
import { isObjectOrArray, isUndefined } from "./utility"
22

3+
const UNPROXIED_OBJ_KEY = "__initialValue"
4+
5+
const getUnproxiedObject = target =>
6+
target[UNPROXIED_OBJ_KEY] !== undefined ? target[UNPROXIED_OBJ_KEY] : target
7+
38
export const createMakeProxyFunction = ({
49
keepOriginalValues = false,
5-
shouldSkipProxy,
610
accessedProperties,
11+
shouldSkipProxy = () => false,
712
getBreakpoint = () => {},
813
onChange = () => {}
914
}) => {
1015
return function makeProxy(obj, stateLocation = "") {
1116
const handler = {
1217
get(target, propKey) {
18+
// need to be able to actually get the value without triggering another get cycle
19+
if (propKey === UNPROXIED_OBJ_KEY) return target
1320
const value = target[propKey]
14-
if (!isUndefined(shouldSkipProxy) && shouldSkipProxy(target, propKey)) return value
21+
22+
if (!Object.hasOwnProperty.call(target, propKey)) return value
23+
24+
if (shouldSkipProxy()) return JSON.parse(JSON.stringify(value))
1525

1626
const accessedPropertiesPointer = !stateLocation
1727
? accessedProperties
@@ -20,27 +30,33 @@ export const createMakeProxyFunction = ({
2030
}, accessedProperties)
2131

2232
const newStateLocation = stateLocation ? stateLocation + "." + propKey : propKey
23-
onChange(newStateLocation)
2433

2534
// allow people to examine the stack at certain access points
2635
if (getBreakpoint() === newStateLocation) {
2736
// explore the callstack to see when your app accesses a value
2837
debugger
2938
}
3039
if (isObjectOrArray(value)) {
31-
if (!accessedPropertiesPointer[propKey]) {
40+
if (isUndefined(accessedPropertiesPointer[propKey])) {
3241
accessedPropertiesPointer[propKey] = Array.isArray(value) ? [] : {}
42+
onChange(newStateLocation)
3343
}
3444
return makeProxy(value, newStateLocation)
3545
} else {
36-
if (isUndefined(accessedPropertiesPointer[propKey]) || !keepOriginalValues) {
46+
if (
47+
isUndefined(accessedPropertiesPointer[propKey]) ||
48+
(!keepOriginalValues && value !== accessedPropertiesPointer[propKey])
49+
) {
3750
accessedPropertiesPointer[propKey] = value
51+
onChange(newStateLocation)
3852
}
3953
return value
4054
}
4155
}
4256
}
43-
return new Proxy(obj, handler)
57+
// prevent double-wrapping proxies
58+
const unproxiedObj = getUnproxiedObject(obj)
59+
return new Proxy(unproxiedObj, handler)
4460
}
4561
}
4662

todomvc-example/config/webpack.config.dev.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const env = getClientEnvironment(publicUrl);
2828
module.exports = {
2929
// You may want 'eval' instead if you prefer to see the compiled output in DevTools.
3030
// See the discussion in https://github.com/facebookincubator/create-react-app/issues/343.
31-
devtool: 'cheap-module-source-map',
31+
devtool: 'source-map',
3232
// These are the "entry points" to our application.
3333
// This means they will be the "root" imports that are included in JS bundle.
3434
// The first two entry points enable "hot" CSS and auto-refreshes for JS.

todomvc-example/src/components/TodoItem.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export default class TodoItem extends Component {
3030

3131
render() {
3232
const { todo, completeTodo, deleteTodo } = this.props
33-
3433
let element
3534
if (this.state.editing) {
3635
element = (
@@ -48,7 +47,7 @@ export default class TodoItem extends Component {
4847
type="checkbox"
4948
checked={todo.completed}
5049
onChange={() => {
51-
console.log(this.props.todo.someExtraData.thisObj2)
50+
console.log(`todo.someExtraData.thisObj2: ${todo.someExtraData.thisObj2}`)
5251
completeTodo(todo.id)
5352
}}
5453
/>

todomvc-example/src/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import "todomvc-app-css/index.css"
1010
import generateReduxReport from "./redux-usage-report"
1111
import DevTools from "./containers/DevTools"
1212

13-
debugger
1413
const enhancer = compose(
1514
DevTools.instrument(),
1615
generateReduxReport(),

todomvc-example/src/reducers/todos.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,15 @@ const initialState = [
1414
id: 0,
1515
someExtraData: {
1616
thisObj: "isUnused",
17-
thisObj2: 'isAccessedWhenCompletedIsToggled'
18-
17+
thisObj2: "isAccessedWhenCompletedIsToggled"
1918
}
2019
},
2120
{
2221
text: "Did gyre and gimbel in the wabe",
2322
completed: false,
2423
someExtraData: {
2524
thisObj: "isUnused",
26-
thisObj2: 'isAccessedWhenCompletedIsToggled'
27-
25+
thisObj2: "isAccessedWhenCompletedIsToggled"
2826
},
2927
id: 1
3028
},
@@ -33,7 +31,7 @@ const initialState = [
3331
completed: false,
3432
someExtraData: {
3533
thisObj: "isUnused",
36-
thisObj2: 'isAccessedWhenCompletedIsToggled'
34+
thisObj2: "isAccessedWhenCompletedIsToggled"
3735
},
3836
id: 3
3937
}
@@ -52,9 +50,10 @@ export default function todos(state = initialState, action) {
5250
a: Math.random().toFixed(3),
5351
b: Math.random().toFixed(4)
5452
},
55-
thisValueIsntAccessed: "foo",
56-
thisObj2: 'isAccessedWhenCompletedIsToggled'
57-
53+
someExtraData: {
54+
thisObj: "isUnused",
55+
thisObj2: "isAccessedWhenCompletedIsToggled"
56+
}
5857
}
5958
]
6059

0 commit comments

Comments
 (0)