diff --git a/OVERVIEW.md b/OVERVIEW.md index af1535faa8..05069783d8 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 UTF encoded 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) ] ``` @@ -358,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. diff --git a/src/backend/renderer.js b/src/backend/renderer.js index 07be08f9d1..c4e515829b 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -590,6 +590,8 @@ export function attach( let pendingRealUnmountedIDs: Array = []; let pendingSimulatedUnmountedIDs: Array = []; let pendingOperationsQueue: Array | null = []; + let pendingStringTable: Map = new Map(); + let pendingStringTableLength = 0; function pushOperation(op: number): void { if (__DEV__) { @@ -620,6 +622,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 +641,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 +687,25 @@ export function attach( pendingOperations.length = 0; pendingRealUnmountedIDs.length = 0; pendingSimulatedUnmountedIDs.length = 0; + pendingStringTable.clear(); + pendingStringTableLength = 0; + } + + function getStringID(str: string | null): number { + if (str === null) { + return 0; + } + const existingID = pendingStringTable.get(str); + if (existingID !== undefined) { + return existingID; + } + 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 + // that contains the length itself. Hence + 1. + pendingStringTableLength += str.length + 1; + return id; } function recordMount(fiber: Fiber, parentFiber: Fiber | null) { @@ -700,41 +734,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..60a99ed4b2 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. + const stringTable = [ + null, // ID = 0 corresponds to the null string. + ]; + const stringTableSize = operations[i++]; + const stringTableEnd = i + stringTableSize; + while (i < stringTableEnd) { + const nextLength = operations[i++]; + const 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..09bde587b5 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. + const stringTable = [ + null, // ID = 0 corresponds to the null string. + ]; + const stringTableSize = operations[i++]; + const stringTableEnd = i + stringTableSize; + while (i < stringTableEnd) { + const nextLength = operations[i++]; + const 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(