Skip to content

Commit e01693e

Browse files
committed
slight cleanup
1 parent 6f6adc3 commit e01693e

File tree

8 files changed

+166
-38
lines changed

8 files changed

+166
-38
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,6 @@ The `generateReduxReport` enhancer wraps the store in a proxy, so that each obje
7474
It tries to be smart about ignoring object accesses that come from outside your app's code. For instance, if you're also using the `persistStore` Devtools plugin, even though that plugin accesses every key in your store, you shouldn't see that reflected in the Usage Report monitor. The monitor attempts to filter out object access that originates in any module located in the `node_modules` folder or from a browser extension. This filtering logic only works in Chrome, or failing that, if you are using something like the [eval option](https://webpack.js.org/configuration/devtool/#development) or some other lightweight type of source map that preserves file pathnames in stacktraces.
7575

7676
If you are curious as to why a value is marked "accessed", you can always `shift + click` the relevant key in the monitor to set a breakpoint.
77+
78+
## Performance
79+
As you might expect there's a performance cost to proxying object access, mostly due to checking the stack trace to see if the object access can be ignored because it originates from `node_modules`. It should not be noticable in most cases.

__tests__/makeProxyTest.js renamed to __tests__/basicProxyBehaviorTest.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { createMakeProxyFunction } from "../src/trackObjectUse"
2-
31
describe("basic proxy behavior", () => {
42
it("if a proxied object is proxied again, both proxies will be active, rather than the second proxy simply overriding the first", () => {
53
const handler1 = {
@@ -18,6 +16,7 @@ describe("basic proxy behavior", () => {
1816
expect(proxy.test).toBe("proxied get")
1917
expect(wrappedProxy.test).toBe("proxied get is wrapped with an outer proxy")
2018
})
19+
2120
it("there can be a special hidden key that just returns the unproxied target to avoid this proxy nesting", () => {
2221
const handler1 = {
2322
get(target, propKey) {
@@ -40,10 +39,10 @@ describe("basic proxy behavior", () => {
4039
expect(proxy.__initialVal.test).toBe(1)
4140
expect(wrappedProxy.test).toBe("1 is wrapped with an outer proxy")
4241
})
42+
4343
it("JSON.parse(JSON.stringify(obj)) will return a non-proxied object from a proxied object, after calling the proxy's get methods", () => {
4444
const handler1 = {
4545
get: jest.fn(function(target, propKey) {
46-
debugger
4746
return "proxied get"
4847
})
4948
}
+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { createMakeProxyFunction, getChildObject, UNPROXIED_OBJ_KEY } from "../src/trackObjectUse"
2+
import { isObjectOrArray } from "../src/utility"
3+
describe("getChildObject", () => {
4+
const testObj = {
5+
a: 1,
6+
b: {
7+
c: {
8+
d: [
9+
1,
10+
2,
11+
{
12+
e: { f: "g" }
13+
}
14+
]
15+
}
16+
}
17+
}
18+
19+
it("returns the part of the object indicated by the provided string of keys", () => {
20+
expect(getChildObject(testObj, "b.c.d.2.e")).toEqual({ f: "g" })
21+
})
22+
it("returns the object if the key list is empty", () => {
23+
expect(getChildObject(testObj, "")).toEqual(testObj)
24+
})
25+
})
26+
27+
describe("createMakeProxyFunction", () => {
28+
const isProxy = obj => isObjectOrArray(obj[UNPROXIED_OBJ_KEY])
29+
const isDoubleProxied = obj =>
30+
isObjectOrArray(obj[UNPROXIED_OBJ_KEY]) &&
31+
isObjectOrArray(obj[UNPROXIED_OBJ_KEY][UNPROXIED_OBJ_KEY])
32+
33+
it("returns a function that creates a proxy to track object use", () => {
34+
const accessedProperties = {}
35+
const makeProxy = createMakeProxyFunction({ accessedProperties })
36+
const object = { a: { b: "c" }, d: [1, 2, 3, 4, 5] }
37+
const proxy = makeProxy(object)
38+
39+
const test1 = proxy.a.b
40+
const test2 = proxy.d[2]
41+
expect(accessedProperties).toEqual({
42+
a: { b: "c" },
43+
d: [undefined, undefined, 3]
44+
})
45+
})
46+
47+
it("will create proxies of child objects when they are accessed", () => {
48+
const accessedProperties = {}
49+
const makeProxy = createMakeProxyFunction({ accessedProperties })
50+
const object = { a: { b: "c" }, d: [1, 2, 3, 4, 5] }
51+
const proxy = makeProxy(object)
52+
53+
const test1 = proxy.a.b
54+
55+
expect(isProxy(proxy.a)).toBe(true)
56+
// the original child is preserved as a non-proxy
57+
expect(isProxy(object.a)).toBe(false)
58+
})
59+
60+
it("does not try to proxy non-object or non-array values", () => {
61+
const accessedProperties = {}
62+
const makeProxy = createMakeProxyFunction({ accessedProperties })
63+
const object = { a: { b: "c" }, d: [1, 2, 3, 4, 5] }
64+
const proxy = makeProxy(object)
65+
66+
const test1 = proxy.a.b
67+
68+
expect(isProxy(proxy.a.b)).toBe(false)
69+
})
70+
71+
it("updates values in accessed properties if they change and are accessed again", () => {
72+
const accessedProperties = {}
73+
const makeProxy = createMakeProxyFunction({ accessedProperties })
74+
const object = { a: { b: "c" }, d: [1, 2, 3, 4, 5] }
75+
const proxy = makeProxy(object)
76+
77+
const test1 = proxy.a.b
78+
expect(accessedProperties).toEqual({
79+
a: { b: "c" }
80+
})
81+
82+
object.a.b = "e"
83+
const test2 = proxy.a.b
84+
expect(accessedProperties).toEqual({
85+
a: { b: "e" }
86+
})
87+
})
88+
89+
it("will not augment the accessedProperties object if shouldSkipProxy argument returns true", () => {
90+
const accessedProperties = {}
91+
const makeProxy = createMakeProxyFunction({
92+
accessedProperties,
93+
shouldSkipProxy: () => true
94+
})
95+
const object = { a: { b: "c" }, d: [1, 2, 3, 4, 5] }
96+
const proxy = makeProxy(object)
97+
98+
const test1 = proxy.a.b
99+
expect(accessedProperties).toEqual({})
100+
})
101+
102+
it("just returns the value if the key is not on the objects prototype", () => {
103+
const accessedProperties = {}
104+
const makeProxy = createMakeProxyFunction({ accessedProperties })
105+
const object = { a: { b: "c" }, d: [1, 2, 3, 4, 5] }
106+
const proxy = makeProxy(object)
107+
108+
const isPrototypeOf = proxy.isPrototypeOf
109+
expect(accessedProperties).toEqual({})
110+
})
111+
112+
it("has a hidden key for accessing the unproxied object from the proxy", () => {
113+
const accessedProperties = {}
114+
const makeProxy = createMakeProxyFunction({ accessedProperties })
115+
const object = { a: { b: "c" }, d: [1, 2, 3, 4, 5] }
116+
const proxy = makeProxy(object)
117+
118+
expect(isProxy(proxy)).toBe(true)
119+
expect(isProxy(proxy[UNPROXIED_OBJ_KEY])).toBe(false)
120+
expect(object).toEqual(proxy[UNPROXIED_OBJ_KEY])
121+
})
122+
123+
it("makes sure never to proxy an already-proxied object", () => {
124+
const accessedProperties = {}
125+
const makeProxy = createMakeProxyFunction({ accessedProperties })
126+
const object = { a: { b: "c" }, d: [1, 2, 3, 4, 5] }
127+
const doubleProxy = makeProxy(makeProxy(object))
128+
129+
expect(isProxy(doubleProxy)).toBe(true)
130+
expect(isDoubleProxied(doubleProxy)).toBe(false)
131+
})
132+
})

__tests__/generateReduxReportTest.js

-2
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ describe('generateReduxReport', () => {
4343
const val3 = store.getState().a.d
4444
const val4 = store.getState().g[2].j
4545

46-
debugger
47-
4846
const expectedUsed = {
4947
a: {
5048
b: {

src/generateReduxReport.js

-5
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,6 @@ let globalObjectCache
3535
const shouldSkipProxy = () => {
3636
if (global.reduxReport.__inProgress || global.reduxReport.__reducerInProgress) return true
3737

38-
// this is kind of hacky, but webpack dev server serves non-local files
39-
// that look like this: `webpack:///./~/react-redux/lib/components/connect.js `
40-
// whereas local files look like this: webpack:///./containers/TodoApp.js
41-
// also trying to avoid functions emanating from browser extensions
42-
4338
const stackFrames = StackTrace.getSync()
4439
const initiatingFunc =
4540
stackFrames[stackFrames.findIndex(s => s.functionName === "Object.get") + 1]

src/monitor/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const Container = styled.div`
1616
right: 0;
1717
bottom: 0;
1818
overflow-y: auto;
19-
font-size: 16.5px;
19+
font-size: 16px;
2020
font-weight: normal;
2121
color: ${props => props.theme.base05};
2222

src/trackObjectUse.js

+13-10
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
import { isObjectOrArray, isUndefined } from "./utility"
22

3-
const UNPROXIED_OBJ_KEY = "__initialValue"
3+
export const UNPROXIED_OBJ_KEY = "**__GET_INITIAL_PROXY_VAL__**"
44

55
const getUnproxiedObject = target =>
66
target[UNPROXIED_OBJ_KEY] !== undefined ? target[UNPROXIED_OBJ_KEY] : target
77

8+
export const getChildObject = (obj, stateLocation) => {
9+
if (!stateLocation) return obj
10+
return stateLocation.split(".").reduce((acc, key) => {
11+
return acc[key]
12+
}, obj)
13+
}
14+
815
export const createMakeProxyFunction = ({
9-
keepOriginalValues = false,
1016
accessedProperties,
17+
keepOriginalValues = false,
1118
shouldSkipProxy = () => false,
1219
getBreakpoint = () => {},
1320
onChange = () => {}
1421
}) => {
1522
return function makeProxy(obj, stateLocation = "") {
1623
const handler = {
1724
get(target, propKey) {
18-
// need to be able to actually get the value without triggering another get cycle
1925
if (propKey === UNPROXIED_OBJ_KEY) return target
2026
const value = target[propKey]
2127

2228
if (!Object.hasOwnProperty.call(target, propKey)) return value
2329

24-
if (shouldSkipProxy()) return JSON.parse(JSON.stringify(value))
25-
26-
const accessedPropertiesPointer = !stateLocation
27-
? accessedProperties
28-
: stateLocation.split(".").reduce((acc, key) => {
29-
return acc[key]
30-
}, accessedProperties)
30+
if (shouldSkipProxy()) return value
3131

3232
const newStateLocation = stateLocation ? stateLocation + "." + propKey : propKey
3333

@@ -36,6 +36,9 @@ export const createMakeProxyFunction = ({
3636
// explore the callstack to see when your app accesses a value
3737
debugger
3838
}
39+
40+
const accessedPropertiesPointer = getChildObject(accessedProperties, stateLocation)
41+
3942
if (isObjectOrArray(value)) {
4043
if (isUndefined(accessedPropertiesPointer[propKey])) {
4144
accessedPropertiesPointer[propKey] = Array.isArray(value) ? [] : {}

todomvc-example/src/redux-usage-report/trackObjectUse.js

+15-17
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,28 @@
33
Object.defineProperty(exports, "__esModule", {
44
value: true
55
});
6-
exports.createMakeProxyFunction = undefined;
7-
8-
var _stringify = require("babel-runtime/core-js/json/stringify");
9-
10-
var _stringify2 = _interopRequireDefault(_stringify);
11-
6+
exports.createMakeProxyFunction = exports.getChildObject = exports.UNPROXIED_OBJ_KEY = undefined;
127
exports.default = trackObjectUse;
138

149
var _utility = require("./utility");
1510

16-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17-
18-
var UNPROXIED_OBJ_KEY = "__initialValue";
11+
var UNPROXIED_OBJ_KEY = exports.UNPROXIED_OBJ_KEY = "**__GET_INITIAL_PROXY_VAL__**";
1912

2013
var getUnproxiedObject = function getUnproxiedObject(target) {
2114
return target[UNPROXIED_OBJ_KEY] !== undefined ? target[UNPROXIED_OBJ_KEY] : target;
2215
};
2316

17+
var getChildObject = exports.getChildObject = function getChildObject(obj, stateLocation) {
18+
if (!stateLocation) return obj;
19+
return stateLocation.split(".").reduce(function (acc, key) {
20+
return acc[key];
21+
}, obj);
22+
};
23+
2424
var createMakeProxyFunction = exports.createMakeProxyFunction = function createMakeProxyFunction(_ref) {
25-
var _ref$keepOriginalValu = _ref.keepOriginalValues,
25+
var accessedProperties = _ref.accessedProperties,
26+
_ref$keepOriginalValu = _ref.keepOriginalValues,
2627
keepOriginalValues = _ref$keepOriginalValu === undefined ? false : _ref$keepOriginalValu,
27-
accessedProperties = _ref.accessedProperties,
2828
_ref$shouldSkipProxy = _ref.shouldSkipProxy,
2929
shouldSkipProxy = _ref$shouldSkipProxy === undefined ? function () {
3030
return false;
@@ -39,17 +39,12 @@ var createMakeProxyFunction = exports.createMakeProxyFunction = function createM
3939

4040
var handler = {
4141
get: function get(target, propKey) {
42-
// need to be able to actually get the value without triggering another get cycle
4342
if (propKey === UNPROXIED_OBJ_KEY) return target;
4443
var value = target[propKey];
4544

4645
if (!Object.hasOwnProperty.call(target, propKey)) return value;
4746

48-
if (shouldSkipProxy()) return JSON.parse((0, _stringify2.default)(value));
49-
50-
var accessedPropertiesPointer = !stateLocation ? accessedProperties : stateLocation.split(".").reduce(function (acc, key) {
51-
return acc[key];
52-
}, accessedProperties);
47+
if (shouldSkipProxy()) return value;
5348

5449
var newStateLocation = stateLocation ? stateLocation + "." + propKey : propKey;
5550

@@ -58,6 +53,9 @@ var createMakeProxyFunction = exports.createMakeProxyFunction = function createM
5853
// explore the callstack to see when your app accesses a value
5954
debugger;
6055
}
56+
57+
var accessedPropertiesPointer = getChildObject(accessedProperties, stateLocation);
58+
6159
if ((0, _utility.isObjectOrArray)(value)) {
6260
if ((0, _utility.isUndefined)(accessedPropertiesPointer[propKey])) {
6361
accessedPropertiesPointer[propKey] = Array.isArray(value) ? [] : {};

0 commit comments

Comments
 (0)