Profiler.js231 lines
mainRaw
/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import * as React from 'react';
import {Fragment, useContext, useEffect, useRef, useEffectEvent} from 'react';
import {ModalDialog} from '../ModalDialog';
import {ProfilerContext} from './ProfilerContext';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import TabBar from '../TabBar';
import ClearProfilingDataButton from './ClearProfilingDataButton';
import CommitFlamegraph from './CommitFlamegraph';
import CommitRanked from './CommitRanked';
import RootSelector from './RootSelector';
import RecordToggle from './RecordToggle';
import ReloadAndProfileButton from './ReloadAndProfileButton';
import ProfilingImportExportButtons from './ProfilingImportExportButtons';
import SnapshotSelector from './SnapshotSelector';
import ProfilerSearchInput from './ProfilerSearchInput';
import SidebarCommitInfo from './SidebarCommitInfo';
import NoProfilingData from './NoProfilingData';
import RecordingInProgress from './RecordingInProgress';
import ProcessingData from './ProcessingData';
import ProfilingNotSupported from './ProfilingNotSupported';
import SidebarSelectedFiberInfo from './SidebarSelectedFiberInfo';
import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
import portaledContent from '../portaledContent';

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,
    isProfiling,
    selectedCommitIndex,
    selectedFiberID,
    selectedTabID,
    selectTab,
    supportsProfiling,
    startProfiling,
    stopProfiling,
    selectPrevCommitIndex,
    selectNextCommitIndex,
    isSearchInputVisible,
    showSearchInput,
    hideSearchInput,
  } = useContext(ProfilerContext);

  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();
      } else {
        startProfiling();
      }
      event.preventDefault();
      event.stopPropagation();
    } else if (didRecordCommits && correctModifier && event.key === 'f') {
      // Cmd+F (Mac) or Ctrl+F (Windows/Linux) to search components in the commit
      showSearchInput();
      event.preventDefault();
      event.stopPropagation();
    } else if (isSearchInputVisible && event.key === 'Escape') {
      // Escape closes the search input.
      hideSearchInput();
      event.preventDefault();
      event.stopPropagation();
    } else if (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();
      }
    }
  });

  useEffect(() => {
    const div = profilerRef.current;
    if (!div) {
      return;
    }
    const ownerWindow = div.ownerDocument.defaultView;
    // Capture phase: Cmd/Ctrl+F is a reserved browser shortcut (Find), so we
    // must intercept it before the browser to open our own search instead.
    ownerWindow.addEventListener('keydown', handleKeyDown, true);
    return () => {
      ownerWindow.removeEventListener('keydown', handleKeyDown, true);
    };
  }, []);

  let view = null;
  if (didRecordCommits) {
    switch (selectedTabID) {
      case 'flame-chart':
        view = <CommitFlamegraph />;
        break;
      case 'ranked-chart':
        view = <CommitRanked />;
        break;
      default:
        break;
    }
  } else if (isProfiling) {
    view = <RecordingInProgress />;
  } else if (isProcessingData) {
    view = <ProcessingData />;
  } else if (supportsProfiling) {
    view = <NoProfilingData />;
  } else {
    view = <ProfilingNotSupported />;
  }

  let sidebar = null;
  if (!isProfiling && !isProcessingData && didRecordCommits) {
    switch (selectedTabID) {
      case 'flame-chart':
      case 'ranked-chart':
        // TRICKY
        // Handle edge case where no commit is selected because of a min-duration filter update.
        // In that case, the selected commit index would be null.
        // We could still show a sidebar for the previously selected fiber,
        // but it would be an odd user experience.
        // TODO (ProfilerContext) This check should not be necessary.
        if (selectedCommitIndex !== null) {
          if (selectedFiberID !== null) {
            sidebar = <SidebarSelectedFiberInfo />;
          } else {
            sidebar = <SidebarCommitInfo />;
          }
        }
        break;
      default:
        break;
    }
  }

  return (
    <SettingsModalContextController>
      <div ref={profilerRef} className={styles.Profiler}>
        <div className={styles.LeftColumn}>
          <div className={styles.Toolbar}>
            <RecordToggle disabled={!supportsProfiling} />
            <ReloadAndProfileButton disabled={!supportsProfiling} />
            <ClearProfilingDataButton />
            <ProfilingImportExportButtons />
            <div className={styles.VRule} />
            <TabBar
              currentTab={selectedTabID}
              id="Profiler"
              selectTab={selectTab}
              tabs={tabs}
              type="profiler"
            />
            <RootSelector />
            <div className={styles.Spacer} />
            <SettingsModalContextToggle />
            {didRecordCommits && (
              <Fragment>
                <div className={styles.VRule} />
                <Button
                  onClick={
                    isSearchInputVisible ? hideSearchInput : showSearchInput
                  }
                  title={`Search components in this commit (${
                    isMac ? '⌘' : 'Ctrl+'
                  }F)`}
                  data-testname="ProfilerSearchButton">
                  <ButtonIcon type="find" />
                </Button>
                <SnapshotSelector />
              </Fragment>
            )}
          </div>
          <div className={styles.Content}>
            {didRecordCommits && isSearchInputVisible && (
              <div className={styles.SearchInputOverlay}>
                <ProfilerSearchInput />
              </div>
            )}
            {view}
            <ModalDialog />
          </div>
        </div>
        <div className={styles.RightColumn}>{sidebar}</div>
        <SettingsModal />
      </div>
    </SettingsModalContextController>
  );
}

const tabs = [
  {
    id: 'flame-chart',
    icon: 'flame-chart',
    label: 'Flamegraph',
    title: 'Flamegraph chart',
  },
  {
    id: 'ranked-chart',
    icon: 'ranked-chart',
    label: 'Ranked',
    title: 'Ranked chart',
  },
];

export default portaledContent(Profiler) as component();
Loading syntax highlighting…