Throttle bridge traffic for Elements tree updates
Brian Vaughn committed
Jan 23, 2019 at 08:27 UTC
d02b4a27842228945c71b6043b5ba145f5c41b41
3 files changed
+88
-40
shells/dev/app/ElementTypes/index.js
+2
-1
@@ -8,12 +8,13 @@ import React, {
8
Component,
9
ConcurrentMode,
10
Fragment,
11
+ // $FlowFixMe Flow doesn't know about the Profiler import yet
12
Profiler,
13
StrictMode,
14
Suspense,
15
} from 'react';
16
16
-class ClassComponent extends Component {
17
+class ClassComponent extends Component<any> {
18
render() {
19
return null;
20
}
src/backend/agent.js
+78
-35
@@ -9,14 +9,28 @@ import type {Fiber, RendererData, RendererID, RendererInterface} from './types';
9
import type {Bridge} from '../types';
10
import type {Element} from 'src/devtools/types';
11
12
+const debug = (methodName, ...args) => {
13
+ //console.log(`%cAgent %c${methodName}`, 'color: blue; font-weight: bold;', 'font-weight: bold;', ...args);
14
+};
15
+
16
+const THROTTLE_BY_MS = 350;
17
+
18
export default class Agent extends EventEmitter {
13
- fiberToID: WeakMap<Fiber, string> = new WeakMap();
14
- idToElement: Map<string, Element> = new Map();
15
- idToFiber: Map<string, Fiber> = new Map();
16
- idToRendererData: Map<string, RendererData> = new Map();
17
- idToRendererID: Map<string, RendererID> = new Map();
18
- rendererInterfaces: {[key: RendererID]: RendererInterface} = {};
19
- roots: Set<RendererID> = new Set();
19
+ _fiberToID: WeakMap<Fiber, string> = new WeakMap();
20
+ _idToElement: Map<string, Element> = new Map();
21
+ _idToFiber: Map<string, Fiber> = new Map();
22
+ _idToRendererData: Map<string, RendererData> = new Map();
23
+ _idToRendererID: Map<string, RendererID> = new Map();
24
+ _isRenderInProgress: boolean = false;
25
+ _rendererInterfaces: {[key: RendererID]: RendererInterface} = {};
26
+ _roots: Set<RendererID> = new Set();
27
+
28
+ // Tree updates are debounced to reduce bridge traffic and improve performance.
29
+ // This reduces the potential negative impact that DevTools has on React performance.
30
+ // Eventual consistency is good enough for the Elements tree.
31
+ _lastCrawlTime: number = -THROTTLE_BY_MS;
32
+ _pendingRoots: Set<string> = new Set();
33
+ _pendingRootTimeoutID: TimeoutID | null = null;
34
35
addBridge(bridge: Bridge) {
36
// TODO Listen to bridge for things like selection.
@@ -30,25 +44,25 @@ export default class Agent extends EventEmitter {
44
}
45
46
setRendererInterface(rendererID: RendererID, rendererInterface: RendererInterface) {
33
- this.rendererInterfaces[rendererID] = rendererInterface;
47
+ this._rendererInterfaces[rendererID] = rendererInterface;
48
}
49
50
_getId(fiber: Fiber): string {
51
if (typeof fiber !== 'object' || !fiber) {
52
return fiber;
53
}
40
- if (!this.fiberToID.has(fiber)) {
41
- this.fiberToID.set(fiber, guid());
42
- this.idToFiber.set(
43
- nullthrows(this.fiberToID.get(fiber)),
54
+ if (!this._fiberToID.has(fiber)) {
55
+ this._fiberToID.set(fiber, guid());
56
+ this._idToFiber.set(
57
+ nullthrows(this._fiberToID.get(fiber)),
58
fiber
59
);
60
}
47
- return nullthrows(this.fiberToID.get(fiber));
61
+ return nullthrows(this._fiberToID.get(fiber));
62
}
63
64
_crawl(parent: Element, id: string): void {
51
- const data: RendererData = ((this.idToRendererData.get(id): any): RendererData);
65
+ const data: RendererData = ((this._idToRendererData.get(id): any): RendererData);
66
if (data.type === ElementTypeOtherOrUnknown) {
67
data.children.forEach(childFiber => {
68
if (childFiber !== null) {
@@ -62,8 +76,28 @@ export default class Agent extends EventEmitter {
76
}
77
}
78
79
+ _crawlPendingRoots(): void {
80
+console.log('_crawlPendingRoots', this._pendingRoots)
81
+ this._pendingRoots.forEach(id => this._crawlRoot(id));
82
+ this._pendingRoots.clear();
83
+ this._lastCrawlTime = performance.now();
84
+ }
85
+
86
+ _crawlRoot(id: string): void {
87
+ const data = ((this._idToRendererData.get(id): any): RendererData);
88
+
89
+ // TODO: Can we use the effects list on update for a faster path?
90
+ this._createOrUpdateElement(id, data);
91
+
92
+ if (!this._roots.has(id)) {
93
+ this._roots.add(id);
94
+ debug('emit("root")', id);
95
+ this.emit('root', id);
96
+ }
97
+ }
98
+
99
_createOrUpdateElement(id: string, data: RendererData): void {
66
- const prevElement: ?Element = this.idToElement.get(id);
100
+ const prevElement: ?Element = this._idToElement.get(id);
101
const nextElement: Element = {
102
id,
103
key: data.key,
@@ -72,7 +106,7 @@ export default class Agent extends EventEmitter {
106
type: data.type,
107
};
108
75
- this.idToElement.set(id, nextElement);
109
+ this._idToElement.set(id, nextElement);
110
111
data.children.forEach(childFiber => {
112
if (childFiber !== null) {
@@ -81,57 +115,66 @@ export default class Agent extends EventEmitter {
115
});
116
117
if (prevElement == null) {
84
-console.log('%cAgent%c emit("mount")', 'color: blue; font-weight: bold;', 'font-weight: bold;', id, nextElement)
118
+ debug('emit("mount")', id, nextElement)
119
this.emit('mount', nextElement);
120
} else if (!areElementsEqual(prevElement, nextElement)) {
87
-console.log('%cAgent%c emit("update")', 'color: blue; font-weight: bold;', 'font-weight: bold;', id, nextElement)
121
+ debug('emit("update")', id, nextElement);
122
this.emit('update', nextElement);
123
}
124
}
125
126
+ _maybeCrawlRoot = () => {
127
+ this._pendingRootTimeoutID = null;
128
+ if (!this._isRenderInProgress) {
129
+ this._crawlPendingRoots();
130
+ }
131
+ };
132
+
133
onHookMount = ({data, fiber, renderer}: {data: RendererData, fiber: Fiber, renderer: RendererID}) => {
134
const id = this._getId(fiber);
135
95
- this.idToRendererData.set(id, data);
96
- this.idToRendererID.set(id, renderer);
136
+ this._idToRendererData.set(id, data);
137
+ this._idToRendererID.set(id, renderer);
138
};
139
140
onHookRootCommitted = ({data, fiber, renderer}: {data: RendererData, fiber: Fiber, renderer: RendererID}) => {
141
const id = this._getId(fiber);
101
-console.log('%cAgent%c onHookRootCommitted()', 'color: blue; font-weight: bold;', 'font-weight: bold;', id);
102
-
103
- this._createOrUpdateElement(id, data);
142
105
- if (!this.roots.has(id)) {
106
- this.roots.add(id);
143
+ this._isRenderInProgress = false;
144
+ this._pendingRoots.add(id);
145
108
- this.emit('root', id);
146
+ const delta = performance.now() - this._lastCrawlTime;
147
+ if (delta >= THROTTLE_BY_MS) {
148
+ this._crawlPendingRoots();
149
+ } else if (this._pendingRootTimeoutID === null) {
150
+ this._pendingRootTimeoutID = setTimeout(this._maybeCrawlRoot, THROTTLE_BY_MS - delta);
151
}
152
};
153
154
onHookUnmount = ({fiber}: {fiber: Fiber}) => {
155
const id = this._getId(fiber);
156
115
- if (this.roots.has(id)) {
116
- this.roots.delete(id);
157
+ if (this._roots.has(id)) {
158
+ this._roots.delete(id);
159
this.emit('rootUnmounted', id);
160
}
161
120
- if (this.idToElement.has(id)) {
121
- this.idToElement.delete(id);
122
-console.log('%cAgent%c emit("unmount")', 'color: blue; font-weight: bold;', 'font-weight: bold;', id)
162
+ if (this._idToElement.has(id)) {
163
+ this._idToElement.delete(id);
164
+ debug('emit("unmount")', id);
165
this.emit('unmount', id);
166
}
167
126
- this.fiberToID.delete(fiber);
127
- this.idToRendererData.delete(id);
128
- this.idToRendererID.delete(id);
168
+ this._fiberToID.delete(fiber);
169
+ this._idToRendererData.delete(id);
170
+ this._idToRendererID.delete(id);
171
};
172
173
onHookUpdate = ({data, fiber}: {data: RendererData, fiber: Fiber}) => {
174
const id = this._getId(fiber);
175
134
- this.idToRendererData.set(id, data);
176
+ this._isRenderInProgress = true;
177
+ this._idToRendererData.set(id, data);
178
};
179
}
180
src/devtools/store.js
+8
-4
@@ -5,6 +5,10 @@ import EventEmitter from 'events';
5
import type { Element } from './types';
6
import type { Bridge } from '../types';
7
8
+const debug = (methodName, ...args) => {
9
+ //console.log(`%cAgent %c${methodName}`, 'color: red; font-weight: bold;', 'font-weight: bold;', ...args);
10
+};
11
+
12
/**
13
* The store is the single source of truth for updates from the backend.
14
* ContextProviders can subscribe to the Store for specific things they want to provide.
@@ -34,7 +38,7 @@ class Store extends EventEmitter {
38
39
onBridgeMount = (element: Element) => {
40
const {id} = element;
37
- console.log('%cStore%c onBridgeMount()', 'color: red; font-weight: bold;', 'font-weight: bold;', element);
41
+ debug('onBridgeMount()', element);
42
this._idToElement.set(id, element);
43
44
element.children.forEach(childID => {
@@ -45,7 +49,7 @@ class Store extends EventEmitter {
49
};
50
51
onBridgeRoot = (id: string) => {
48
- console.log('%cStore%c onBridgeRoot()', 'color: red; font-weight: bold;', 'font-weight: bold;', id);
52
+ debug('onBridgeRoot()', id);
53
if (!this.roots.has(id)) {
54
this.roots.add(id);
55
this.emit('roots');
@@ -53,7 +57,7 @@ class Store extends EventEmitter {
57
};
58
59
onBridgeUnmounted = (id: string) => {
56
- console.log('%cStore%c onBridgeUnmounted()', 'color: red; font-weight: bold;', 'font-weight: bold;', id);
60
+ debug('onBridgeUnmounted()', id);
61
this._idToElement.delete(id);
62
63
if (this._idToParentID.has(id)) {
@@ -68,7 +72,7 @@ class Store extends EventEmitter {
72
73
onBridgeUpdated = (element: Element) => {
74
const {id} = element;
71
- console.log('%cStore%c onBridgeUpdated()', 'color: red; font-weight: bold;', 'font-weight: bold;', element);
75
+ debug('onBridgeUpdated()', element);
76
this._idToElement.set(id, element);
77
78
this.emit(id);