@samitouri / QOS-React-2 / commits / fd524fe02a

[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>

emily8rown committed Nov 21, 2025 at 16:37 UTC fd524fe02a86c3e92a207d90da970941320f337f
3 files changed +113 -3
packages/react-devtools-shared/src/__tests__/profilerContext-test.js
+71
@@ -584,4 +584,75 @@ describe('ProfilerContext', () => {
584 await utils.actAsync(() => context.selectFiber(childID, 'Child'));
585 expect(inspectedElementID).toBe(parentID);
586 });
587 +
588 + it('should toggle profiling when the keyboard shortcut is pressed', async () => {
589 + // Context providers
590 + const Profiler =
591 + require('react-devtools-shared/src/devtools/views/Profiler/Profiler').default;
592 + const {
593 + TimelineContextController,
594 + } = require('react-devtools-timeline/src/TimelineContext');
595 + const {
596 + SettingsContextController,
597 + } = require('react-devtools-shared/src/devtools/views/Settings/SettingsContext');
598 + const {
599 + ModalDialogContextController,
600 + } = require('react-devtools-shared/src/devtools/views/ModalDialog');
601 +
602 + // Dom component for profiling to be enabled
603 + const Component = () => null;
604 + utils.act(() => render(<Component />));
605 +
606 + const profilerContainer = document.createElement('div');
607 + document.body.appendChild(profilerContainer);
608 +
609 + // Create a root for the profiler
610 + const profilerRoot = ReactDOMClient.createRoot(profilerContainer);
611 +
612 + // Render the profiler
613 + utils.act(() => {
614 + profilerRoot.render(
615 + <Contexts>
616 + <SettingsContextController browserTheme="light">
617 + <ModalDialogContextController>
618 + <TimelineContextController>
619 + <Profiler />
620 + </TimelineContextController>
621 + </ModalDialogContextController>
622 + </SettingsContextController>
623 + </Contexts>,
624 + );
625 + });
626 +
627 + // Verify that the profiler is not profiling.
628 + expect(store.profilerStore.isProfilingBasedOnUserInput).toBe(false);
629 +
630 + // Trigger the keyboard shortcut.
631 + const ownerWindow = profilerContainer.ownerDocument.defaultView;
632 + const isMac =
633 + typeof navigator !== 'undefined' &&
634 + navigator.platform.toUpperCase().indexOf('MAC') >= 0;
635 +
636 + const keyEvent = new KeyboardEvent('keydown', {
637 + key: 'e',
638 + metaKey: isMac,
639 + ctrlKey: !isMac,
640 + bubbles: true,
641 + });
642 +
643 + // Dispatch keyboard event to toggle profiling on
644 + // Try utils.actAsync with recursivelyFlush=false
645 + await utils.actAsync(() => {
646 + ownerWindow.dispatchEvent(keyEvent);
647 + }, false);
648 + expect(store.profilerStore.isProfilingBasedOnUserInput).toBe(true);
649 +
650 + // Dispatch keyboard event to toggle profiling off
651 + await utils.actAsync(() => {
652 + ownerWindow.dispatchEvent(keyEvent);
653 + }, false);
654 + expect(store.profilerStore.isProfilingBasedOnUserInput).toBe(false);
655 +
656 + document.body.removeChild(profilerContainer);
657 + });
658 });
packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js
+35 -2
@@ -8,7 +8,7 @@
8 */
9
10 import * as React from 'react';
11 -import {Fragment, useContext} from 'react';
11 +import {Fragment, useContext, useEffect, useRef, useEffectEvent} from 'react';
12 import {ModalDialog} from '../ModalDialog';
13 import {ProfilerContext} from './ProfilerContext';
14 import TabBar from '../TabBar';
@@ -38,6 +38,11 @@ import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
38 import styles from './Profiler.css';
39
40 function Profiler(_: {}) {
41 + const profilerRef = useRef<HTMLDivElement | null>(null);
42 + const isMac =
43 + typeof navigator !== 'undefined' &&
44 + navigator.platform.toUpperCase().indexOf('MAC') >= 0;
45 +
46 const {
47 didRecordCommits,
48 isProcessingData,
@@ -47,6 +52,8 @@ function Profiler(_: {}) {
52 selectedTabID,
53 selectTab,
54 supportsProfiling,
55 + startProfiling,
56 + stopProfiling,
57 } = useContext(ProfilerContext);
58
59 const {file: timelineTraceEventData, searchInputContainerRef} =
@@ -56,6 +63,32 @@ function Profiler(_: {}) {
63
64 const isLegacyProfilerSelected = selectedTabID !== 'timeline';
65
66 + // Cmd+E to start/stop profiler recording
67 + const handleKeyDown = useEffectEvent((event: KeyboardEvent) => {
68 + const correctModifier = isMac ? event.metaKey : event.ctrlKey;
69 + if (correctModifier && event.key === 'e') {
70 + if (isProfiling) {
71 + stopProfiling();
72 + } else {
73 + startProfiling();
74 + }
75 + event.preventDefault();
76 + event.stopPropagation();
77 + }
78 + });
79 +
80 + useEffect(() => {
81 + const div = profilerRef.current;
82 + if (!div) {
83 + return;
84 + }
85 + const ownerWindow = div.ownerDocument.defaultView;
86 + ownerWindow.addEventListener('keydown', handleKeyDown);
87 + return () => {
88 + ownerWindow.removeEventListener('keydown', handleKeyDown);
89 + };
90 + }, []);
91 +
92 let view = null;
93 if (didRecordCommits || selectedTabID === 'timeline') {
94 switch (selectedTabID) {
@@ -112,7 +145,7 @@ function Profiler(_: {}) {
145
146 return (
147 <SettingsModalContextController>
115 - <div className={styles.Profiler}>
148 + <div ref={profilerRef} className={styles.Profiler}>
149 <div className={styles.LeftColumn}>
150 <div className={styles.Toolbar}>
151 <RecordToggle disabled={!supportsProfiling} />
packages/react-devtools-shared/src/devtools/views/Profiler/RecordToggle.js
+7 -1
@@ -30,13 +30,19 @@ export default function RecordToggle({disabled}: Props): React.Node {
30 className = styles.ActiveRecordToggle;
31 }
32
33 + const isMac =
34 + typeof navigator !== 'undefined' &&
35 + navigator.platform.toUpperCase().indexOf('MAC') >= 0;
36 + const shortcut = isMac ? '⌘E' : 'Ctrl+E';
37 + const title = `${isProfiling ? 'Stop' : 'Start'} profiling - ${shortcut}`;
38 +
39 return (
40 <Button
41 className={className}
42 disabled={disabled}
43 onClick={isProfiling ? stopProfiling : startProfiling}
44 testName="ProfilerToggleButton"
39 - title={isProfiling ? 'Stop profiling' : 'Start profiling'}>
45 + title={title}>
46 <ButtonIcon type="record" />
47 </Button>
48 );