From c8f8943c9ad60a5416cf1a5c57f9c0e966a49b4e Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 25 Apr 2019 17:11:24 +0100 Subject: [PATCH 1/4] Transfer strings in a string table --- OVERVIEW.md | 43 +++++++++--- src/backend/renderer.js | 70 +++++++++++-------- src/devtools/store.js | 34 +++++---- .../views/Profiler/CommitTreeBuilder.js | 34 +++++---- 4 files changed, 114 insertions(+), 67 deletions(-) diff --git a/OVERVIEW.md b/OVERVIEW.md index af1535faa8..f151512ee9 100644 --- a/OVERVIEW.md +++ b/OVERVIEW.md @@ -14,10 +14,39 @@ The old DevTools also rendered the entire application tree in the form of a larg 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.) -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. +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. 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. +#### String table + +The string table is encoded right after the first two numbers. + +It consists of: + +1. the total length of next items that belong to string table +2. for each string in a table: + 1. encoded size + 2. a list of its codepoints + +For example, for `Foo` and `Bar` we would see: + +``` +[ + 8, // string table length + 3, // encoded display name size + 70, // "F" + 111, // "o" + 111, // "o" + 3, // encoded display name size + 66, // "B" + 97, // "a" + 114, // "r" +] +``` + +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. + #### Adding a root node 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 1. element type constant (e.g. `1 === ElementTypeClass`) 1. parent fiber id 1. owner fiber id -1. UTF encoded display name size - * (followed by this number of encoded values) -1. UTF encoded key size - * (followed by this number of encoded values) +1. string table id for `displayName` +1. string table id for `key` For example, adding a function component `` with an id 2: ```js @@ -60,10 +87,8 @@ For example, adding a function component `` with an id 2: 1, // parent id 0, // owner id 3, // encoded display name size - 70, // "F" - 111, // "o" - 111, // "o" - 0, // encoded key (null) + 1, // id of "Foo" displayName in the string table + 0, // id of null key in the string table (always zero for null) ] ``` diff --git a/src/backend/renderer.js b/src/backend/renderer.js index 07be08f9d1..89facf5165 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -590,6 +590,9 @@ export function attach( let pendingRealUnmountedIDs: Array = []; let pendingSimulatedUnmountedIDs: Array = []; let pendingOperationsQueue: Array | null = []; + let pendingStringTable: Map = new Map(); + let pendingStringTableLength = 0; + let pendingStringTableCounter = 0; function pushOperation(op: number): void { if (__DEV__) { @@ -620,6 +623,10 @@ export function attach( const ops = new Uint32Array( // Identify which renderer this update is coming from. 2 + // [rendererID, rootFiberID] + // How big is the string table? + 1 + // [stringTableLength] + // Then goes the actual string table. + pendingStringTableLength + // All unmounts are batched in a single message. 2 + // [TREE_OPERATION_REMOVE, removedIDLength] pendingRealUnmountedIDs.length + @@ -635,6 +642,15 @@ export function attach( ops[i++] = rendererID; ops[i++] = getFiberID(getPrimaryFiber(root.current)); + // Now fill in the string table. + // [stringTableLength, str1Length, ...str1, str2Length, ...str2, ...] + ops[i++] = pendingStringTableLength; + pendingStringTable.forEach((value, key) => { + ops[i++] = key.length; + ops.set(utfEncodeString(key), i); + i += key.length; + }); + // All unmounts except roots are batched in a single message. ops[i++] = TREE_OPERATION_REMOVE; // The first number is how many unmounted IDs we're gonna send. @@ -672,6 +688,26 @@ export function attach( pendingOperations.length = 0; pendingRealUnmountedIDs.length = 0; pendingSimulatedUnmountedIDs.length = 0; + pendingStringTable.clear(); + pendingStringTableLength = 0; + pendingStringTableCounter = 0; + } + + function getStringID(str: string | null): number { + if (str === null) { + return 0; + } + const existingID = pendingStringTable.get(str); + if (existingID !== undefined) { + return existingID; + } + let id = ++pendingStringTableCounter; + pendingStringTable.set(str, id); + // The string table total length needs to account + // both for the string length, and for the array item + // that contains the length itself. Hence + 1. + pendingStringTableLength += str.length + 1; + return id; } function recordMount(fiber: Fiber, parentFiber: Fiber | null) { @@ -700,41 +736,15 @@ export function attach( _debugOwner != null ? getFiberID(getPrimaryFiber(_debugOwner)) : 0; const parentID = getFiberID(getPrimaryFiber(parentFiber)); - let encodedDisplayName = ((null: any): Uint8Array); - let encodedKey = ((null: any): Uint8Array); - - if (displayName !== null) { - encodedDisplayName = utfEncodeString(displayName); - } - - if (key !== null) { - // React$Key supports string and number types as inputs, - // But React converts numeric keys to strings, so we only have to handle that type here. - // https://github.com/facebook/react/blob/0e67969cb1ad8c27a72294662e68fa5d7c2c9783/packages/react/src/ReactElement.js#L187 - encodedKey = utfEncodeString(((key: any): string)); - } - - const encodedDisplayNameSize = - displayName === null ? 0 : encodedDisplayName.length; - const encodedKeySize = key === null ? 0 : encodedKey.length; - + let displayNameStringID = getStringID(displayName); + let keyStringID = getStringID(key); pushOperation(TREE_OPERATION_ADD); pushOperation(id); pushOperation(type); pushOperation(parentID); pushOperation(ownerID); - pushOperation(encodedDisplayNameSize); - if (displayName !== null) { - for (let i = 0; i < encodedDisplayName.length; i++) { - pushOperation(encodedDisplayName[i]); - } - } - pushOperation(encodedKeySize); - if (key !== null) { - for (let i = 0; i < encodedKey.length; i++) { - pushOperation(encodedKey[i]); - } - } + pushOperation(displayNameStringID); + pushOperation(keyStringID); } if (isProfiling) { diff --git a/src/devtools/store.js b/src/devtools/store.js index 130ba146de..85ece39dd9 100644 --- a/src/devtools/store.js +++ b/src/devtools/store.js @@ -630,6 +630,22 @@ export default class Store extends EventEmitter { // We'll use the parent ID to adjust selection if it gets deleted. let i = 2; + + // Reassemble the string table. + let stringTable = [ + null, // ID = 0 corresponds to the null string. + ]; + const stringTableSize = operations[i++]; + const stringTableEnd = i + stringTableSize; + while (i < stringTableEnd) { + let nextLength = operations[i++]; + let nextString = utfDecodeString( + (operations.slice(i, i + nextLength): any) + ); + stringTable.push(nextString); + i += nextLength; + } + while (i < operations.length) { const operation = operations[i]; switch (operation) { @@ -686,23 +702,13 @@ export default class Store extends EventEmitter { ownerID = ((operations[i]: any): number); i++; - const displayNameLength = operations[i]; + const displayNameStringID = operations[i]; + const displayName = stringTable[displayNameStringID]; i++; - const displayName = - displayNameLength === 0 - ? null - : utfDecodeString( - (operations.slice(i, i + displayNameLength): any) - ); - i += displayNameLength; - const keyLength = operations[i]; + const keyStringID = operations[i]; + const key = stringTable[keyStringID]; i++; - const key = - keyLength === 0 - ? null - : utfDecodeString((operations.slice(i, i + keyLength): any)); - i += +keyLength; if (__DEBUG__) { debug( diff --git a/src/devtools/views/Profiler/CommitTreeBuilder.js b/src/devtools/views/Profiler/CommitTreeBuilder.js index a91c7f0ab3..4e02f912fe 100644 --- a/src/devtools/views/Profiler/CommitTreeBuilder.js +++ b/src/devtools/views/Profiler/CommitTreeBuilder.js @@ -167,6 +167,22 @@ function updateTree( }; let i = 2; + + // Reassemble the string table. + let stringTable = [ + null, // ID = 0 corresponds to the null string. + ]; + const stringTableSize = operations[i++]; + const stringTableEnd = i + stringTableSize; + while (i < stringTableEnd) { + let nextLength = operations[i++]; + let nextString = utfDecodeString( + (operations.slice(i, i + nextLength): any) + ); + stringTable.push(nextString); + i += nextLength; + } + while (i < operations.length) { const operation = operations[i]; @@ -209,23 +225,13 @@ function updateTree( i++; // ownerID - const displayNameLength = operations[i]; + const displayNameStringID = operations[i]; + const displayName = stringTable[displayNameStringID]; i++; - const displayName = - displayNameLength === 0 - ? null - : utfDecodeString( - (operations.slice(i, i + displayNameLength): any) - ); - i += displayNameLength; - const keyLength = operations[i]; + const keyStringID = operations[i]; + const key = stringTable[keyStringID]; i++; - const key = - keyLength === 0 - ? null - : utfDecodeString((operations.slice(i, i + keyLength): any)); - i += +keyLength; if (__DEBUG__) { debug( From 3920ebd0a108144453e6d19f2d4f799201d3c9a1 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 25 Apr 2019 17:32:46 +0100 Subject: [PATCH 2/4] Clarify encoding in overview --- OVERVIEW.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OVERVIEW.md b/OVERVIEW.md index f151512ee9..05069783d8 100644 --- a/OVERVIEW.md +++ b/OVERVIEW.md @@ -27,7 +27,7 @@ It consists of: 1. the total length of next items that belong to string table 2. for each string in a table: 1. encoded size - 2. a list of its codepoints + 2. a list of its UTF encoded codepoints For example, for `Foo` and `Bar` we would see: @@ -383,4 +383,4 @@ Here is an example of a profiling session consisting of two interactions: } ``` -The backend does not need to resend the timestamp for each of the commits because that was already sent as part of the "_profilingSummary_" message. \ No newline at end of file +The backend does not need to resend the timestamp for each of the commits because that was already sent as part of the "_profilingSummary_" message. From af2d1374c4737b2e241528854650d1b4a0f4ce50 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 25 Apr 2019 17:38:55 +0100 Subject: [PATCH 3/4] Let -> const --- src/devtools/store.js | 6 +++--- src/devtools/views/Profiler/CommitTreeBuilder.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/devtools/store.js b/src/devtools/store.js index 85ece39dd9..60a99ed4b2 100644 --- a/src/devtools/store.js +++ b/src/devtools/store.js @@ -632,14 +632,14 @@ export default class Store extends EventEmitter { let i = 2; // Reassemble the string table. - let stringTable = [ + const stringTable = [ null, // ID = 0 corresponds to the null string. ]; const stringTableSize = operations[i++]; const stringTableEnd = i + stringTableSize; while (i < stringTableEnd) { - let nextLength = operations[i++]; - let nextString = utfDecodeString( + const nextLength = operations[i++]; + const nextString = utfDecodeString( (operations.slice(i, i + nextLength): any) ); stringTable.push(nextString); diff --git a/src/devtools/views/Profiler/CommitTreeBuilder.js b/src/devtools/views/Profiler/CommitTreeBuilder.js index 4e02f912fe..09bde587b5 100644 --- a/src/devtools/views/Profiler/CommitTreeBuilder.js +++ b/src/devtools/views/Profiler/CommitTreeBuilder.js @@ -169,14 +169,14 @@ function updateTree( let i = 2; // Reassemble the string table. - let stringTable = [ + const stringTable = [ null, // ID = 0 corresponds to the null string. ]; const stringTableSize = operations[i++]; const stringTableEnd = i + stringTableSize; while (i < stringTableEnd) { - let nextLength = operations[i++]; - let nextString = utfDecodeString( + const nextLength = operations[i++]; + const nextString = utfDecodeString( (operations.slice(i, i + nextLength): any) ); stringTable.push(nextString); From 1bea469db0bdedd99fcc7257aa41f794c3fa4093 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 25 Apr 2019 17:46:48 +0100 Subject: [PATCH 4/4] Remove unneeded variable --- src/backend/renderer.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/backend/renderer.js b/src/backend/renderer.js index 89facf5165..c4e515829b 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -592,7 +592,6 @@ export function attach( let pendingOperationsQueue: Array | null = []; let pendingStringTable: Map = new Map(); let pendingStringTableLength = 0; - let pendingStringTableCounter = 0; function pushOperation(op: number): void { if (__DEV__) { @@ -690,7 +689,6 @@ export function attach( pendingSimulatedUnmountedIDs.length = 0; pendingStringTable.clear(); pendingStringTableLength = 0; - pendingStringTableCounter = 0; } function getStringID(str: string | null): number { @@ -701,7 +699,7 @@ export function attach( if (existingID !== undefined) { return existingID; } - let id = ++pendingStringTableCounter; + const id = pendingStringTable.size + 1; pendingStringTable.set(str, id); // The string table total length needs to account // both for the string length, and for the array item