Transfer strings in a string table
Dan Abramov committed
Apr 25, 2019 at 17:11 UTC
c8f8943c9ad60a5416cf1a5c57f9c0e966a49b4e
4 files changed
+116
-69
OVERVIEW.md
+34
-9
@@ -14,10 +14,39 @@ The old DevTools also rendered the entire application tree in the form of a larg
14
15
Every React commit that changes the tree in a way DevTools cares about results in an "_operations_" message being sent across the bridge. These messages are lightweight patches that describe the changes that were made. (We don't resend the full tree structure like in legacy DevTools.)
16
17
-The payload for each message is a typed array. The first two entries are numbers that identify which renderer and root the update belongs to (for multi-root support). The rest of the array depends on the operations being made to the tree.
17
+The payload for each message is a typed array. The first two entries are numbers that identify which renderer and root the update belongs to (for multi-root support). Then the strings are encoded in a string table. The rest of the array depends on the operations being made to the tree.
18
19
No updates are required for many commits because we only send the following bits of information: element type, id, parent id, owner id, name, and key. Additional information (e.g. props, state) requires a separate "_inspectElement_" message.
20
21
+#### String table
22
+
23
+The string table is encoded right after the first two numbers.
24
+
25
+It consists of:
26
+
27
+1. the total length of next items that belong to string table
28
+2. for each string in a table:
29
+ 1. encoded size
30
+ 2. a list of its codepoints
31
+
32
+For example, for `Foo` and `Bar` we would see:
33
+
34
+```
35
+[
36
+ 8, // string table length
37
+ 3, // encoded display name size
38
+ 70, // "F"
39
+ 111, // "o"
40
+ 111, // "o"
41
+ 3, // encoded display name size
42
+ 66, // "B"
43
+ 97, // "a"
44
+ 114, // "r"
45
+]
46
+```
47
+
48
+Later operations will reference strings by a one-based index. For example, `1` would mean `"Foo"`, and `2` would mean `"Bar"`. The `0` string id always represents `null` and isn't explicitly encoded in the table.
49
+
50
#### Adding a root node
51
52
Adding a root to the tree requires sending 4 numbers:
@@ -46,10 +75,8 @@ Adding a leaf node takes a variable number of numbers since we need to decode th
75
1. element type constant (e.g. `1 === ElementTypeClass`)
76
1. parent fiber id
77
1. owner fiber id
49
-1. UTF encoded display name size
50
- * (followed by this number of encoded values)
51
-1. UTF encoded key size
52
- * (followed by this number of encoded values)
78
+1. string table id for `displayName`
79
+1. string table id for `key`
80
81
For example, adding a function component `<Foo>` with an id 2:
82
```js
@@ -60,10 +87,8 @@ For example, adding a function component `<Foo>` with an id 2:
87
1, // parent id
88
0, // owner id
89
3, // encoded display name size
63
- 70, // "F"
64
- 111, // "o"
65
- 111, // "o"
66
- 0, // encoded key (null)
90
+ 1, // id of "Foo" displayName in the string table
91
+ 0, // id of null key in the string table (always zero for null)
92
]
93
```
94
src/backend/renderer.js
+40
-30
@@ -590,6 +590,9 @@ export function attach(
590
let pendingRealUnmountedIDs: Array<number> = [];
591
let pendingSimulatedUnmountedIDs: Array<number> = [];
592
let pendingOperationsQueue: Array<Uint32Array> | null = [];
593
+ let pendingStringTable: Map<string, number> = new Map();
594
+ let pendingStringTableLength = 0;
595
+ let pendingStringTableCounter = 0;
596
597
function pushOperation(op: number): void {
598
if (__DEV__) {
@@ -620,6 +623,10 @@ export function attach(
623
const ops = new Uint32Array(
624
// Identify which renderer this update is coming from.
625
2 + // [rendererID, rootFiberID]
626
+ // How big is the string table?
627
+ 1 + // [stringTableLength]
628
+ // Then goes the actual string table.
629
+ pendingStringTableLength +
630
// All unmounts are batched in a single message.
631
2 + // [TREE_OPERATION_REMOVE, removedIDLength]
632
pendingRealUnmountedIDs.length +
@@ -635,6 +642,15 @@ export function attach(
642
ops[i++] = rendererID;
643
ops[i++] = getFiberID(getPrimaryFiber(root.current));
644
645
+ // Now fill in the string table.
646
+ // [stringTableLength, str1Length, ...str1, str2Length, ...str2, ...]
647
+ ops[i++] = pendingStringTableLength;
648
+ pendingStringTable.forEach((value, key) => {
649
+ ops[i++] = key.length;
650
+ ops.set(utfEncodeString(key), i);
651
+ i += key.length;
652
+ });
653
+
654
// All unmounts except roots are batched in a single message.
655
ops[i++] = TREE_OPERATION_REMOVE;
656
// The first number is how many unmounted IDs we're gonna send.
@@ -672,6 +688,26 @@ export function attach(
688
pendingOperations.length = 0;
689
pendingRealUnmountedIDs.length = 0;
690
pendingSimulatedUnmountedIDs.length = 0;
691
+ pendingStringTable.clear();
692
+ pendingStringTableLength = 0;
693
+ pendingStringTableCounter = 0;
694
+ }
695
+
696
+ function getStringID(str: string | null): number {
697
+ if (str === null) {
698
+ return 0;
699
+ }
700
+ const existingID = pendingStringTable.get(str);
701
+ if (existingID !== undefined) {
702
+ return existingID;
703
+ }
704
+ let id = ++pendingStringTableCounter;
705
+ pendingStringTable.set(str, id);
706
+ // The string table total length needs to account
707
+ // both for the string length, and for the array item
708
+ // that contains the length itself. Hence + 1.
709
+ pendingStringTableLength += str.length + 1;
710
+ return id;
711
}
712
713
function recordMount(fiber: Fiber, parentFiber: Fiber | null) {
@@ -700,41 +736,15 @@ export function attach(
736
_debugOwner != null ? getFiberID(getPrimaryFiber(_debugOwner)) : 0;
737
const parentID = getFiberID(getPrimaryFiber(parentFiber));
738
703
- let encodedDisplayName = ((null: any): Uint8Array);
704
- let encodedKey = ((null: any): Uint8Array);
705
-
706
- if (displayName !== null) {
707
- encodedDisplayName = utfEncodeString(displayName);
708
- }
709
-
710
- if (key !== null) {
711
- // React$Key supports string and number types as inputs,
712
- // But React converts numeric keys to strings, so we only have to handle that type here.
713
- // https://github.com/facebook/react/blob/0e67969cb1ad8c27a72294662e68fa5d7c2c9783/packages/react/src/ReactElement.js#L187
714
- encodedKey = utfEncodeString(((key: any): string));
715
- }
716
-
717
- const encodedDisplayNameSize =
718
- displayName === null ? 0 : encodedDisplayName.length;
719
- const encodedKeySize = key === null ? 0 : encodedKey.length;
720
-
739
+ let displayNameStringID = getStringID(displayName);
740
+ let keyStringID = getStringID(key);
741
pushOperation(TREE_OPERATION_ADD);
742
pushOperation(id);
743
pushOperation(type);
744
pushOperation(parentID);
745
pushOperation(ownerID);
726
- pushOperation(encodedDisplayNameSize);
727
- if (displayName !== null) {
728
- for (let i = 0; i < encodedDisplayName.length; i++) {
729
- pushOperation(encodedDisplayName[i]);
730
- }
731
- }
732
- pushOperation(encodedKeySize);
733
- if (key !== null) {
734
- for (let i = 0; i < encodedKey.length; i++) {
735
- pushOperation(encodedKey[i]);
736
- }
737
- }
746
+ pushOperation(displayNameStringID);
747
+ pushOperation(keyStringID);
748
}
749
750
if (isProfiling) {
src/devtools/store.js
+21
-15
@@ -630,6 +630,22 @@ export default class Store extends EventEmitter {
630
// We'll use the parent ID to adjust selection if it gets deleted.
631
632
let i = 2;
633
+
634
+ // Reassemble the string table.
635
+ let stringTable = [
636
+ null, // ID = 0 corresponds to the null string.
637
+ ];
638
+ const stringTableSize = operations[i++];
639
+ const stringTableEnd = i + stringTableSize;
640
+ while (i < stringTableEnd) {
641
+ let nextLength = operations[i++];
642
+ let nextString = utfDecodeString(
643
+ (operations.slice(i, i + nextLength): any)
644
+ );
645
+ stringTable.push(nextString);
646
+ i += nextLength;
647
+ }
648
+
649
while (i < operations.length) {
650
const operation = operations[i];
651
switch (operation) {
@@ -686,23 +702,13 @@ export default class Store extends EventEmitter {
702
ownerID = ((operations[i]: any): number);
703
i++;
704
689
- const displayNameLength = operations[i];
705
+ const displayNameStringID = operations[i];
706
+ const displayName = stringTable[displayNameStringID];
707
i++;
691
- const displayName =
692
- displayNameLength === 0
693
- ? null
694
- : utfDecodeString(
695
- (operations.slice(i, i + displayNameLength): any)
696
- );
697
- i += displayNameLength;
698
-
699
- const keyLength = operations[i];
708
+
709
+ const keyStringID = operations[i];
710
+ const key = stringTable[keyStringID];
711
i++;
701
- const key =
702
- keyLength === 0
703
- ? null
704
- : utfDecodeString((operations.slice(i, i + keyLength): any));
705
- i += +keyLength;
712
713
if (__DEBUG__) {
714
debug(
src/devtools/views/Profiler/CommitTreeBuilder.js
+21
-15
@@ -167,6 +167,22 @@ function updateTree(
167
};
168
169
let i = 2;
170
+
171
+ // Reassemble the string table.
172
+ let stringTable = [
173
+ null, // ID = 0 corresponds to the null string.
174
+ ];
175
+ const stringTableSize = operations[i++];
176
+ const stringTableEnd = i + stringTableSize;
177
+ while (i < stringTableEnd) {
178
+ let nextLength = operations[i++];
179
+ let nextString = utfDecodeString(
180
+ (operations.slice(i, i + nextLength): any)
181
+ );
182
+ stringTable.push(nextString);
183
+ i += nextLength;
184
+ }
185
+
186
while (i < operations.length) {
187
const operation = operations[i];
188
@@ -209,23 +225,13 @@ function updateTree(
225
226
i++; // ownerID
227
212
- const displayNameLength = operations[i];
228
+ const displayNameStringID = operations[i];
229
+ const displayName = stringTable[displayNameStringID];
230
i++;
214
- const displayName =
215
- displayNameLength === 0
216
- ? null
217
- : utfDecodeString(
218
- (operations.slice(i, i + displayNameLength): any)
219
- );
220
- i += displayNameLength;
221
-
222
- const keyLength = operations[i];
231
+
232
+ const keyStringID = operations[i];
233
+ const key = stringTable[keyStringID];
234
i++;
224
- const key =
225
- keyLength === 0
226
- ? null
227
- : utfDecodeString((operations.slice(i, i + keyLength): any));
228
- i += +keyLength;
235
236
if (__DEBUG__) {
237
debug(