Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
557c800
add shortcut to navigate between commits in the performance panel
emily8rown Nov 27, 2025
6a7dc2f
add indicator into tooltips for the hotkeys
emily8rown Nov 27, 2025
99f12da
yarn prettier
emily8rown Nov 27, 2025
3ac3483
changed tooltip for left and right arrow to symbols
emily8rown Nov 27, 2025
d53d7a5
move state management to profiler context from snapshot selector
emily8rown Nov 28, 2025
dc73de4
Refactor to avoid cascading
emily8rown Dec 2, 2025
00fe07d
Ran yarn prettier
emily8rown Dec 2, 2025
f3cacf4
removed duplication
emily8rown Dec 2, 2025
7d0407c
remove unused imports
emily8rown Dec 2, 2025
cee1159
yarn prettier
emily8rown Dec 2, 2025
075762e
removed legacy test
emily8rown Dec 2, 2025
694abec
Merge branch 'facebook:main' into devtools-navigating-commits-perform…
emily8rown Dec 2, 2025
b0a6257
removed legacy testing
emily8rown Dec 2, 2025
922fb23
fix autoselect commit bug
emily8rown Dec 2, 2025
ebb9e16
Merge branch 'facebook:main' into devtools-navigating-commits-perform…
emily8rown Dec 3, 2025
e4247bc
fix profiler Context test
emily8rown Dec 4, 2025
884c16b
check edge cases in profiler context test
emily8rown Dec 4, 2025
466f0d9
test edge cases with varying commit durations in profiler context
emily8rown Dec 4, 2025
9ec729c
Merge branch 'devtools-navigating-commits-performance-panel-hot-key' …
emily8rown Dec 4, 2025
c661f24
remove extra babel config
emily8rown Dec 4, 2025
5c84920
linting and yarn prettier
emily8rown Dec 4, 2025
aa0b246
undo lint formatting where it breaks expected patterns in tests
emily8rown Dec 4, 2025
e719902
remove formatting of react fibre hooks
emily8rown Dec 4, 2025
5b3d5b2
revert local yarn prettier changes to console-test
emily8rown Dec 4, 2025
8dbeef0
removed unecessary comments
emily8rown Dec 4, 2025
69cda16
extracted filtering and navigation from profiler context
emily8rown Dec 4, 2025
46ab188
yarn prettier
emily8rown Dec 4, 2025
0bd3009
removed unecessary comments
emily8rown Dec 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 284 additions & 0 deletions packages/react-devtools-shared/src/__tests__/profilerContext-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -655,4 +655,288 @@ describe('ProfilerContext', () => {

document.body.removeChild(profilerContainer);
});

it('should navigate between commits when the keyboard shortcut is pressed', async () => {
const Parent = () => <Child />;
const Child = () => null;

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
utils.act(() => root.render(<Parent />));

// Profile and record multiple commits
await utils.actAsync(() => store.profilerStore.startProfiling());
await utils.actAsync(() => root.render(<Parent />)); // Commit 1
await utils.actAsync(() => root.render(<Parent />)); // Commit 2
await utils.actAsync(() => root.render(<Parent />)); // Commit 3
await utils.actAsync(() => store.profilerStore.stopProfiling());

const Profiler =
require('react-devtools-shared/src/devtools/views/Profiler/Profiler').default;
const {
TimelineContextController,
} = require('react-devtools-timeline/src/TimelineContext');
const {
SettingsContextController,
} = require('react-devtools-shared/src/devtools/views/Settings/SettingsContext');
const {
ModalDialogContextController,
} = require('react-devtools-shared/src/devtools/views/ModalDialog');

let context: Context = ((null: any): Context);
function ContextReader() {
context = React.useContext(ProfilerContext);
return null;
}

const profilerContainer = document.createElement('div');
document.body.appendChild(profilerContainer);

const profilerRoot = ReactDOMClient.createRoot(profilerContainer);

await utils.actAsync(() => {
profilerRoot.render(
<Contexts>
<SettingsContextController browserTheme="light">
<ModalDialogContextController>
<TimelineContextController>
<Profiler />
<ContextReader />
</TimelineContextController>
</ModalDialogContextController>
</SettingsContextController>
</Contexts>,
);
});

// Verify we have profiling data with 3 commits
expect(context.didRecordCommits).toBe(true);
expect(context.profilingData).not.toBeNull();
const rootID = context.rootID;
expect(rootID).not.toBeNull();
const dataForRoot = context.profilingData.dataForRoots.get(rootID);
expect(dataForRoot.commitData.length).toBe(3);
// Should start at the first commit
expect(context.selectedCommitIndex).toBe(0);

const ownerWindow = profilerContainer.ownerDocument.defaultView;
const isMac =
typeof navigator !== 'undefined' &&
navigator.platform.toUpperCase().indexOf('MAC') >= 0;

// Test ArrowRight navigation (forward) with correct modifier
const arrowRightEvent = new KeyboardEvent('keydown', {
key: 'ArrowRight',
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
});

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowRightEvent);
}, false);
expect(context.selectedCommitIndex).toBe(1);

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowRightEvent);
}, false);
expect(context.selectedCommitIndex).toBe(2);

// Test wrap-around (last -> first)
await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowRightEvent);
}, false);
expect(context.selectedCommitIndex).toBe(0);

// Test ArrowLeft navigation (backward) with correct modifier
const arrowLeftEvent = new KeyboardEvent('keydown', {
key: 'ArrowLeft',
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
});

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowLeftEvent);
}, false);
expect(context.selectedCommitIndex).toBe(2);

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowLeftEvent);
}, false);
expect(context.selectedCommitIndex).toBe(1);

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowLeftEvent);
}, false);
expect(context.selectedCommitIndex).toBe(0);

// Cleanup
await utils.actAsync(() => profilerRoot.unmount());
document.body.removeChild(profilerContainer);
});

it('should handle commit selection edge cases when filtering commits', async () => {
const Scheduler = require('scheduler');

// Create components that do varying amounts of work to generate different commit durations
const Parent = ({count}) => {
Scheduler.unstable_advanceTime(10);
const items = [];
for (let i = 0; i < count; i++) {
items.push(<Child key={i} duration={i} />);
}
return <div>{items}</div>;
};
const Child = ({duration}) => {
Scheduler.unstable_advanceTime(duration);
return <span>{duration}</span>;
};

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
utils.act(() => root.render(<Parent count={1} />));

// Profile and record multiple commits with different amounts of work
await utils.actAsync(() => store.profilerStore.startProfiling());
await utils.actAsync(() => root.render(<Parent count={5} />)); // Commit 1 - 20ms
await utils.actAsync(() => root.render(<Parent count={20} />)); // Commit 2 - 200ms
await utils.actAsync(() => root.render(<Parent count={50} />)); // Commit 3 - 1235ms
await utils.actAsync(() => root.render(<Parent count={10} />)); // Commit 4 - 55ms
await utils.actAsync(() => store.profilerStore.stopProfiling());

// Context providers
const Profiler =
require('react-devtools-shared/src/devtools/views/Profiler/Profiler').default;
const {
TimelineContextController,
} = require('react-devtools-timeline/src/TimelineContext');
const {
SettingsContextController,
} = require('react-devtools-shared/src/devtools/views/Settings/SettingsContext');
const {
ModalDialogContextController,
} = require('react-devtools-shared/src/devtools/views/ModalDialog');

let context: Context = ((null: any): Context);
function ContextReader() {
context = React.useContext(ProfilerContext);
return null;
}

const profilerContainer = document.createElement('div');
document.body.appendChild(profilerContainer);

const profilerRoot = ReactDOMClient.createRoot(profilerContainer);

await utils.actAsync(() => {
profilerRoot.render(
<Contexts>
<SettingsContextController browserTheme="light">
<ModalDialogContextController>
<TimelineContextController>
<Profiler />
<ContextReader />
</TimelineContextController>
</ModalDialogContextController>
</SettingsContextController>
</Contexts>,
);
});

// Verify we have profiling data with 4 commits
expect(context.didRecordCommits).toBe(true);
expect(context.profilingData).not.toBeNull();
const rootID = context.rootID;
expect(rootID).not.toBeNull();
const dataForRoot = context.profilingData.dataForRoots.get(rootID);
expect(dataForRoot.commitData.length).toBe(4);
// Edge case 1: Should start at the first commit
expect(context.selectedCommitIndex).toBe(0);

const ownerWindow = profilerContainer.ownerDocument.defaultView;
const isMac =
typeof navigator !== 'undefined' &&
navigator.platform.toUpperCase().indexOf('MAC') >= 0;

const arrowRightEvent = new KeyboardEvent('keydown', {
key: 'ArrowRight',
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
});

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowRightEvent);
}, false);
expect(context.selectedCommitIndex).toBe(1);

await utils.actAsync(() => {
context.setIsCommitFilterEnabled(true);
});

// Edge case 2: When filtering is enabled, selected commit should remain if it's still visible
expect(context.filteredCommitIndices.length).toBe(4);
expect(context.selectedCommitIndex).toBe(1);
expect(context.selectedFilteredCommitIndex).toBe(1);

await utils.actAsync(() => {
context.setMinCommitDuration(1000000);
});

// Edge case 3: When all commits are filtered out, selection should be null
expect(context.filteredCommitIndices).toEqual([]);
expect(context.selectedCommitIndex).toBe(null);
expect(context.selectedFilteredCommitIndex).toBe(null);

await utils.actAsync(() => {
context.setMinCommitDuration(0);
});

// Edge case 4: After restoring commits, first commit should be auto-selected
expect(context.filteredCommitIndices.length).toBe(4);
expect(context.selectedCommitIndex).toBe(0);
expect(context.selectedFilteredCommitIndex).toBe(0);

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowRightEvent);
}, false);
expect(context.selectedCommitIndex).toBe(1);

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowRightEvent);
}, false);
expect(context.selectedCommitIndex).toBe(2);

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowRightEvent);
}, false);
expect(context.selectedCommitIndex).toBe(3);

// Filter out the currently selected commit using actual commit data
const commitDurations = dataForRoot.commitData.map(
commit => commit.duration,
);
const selectedCommitDuration = commitDurations[3];
const filterThreshold = selectedCommitDuration + 0.001;
await utils.actAsync(() => {
context.setMinCommitDuration(filterThreshold);
});

// Edge case 5: Should auto-select first available commit when current one is filtered
expect(context.selectedCommitIndex).not.toBe(null);
expect(context.selectedFilteredCommitIndex).toBe(1);

await utils.actAsync(() => {
context.setIsCommitFilterEnabled(false);
});

// Edge case 6: When filtering is disabled, selected commit should remain
expect(context.filteredCommitIndices.length).toBe(4);
expect(context.selectedCommitIndex).toBe(2);
expect(context.selectedFilteredCommitIndex).toBe(2);

await utils.actAsync(() => profilerRoot.unmount());
document.body.removeChild(profilerContainer);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ function Profiler(_: {}) {
supportsProfiling,
startProfiling,
stopProfiling,
selectPrevCommitIndex,
selectNextCommitIndex,
} = useContext(ProfilerContext);

const {file: timelineTraceEventData, searchInputContainerRef} =
Expand All @@ -63,9 +65,9 @@ function Profiler(_: {}) {

const isLegacyProfilerSelected = selectedTabID !== 'timeline';

// Cmd+E to start/stop profiler recording
const handleKeyDown = useEffectEvent((event: KeyboardEvent) => {
const correctModifier = isMac ? event.metaKey : event.ctrlKey;
// Cmd+E to start/stop profiler recording
if (correctModifier && event.key === 'e') {
if (isProfiling) {
stopProfiling();
Expand All @@ -74,6 +76,24 @@ function Profiler(_: {}) {
}
event.preventDefault();
event.stopPropagation();
} else if (
isLegacyProfilerSelected &&
didRecordCommits &&
selectedCommitIndex !== null
) {
// Cmd+Left/Right (Mac) or Ctrl+Left/Right (Windows/Linux) to navigate commits
if (
correctModifier &&
(event.key === 'ArrowLeft' || event.key === 'ArrowRight')
) {
if (event.key === 'ArrowLeft') {
selectPrevCommitIndex();
} else {
selectNextCommitIndex();
}
event.preventDefault();
event.stopPropagation();
}
}
});

Expand Down
Loading
Loading