@samitouri / QOS-React-2 / commits / 4b5d1c937e

[DevTools] Fix printOperationsArray decode of applied activity slice change (#36935)

`printOperationsArray` in `react-devtools-shared` walks an operations array under the invariant that each `switch` case leaves `i` pointing at the next opcode (loop header at `packages/react-devtools-shared/src/utils.js`). The `TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE` case broke that invariant: ```js case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: { i++; // skip opcode -> i now at the value slot const activitySliceIDChange = operations[i + 1]; // reads the slot AFTER the value; i not advanced ... } ``` The operation is exactly two slots, `[opcode, activitySliceID]` (see the writer in `packages/react-devtools-shared/src/backend/fiber/renderer.js`, which pushes the opcode then the id). So the case did two things wrong: 1. It logged the wrong number: `operations[i + 1]` reads the slot *after* the value (the next operation's opcode, or `undefined` at the end of the array). 2. It left `i` pointing at the value slot, so the outer `while (i < operations.length)` loop re-read the activity-slice id as an opcode. For any non-zero slice id that falls through to `default: throw Error("Unsupported Bridge operation ...")`, aborting the whole dump. The two canonical decoders of this same operation both use the correct pattern (skip the opcode, then read *and* advance past the value): - `devtools/store.js`: `i++; nextActivitySliceID = operations[i++];` - `devtools/views/Profiler/CommitTreeBuilder.js`: `i++; const activitySliceIDChange = operations[i++];` This change makes `printOperationsArray` match them by reading `operations[i++]`. This is a debug-only diagnostic path: the only caller is the `__DEBUG__`-guarded dump in `backend/legacy/renderer.js`, so it is not a production crash. The bug was introduced in #34908. ## How did you test this change? Added a regression test for `printOperationsArray` in `packages/react-devtools-shared/src/__tests__/utils-test.js`. The fixture chains two activity-slice operations, `[rendererID, rootID, stringTableSize=0, opcode, 42, opcode, 0]`; the trailing operation is what forces the reader to advance past the first value slot rather than re-read it. It asserts the call does not throw, logs once, and that the message contains both `Applied activity slice change to 42` and `Reset applied activity slice`. Ran the DevTools Jest project (built first, as that project requires a build): - With the fix: 51/51 pass, including the new test. - Reverting only the one-line fix back to `operations[i + 1]` and rebuilding: the new test fails with `Unsupported Bridge operation "42"` (exactly the predicted failure), 50 pass / 1 fail. Restored the fix and it is green again. `yarn prettier-check` and `yarn linc` are clean on the changed files.

Anas Khan committed Jul 6, 2026 at 18:57 UTC 4b5d1c937e365ae4d4c27abd5e26f7467acb4fb0
2 files changed +39 -1
packages/react-devtools-shared/src/__tests__/utils-test.js
+38
@@ -11,7 +11,9 @@ import {
11 getDisplayName,
12 getDisplayNameForReactElement,
13 isPlainObject,
14 + printOperationsArray,
15 } from 'react-devtools-shared/src/utils';
16 +import {TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE} from 'react-devtools-shared/src/constants';
17 import {stackToComponentLocations} from 'react-devtools-shared/src/devtools/utils';
18 import {
19 formatConsoleArguments,
@@ -531,4 +533,40 @@ function f() { }
533 expect(formatConsoleArguments('%s %f', 'value')).toEqual(['value %f']);
534 });
535 });
536 +
537 + describe('printOperationsArray', () => {
538 + let log;
539 + beforeEach(() => {
540 + log = jest.spyOn(console, 'log').mockImplementation(() => {});
541 + });
542 + afterEach(() => {
543 + log.mockRestore();
544 + });
545 +
546 + // The operation is [opcode, activitySliceID] (2 slots). A trailing operation
547 + // after it verifies that the reader advances past the value slot instead of
548 + // re-reading it as the next opcode.
549 + it('should log an applied activity slice change and advance past its value', () => {
550 + const rendererID = 1;
551 + const rootID = 1;
552 + const stringTableSize = 0;
553 + const operations = [
554 + rendererID,
555 + rootID,
556 + stringTableSize,
557 + TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,
558 + 42,
559 + TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,
560 + 0,
561 + ];
562 +
563 + expect(() => printOperationsArray(operations)).not.toThrow();
564 +
565 + expect(log).toHaveBeenCalledTimes(1);
566 + expect(log.mock.calls[0][0]).toContain(
567 + 'Applied activity slice change to 42',
568 + );
569 + expect(log.mock.calls[0][0]).toContain('Reset applied activity slice');
570 + });
571 + });
572 });
packages/react-devtools-shared/src/utils.js
+1 -1
@@ -440,7 +440,7 @@ export function printOperationsArray(operations: Array<number>) {
440 }
441 case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: {
442 i++;
443 - const activitySliceIDChange = operations[i + 1];
443 + const activitySliceIDChange = operations[i++];
444 logs.push(
445 activitySliceIDChange === 0
446 ? 'Reset applied activity slice'