@samitouri / QOS-React-2 / commits / d02b4a2784

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);