@samitouri / QOS-React-2 / commits / 3f93b029a4

Replaced node objects with typed array of tree operations; windowing works in small test harness

Brian Vaughn committed Jan 28, 2019 at 15:50 UTC 3f93b029a413f82ef021d2b4b5dfcf224d6d07e7
14 files changed +540 -580
package.json
+1 -1
@@ -90,7 +90,7 @@
90 "react-dom": "^16.8.0-alpha.1",
91 "react-portal": "^3.1.0",
92 "react-virtualized-auto-sizer": "^1.0.2",
93 - "react-window": "^1.5.0",
93 + "react-window": "^1.5.1",
94 "semver": "^5.5.1",
95 "style-loader": "^0.23.1",
96 "web-ext": "^1.10.1",
src/backend/agent.js
+22 -163
@@ -1,44 +1,28 @@
1 // @flow
2
3 -import nullthrows from 'nullthrows';
3 import EventEmitter from 'events';
5 -import { guid } from './utils';
6 -import { ElementTypeOtherOrUnknown } from 'src/devtools/types';
4
8 -import type {
9 - Fiber,
10 - RendererData,
11 - RendererID,
12 - RendererInterface,
13 -} from './types';
5 +import type { RendererID, RendererInterface } from './types';
6 import type { Bridge } from '../types';
15 -import type { Element } from 'src/devtools/types';
7
8 const debug = (methodName, ...args) => {
18 - console.log(`%cAgent %c${methodName}`, 'color: blue; font-weight: bold;', 'font-weight: bold;', ...args);
9 + console.log(
10 + `%cAgent %c${methodName}`,
11 + 'color: purple; font-weight: bold;',
12 + 'font-weight: bold;',
13 + ...args
14 + );
15 };
16
21 -const THROTTLE_BY_MS = 350;
22 -
17 export default class Agent extends EventEmitter {
24 - _fiberToID: WeakMap<Fiber, string> = new WeakMap();
25 - _idToElement: Map<string, Element> = new Map();
26 - _idToFiber: Map<string, Fiber> = new Map();
27 - _idToRendererData: Map<string, RendererData> = new Map();
28 - _idToRendererID: Map<string, RendererID> = new Map();
18 + _bridge: Bridge = ((null: any): Bridge);
19 _rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
30 - _roots: Set<RendererID> = new Set();
20
21 addBridge(bridge: Bridge) {
22 + this._bridge = bridge;
23 +
24 // TODO Listen to bridge for things like selection.
25 // bridge.on('...'), this...);
35 -
36 - this.addListener('root', id => bridge.send('root', id));
37 - this.addListener('rootCommitted', id => bridge.send('rootCommitted', id));
38 - this.addListener('mount', data => bridge.send('mount', data));
39 - this.addListener('update', data => bridge.send('update', data));
40 - this.addListener('unmount', data => bridge.send('unmount', data));
41 - // TODO Add other methods for e.g. profiling.
26 }
27
28 setRendererInterface(
@@ -48,148 +32,23 @@ export default class Agent extends EventEmitter {
32 this._rendererInterfaces[rendererID] = rendererInterface;
33 }
34
51 - _getId(fiber: Fiber): string {
52 - if (typeof fiber !== 'object' || !fiber) {
53 - return fiber;
54 - }
55 - if (!this._fiberToID.has(fiber)) {
56 - this._fiberToID.set(fiber, guid());
57 - this._idToFiber.set(nullthrows(this._fiberToID.get(fiber)), fiber);
58 - }
59 - return nullthrows(this._fiberToID.get(fiber));
60 - }
61 -
62 - _crawl(parent: Element, id: string): void {
63 - const data: RendererData = ((this._idToRendererData.get(
64 - id
65 - ): any): RendererData);
66 - if (data.type === ElementTypeOtherOrUnknown) {
67 - data.children.forEach(childFiber => {
68 - if (childFiber !== null) {
69 - this._crawl(parent, this._getId(childFiber));
70 - }
71 - });
72 - } else {
73 - parent.children.push(id);
74 -
75 - this._createOrUpdateElement(id, data);
76 - }
77 - }
78 -
79 - _createOrUpdateElement(id: string, data: RendererData): void {
80 - const prevElement: ?Element = this._idToElement.get(id);
81 - const nextElement: Element = {
82 - id,
83 - key: data.key,
84 - displayName: data.displayName,
85 - children: [],
86 - type: data.type,
87 - };
88 -
89 - this._idToElement.set(id, nextElement);
90 -
91 - data.children.forEach(childFiber => {
92 - if (childFiber !== null) {
93 - this._crawl(nextElement, this._getId(childFiber));
94 - }
95 - });
96 -
97 - if (prevElement == null) {
98 - debug('emit("mount")', id, nextElement);
99 - this.emit('mount', nextElement);
100 - } else if (!areElementsEqual(prevElement, nextElement)) {
101 - debug('emit("update")', id, nextElement);
102 - this.emit('update', nextElement);
103 - }
104 - }
105 -
106 - onHookMount = ({
107 - data,
108 - fiber,
109 - renderer,
110 - }: {
111 - data: RendererData,
112 - fiber: Fiber,
113 - renderer: RendererID,
114 - }) => {
115 - const id = this._getId(fiber);
116 -
117 - this._idToRendererData.set(id, data);
118 - this._idToRendererID.set(id, renderer);
35 + onHookDisplayNames = (displayNames: Map<number, string>) => {
36 + debug('onHookDisplayNames', displayNames);
37 + this._bridge.send('displayNames', displayNames);
38 };
39
121 - onHookRootCommitted = ({
122 - data,
123 - fiber,
124 - renderer,
125 - }: {
126 - data: RendererData,
127 - fiber: Fiber,
128 - renderer: RendererID,
129 - }) => {
130 - const id = this._getId(fiber);
131 -
132 - // TODO: Can we use the effects list on update for a faster path?
133 - // Mounts (Placements) and unmounts (Deletions) are on the child; need to update parent children in that case.
134 - this._createOrUpdateElement(id, data);
135 -
136 - if (!this._roots.has(id)) {
137 - this._roots.add(id);
138 - debug('emit("root")', id);
139 - this.emit('root', id);
140 - }
141 -
142 - this.emit('rootCommitted', id);
40 + onHookKeys = (keys: Map<number, string>) => {
41 + debug('onHookKeys', keys);
42 + this._bridge.send('keys', keys);
43 };
44
145 - onHookUnmount = ({ fiber }: { fiber: Fiber }) => {
146 - const id = this._getId(fiber);
147 -
148 - if (this._roots.has(id)) {
149 - this._roots.delete(id);
150 - this.emit('rootUnmounted', id);
151 - }
152 -
153 - if (this._idToElement.has(id)) {
154 - this._idToElement.delete(id);
155 - debug('emit("unmount")', id);
156 - this.emit('unmount', id);
157 - }
158 -
159 - this._fiberToID.delete(fiber);
160 - this._idToRendererData.delete(id);
161 - this._idToRendererID.delete(id);
45 + onHookOperations = (operations: Uint32Array) => {
46 + debug('onHookOperations', operations);
47 + this._bridge.send('operations', operations);
48 };
49
164 - onHookUpdate = ({ data, fiber }: { data: RendererData, fiber: Fiber }) => {
165 - const id = this._getId(fiber);
166 -
167 - this._idToRendererData.set(id, data);
50 + onHookRootCommitted = (rootID: string) => {
51 + debug('onHookRootCommitted', rootID);
52 + this._bridge.send('rootCommitted', rootID);
53 };
54 }
170 -
171 -function areElementsEqual(
172 - prevElement: ?Element,
173 - nextElement: ?Element
174 -): boolean {
175 - if (!prevElement || !nextElement) {
176 - return false;
177 - }
178 -
179 - if (
180 - prevElement.key !== nextElement.key ||
181 - prevElement.displayName !== nextElement.displayName ||
182 - prevElement.children.length !== nextElement.children.length ||
183 - prevElement.type !== nextElement.type
184 - ) {
185 - return false;
186 - }
187 -
188 - for (let i = 0; i < prevElement.children.length; i++) {
189 - if (prevElement.children[i] !== nextElement.children[i]) {
190 - return false;
191 - }
192 - }
193 -
194 - return true;
195 -}
src/backend/index.js
+1 -3
@@ -12,10 +12,8 @@ export function initBackend(hook: Hook, agent: Agent): void {
12 rendererInterface.walkTree();
13 }),
14
15 + hook.sub('operations', agent.onHookOperations),
16 hook.sub('rootCommitted', agent.onHookRootCommitted),
16 - hook.sub('mount', agent.onHookMount),
17 - hook.sub('update', agent.onHookUpdate),
18 - hook.sub('unmount', agent.onHookUnmount),
17
18 // TODO Add additional subscriptions required for profiling mode
19 ];
src/backend/renderer.js
+262 -173
@@ -8,21 +8,25 @@ import {
8 ElementTypeMemo,
9 ElementTypeOtherOrUnknown,
10 ElementTypeProfiler,
11 + ElementTypeRoot,
12 ElementTypeSuspense,
13 } from 'src/devtools/types';
14 +import { utfEncodeString } from '../utils';
15 import { getDisplayName } from './utils';
16 +import {
17 + TREE_OPERATION_ADD,
18 + TREE_OPERATION_REMOVE,
19 + TREE_OPERATION_RESET_CHILDREN,
20 +} from '../constants';
21
22 import type {
23 Fiber,
24 Hook,
25 ReactRenderer,
19 - RendererData,
26 + FiberData,
27 RendererInterface,
28 } from './types';
29
23 -// TODO: If we're profiling, process update
24 -// TODO: Throttle process update to app tree
25 -
30 function getInternalReactConstants(version) {
31 const ReactSymbols = {
32 CONCURRENT_MODE_NUMBER: 0xeacf,
@@ -184,10 +188,61 @@ export function attach(
188
189 const primaryFibers: WeakSet<Fiber> = new WeakSet();
190
187 - // TODO: we might want to change the data structure
188 - // once we no longer suppport Stack versions of `getData`.
191 + // Keep this function in sync with getDataForFiber()
192 + function shouldFilterFiber(fiber: Fiber): boolean {
193 + const { type, tag } = fiber;
194 +
195 + switch (tag) {
196 + case ClassComponent:
197 + case FunctionComponent:
198 + case IncompleteClassComponent:
199 + case IndeterminateComponent:
200 + case ForwardRef:
201 + case HostRoot:
202 + case MemoComponent:
203 + case SimpleMemoComponent:
204 + return false;
205 + case HostPortal:
206 + case HostComponent:
207 + case HostText:
208 + case Fragment:
209 + return true;
210 + default:
211 + const symbolOrNumber =
212 + typeof type === 'object' && type !== null ? type.$$typeof : type;
213 +
214 + const switchValue =
215 + // $FlowFixMe facebook/flow/issues/2362
216 + typeof symbolOrNumber === 'symbol'
217 + ? symbolOrNumber.toString()
218 + : symbolOrNumber;
219 +
220 + switch (switchValue) {
221 + case CONCURRENT_MODE_NUMBER:
222 + case CONCURRENT_MODE_SYMBOL_STRING:
223 + case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
224 + case STRICT_MODE_NUMBER:
225 + case STRICT_MODE_SYMBOL_STRING:
226 + return true;
227 + case CONTEXT_PROVIDER_NUMBER:
228 + case CONTEXT_PROVIDER_SYMBOL_STRING:
229 + case CONTEXT_CONSUMER_NUMBER:
230 + case CONTEXT_CONSUMER_SYMBOL_STRING:
231 + case SUSPENSE_NUMBER:
232 + case SUSPENSE_SYMBOL_STRING:
233 + case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
234 + case PROFILER_NUMBER:
235 + case PROFILER_SYMBOL_STRING:
236 + return false;
237 + default:
238 + return false;
239 + }
240 + }
241 + }
242 +
243 + // TODO: we might want to change the data structure once we no longer suppport Stack versions of `getData`.
244 // TODO: Keep in sync with getElementType()
190 - function getRendererDataFromFiber(fiber: Fiber): RendererData {
245 + function getDataForFiber(fiber: Fiber): FiberData {
246 const { elementType, type, key, tag } = fiber;
247
248 // This is to support lazy components with a Promise as the type.
@@ -199,11 +254,7 @@ export function attach(
254 }
255 }
256
202 - // Suspense has some special behavior when timed out that we have to handle.
203 - // see https://github.com/facebook/react/pull/13823
204 - let isTimedOutSuspense: boolean = false;
205 -
206 - let rendererData: RendererData = ((null: any): RendererData);
257 + let fiberData: FiberData = ((null: any): FiberData);
258 let displayName: string = ((null: any): string);
259 let resolvedContext: any = null;
260
@@ -212,8 +263,7 @@ export function attach(
263 case FunctionComponent:
264 case IncompleteClassComponent:
265 case IndeterminateComponent:
215 - rendererData = {
216 - children: [],
266 + fiberData = {
267 displayName: getDisplayName(resolvedType),
268 key,
269 type: ElementTypeClassOrFunction,
@@ -224,26 +274,28 @@ export function attach(
274 displayName =
275 resolvedType.displayName ||
276 (functionName !== '' ? `ForwardRef(${functionName})` : 'ForwardRef');
227 - rendererData = {
228 - children: [],
277 +
278 + fiberData = {
279 displayName,
280 key,
281 type: ElementTypeForwardRef,
282 };
283 break;
284 case HostRoot:
285 + return {
286 + displayName: null,
287 + key: null,
288 + type: ElementTypeRoot,
289 + };
290 case HostPortal:
291 case HostComponent:
292 case HostText:
293 case Fragment:
239 - // Not displayed in Elements tree
240 - rendererData = {
241 - children: [],
294 + return {
295 displayName: null,
243 - key,
296 + key: null,
297 type: ElementTypeOtherOrUnknown,
298 };
246 - break;
299 case MemoComponent:
300 case SimpleMemoComponent:
301 if (elementType.displayName) {
@@ -252,8 +304,7 @@ export function attach(
304 displayName = type.displayName || type.name;
305 displayName = displayName ? `Memo(${displayName})` : 'Memo';
306 }
255 - rendererData = {
256 - children: [],
307 + fiberData = {
308 displayName,
309 key,
310 type: ElementTypeMemo,
@@ -273,14 +324,11 @@ export function attach(
324 case CONCURRENT_MODE_NUMBER:
325 case CONCURRENT_MODE_SYMBOL_STRING:
326 case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
276 - // Not displayed in Elements tree
277 - rendererData = {
278 - children: [],
327 + return {
328 displayName: null,
280 - key,
329 + key: null,
330 type: ElementTypeOtherOrUnknown,
331 };
283 - break;
332 case CONTEXT_PROVIDER_NUMBER:
333 case CONTEXT_PROVIDER_SYMBOL_STRING:
334 // 16.3.0 exposed the context object as "context"
@@ -289,8 +337,7 @@ export function attach(
337 displayName = `${resolvedContext.displayName ||
338 'Context'}.Provider`;
339
292 - rendererData = {
293 - children: [],
340 + fiberData = {
341 displayName,
342 key,
343 type: ElementTypeContext,
@@ -307,8 +354,7 @@ export function attach(
354 displayName = `${resolvedContext.displayName ||
355 'Context'}.Consumer`;
356
310 - rendererData = {
311 - children: [],
357 + fiberData = {
358 displayName,
359 key,
360 type: ElementTypeContext,
@@ -316,8 +362,7 @@ export function attach(
362 break;
363 case STRICT_MODE_NUMBER:
364 case STRICT_MODE_SYMBOL_STRING:
319 - rendererData = {
320 - children: [],
365 + fiberData = {
366 displayName: null,
367 key,
368 type: ElementTypeOtherOrUnknown,
@@ -326,11 +371,7 @@ export function attach(
371 case SUSPENSE_NUMBER:
372 case SUSPENSE_SYMBOL_STRING:
373 case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
329 - // Suspense components only have a non-null memoizedState if they're timed-out.
330 - isTimedOutSuspense = fiber.memoizedState !== null;
331 -
332 - rendererData = {
333 - children: [],
374 + fiberData = {
375 displayName: 'Suspense',
376 key,
377 type: ElementTypeSuspense,
@@ -338,8 +379,7 @@ export function attach(
379 break;
380 case PROFILER_NUMBER:
381 case PROFILER_SYMBOL_STRING:
341 - rendererData = {
342 - children: [],
382 + fiberData = {
383 displayName: `Profiler(${fiber.memoizedProps.id})`,
384 key,
385 type: ElementTypeProfiler,
@@ -348,8 +388,7 @@ export function attach(
388 default:
389 // Unknown element type.
390 // This may mean a new element type that has not yet been added to DevTools.
351 - rendererData = {
352 - children: [],
391 + fiberData = {
392 displayName: null,
393 key,
394 type: ElementTypeOtherOrUnknown,
@@ -359,30 +398,7 @@ export function attach(
398 break;
399 }
400
362 - const { children } = rendererData;
363 - if (isTimedOutSuspense) {
364 - // The behavior of timed-out Suspense trees is unique.
365 - // Rather than unmount the timed out content (and possibly lose important state),
366 - // React re-parents this content within a hidden Fragment while the fallback is showing.
367 - // This behavior doesn't need to be observable in the DevTools though.
368 - // It might even result in a bad user experience for e.g. node selection in the Elements panel.
369 - // The easiest fix is to strip out the intermediate Fragment fibers,
370 - // so the Elements panel and Profiler don't need to special case them.
371 - const primaryChildFragment = fiber.child;
372 - const primaryChild = primaryChildFragment.child;
373 - const fallbackChildFragment = primaryChildFragment.sibling;
374 - const fallbackChild = fallbackChildFragment.child;
375 - children.push(primaryChild);
376 - children.push(fallbackChild);
377 - } else {
378 - let child = fiber.child;
379 - while (child) {
380 - children.push(getPrimaryFiber(child));
381 - child = child.sibling;
382 - }
383 - }
384 -
385 - return rendererData;
401 + return fiberData;
402 }
403
404 // This is a slightly annoying indirection.
@@ -401,6 +417,17 @@ export function attach(
417 return fiber;
418 }
419
420 + let uidCounter: number = 0;
421 + const fiberToIDMap: WeakMap<Fiber, number> = new WeakMap();
422 +
423 + function getFiberID(primaryFiber: Fiber): number {
424 + if (!fiberToIDMap.has(primaryFiber)) {
425 + fiberToIDMap.set(primaryFiber, ++uidCounter);
426 + }
427 + return ((fiberToIDMap.get(primaryFiber): any): number);
428 + }
429 +
430 + // eslint-disable-next-line no-unused-vars
431 function hasDataChanged(prevFiber: Fiber, nextFiber: Fiber): boolean {
432 switch (nextFiber.tag) {
433 case ClassComponent:
@@ -425,6 +452,7 @@ export function attach(
452 }
453 }
454
455 + // eslint-disable-next-line no-unused-vars
456 function haveProfilerTimesChanged(
457 prevFiber: Fiber,
458 nextFiber: Fiber
@@ -437,127 +465,176 @@ export function attach(
465 );
466 }
467
440 - let pendingEvents = [];
441 -
442 - // TODO: Do we still need to send events like this?
443 - function flushPendingEvents() {
444 - const events = pendingEvents;
445 - pendingEvents = [];
446 - for (let i = 0; i < events.length; i++) {
447 - const event = events[i];
448 - hook.emit(event.type, event);
468 + let pendingOperations: Uint32Array = new Uint32Array(0);
469 +
470 + function addOperation(
471 + newAction: Uint32Array,
472 + addToStartOfQueue: boolean = false
473 + ): void {
474 + const oldActions = pendingOperations;
475 + pendingOperations = new Uint32Array(oldActions.length + newAction.length);
476 + if (addToStartOfQueue) {
477 + pendingOperations.set(newAction);
478 + pendingOperations.set(oldActions, newAction.length);
479 + } else {
480 + pendingOperations.set(oldActions);
481 + pendingOperations.set(newAction, oldActions.length);
482 }
483 }
484
452 - function enqueueMount(fiber) {
453 - pendingEvents.push({
454 - fiber: getPrimaryFiber(fiber),
455 - data: getRendererDataFromFiber(fiber),
456 - renderer: rendererID,
457 - type: 'mount',
458 - });
485 + function flushPendingEvents(root: Object): void {
486 + // Let the frontend know about tree operations.
487 + hook.emit('operations', pendingOperations);
488 + pendingOperations = new Uint32Array(0);
489
490 + // Let the frontend know that we're done working on this root.
491 + // Technically this could be inferred, but it's better to explicitly do this for the case of multi roots.
492 + // Else the frontend would need to traverse the tree to identify which updates corresponded to which roots.
493 + hook.emit('rootCommitted', getFiberID(getPrimaryFiber(root.current)));
494 + }
495 +
496 + function enqueueMount(fiber: Fiber, parentFiber: Fiber | null) {
497 const isRoot = fiber.tag === HostRoot;
498 + const id = getFiberID(getPrimaryFiber(fiber));
499 +
500 if (isRoot) {
462 - pendingEvents.push({
463 - fiber: getPrimaryFiber(fiber),
464 - renderer: rendererID,
465 - type: 'root',
466 - });
467 - }
468 - }
501 + const operation = new Uint32Array(4);
502 + operation[0] = TREE_OPERATION_ADD;
503 + operation[1] = id;
504 + operation[2] = ElementTypeRoot;
505 + operation[3] = 0; // Identifies this fiber as a root
506 + addOperation(operation);
507 + } else {
508 + const { displayName, key, type } = getDataForFiber(fiber);
509
470 - function enqueueUpdateIfNecessary(fiber, hasChildOrderChanged) {
471 - const data = getRendererDataFromFiber(fiber);
472 -
473 - if (!hasChildOrderChanged && !hasDataChanged(fiber.alternate, fiber)) {
474 - // If only timing information has changed, we still need to update the nodes.
475 - // But we can do it in a faster way since we know it's safe to skip the children.
476 - // It's also important to avoid emitting an "update" signal for the node in this case,
477 - // Since that would indicate to the Profiler that it was part of the "commit" when it wasn't.
478 - if (haveProfilerTimesChanged(fiber.alternate, fiber)) {
479 - pendingEvents.push({
480 - fiber: getPrimaryFiber(fiber),
481 - data,
482 - renderer: rendererID,
483 - type: 'updateProfileTimes',
484 - });
510 + let encodedDisplayName = ((null: any): Uint8Array);
511 + let encodedKey = ((null: any): Uint8Array);
512 +
513 + if (displayName !== null) {
514 + encodedDisplayName = utfEncodeString(displayName);
515 }
486 - return;
516 +
517 + if (key !== null) {
518 + if (typeof key === 'number') {
519 + encodedKey = new Uint8Array(1);
520 + encodedKey[0] = key;
521 + } else {
522 + encodedKey = utfEncodeString(key);
523 + }
524 + }
525 +
526 + const encodedDisplayNameSize =
527 + displayName === null ? 0 : encodedDisplayName.length;
528 + const encodedKeySize = key === null ? 0 : encodedKey.length;
529 +
530 + const operation = new Uint32Array(
531 + 6 + encodedDisplayNameSize + encodedKeySize
532 + );
533 + operation[0] = TREE_OPERATION_ADD;
534 + operation[1] = id;
535 + operation[2] = type;
536 + operation[3] = getFiberID(getPrimaryFiber(parentFiber));
537 + operation[4] = encodedDisplayNameSize;
538 + if (displayName !== null) {
539 + operation.set(encodedDisplayName, 5);
540 + }
541 + operation[5 + encodedDisplayNameSize] = encodedKeySize;
542 + if (key !== null) {
543 + operation.set(encodedKey, 5 + encodedDisplayNameSize + 1);
544 + }
545 + addOperation(operation);
546 }
488 - pendingEvents.push({
489 - fiber: getPrimaryFiber(fiber),
490 - data,
491 - renderer: rendererID,
492 - type: 'update',
493 - });
547 }
548
549 function enqueueUnmount(fiber) {
550 const isRoot = fiber.tag === HostRoot;
551 const primaryFiber = getPrimaryFiber(fiber);
499 - const event = {
500 - fiber: primaryFiber,
501 - renderer: rendererID,
502 - type: 'unmount',
503 - };
552 if (isRoot) {
505 - pendingEvents.push(event);
506 - } else {
553 + const id = getFiberID(getPrimaryFiber(fiber));
554 + const operation = new Uint32Array(2);
555 + operation[0] = TREE_OPERATION_REMOVE;
556 + operation[1] = id;
557 + addOperation(operation);
558 + } else if (!shouldFilterFiber(fiber)) {
559 // Non-root fibers are deleted during the commit phase.
560 // They are deleted in the child-first order. However
561 // DevTools currently expects deletions to be parent-first.
562 // This is why we unshift deletions rather than push them.
511 - pendingEvents.unshift(event);
563 + const id = getFiberID(getPrimaryFiber(fiber));
564 + const operation = new Uint32Array(2);
565 + operation[0] = TREE_OPERATION_REMOVE;
566 + operation[1] = id;
567 + addOperation(operation, true);
568 }
569 primaryFibers.delete(primaryFiber);
570 + fiberToIDMap.delete(primaryFiber);
571 }
572
516 - function markRootCommitted(fiber) {
517 - pendingEvents.push({
518 - fiber: getPrimaryFiber(fiber),
519 - data: getRendererDataFromFiber(fiber),
520 - renderer: rendererID,
521 - type: 'rootCommitted',
522 - });
573 + function mountFiber(fiber: Fiber, parentFiber: Fiber | null) {
574 + const shouldEnqueueMount = !shouldFilterFiber(fiber);
575 +
576 + if (shouldEnqueueMount) {
577 + enqueueMount(fiber, parentFiber);
578 + }
579 +
580 + if (fiber.child !== null) {
581 + mountFiber(fiber.child, shouldEnqueueMount ? fiber : parentFiber);
582 + }
583 +
584 + if (fiber.sibling) {
585 + mountFiber(fiber.sibling, parentFiber);
586 + }
587 }
588
525 - function mountFiber(fiber) {
526 - // Depth-first.
527 - // Logs mounting of children first, parents later.
528 - let node = fiber;
529 - outer: while (true) {
530 - if (node.child) {
531 - node.child.return = node;
532 - node = node.child;
533 - continue;
534 - }
535 - enqueueMount(node);
536 - if (node === fiber) {
537 - return;
538 - }
539 - if (node.sibling) {
540 - node.sibling.return = node.return;
541 - node = node.sibling;
542 - continue;
589 + function enqueueUpdateIfNecessary(
590 + fiber: Fiber,
591 + hasChildOrderChanged: boolean
592 + ) {
593 + // The frontend only really cares about the displayName, key, and children.
594 + // The first two don't really change, so we are only concerned with the order of children here.
595 + // This is trickier than a simple comparison though, since certain types of fibers are filtered.
596 + if (hasChildOrderChanged) {
597 + const nextChildren: Array<number> = [];
598 +
599 + // This is a naive implimentation that shallowly recurses children.
600 + // We might want to revisit this if it proves to be too inefficient.
601 + let child = fiber.child;
602 + while (child !== null) {
603 + findReorderedChildren(child, nextChildren);
604 + child = child.sibling;
605 }
544 - while (node.return) {
545 - node = node.return;
546 - enqueueMount(node);
547 - if (node === fiber) {
548 - return;
549 - }
550 - if (node.sibling) {
551 - node.sibling.return = node.return;
552 - node = node.sibling;
553 - continue outer;
554 - }
606 +
607 + const numChildren = nextChildren.length;
608 + const operation = new Uint32Array(3 + numChildren);
609 + operation[0] = TREE_OPERATION_RESET_CHILDREN;
610 + operation[1] = getFiberID(getPrimaryFiber(fiber));
611 + operation[2] = numChildren;
612 + operation.set(nextChildren, 3);
613 + addOperation(operation);
614 + }
615 +
616 + // TODO (profiling) If we're profiling, also check to see if that data has changed.
617 + }
618 +
619 + function findReorderedChildren(fiber: Fiber, nextChildren: Array<number>) {
620 + if (!shouldFilterFiber(fiber)) {
621 + nextChildren.push(getFiberID(getPrimaryFiber(fiber)));
622 + } else {
623 + let child = fiber.child;
624 + while (child !== null) {
625 + findReorderedChildren(child, nextChildren);
626 + child = child.sibling;
627 }
556 - return;
628 }
629 }
630
560 - function updateFiber(nextFiber, prevFiber) {
631 + function updateFiber(
632 + nextFiber: Fiber,
633 + prevFiber: Fiber,
634 + parentFiber: Fiber | null
635 + ) {
636 + const shouldEnqueueUpdate = !shouldFilterFiber(nextFiber);
637 +
638 // Suspense components only have a non-null memoizedState if they're timed-out.
639 const isTimedOutSuspense =
640 nextFiber.tag === ReactTypeOfWork.SuspenseComponent &&
@@ -574,15 +651,19 @@ export function attach(
651 const primaryChildFragment = nextFiber.child;
652 const fallbackChildFragment = primaryChildFragment.sibling;
653 const fallbackChild = fallbackChildFragment.child;
654 +
655 // The primary, hidden child is never actually updated in this case,
656 // so we can skip any updates to its tree.
657 // We only need to track updates to the Fallback UI for now.
658 if (fallbackChild.alternate) {
581 - updateFiber(fallbackChild, fallbackChild.alternate);
659 + updateFiber(fallbackChild, fallbackChild.alternate, nextFiber);
660 } else {
583 - mountFiber(fallbackChild);
661 + mountFiber(fallbackChild, nextFiber);
662 + }
663 +
664 + if (shouldEnqueueUpdate) {
665 + enqueueUpdateIfNecessary(nextFiber, false);
666 }
585 - enqueueUpdateIfNecessary(nextFiber, false);
667 } else {
668 let hasChildOrderChanged = false;
669 if (nextFiber.child !== prevFiber.child) {
@@ -597,7 +678,11 @@ export function attach(
678 // We don't track deletions here because they are reported separately.
679 if (nextChild.alternate) {
680 const prevChild = nextChild.alternate;
600 - updateFiber(nextChild, prevChild);
681 + updateFiber(
682 + nextChild,
683 + prevChild,
684 + shouldEnqueueUpdate ? nextFiber : parentFiber
685 + );
686 // However we also keep track if the order of the children matches
687 // the previous order. They are always different referentially, but
688 // if the instances line up conceptually we'll want to know that.
@@ -605,7 +690,10 @@ export function attach(
690 hasChildOrderChanged = true;
691 }
692 } else {
608 - mountFiber(nextChild);
693 + mountFiber(
694 + nextChild,
695 + shouldEnqueueUpdate ? nextFiber : parentFiber
696 + );
697 if (!hasChildOrderChanged) {
698 hasChildOrderChanged = true;
699 }
@@ -623,7 +711,10 @@ export function attach(
711 hasChildOrderChanged = true;
712 }
713 }
626 - enqueueUpdateIfNecessary(nextFiber, hasChildOrderChanged);
714 +
715 + if (shouldEnqueueUpdate) {
716 + enqueueUpdateIfNecessary(nextFiber, hasChildOrderChanged);
717 + }
718 }
719 }
720
@@ -632,12 +723,11 @@ export function attach(
723 }
724
725 function walkTree() {
726 + // Hydrate all the roots for the first time.
727 hook.getFiberRoots(rendererID).forEach(root => {
636 - // Hydrate all the roots for the first time.
637 - mountFiber(root.current);
638 - markRootCommitted(root.current);
728 + mountFiber(root.current, null);
729 + flushPendingEvents(root);
730 });
640 - flushPendingEvents();
731 }
732
733 function handleCommitFiberUnmount(fiber) {
@@ -660,21 +750,20 @@ export function attach(
750 current.memoizedState != null && current.memoizedState.element != null;
751 if (!wasMounted && isMounted) {
752 // Mount a new root.
663 - mountFiber(current);
753 + mountFiber(current, null);
754 } else if (wasMounted && isMounted) {
755 // Update an existing root.
666 - updateFiber(current, alternate);
756 + updateFiber(current, alternate, null);
757 } else if (wasMounted && !isMounted) {
758 // Unmount an existing root.
759 enqueueUnmount(current);
760 }
761 } else {
762 // Mount a new root.
673 - mountFiber(current);
763 + mountFiber(current, null);
764 }
675 - markRootCommitted(current);
765 // We're done here.
677 - flushPendingEvents();
766 + flushPendingEvents(root);
767 }
768
769 // The naming is confusing.
src/backend/types.js
+1 -2
@@ -12,10 +12,9 @@ export type Fiber = Object;
12 // TODO: If it's useful for the frontend to know which types of data an Element has
13 // (e.g. props, state, context, hooks) then we could add a bitmask field for this
14 // to keep the number of attributes small.
15 -export type RendererData = {|
15 +export type FiberData = {|
16 key: React$Key | null,
17 displayName: string | null,
18 - children: Array<Fiber>,
18 type: ElementType,
19 |};
20
src/constants.js new
+5
@@ -0,0 +1,5 @@
1 +// @flow
2 +
3 +export const TREE_OPERATION_ADD = 1;
4 +export const TREE_OPERATION_REMOVE = 2;
5 +export const TREE_OPERATION_RESET_CHILDREN = 3;
src/devtools/store.js
+194 -200
@@ -1,73 +1,83 @@
1 // @flow
2
3 import EventEmitter from 'events';
4 -
5 -import type { Element, ElementTreeMetadata } from './types';
4 +import {
5 + TREE_OPERATION_ADD,
6 + TREE_OPERATION_REMOVE,
7 + TREE_OPERATION_RESET_CHILDREN,
8 +} from '../constants';
9 +import { utfDecodeString } from '../utils';
10 +
11 +import type { Element, ElementType } from './types';
12 import type { Bridge } from '../types';
13
14 const debug = (methodName, ...args) => {
9 - console.log(`%cStore %c${methodName}`, 'color: red; font-weight: bold;', 'font-weight: bold;', ...args);
15 + console.log(
16 + `%cStore %c${methodName}`,
17 + 'color: green; font-weight: bold;',
18 + 'font-weight: bold;',
19 + ...args
20 + );
21 };
22
23 /**
24 * The store is the single source of truth for updates from the backend.
25 * ContextProviders can subscribe to the Store for specific things they want to provide.
26 */
16 -class Store extends EventEmitter {
17 - _elementToElementTreeMetadata: WeakMap<
18 - Element,
19 - ElementTreeMetadata
20 - > = new WeakMap();
21 - _idToElement: Map<string, Element> = new Map();
22 - _idToParentID: Map<string, string> = new Map();
23 - _pendingDeletions: Set<string> = new Set();
27 +export default class Store extends EventEmitter {
28 + // TODO Should items in this map be read-only for easier props comparison?
29 + _idToElement: Map<number, Element> = new Map();
30
31 // Total number of visible elements (within all roots).
32 // Used for windowing purposes.
27 - numElements: number = 0;
33 + _numElements: number = 0;
34
35 // This Array must be treated as immutable!
36 // Passive effects will check it for changes between render and mount.
31 - roots: $ReadOnlyArray<string> = [];
37 + _roots: $ReadOnlyArray<number> = [];
38
39 constructor(bridge: Bridge) {
40 super();
41
36 - bridge.on('root', this.onBridgeRoot);
42 + bridge.on('operations', this.onBridgeOperations);
43 bridge.on('rootCommitted', this.onBridgeRootCommitted);
44 + }
45 +
46 + get numElements(): number {
47 + return this._numElements;
48 + }
49
39 - bridge.on('mount', this.onBridgeMount);
40 - bridge.on('update', this.onBridgeUpdated);
41 - bridge.on('unmount', this.onBridgeUnmounted);
50 + get roots(): $ReadOnlyArray<number> {
51 + return this._roots;
52 }
53
44 - getElementAtIndex(index: number): Element | null {
54 + getElementAtIndex(index: number): Element {
55 if (index < 0 || index >= this.numElements) {
46 - return null;
56 + throw Error(`Invalid index ${index} specified`);
57 }
58
59 + // Find wich root this element is in...
60 let rootID;
61 let root;
51 -
62 let rootWeight = 0;
53 - for (let i = 0; i < this.roots.length; i++) {
54 - rootID = this.roots[i];
55 - root = this._idToElement.get(rootID);
56 - const { weight } = this._elementToElementTreeMetadata.get(root);
57 -
58 - if (rootWeight + weight > index) {
63 + for (let i = 0; i < this._roots.length; i++) {
64 + rootID = this._roots[i];
65 + root = ((this._idToElement.get(rootID): any): Element);
66 + if (rootWeight + root.weight > index) {
67 break;
68 + } else {
69 + rootWeight += root.weight;
70 }
71 }
72
63 - let currentElement = root;
73 + // Crawl the tree to find the correct root...
74 + let currentElement = ((root: any): Element);
75 let currentWeight = 0;
65 -
76 while (index !== currentWeight) {
77 for (let i = 0; i < currentElement.children.length; i++) {
78 const childID = currentElement.children[i];
69 - const child = this._idToElement.get(childID);
70 - const { weight } = this._elementToElementTreeMetadata.get(child);
79 + const child = ((this._idToElement.get(childID): any): Element);
80 + const { weight } = child;
81 if (index <= currentWeight + weight) {
82 currentWeight++;
83 currentElement = child;
@@ -78,207 +88,191 @@ class Store extends EventEmitter {
88 }
89 }
90
81 - return currentElement;
82 - }
83 -
84 - getElementByID(id: string) {
85 - return this._idToElement.get(id);
86 - }
87 -
88 - geParent(id: string) {
89 - return this._idToParentID.get(id);
90 - }
91 -
92 - getTreeMetadataForElement(element: Element) {
93 - return this._elementToElementTreeMetadata.get(element);
91 + return ((currentElement: any): Element);
92 }
93
96 - _crawlForTreeMetadata(id: string, depth: number = 0): number {
97 - let weight = 1;
98 -
94 + getElementByID(id: number): Element {
95 const element = this._idToElement.get(id);
96
101 - // TODO: Figure out why sometimes items aren't being sent across the bridge.
102 - // It always seems to be one of the ListItems...
97 if (element == null) {
104 - console.log(`%cNo element found for id "${id}"`, 'background-color: yellow; font-weight: bold;');
105 - return 0;
98 + throw Error(`No element found with id "${id}`);
99 }
100
108 - element.children.forEach(childID => {
109 - weight += this._crawlForTreeMetadata(childID, depth + 1);
110 - });
111 -
112 - this._elementToElementTreeMetadata.set(element, {
113 - depth,
114 - weight,
115 - });
116 -
117 - return weight;
101 + return ((element: any): Element);
102 }
103
120 - _updateElementTreeMetadata(prevElement: Element, element: Element): void {
121 - if (prevElement.children === element.children) {
122 - return;
123 - }
124 -
125 - // Compare children in case they have changed.
126 - // For each child that was removed, we need to shrink the list by this many elements.
127 - // For each child that was added, we need to grow the list by this many elements.
128 -
129 - const prevChildren = prevElement.children;
130 - const prevNumChildren = prevChildren.length;
131 -
132 - const children = element.children;
133 - const numChildren = children.length;
134 -
135 - // TODO: The below diffing could be optimized more.
136 -
137 - // Scan for deletions
138 - for (let i = 0; i < prevNumChildren; i++) {
139 - const childID = prevChildren[i];
140 - if (!children.includes(childID)) {
141 - const child = this._idToElement.get(childID);
142 - const { weight } = this._elementToElementTreeMetadata.get(child);
143 -
144 - this.numElements -= weight;
145 -
146 - let current = element;
147 - while (current !== null) {
148 - const datum = this._elementToElementTreeMetadata.get(current);
149 - datum.weight -= weight;
104 + onBridgeOperations = (operations: Uint32Array) => {
105 + debug('onBridgeOperations', operations);
106 +
107 + let haveRootsChanged = false;
108 +
109 + let i = 0;
110 + while (i < operations.length) {
111 + let id: number = ((null: any): number);
112 + let element: Element = ((null: any): Element);
113 + let parentID: number = ((null: any): number);
114 + let parentElement: Element = ((null: any): Element);
115 + let type: ElementType = ((null: any): ElementType);
116 + let weightDelta: number = 0;
117 +
118 + const operation = operations[i];
119 +
120 + switch (operation) {
121 + case TREE_OPERATION_ADD:
122 + id = ((operations[i + 1]: any): number);
123 + type = ((operations[i + 2]: any): ElementType);
124 + parentID = ((operations[i + 3]: any): number);
125 +
126 + i = i + 4;
127 +
128 + if (parentID === 0) {
129 + debug('Add', `new root fiber ${id}`);
130 +
131 + this._roots = this._roots.concat(id);
132 +
133 + this._idToElement.set(id, {
134 + children: [],
135 + depth: 0,
136 + displayName: null,
137 + id,
138 + key: null,
139 + parentID: 0,
140 + type,
141 + weight: 1,
142 + });
143 +
144 + haveRootsChanged = true;
145 + } else {
146 + const displayNameLength = operations[i];
147 + i++;
148 + const displayName =
149 + displayNameLength === 0
150 + ? null
151 + : utfDecodeString(
152 + (operations.slice(i, i + displayNameLength): any)
153 + );
154 + i += displayNameLength;
155 +
156 + const keyLength = operations[i];
157 + i++;
158 + const key =
159 + keyLength === 0
160 + ? null
161 + : utfDecodeString((operations.slice(i, i + keyLength): any));
162 + i += +keyLength;
163 +
164 + debug('Add', `fiber ${id}, type ${type}, as child of ${parentID}`);
165 +
166 + // TODO Fix this; there should not be duplicate "ADD" operations for a given element.
167 + if (this._idToElement.has(id)) {
168 + console.warn(
169 + `fiber ${id}, type ${type}, already added as child of ${parentID}`
170 + );
171 + } else {
172 + parentElement = ((this._idToElement.get(parentID): any): Element);
173 + parentElement.children = parentElement.children.concat(id);
174 +
175 + this._idToElement.set(id, {
176 + children: [],
177 + depth: parentElement.depth + 1,
178 + displayName,
179 + id,
180 + key,
181 + parentID: parentElement.id,
182 + type,
183 + weight: 1,
184 + });
185 +
186 + weightDelta = 1;
187 + }
188 + }
189 + break;
190 + case TREE_OPERATION_REMOVE:
191 + id = ((operations[i + 1]: any): number);
192
151 - const parent = this._idToElement.get(datum.parentID);
152 - current =
153 - parent != null
154 - ? this._elementToElementTreeMetadata.get(parent)
155 - : null;
156 - }
157 - }
158 - }
193 + i = i + 2;
194
160 - // Scan for additions
161 - for (let i = 0; i < numChildren; i++) {
162 - const childID = children[i];
163 - if (!prevChildren.includes(childID)) {
164 - const child = this._idToElement.get(childID);
165 - const { depth } = this._elementToElementTreeMetadata.get(element);
166 - const weight = this._crawlForTreeMetadata(
167 - childID,
168 - depth + 1
169 - );
170 -
171 - this.numElements += weight;
172 -
173 - let current = element;
174 - while (current !== null) {
175 - const datum = this._elementToElementTreeMetadata.get(current);
176 - datum.weight += weight;
177 -
178 - const parent = this._idToElement.get(datum.parentID);
179 - current =
180 - parent != null
181 - ? this._elementToElementTreeMetadata.get(parent)
182 - : null;
183 - }
184 - }
185 - }
186 - }
195 + debug('Remove', `fiber ${id} from tree`);
196
188 -__printTree() {
189 - let i = 0;
190 - this.roots.forEach(rootID => {
191 - const root = this._idToElement.get(rootID);
192 - const { weight } = this._elementToElementTreeMetadata.get(root);
193 - for (let j = i; j < i + weight; j++) {
194 - const element = this.getElementAtIndex(j)
195 - const { depth } = this._elementToElementTreeMetadata.get(element);
197 + element = ((this._idToElement.get(id): any): Element);
198 + parentID = element.parentID;
199
197 - console.log(' '.repeat(depth) + element.displayName);
198 - }
199 - i += weight;
200 - });
201 -}
200 + weightDelta = -element.weight;
201
203 - onBridgeMount = (element: Element) => {
204 - const { id } = element;
205 - debug('onBridgeMount()', element);
206 - this._idToElement.set(id, element);
202 + this._idToElement.delete(id);
203
208 - element.children.forEach(childID => {
209 - this._idToParentID.set(childID, id);
210 - });
204 + parentElement = ((this._idToElement.get(parentID): any): Element);
205 + if (parentElement != null) {
206 + parentElement.children = parentElement.children.filter(
207 + childID => childID !== id
208 + );
209 + }
210 + break;
211 + case TREE_OPERATION_RESET_CHILDREN:
212 + id = ((operations[i + 1]: any): number);
213 + const numChildren = ((operations[i + 2]: any): number);
214 + const children = ((operations.slice(
215 + i + 3,
216 + i + 3 + numChildren
217 + ): any): Array<number>);
218
212 - this.emit(id);
213 - };
219 + i = i + 3 + numChildren;
220
215 - onBridgeRoot = (id: string) => {
216 - debug('onBridgeRoot()', id);
217 - if (!this.roots.includes(id)) {
218 - this.roots = this.roots.concat(id);
221 + debug('Re-order', `fiber ${id} children ${children.join(',')}`);
222
220 - // Generate tree metadata used for windowing.
221 - this.numElements += this._crawlForTreeMetadata(id);
223 + element = ((this._idToElement.get(id): any): Element);
224 + element.children = children;
225
223 - this.emit('roots');
224 - }
225 - };
226 + const prevWeight = element.weight;
227 + let childWeight = 0;
228
227 - onBridgeRootCommitted = (rootID: string) => {
228 - this._pendingDeletions.forEach(id => {
229 - this._idToElement.delete(id);
229 + children.forEach(childID => {
230 + const child = ((this._idToElement.get(childID): any): Element);
231 + childWeight += child.weight;
232 + });
233
231 - if (this._idToParentID.has(id)) {
232 - this._idToParentID.delete(id);
234 + element.weight = childWeight + 1;
235 + weightDelta = childWeight + 1 - prevWeight;
236 + break;
237 + default:
238 + throw Error(`Unsupported Bridge operation ${operation}`);
239 }
234 - });
235 - this._pendingDeletions.clear();
236 -
237 - debug('onBridgeRootCommitted()', rootID);
238 - this.emit('rootCommitted', rootID);
239 -
240 -this.__printTree();
241 - };
242 -
243 - // TODO: Unmounting removes id-to-element before crawling, which breaks it.
244 - // Should I just ditch the idea of a WeakMap in favor of an explicit it-to-metadata mapping like with parents?
245 - onBridgeUnmounted = (id: string) => {
246 - debug('onBridgeUnmounted()', id);
247 - this._pendingDeletions.add(id);
240
249 - const index = this.roots.indexOf(id);
250 - if (index >= 0) {
251 - this.roots = this.roots
252 - .slice(0, index)
253 - .concat(this.roots.slice(index + 1));
241 + this._numElements += weightDelta;
242
255 - const root = this._idToElement.get(id);
256 - const {weight} = this._elementToElementTreeMetadata.get(root);
257 -
258 - this.numElements -= weight;
243 + while (parentElement != null) {
244 + parentElement.weight += weightDelta;
245 + parentElement = ((this._idToElement.get(
246 + parentElement.parentID
247 + ): any): Element);
248 + }
249 + }
250
251 + if (haveRootsChanged) {
252 this.emit('roots');
253 }
254 };
255
264 - onBridgeUpdated = (element: Element) => {
265 - const { id } = element;
266 - debug('onBridgeUpdated()', element);
256 + onBridgeRootCommitted = (rootID: number) => {
257 + debug('onBridgeRootCommitted', rootID);
258
268 - const prevElement = ((this._idToElement.get(id): any): Element);
269 - const prevElementTreeMetadata = ((this._elementToElementTreeMetadata.get(prevElement): any): ElementTreeMetadata);
259 + this.emit('rootCommitted');
260
271 - this._idToElement.set(id, element);
272 - this._elementToElementTreeMetadata.set(
273 - element,
274 - prevElementTreeMetadata
275 - );
276 -
277 - // Update tree metadata used for windowing.
278 - this._updateElementTreeMetadata(prevElement, element);
261 + // this.__printTree(rootID);
262 + };
263
280 - this.emit(id);
264 + // DEBUG
265 + __printTree = (rootID: number) => {
266 + const printElement = (id: number) => {
267 + const element = ((this._idToElement.get(id): any): Element);
268 + console.log(
269 + `${' '.repeat(element.depth)}${element.id}:${element.displayName ||
270 + ''}${element.key ? `key:"${element.key}"` : ''} (${element.weight})`
271 + );
272 + element.children.forEach(printElement);
273 + };
274 + const root = ((this._idToElement.get(rootID): any): Element);
275 + console.log('printing root:', rootID);
276 + root.children.forEach(printElement);
277 };
278 }
283 -
284 -export default Store;
src/devtools/types.js
+13 -15
@@ -1,6 +1,6 @@
1 // @flow
2
3 -import typeof Store from './Store';
3 +import Store from './Store';
4
5 export const ElementTypeClassOrFunction = 1;
6 export const ElementTypeContext = 2;
@@ -8,28 +8,26 @@ export const ElementTypeForwardRef = 3;
8 export const ElementTypeMemo = 4;
9 export const ElementTypeOtherOrUnknown = 5;
10 export const ElementTypeProfiler = 6;
11 -export const ElementTypeSuspense = 7;
11 +export const ElementTypeRoot = 7;
12 +export const ElementTypeSuspense = 8;
13
13 -export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7;
14 +export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
15
16 // TODO: Add profiling node
17
18 export type Element = {|
18 - id: string,
19 - type: ElementType,
20 - key: React$Key | null,
21 - displayName: string | null,
22 - children: Array<string>,
23 -|};
24 -
25 -export type ElementTreeMetadata = {|
19 + children: Array<number>,
20 depth: number,
27 - rootID: string,
21 + displayName: string | null,
22 + id: number,
23 + key: number | string | null,
24 + parentID: number,
25 + type: ElementType,
26 weight: number,
27 |};
28
29 export type InspectedElement = {|
32 - id: string,
30 + id: number,
31 context: Object | null,
32 hooks: Object | null,
33 props: Object | null,
@@ -38,7 +36,7 @@ export type InspectedElement = {|
36 source: Object,
37 |};
38
41 -export type TreeContext = {|
39 +export type TreeMetadataType = {|
40 size: number,
41 store: Store,
44 -|};
\ No newline at end of file
42 +|};
src/devtools/views/Element.js
+5 -8
@@ -5,13 +5,13 @@ import { TreeContext } from './contexts';
5
6 import styles from './Element.css';
7
8 -type Props = {|
8 +type Props = {
9 index: number,
10 style: Object,
11 -|};
11 +};
12
13 export default function Element({ index, style }: Props) {
14 - const {store} = useContext(TreeContext);
14 + const { store } = useContext(TreeContext);
15 const element = store.getElementAtIndex(index);
16
17 // DevTools are rendered in concurrent mode.
@@ -22,10 +22,7 @@ export default function Element({ index, style }: Props) {
22 return null;
23 }
24
25 - const elementTreeMetadata = store.getTreeMetadataForElement(element);
26 -
27 - const { children, displayName, key } = element;
28 - const { depth } = elementTreeMetadata;
25 + const { children, depth, displayName, key } = element;
26
27 // TODO: Add state for toggling element open/close
28
@@ -34,7 +31,7 @@ export default function Element({ index, style }: Props) {
31 className={styles.Element}
32 style={{
33 ...style,
37 - paddingLeft: `${1 + depth}rem`
34 + paddingLeft: `${1 + depth}rem`,
35 }}
36 >
37 {children.length > 0 && <span className={styles.ArrowOpen} />}
src/devtools/views/Elements.js
+3 -3
@@ -15,12 +15,12 @@ export type Props = {|
15 |};
16
17 export default function Elements({ bridge, browserName, themeName }: Props) {
18 - const store = useMemo(() => new Store(bridge), []);
18 + const store = useMemo<Store>(() => new Store(bridge), []);
19
20 - const [treeContext, setTreeContext] = useState(({
20 + const [treeContext, setTreeContext] = useState({
21 size: store.numElements,
22 store,
23 - }));
23 + });
24
25 useLayoutEffect(() => {
26 const handler = () => {
src/devtools/views/contexts.js
+4 -2
@@ -2,10 +2,12 @@
2
3 import { createContext } from 'react';
4
5 -import type { TreeMetadata } from '../types';
5 +import type { TreeMetadataType } from '../types';
6
7 import Store from '../store';
8
9 export const RootsContext = createContext<Array<string>>([]);
10 export const StoreContext = createContext<Store>(((null: any): Store));
11 -export const TreeContext = createContext<TreeMetadata>(((null: any): TreeMetadata));
\ No newline at end of file
11 +export const TreeContext = createContext<TreeMetadataType>(
12 + ((null: any): TreeMetadataType)
13 +);
src/devtools/views/hooks.js
+6 -6
@@ -2,14 +2,14 @@
2
3 import { useLayoutEffect, useState } from 'react';
4
5 -import type { Element, ElementTreeMetadata } from '../types';
5 +import type { Element } from '../types';
6 import Store from '../store';
7
8 // TODO useEffect has a bug where sometimes subscriptions don't get cleaned up correctly.
9 // Potentially related to github.com/facebookincubator/redux-react-hook/issues/17
10 // As a temporary work around, switch back to layout effect.
11
12 -export function useElement(store: Store, id: string): ?Element {
12 +export function useElement(store: Store, id: number): ?Element {
13 const [element, setElement] = useState<?Element>(store.getElementByID(id));
14
15 useLayoutEffect(() => {
@@ -17,7 +17,7 @@ export function useElement(store: Store, id: string): ?Element {
17 setElement(((store.getElementByID(id): any): Element));
18
19 // Listen for changes to the element.
20 - store.addListener(id, handler);
20 + store.addListener((id: any), handler);
21
22 // Check for changes that may have happened between render and mount.
23 const newElement = store.getElementByID(id);
@@ -26,14 +26,14 @@ export function useElement(store: Store, id: string): ?Element {
26 }
27
28 // Remove event listener on unmount.
29 - return () => store.removeListener(id, handler);
29 + return () => store.removeListener((id: any), handler);
30 }, [store, id]);
31
32 return element;
33 }
34
35 -export function useRoots(store: Store): $ReadOnlyArray<string> {
36 - const [roots, setRoots] = useState<$ReadOnlyArray<string>>(store.roots);
35 +export function useRoots(store: Store): $ReadOnlyArray<number> {
36 + const [roots, setRoots] = useState<$ReadOnlyArray<number>>(store.roots);
37
38 useLayoutEffect(() => {
39 const handler = () => setRoots(store.roots);
src/utils.js new
+19
@@ -0,0 +1,19 @@
1 +// @flow
2 +
3 +export function utfDecodeString(array: Uint8Array): string {
4 + let string = '';
5 + const { length } = array;
6 + for (let i = 0; i < length; i++) {
7 + string += String.fromCharCode(array[i]);
8 + }
9 + return string;
10 +}
11 +
12 +export function utfEncodeString(string: string): Uint8Array {
13 + const array = new Uint8Array(string.length);
14 + const { length } = string;
15 + for (let i = 0; i < length; i++) {
16 + array[i] = string.charCodeAt(i);
17 + }
18 + return array;
19 +}
yarn.lock
+4 -4
@@ -8422,10 +8422,10 @@ react-virtualized-auto-sizer@^1.0.2:
8422 resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd"
8423 integrity sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg==
8424
8425 -react-window@^1.5.0:
8426 - version "1.5.0"
8427 - resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.5.0.tgz#3e56b720b97666bce38a9e932bdd238d56e258f1"
8428 - integrity sha512-55WeZKjMNF5JdCuKghc/H65DBecoeGgH8MOX3CgT7BJ66xb4ITRuXPUlz0qU6r50wetdF/oLhorYBRvKD4Z1IQ==
8425 +react-window@^1.5.1:
8426 + version "1.5.1"
8427 + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.5.1.tgz#9d68624d5ba58ddf331321fb74b0a75184523d16"
8428 + integrity sha512-D855yW104ek8+gSLW8G6Dt69WnhSlGXR4Ld+IoZW4aQg6a1pt7cZLb+fcOugVDVgl4RfrLSfg9aLBnvS7zsOaw==
8429 dependencies:
8430 "@babel/runtime" "^7.0.0"
8431 memoize-one "^3.1.1"