mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
[DevTools] hotkey to start/stop profiling (#35160)
## Summary The built-in browser profiler supports starting/stopping with Cmd+E. For Symmetry this adds the same hotkey for react devtools profiler. ## How did you test this change? yarn build:\<browser name\> yarn run test:\<browser name\> <img width="483" height="135" alt="Screenshot 2025-11-17 at 14 30 34" src="https://github.com/user-attachments/assets/426939aa-15da-4c21-87a4-e949e6949482" /> firefox: https://github.com/user-attachments/assets/6f225b90-828f-4e79-a364-59d6bc942f83 edge: https://github.com/user-attachments/assets/5b2e9242-f0e8-481b-99a2-2dd78099f3ac chrome: https://github.com/user-attachments/assets/790aab02-2867-4499-aec1-e32e38c763f9 --------- Co-authored-by: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com>
This commit is contained in:
@@ -584,4 +584,75 @@ describe('ProfilerContext', () => {
|
||||
await utils.actAsync(() => context.selectFiber(childID, 'Child'));
|
||||
expect(inspectedElementID).toBe(parentID);
|
||||
});
|
||||
|
||||
it('should toggle profiling when the keyboard shortcut is pressed', async () => {
|
||||
// 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');
|
||||
|
||||
// Dom component for profiling to be enabled
|
||||
const Component = () => null;
|
||||
utils.act(() => render(<Component />));
|
||||
|
||||
const profilerContainer = document.createElement('div');
|
||||
document.body.appendChild(profilerContainer);
|
||||
|
||||
// Create a root for the profiler
|
||||
const profilerRoot = ReactDOMClient.createRoot(profilerContainer);
|
||||
|
||||
// Render the profiler
|
||||
utils.act(() => {
|
||||
profilerRoot.render(
|
||||
<Contexts>
|
||||
<SettingsContextController browserTheme="light">
|
||||
<ModalDialogContextController>
|
||||
<TimelineContextController>
|
||||
<Profiler />
|
||||
</TimelineContextController>
|
||||
</ModalDialogContextController>
|
||||
</SettingsContextController>
|
||||
</Contexts>,
|
||||
);
|
||||
});
|
||||
|
||||
// Verify that the profiler is not profiling.
|
||||
expect(store.profilerStore.isProfilingBasedOnUserInput).toBe(false);
|
||||
|
||||
// Trigger the keyboard shortcut.
|
||||
const ownerWindow = profilerContainer.ownerDocument.defaultView;
|
||||
const isMac =
|
||||
typeof navigator !== 'undefined' &&
|
||||
navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
|
||||
const keyEvent = new KeyboardEvent('keydown', {
|
||||
key: 'e',
|
||||
metaKey: isMac,
|
||||
ctrlKey: !isMac,
|
||||
bubbles: true,
|
||||
});
|
||||
|
||||
// Dispatch keyboard event to toggle profiling on
|
||||
// Try utils.actAsync with recursivelyFlush=false
|
||||
await utils.actAsync(() => {
|
||||
ownerWindow.dispatchEvent(keyEvent);
|
||||
}, false);
|
||||
expect(store.profilerStore.isProfilingBasedOnUserInput).toBe(true);
|
||||
|
||||
// Dispatch keyboard event to toggle profiling off
|
||||
await utils.actAsync(() => {
|
||||
ownerWindow.dispatchEvent(keyEvent);
|
||||
}, false);
|
||||
expect(store.profilerStore.isProfilingBasedOnUserInput).toBe(false);
|
||||
|
||||
document.body.removeChild(profilerContainer);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Fragment, useContext} from 'react';
|
||||
import {Fragment, useContext, useEffect, useRef, useEffectEvent} from 'react';
|
||||
import {ModalDialog} from '../ModalDialog';
|
||||
import {ProfilerContext} from './ProfilerContext';
|
||||
import TabBar from '../TabBar';
|
||||
@@ -38,6 +38,11 @@ import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
|
||||
import styles from './Profiler.css';
|
||||
|
||||
function Profiler(_: {}) {
|
||||
const profilerRef = useRef<HTMLDivElement | null>(null);
|
||||
const isMac =
|
||||
typeof navigator !== 'undefined' &&
|
||||
navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
|
||||
const {
|
||||
didRecordCommits,
|
||||
isProcessingData,
|
||||
@@ -47,6 +52,8 @@ function Profiler(_: {}) {
|
||||
selectedTabID,
|
||||
selectTab,
|
||||
supportsProfiling,
|
||||
startProfiling,
|
||||
stopProfiling,
|
||||
} = useContext(ProfilerContext);
|
||||
|
||||
const {file: timelineTraceEventData, searchInputContainerRef} =
|
||||
@@ -56,6 +63,32 @@ 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;
|
||||
if (correctModifier && event.key === 'e') {
|
||||
if (isProfiling) {
|
||||
stopProfiling();
|
||||
} else {
|
||||
startProfiling();
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const div = profilerRef.current;
|
||||
if (!div) {
|
||||
return;
|
||||
}
|
||||
const ownerWindow = div.ownerDocument.defaultView;
|
||||
ownerWindow.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
ownerWindow.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
let view = null;
|
||||
if (didRecordCommits || selectedTabID === 'timeline') {
|
||||
switch (selectedTabID) {
|
||||
@@ -112,7 +145,7 @@ function Profiler(_: {}) {
|
||||
|
||||
return (
|
||||
<SettingsModalContextController>
|
||||
<div className={styles.Profiler}>
|
||||
<div ref={profilerRef} className={styles.Profiler}>
|
||||
<div className={styles.LeftColumn}>
|
||||
<div className={styles.Toolbar}>
|
||||
<RecordToggle disabled={!supportsProfiling} />
|
||||
|
||||
@@ -30,13 +30,19 @@ export default function RecordToggle({disabled}: Props): React.Node {
|
||||
className = styles.ActiveRecordToggle;
|
||||
}
|
||||
|
||||
const isMac =
|
||||
typeof navigator !== 'undefined' &&
|
||||
navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
const shortcut = isMac ? '⌘E' : 'Ctrl+E';
|
||||
const title = `${isProfiling ? 'Stop' : 'Start'} profiling - ${shortcut}`;
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
onClick={isProfiling ? stopProfiling : startProfiling}
|
||||
testName="ProfilerToggleButton"
|
||||
title={isProfiling ? 'Stop profiling' : 'Start profiling'}>
|
||||
title={title}>
|
||||
<ButtonIcon type="record" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user