refactor[react-devtools-shared]: minor parsing improvements and modifications (#27661)
Had these stashed for some time, it includes: - Some refactoring to remove unnecessary `FlowFixMe`s and type castings via `any`. - Optimized version of parsing component names. We encode string names to utf8 and then pass it serialized from backend to frontend in a single array of numbers. Previously we would call `slice` to get the corresponding encoded string as a subarray and then parse each character. New implementation skips `slice` step and just receives `left` and `right` ranges for the string to parse. - Early `break` instead of `continue` when Store receives unexpected operation, like removing an element from the Store, which is not registered yet.
Ruslan Lesiutin committed
Nov 7, 2023 at 16:39 UTC
c897260cffb6a237d5ad707a6043f68ddf9ab014
6 files changed
+106
-65
packages/react-devtools-shared/src/backend/renderer.js
-1
@@ -439,7 +439,6 @@ export function getInternalReactConstants(version: string): {
439
return 'Cache';
440
case ClassComponent:
441
case IncompleteClassComponent:
442
- return getDisplayName(resolvedType);
442
case FunctionComponent:
443
case IndeterminateComponent:
444
return getDisplayName(resolvedType);
packages/react-devtools-shared/src/devtools/store.js
+80
-39
@@ -27,7 +27,7 @@ import {
27
setSavedComponentFilters,
28
separateDisplayNameAndHOCs,
29
shallowDiffers,
30
- utfDecodeString,
30
+ utfDecodeStringWithRanges,
31
} from '../utils';
32
import {localStorageGetItem, localStorageSetItem} from '../storage';
33
import {__DEBUG__} from '../constants';
@@ -450,7 +450,7 @@ export default class Store extends EventEmitter<{
450
}
451
452
// This build of DevTools supports the legacy profiler.
453
- // This is a static flag, controled by the Store config.
453
+ // This is a static flag, controlled by the Store config.
454
get supportsProfiling(): boolean {
455
return this._supportsProfiling;
456
}
@@ -467,7 +467,7 @@ export default class Store extends EventEmitter<{
467
}
468
469
// This build of DevTools supports the Timeline profiler.
470
- // This is a static flag, controled by the Store config.
470
+ // This is a static flag, controlled by the Store config.
471
get supportsTimeline(): boolean {
472
return this._supportsTimeline;
473
}
@@ -502,30 +502,58 @@ export default class Store extends EventEmitter<{
502
}
503
504
// Find which root this element is in...
505
- let rootID;
505
let root;
506
let rootWeight = 0;
507
for (let i = 0; i < this._roots.length; i++) {
509
- rootID = this._roots[i];
510
- root = ((this._idToElement.get(rootID): any): Element);
508
+ const rootID = this._roots[i];
509
+ root = this._idToElement.get(rootID);
510
+
511
+ if (root === undefined) {
512
+ this._throwAndEmitError(
513
+ Error(
514
+ `Couldn't find root with id "${rootID}": no matching node was found in the Store.`,
515
+ ),
516
+ );
517
+
518
+ return null;
519
+ }
520
+
521
if (root.children.length === 0) {
522
continue;
513
- } else if (rootWeight + root.weight > index) {
523
+ }
524
+
525
+ if (rootWeight + root.weight > index) {
526
break;
527
} else {
528
rootWeight += root.weight;
529
}
530
}
531
532
+ if (root === undefined) {
533
+ return null;
534
+ }
535
+
536
// Find the element in the tree using the weight of each node...
537
// Skip over the root itself, because roots aren't visible in the Elements tree.
522
- let currentElement = ((root: any): Element);
538
+ let currentElement: Element = root;
539
let currentWeight = rootWeight - 1;
540
+
541
while (index !== currentWeight) {
542
const numChildren = currentElement.children.length;
543
for (let i = 0; i < numChildren; i++) {
544
const childID = currentElement.children[i];
528
- const child = ((this._idToElement.get(childID): any): Element);
545
+ const child = this._idToElement.get(childID);
546
+
547
+ if (child === undefined) {
548
+ this._throwAndEmitError(
549
+ Error(
550
+ `Couldn't child element with id "${childID}": no matching node was found in the Store.`,
551
+ ),
552
+ );
553
+
554
+ return null;
555
+ }
556
+
557
const childWeight = child.isCollapsed ? 1 : child.weight;
558
559
if (index <= currentWeight + childWeight) {
@@ -538,7 +566,7 @@ export default class Store extends EventEmitter<{
566
}
567
}
568
541
- return ((currentElement: any): Element) || null;
569
+ return currentElement || null;
570
}
571
572
getElementIDAtIndex(index: number): number | null {
@@ -560,32 +588,31 @@ export default class Store extends EventEmitter<{
588
getElementsWithErrorsAndWarnings(): Array<{id: number, index: number}> {
589
if (this._cachedErrorAndWarningTuples !== null) {
590
return this._cachedErrorAndWarningTuples;
563
- } else {
564
- const errorAndWarningTuples: ErrorAndWarningTuples = [];
565
-
566
- this._errorsAndWarnings.forEach((_, id) => {
567
- const index = this.getIndexOfElementID(id);
568
- if (index !== null) {
569
- let low = 0;
570
- let high = errorAndWarningTuples.length;
571
- while (low < high) {
572
- const mid = (low + high) >> 1;
573
- if (errorAndWarningTuples[mid].index > index) {
574
- high = mid;
575
- } else {
576
- low = mid + 1;
577
- }
578
- }
591
+ }
592
580
- errorAndWarningTuples.splice(low, 0, {id, index});
593
+ const errorAndWarningTuples: ErrorAndWarningTuples = [];
594
+
595
+ this._errorsAndWarnings.forEach((_, id) => {
596
+ const index = this.getIndexOfElementID(id);
597
+ if (index !== null) {
598
+ let low = 0;
599
+ let high = errorAndWarningTuples.length;
600
+ while (low < high) {
601
+ const mid = (low + high) >> 1;
602
+ if (errorAndWarningTuples[mid].index > index) {
603
+ high = mid;
604
+ } else {
605
+ low = mid + 1;
606
+ }
607
}
582
- });
608
584
- // Cache for later (at least until the tree changes again).
585
- this._cachedErrorAndWarningTuples = errorAndWarningTuples;
609
+ errorAndWarningTuples.splice(low, 0, {id, index});
610
+ }
611
+ });
612
587
- return errorAndWarningTuples;
588
- }
613
+ // Cache for later (at least until the tree changes again).
614
+ this._cachedErrorAndWarningTuples = errorAndWarningTuples;
615
+ return errorAndWarningTuples;
616
}
617
618
getErrorAndWarningCountForElementID(id: number): {
@@ -923,7 +950,11 @@ export default class Store extends EventEmitter<{
950
const nextLength = operations[i];
951
i++;
952
926
- const nextString = utfDecodeString(operations.slice(i, i + nextLength));
953
+ const nextString = utfDecodeStringWithRanges(
954
+ operations,
955
+ i,
956
+ i + nextLength - 1,
957
+ );
958
stringTable.push(nextString);
959
i += nextLength;
960
}
@@ -1035,7 +1066,7 @@ export default class Store extends EventEmitter<{
1066
),
1067
);
1068
1038
- continue;
1069
+ break;
1070
}
1071
1072
parentElement.children.push(id);
@@ -1088,7 +1119,7 @@ export default class Store extends EventEmitter<{
1119
),
1120
);
1121
1091
- continue;
1122
+ break;
1123
}
1124
1125
i += 1;
@@ -1126,7 +1157,7 @@ export default class Store extends EventEmitter<{
1157
),
1158
);
1159
1129
- continue;
1160
+ break;
1161
}
1162
1163
const index = parentElement.children.indexOf(id);
@@ -1172,7 +1203,17 @@ export default class Store extends EventEmitter<{
1203
}
1204
};
1205
1175
- const root = ((this._idToElement.get(id): any): Element);
1206
+ const root = this._idToElement.get(id);
1207
+ if (root === undefined) {
1208
+ this._throwAndEmitError(
1209
+ Error(
1210
+ `Cannot remove root "${id}": no matching node was found in the Store.`,
1211
+ ),
1212
+ );
1213
+
1214
+ break;
1215
+ }
1216
+
1217
recursivelyDeleteElements(id);
1218
1219
this._rootIDToCapabilities.delete(id);
@@ -1194,7 +1235,7 @@ export default class Store extends EventEmitter<{
1235
),
1236
);
1237
1197
- continue;
1238
+ break;
1239
}
1240
1241
const children = element.children;
@@ -1279,7 +1320,7 @@ export default class Store extends EventEmitter<{
1320
1321
this._revision++;
1322
1282
- // Any time the tree changes (e.g. elements added, removed, or reordered) cached inidices may be invalid.
1323
+ // Any time the tree changes (e.g. elements added, removed, or reordered) cached indices may be invalid.
1324
this._cachedErrorAndWarningTuples = null;
1325
1326
if (haveErrorsOrWarningsChanged) {
packages/react-devtools-shared/src/devtools/views/Components/Element.js
+4
-5
@@ -122,7 +122,7 @@ export default function Element({data, index, style}: Props): React.Node {
122
isStrictModeNonCompliant,
123
key,
124
type,
125
- } = ((element: any): ElementType);
125
+ } = element;
126
127
// Only show strict mode non-compliance badges for top level elements.
128
// Showing an inline badge for every element in the tree would be noisy.
@@ -173,17 +173,16 @@ export default function Element({data, index, style}: Props): React.Node {
173
"
174
</Fragment>
175
)}
176
+
177
{hocDisplayNames !== null && hocDisplayNames.length > 0 ? (
178
<Badge
179
className={styles.Badge}
180
hocDisplayNames={hocDisplayNames}
181
type={type}>
181
- <DisplayName
182
- displayName={hocDisplayNames[0]}
183
- id={((id: any): number)}
184
- />
182
+ <DisplayName displayName={hocDisplayNames[0]} id={id} />
183
</Badge>
184
) : null}
185
+
186
{showInlineWarningsAndErrors && errorCount > 0 && (
187
<Icon
188
type="error"
packages/react-devtools-shared/src/devtools/views/Components/HocBadges.js
+5
-6
@@ -25,12 +25,11 @@ export default function HocBadges({element}: Props): React.Node {
25
26
return (
27
<div className={styles.HocBadges}>
28
- {hocDisplayNames !== null &&
29
- hocDisplayNames.map(hocDisplayName => (
30
- <div key={hocDisplayName} className={styles.Badge}>
31
- {hocDisplayName}
32
- </div>
33
- ))}
28
+ {hocDisplayNames.map(hocDisplayName => (
29
+ <div key={hocDisplayName} className={styles.Badge}>
30
+ {hocDisplayName}
31
+ </div>
32
+ ))}
33
</div>
34
);
35
}
packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js
+5
-3
@@ -17,7 +17,7 @@ import {
17
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
18
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
19
} from 'react-devtools-shared/src/constants';
20
-import {utfDecodeString} from 'react-devtools-shared/src/utils';
20
+import {utfDecodeStringWithRanges} from 'react-devtools-shared/src/utils';
21
import {ElementTypeRoot} from 'react-devtools-shared/src/frontend/types';
22
import ProfilerStore from 'react-devtools-shared/src/devtools/ProfilerStore';
23
@@ -170,8 +170,10 @@ function updateTree(
170
const stringTableEnd = i + stringTableSize;
171
while (i < stringTableEnd) {
172
const nextLength = operations[i++];
173
- const nextString = utfDecodeString(
174
- (operations.slice(i, i + nextLength): any),
173
+ const nextString = utfDecodeStringWithRanges(
174
+ operations,
175
+ i,
176
+ i + nextLength - 1,
177
);
178
stringTable.push(nextString);
179
i += nextLength;
packages/react-devtools-shared/src/utils.js
+12
-11
@@ -116,7 +116,7 @@ export function getWrappedDisplayName(
116
wrapperName: string,
117
fallbackName?: string,
118
): string {
119
- const displayName = (outerType: any).displayName;
119
+ const displayName = (outerType: any)?.displayName;
120
return (
121
displayName || `${wrapperName}(${getDisplayName(innerType, fallbackName)})`
122
);
@@ -152,15 +152,14 @@ export function getUID(): number {
152
return ++uidCounter;
153
}
154
155
-export function utfDecodeString(array: Array<number>): string {
156
- // Avoid spreading the array (e.g. String.fromCodePoint(...array))
157
- // Functions arguments are first placed on the stack before the function is called
158
- // which throws a RangeError for large arrays.
159
- // See github.com/facebook/react/issues/22293
155
+export function utfDecodeStringWithRanges(
156
+ array: Array<number>,
157
+ left: number,
158
+ right: number,
159
+): string {
160
let string = '';
161
- for (let i = 0; i < array.length; i++) {
162
- const char = array[i];
163
- string += String.fromCodePoint(char);
161
+ for (let i = left; i <= right; i++) {
162
+ string += String.fromCodePoint(array[i]);
163
}
164
return string;
165
}
@@ -216,8 +215,10 @@ export function printOperationsArray(operations: Array<number>) {
215
const stringTableEnd = i + stringTableSize;
216
while (i < stringTableEnd) {
217
const nextLength = operations[i++];
219
- const nextString = utfDecodeString(
220
- (operations.slice(i, i + nextLength): any),
218
+ const nextString = utfDecodeStringWithRanges(
219
+ operations,
220
+ i,
221
+ i + nextLength - 1,
222
);
223
stringTable.push(nextString);
224
i += nextLength;