diff --git a/.circleci/config.yml b/.circleci/config.yml index a1b1fa1cb49ef..efa35f94501d9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -227,48 +227,6 @@ jobs: - store_artifacts: path: ./build/devtools.tgz - build_devtools_scheduling_profiler: - docker: *docker - environment: *environment - steps: - - checkout - - attach_workspace: *attach_workspace - - run: yarn workspaces info | head -n -1 > workspace_info.txt - - *restore_yarn_cache - - *restore_node_modules - - run: - name: Install Packages - command: yarn --frozen-lockfile --cache-folder ~/.cache/yarn - - run: - name: Build and archive - command: | - mkdir -p build/devtools - cd packages/react-devtools-scheduling-profiler - yarn build - cd dist - tar -zcvf ../../../build/devtools-scheduling-profiler.tgz . - - store_artifacts: - path: ./build/devtools-scheduling-profiler.tgz - - persist_to_workspace: - root: packages/react-devtools-scheduling-profiler - paths: - - dist - - deploy_devtools_scheduling_profiler: - docker: *docker - environment: *environment - steps: - - checkout - - attach_workspace: - at: packages/react-devtools-scheduling-profiler - - run: yarn workspaces info | head -n -1 > workspace_info.txt - - *restore_node_modules - - run: - name: Deploy - command: | - cd packages/react-devtools-scheduling-profiler - yarn vercel deploy dist --prod --confirm --token $SCHEDULING_PROFILER_DEPLOY_VERCEL_TOKEN - yarn_lint_build: docker: *docker environment: *environment @@ -408,16 +366,6 @@ workflows: - build_devtools_and_process_artifacts: requires: - yarn_build - - build_devtools_scheduling_profiler: - requires: - - yarn_build - - deploy_devtools_scheduling_profiler: - requires: - - build_devtools_scheduling_profiler - filters: - branches: - only: - - main # New workflow that will replace "stable" and "experimental" build_and_test: diff --git a/packages/react-devtools-core/webpack.standalone.js b/packages/react-devtools-core/webpack.standalone.js index 8f5b4e6dcac93..4d1f8544047c9 100644 --- a/packages/react-devtools-core/webpack.standalone.js +++ b/packages/react-devtools-core/webpack.standalone.js @@ -18,6 +18,15 @@ const __DEV__ = NODE_ENV === 'development'; const DEVTOOLS_VERSION = getVersionString(); +const babelOptions = { + configFile: resolve( + __dirname, + '..', + 'react-devtools-shared', + 'babel.config.js', + ), +}; + module.exports = { mode: __DEV__ ? 'development' : 'production', devtool: __DEV__ ? 'cheap-module-eval-source-map' : 'source-map', @@ -62,17 +71,25 @@ module.exports = { ], module: { rules: [ + { + test: /\.worker\.js$/, + use: [ + { + loader: 'workerize-loader', + options: { + inline: true, + }, + }, + { + loader: 'babel-loader', + options: babelOptions, + }, + ], + }, { test: /\.js$/, loader: 'babel-loader', - options: { - configFile: resolve( - __dirname, - '..', - 'react-devtools-shared', - 'babel.config.js', - ), - }, + options: babelOptions, }, { test: /\.css$/, diff --git a/packages/react-devtools-extensions/package.json b/packages/react-devtools-extensions/package.json index 4cb68ea8973a2..4600dde3f7c4b 100644 --- a/packages/react-devtools-extensions/package.json +++ b/packages/react-devtools-extensions/package.json @@ -37,6 +37,7 @@ "chrome-launch": "^1.1.4", "crx": "^5.0.0", "css-loader": "^1.0.1", + "file-loader": "^6.1.0", "firefox-profile": "^1.0.2", "fs-extra": "^4.0.2", "jest-fetch-mock": "^3.0.3", @@ -52,7 +53,6 @@ "source-map": "^0.8.0-beta.0", "sourcemap-codec": "^1.4.8", "style-loader": "^0.23.1", - "web-ext": "^3.0.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.10.3", diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index b3b2581eda3a8..514e48514d785 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -140,6 +140,8 @@ function createPanelIfReactLoaded() { isProfiling, supportsReloadAndProfile: isChrome, supportsProfiling, + // At this time, the scheduling profiler can only parse Chrome performance profiles. + supportsSchedulingProfiler: isChrome, supportsTraceUpdates: true, }); store.profilerStore.profilingData = profilingData; diff --git a/packages/react-devtools-extensions/src/parseHookNames/index.js b/packages/react-devtools-extensions/src/parseHookNames/index.js index da864aa27085e..ed178c3f200b4 100644 --- a/packages/react-devtools-extensions/src/parseHookNames/index.js +++ b/packages/react-devtools-extensions/src/parseHookNames/index.js @@ -9,9 +9,8 @@ * @flow */ -// This file uses workerize to load ./parseHookNames.worker as a webworker -// and instanciates it, exposing flow typed functions that can be used -// on other files. +// This file uses workerize to load ./parseHookNames.worker as a webworker and instanciates it, +// exposing flow typed functions that can be used on other files. import * as parseHookNamesModule from './parseHookNames'; import WorkerizedParseHookNames from './parseHookNames.worker'; diff --git a/packages/react-devtools-extensions/src/parseHookNames/parseHookNames.worker.js b/packages/react-devtools-extensions/src/parseHookNames/parseHookNames.worker.js index 7843527e4d963..c89f11a55db28 100644 --- a/packages/react-devtools-extensions/src/parseHookNames/parseHookNames.worker.js +++ b/packages/react-devtools-extensions/src/parseHookNames/parseHookNames.worker.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import * as parseHookNamesModule from './parseHookNames'; export const parseHookNames = parseHookNamesModule.parseHookNames; diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index 3b89bf7355eb3..b3a92762ae6ca 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -19,6 +19,15 @@ const DEVTOOLS_VERSION = getVersionString(); const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss'; +const babelOptions = { + configFile: resolve( + __dirname, + '..', + 'react-devtools-shared', + 'babel.config.js', + ), +}; + module.exports = { mode: __DEV__ ? 'development' : 'production', devtool: __DEV__ ? 'cheap-module-eval-source-map' : false, @@ -81,17 +90,25 @@ module.exports = { ], rules: [ + { + test: /\.worker\.js$/, + use: [ + { + loader: 'workerize-loader', + options: { + inline: true, + }, + }, + { + loader: 'babel-loader', + options: babelOptions, + }, + ], + }, { test: /\.js$/, loader: 'babel-loader', - options: { - configFile: resolve( - __dirname, - '..', - 'react-devtools-shared', - 'babel.config.js', - ), - }, + options: babelOptions, }, { test: /\.css$/, @@ -109,11 +126,6 @@ module.exports = { }, ], }, - { - test: /\.worker\.js$/, - // inline: true due to limitations with extensions - use: {loader: 'workerize-loader', options: {inline: true}}, - }, ], }, }; diff --git a/packages/react-devtools-inline/package.json b/packages/react-devtools-inline/package.json index a661fdf839957..21efaf2bbd9a3 100644 --- a/packages/react-devtools-inline/package.json +++ b/packages/react-devtools-inline/package.json @@ -34,10 +34,12 @@ "babel-loader": "^8.0.4", "cross-env": "^3.1.4", "css-loader": "^1.0.1", + "file-loader": "^6.1.0", "raw-loader": "^3.1.0", "style-loader": "^0.23.1", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", - "webpack-dev-server": "^3.10.3" + "webpack-dev-server": "^3.10.3", + "worker-loader": "^3.0.3" } } diff --git a/packages/react-devtools-inline/src/frontend.js b/packages/react-devtools-inline/src/frontend.js index af4b0330d75f8..d9645999b3f0b 100644 --- a/packages/react-devtools-inline/src/frontend.js +++ b/packages/react-devtools-inline/src/frontend.js @@ -24,6 +24,7 @@ export function createStore(bridge: FrontendBridge): Store { return new Store(bridge, { checkBridgeProtocolCompatibility: true, supportsTraceUpdates: true, + supportsSchedulingProfiler: true, }); } diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index 7011e7151c760..040cc629b822d 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -16,6 +16,15 @@ const __DEV__ = NODE_ENV === 'development'; const DEVTOOLS_VERSION = getVersionString(); +const babelOptions = { + configFile: resolve( + __dirname, + '..', + 'react-devtools-shared', + 'babel.config.js', + ), +}; + module.exports = { mode: __DEV__ ? 'development' : 'production', devtool: __DEV__ ? 'eval-cheap-source-map' : 'source-map', @@ -65,17 +74,25 @@ module.exports = { ], module: { rules: [ + { + test: /\.worker\.js$/, + use: [ + { + loader: 'workerize-loader', + options: { + inline: true, + }, + }, + { + loader: 'babel-loader', + options: babelOptions, + }, + ], + }, { test: /\.js$/, loader: 'babel-loader', - options: { - configFile: resolve( - __dirname, - '..', - 'react-devtools-shared', - 'babel.config.js', - ), - }, + options: babelOptions, }, { test: /\.css$/, diff --git a/packages/react-devtools-scheduling-profiler/README.md b/packages/react-devtools-scheduling-profiler/README.md index 5ce54a3d7ea20..457bec25efcf2 100644 --- a/packages/react-devtools-scheduling-profiler/README.md +++ b/packages/react-devtools-scheduling-profiler/README.md @@ -1,15 +1,3 @@ -# Experimental React Concurrent Mode Profiler +# React Concurrent Mode Profiler -https://react-devtools-scheduling-profiler.vercel.app/ - -## Setting up continuous deployment with CircleCI and Vercel - -These instructions are intended for internal use, but may be useful if you are setting up a custom production deployment of the scheduling profiler. - -1. Create a Vercel token at https://vercel.com/account/tokens. -2. Configure CircleCI: - 1. In CircleCI, navigate to the repository's Project Settings. - 2. In the Advanced tab, ensure that "Pass secrets to builds from forked pull requests" is set to false. - 3. In the Environment Variables tab, add the Vercel token as a new `SCHEDULING_PROFILER_DEPLOY_VERCEL_TOKEN` environment variable. - -The Vercel project will be created when the deploy job runs. +This package contains the new/experimental "scheduling profiler" for React 18. This profiler exists as its own project because it was initially deployed as a standalone app. It has since been moved into the DevTools Profiler under the "Scheduling" tab. This package will likely eventually be moved into `react-devtools-shared`. \ No newline at end of file diff --git a/packages/react-devtools-scheduling-profiler/buildUtils.js b/packages/react-devtools-scheduling-profiler/buildUtils.js deleted file mode 100644 index b0971c4861112..0000000000000 --- a/packages/react-devtools-scheduling-profiler/buildUtils.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -const {execSync} = require('child_process'); -const {readFileSync} = require('fs'); -const {resolve} = require('path'); - -function getGitCommit() { - try { - return execSync('git show -s --format=%h') - .toString() - .trim(); - } catch (error) { - // Mozilla runs this command from a git archive. - // In that context, there is no Git revision. - return null; - } -} - -function getVersionString() { - const packageVersion = JSON.parse( - readFileSync(resolve(__dirname, './package.json')), - ).version; - - const commit = getGitCommit(); - - return `${packageVersion}-${commit}`; -} - -module.exports = { - getVersionString, -}; diff --git a/packages/react-devtools-scheduling-profiler/package.json b/packages/react-devtools-scheduling-profiler/package.json index 705261279e304..55b66341bee70 100644 --- a/packages/react-devtools-scheduling-profiler/package.json +++ b/packages/react-devtools-scheduling-profiler/package.json @@ -1,12 +1,8 @@ { "private": true, "name": "react-devtools-scheduling-profiler", - "version": "0.0.0", + "version": "4.14.0", "license": "MIT", - "scripts": { - "build": "cross-env NODE_ENV=production cross-env TARGET=remote webpack --config webpack.config.js", - "start": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open" - }, "dependencies": { "@elg/speedscope": "1.9.0-a6f84db", "clipboard-js": "^0.3.6", diff --git a/packages/react-devtools-scheduling-profiler/src/App.css b/packages/react-devtools-scheduling-profiler/src/App.css deleted file mode 100644 index 1ea3d75fcc595..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/App.css +++ /dev/null @@ -1,19 +0,0 @@ -.DevTools { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - background-color: var(--color-background); - color: var(--color-text); -} - -.TabContent { - flex: 1 1 100%; - overflow: auto; - -webkit-app-region: no-drag; -} - -.DevTools, .DevTools * { - box-sizing: border-box; - -webkit-font-smoothing: var(--font-smoothing); -} diff --git a/packages/react-devtools-scheduling-profiler/src/App.js b/packages/react-devtools-scheduling-profiler/src/App.js deleted file mode 100644 index 9a27253b6c032..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/App.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -// Reach styles need to come before any component styles. -// This makes overriding the styles simpler. -import '@reach/menu-button/styles.css'; -import '@reach/tooltip/styles.css'; - -import * as React from 'react'; - -import {SchedulingProfiler} from './SchedulingProfiler'; -import {useBrowserTheme, useDisplayDensity} from './hooks'; - -import styles from './App.css'; -import 'react-devtools-shared/src/devtools/views/root.css'; - -export default function App() { - useBrowserTheme(); - useDisplayDensity(); - - return ( -
-
- -
-
- ); -} diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.css b/packages/react-devtools-scheduling-profiler/src/CanvasPage.css index e5d238a0d9d2c..8c7633a1b8977 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.css +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.css @@ -1,7 +1,7 @@ .CanvasPage { position: absolute; - top: 0.5rem; - bottom: 0.5rem; - left: 0.5rem; - right: 0.5rem; + top: 0; + bottom: 0; + left: 0; + right: 0; } diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index 9b20cd2fb80d7..9064065e541de 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -52,9 +52,9 @@ import { import {COLORS} from './content-views/constants'; import EventTooltip from './EventTooltip'; -import ContextMenu from './context/ContextMenu'; -import ContextMenuItem from './context/ContextMenuItem'; -import useContextMenu from './context/useContextMenu'; +import ContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenu'; +import ContextMenuItem from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenuItem'; +import useContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/useContextMenu'; import {getBatchRange} from './utils/getBatchRange'; import styles from './CanvasPage.css'; @@ -94,6 +94,7 @@ const copySummary = (data: ReactProfilerData, measure: ReactMeasure) => { ); }; +// TODO (scheduling profiler) Why is the "zoom" feature so much slower than normal rendering? const zoomToBatch = ( data: ReactProfilerData, measure: ReactMeasure, @@ -102,8 +103,7 @@ const zoomToBatch = ( const {batchUID} = measure; const [startTime, stopTime] = getBatchRange(batchUID, data); syncedHorizontalPanAndZoomViews.forEach(syncedView => - // Using time as range works because the views' intrinsic content size is - // based on time. + // Using time as range works because the views' intrinsic content size is based on time. syncedView.zoomToRange(startTime, stopTime), ); }; @@ -243,6 +243,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { defaultFrame, reactMeasuresHorizontalPanAndZoomView, flamechartHorizontalPanAndZoomView, + canvasRef, ); const rootView = new View( @@ -281,13 +282,17 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { useCanvasInteraction(canvasRef, interactor); + const setIsContextMenuShownWrapper = (...args) => { + console.log('setIsContextMenuShown()', ...args); + setIsContextMenuShown(...args); + }; useContextMenu({ data: { data, hoveredEvent, }, id: CONTEXT_MENU_ID, - onChange: setIsContextMenuShown, + onChange: setIsContextMenuShownWrapper, ref: canvasRef, }); diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css index f721295b2f2ba..b6503b7338c35 100644 --- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css @@ -6,9 +6,10 @@ padding: 0.25rem; user-select: none; pointer-events: none; - background-color: #ffffff; - border: 1px solid #ccc; - box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2); + background-color: var(--color-tooltip-background); + border: 1px solid var(border); + box-shadow: 1px 1px 2px var(--color-shadow); + color: var(--color-tooltip-text); font-size: 11px; } @@ -26,7 +27,7 @@ } .DetailsGridLabel { - color: #666; + color: var(--color-dim); text-align: right; } @@ -56,14 +57,14 @@ line-height: 1.5; -webkit-mask-image: linear-gradient( 180deg, - #fff, - #fff 5em, + var(--color-tooltip-background), + var(--color-tooltip-background) 5em, transparent ); mask-image: linear-gradient( 180deg, - #fff, - #fff 5em, + var(--color-tooltip-background), + var(--color-tooltip-background) 5em, transparent ); white-space: pre; diff --git a/packages/react-devtools-scheduling-profiler/src/ImportButton.js b/packages/react-devtools-scheduling-profiler/src/ImportButton.js index acd382b08ad05..6f018bf30934c 100644 --- a/packages/react-devtools-scheduling-profiler/src/ImportButton.js +++ b/packages/react-devtools-scheduling-profiler/src/ImportButton.js @@ -9,17 +9,17 @@ import * as React from 'react'; import {useCallback, useRef} from 'react'; - import Button from 'react-devtools-shared/src/devtools/views/Button'; import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon'; import styles from './ImportButton.css'; type Props = {| + children?: mixed, onFileSelect: (file: File) => void, |}; -export default function ImportButton({onFileSelect}: Props) { +export default function ImportButton({children = null, onFileSelect}: Props) { const inputRef = useRef(null); const handleFiles = useCallback(() => { @@ -51,6 +51,7 @@ export default function ImportButton({onFileSelect}: Props) { /> ); diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.css b/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.css index e16279192b83e..5be33714a8f9f 100644 --- a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.css +++ b/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.css @@ -1,21 +1,5 @@ -.SchedulingProfiler { - width: 100%; - height: 100%; - position: relative; - display: flex; - flex-direction: column; - font-family: var(--font-family-sans); - font-size: var(--font-size-sans-normal); - background-color: var(--color-background); - color: var(--color-text); -} - -.SchedulingProfiler, .SchedulingProfiler * { - box-sizing: border-box; - -webkit-font-smoothing: var(--font-smoothing); -} - .Content { + width: 100%; position: relative; flex: 1 1 auto; display: flex; @@ -52,57 +36,26 @@ margin-bottom: 0.5rem; } -.Toolbar { - height: 2.25rem; - padding: 0 0.25rem; - flex: 0 0 auto; - display: flex; - align-items: center; - border-bottom: 1px solid var(--color-border); -} - -.VRule { - height: 20px; - width: 1px; - border-left: 1px solid var(--color-border); - padding-left: 0.25rem; - margin-left: 0.25rem; -} - -.Spacer { - flex: 1; -} - -.Link { - color: var(--color-button); -} - -.ScreenshotWrapper { - max-width: 30rem; - padding: 0 1rem; - margin-bottom: 2rem; +.WelcomeInstructionsList { } -.Screenshot { - width: 100%; - border-radius: 0.4em; - border: 2px solid var(--color-border); +.WelcomeInstructionsListItem { + display: flex; + align-items: center; + line-height: 1.5rem; + counter-increment: li; } -.AppName { - font-size: var(--font-size-sans-large); +.WelcomeInstructionsListItem::before { + content: counter(li); margin-right: 0.5rem; - user-select: none; } -@media screen and (max-width: 350px) { - .AppName { - display: none; - } +.WelcomeInstructionsListItemLink { + color: var(--color-link); + margin-left: 0.25rem; } -@media screen and (max-height: 600px) { - .ScreenshotWrapper { - display: none; - } +.ImportButtonLabel { + margin-left: 0.25rem; } \ No newline at end of file diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js b/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js index 2f232b6bbe183..dff2398164fd8 100644 --- a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js +++ b/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js @@ -7,102 +7,88 @@ * @flow */ -import type {Resource} from 'react-devtools-shared/src/devtools/cache'; -import type {ReactProfilerData} from './types'; -import type {ImportWorkerOutputData} from './import-worker/import.worker'; +import type {DataResource} from './createDataResourceFromImportedFile'; import * as React from 'react'; -import {Suspense, useCallback, useState} from 'react'; -import {createResource} from 'react-devtools-shared/src/devtools/cache'; -import ReactLogo from 'react-devtools-shared/src/devtools/views/ReactLogo'; - +import { + Suspense, + useContext, + useDeferredValue, + useLayoutEffect, + useState, +} from 'react'; +import {SettingsContext} from 'react-devtools-shared/src/devtools/views/Settings/SettingsContext'; +import {updateColorsToMatchTheme} from './content-views/constants'; +import {SchedulingProfilerContext} from './SchedulingProfilerContext'; import ImportButton from './ImportButton'; import CanvasPage from './CanvasPage'; -import ImportWorker from './import-worker/import.worker'; -import profilerBrowser from './assets/profilerBrowser.png'; import styles from './SchedulingProfiler.css'; -type DataResource = Resource; - -function createDataResourceFromImportedFile(file: File): DataResource { - return createResource( - () => { - return new Promise((resolve, reject) => { - const worker: Worker = new (ImportWorker: any)(); - - worker.onmessage = function(event) { - const data = ((event.data: any): ImportWorkerOutputData); - switch (data.status) { - case 'SUCCESS': - resolve(data.processedData); - break; - case 'INVALID_PROFILE_ERROR': - resolve(data.error); - break; - case 'UNEXPECTED_ERROR': - reject(data.error); - break; - } - worker.terminate(); - }; - - worker.postMessage({file}); - }); - }, - () => file, - {useWeakMap: true}, - ); -} - export function SchedulingProfiler(_: {||}) { - const [dataResource, setDataResource] = useState(null); + const {importSchedulingProfilerData, schedulingProfilerData} = useContext( + SchedulingProfilerContext, + ); - const handleFileSelect = useCallback((file: File) => { - setDataResource(createDataResourceFromImportedFile(file)); - }, []); + // HACK: Canvas rendering uses an imperative API, + // but DevTools colors are stored in CSS variables (see root.css and SettingsContext). + // When the theme changes, we need to trigger update the imperative colors and re-draw the Canvas. + const {theme} = useContext(SettingsContext); + // HACK: SettingsContext also uses a useLayoutEffect to update styles; + // make sure the theme context in SettingsContext updates before this code. + const deferredTheme = useDeferredValue(theme); + // HACK: Schedule a re-render of the Canvas once colors have been updated. + // The easiest way to guarangee this happens is to recreate the inner Canvas component. + const [key, setKey] = useState(theme); + useLayoutEffect(() => { + updateColorsToMatchTheme(); + setKey(deferredTheme); + }, [deferredTheme]); return ( -
-
- - Concurrent Mode Profiler -
- -
-
-
- {dataResource ? ( - }> - - - ) : ( - - )} -
+
+ {schedulingProfilerData ? ( + }> + + + ) : ( + + )}
); } const Welcome = ({onFileSelect}: {|onFileSelect: (file: File) => void|}) => ( -
-
- Profiler screenshot -
-
Welcome!
-
- Click the import button - to import a Chrome - performance profile. -
-
+
    +
  1. + Open a website that's built with the + + profiling build of ReactDOM + + . +
  2. +
  3. + Open the "Performance" tab in Chrome and record some performance data. +
  4. +
  5. + Click the "Save profile..." button in Chrome to export the data. +
  6. +
  7. + Import the data into the profiler: +
    + + Import + +
  8. +
); const ProcessingData = () => ( diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js b/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js new file mode 100644 index 0000000000000..e6638ce728d82 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) Facebook, Inc. and its 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 {createContext, useCallback, useMemo, useState} from 'react'; +import createDataResourceFromImportedFile from './createDataResourceFromImportedFile'; + +import type {DataResource} from './createDataResourceFromImportedFile'; + +export type Context = {| + importSchedulingProfilerData: (file: File) => void, + schedulingProfilerData: DataResource | null, +|}; + +const SchedulingProfilerContext = createContext( + ((null: any): Context), +); +SchedulingProfilerContext.displayName = 'SchedulingProfilerContext'; + +type Props = {| + children: React$Node, +|}; + +function SchedulingProfilerContextController({children}: Props) { + const [ + schedulingProfilerData, + setSchedulingProfilerData, + ] = useState(null); + + const importSchedulingProfilerData = useCallback((file: File) => { + setSchedulingProfilerData(createDataResourceFromImportedFile(file)); + }, []); + + // TODO (scheduling profiler) Start/stop time ref here? + + const value = useMemo( + () => ({ + importSchedulingProfilerData, + schedulingProfilerData, + // TODO (scheduling profiler) + }), + [ + importSchedulingProfilerData, + schedulingProfilerData, + // TODO (scheduling profiler) + ], + ); + + return ( + + {children} + + ); +} + +export {SchedulingProfilerContext, SchedulingProfilerContextController}; diff --git a/packages/react-devtools-scheduling-profiler/src/assets/logo.svg b/packages/react-devtools-scheduling-profiler/src/assets/logo.svg deleted file mode 100644 index 2e5df0d3ab2f2..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/assets/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/react-devtools-scheduling-profiler/src/assets/profilerBrowser.png b/packages/react-devtools-scheduling-profiler/src/assets/profilerBrowser.png deleted file mode 100644 index b0282be2f6828..0000000000000 Binary files a/packages/react-devtools-scheduling-profiler/src/assets/profilerBrowser.png and /dev/null differ diff --git a/packages/react-devtools-scheduling-profiler/src/assets/reactlogo.svg b/packages/react-devtools-scheduling-profiler/src/assets/reactlogo.svg deleted file mode 100644 index 6b60c1042f58d..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/assets/reactlogo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js index 00b022899f657..8b79a30916fed 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js @@ -203,7 +203,7 @@ class FlamechartStackLayerView extends View { ); if (trimmedName !== null) { - context.fillStyle = COLORS.PRIORITY_LABEL; + context.fillStyle = COLORS.FLAME_GRAPH_LABEL; // Prevent text from being drawn outside `viewableArea` const textOverflowsViewableArea = !rectEqualToRect( diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js index 7b0fb87260769..baaedebc64ca7 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -41,34 +41,129 @@ export const FLAMECHART_FONT_SIZE = 10; export const FLAMECHART_FRAME_HEIGHT = 16; export const FLAMECHART_TEXT_PADDING = 3; -export const COLORS = Object.freeze({ - BACKGROUND: '#ffffff', - PRIORITY_BACKGROUND: '#ededf0', - PRIORITY_BORDER: '#d7d7db', - PRIORITY_LABEL: '#272727', - USER_TIMING: '#c9cacd', - USER_TIMING_HOVER: '#93959a', - REACT_IDLE: '#edf6ff', - REACT_IDLE_SELECTED: '#EDF6FF', - REACT_IDLE_HOVER: '#EDF6FF', - REACT_RENDER: '#9fc3f3', - REACT_RENDER_SELECTED: '#64A9F5', - REACT_RENDER_HOVER: '#2683E2', - REACT_COMMIT: '#ff718e', - REACT_COMMIT_SELECTED: '#FF5277', - REACT_COMMIT_HOVER: '#ed0030', - REACT_LAYOUT_EFFECTS: '#c88ff0', - REACT_LAYOUT_EFFECTS_SELECTED: '#934FC1', - REACT_LAYOUT_EFFECTS_HOVER: '#601593', - REACT_PASSIVE_EFFECTS: '#c88ff0', - REACT_PASSIVE_EFFECTS_SELECTED: '#934FC1', - REACT_PASSIVE_EFFECTS_HOVER: '#601593', - REACT_SCHEDULE: '#9fc3f3', - REACT_SCHEDULE_HOVER: '#2683E2', - REACT_SCHEDULE_CASCADING: '#ff718e', - REACT_SCHEDULE_CASCADING_HOVER: '#ed0030', - REACT_SUSPEND: '#a6e59f', - REACT_SUSPEND_HOVER: '#13bc00', - REACT_WORK_BORDER: '#ffffff', - TIME_MARKER_LABEL: '#18212b', -}); +// TODO Replace this with "export let" vars +export let COLORS = { + BACKGROUND: '', + PRIORITY_BACKGROUND: '', + PRIORITY_BORDER: '', + PRIORITY_LABEL: '', + FLAME_GRAPH_LABEL: '', + USER_TIMING: '', + USER_TIMING_HOVER: '', + REACT_IDLE: '', + REACT_IDLE_SELECTED: '', + REACT_IDLE_HOVER: '', + REACT_RENDER: '', + REACT_RENDER_SELECTED: '', + REACT_RENDER_HOVER: '', + REACT_COMMIT: '', + REACT_COMMIT_SELECTED: '', + REACT_COMMIT_HOVER: '', + REACT_LAYOUT_EFFECTS: '', + REACT_LAYOUT_EFFECTS_SELECTED: '', + REACT_LAYOUT_EFFECTS_HOVER: '', + REACT_PASSIVE_EFFECTS: '', + REACT_PASSIVE_EFFECTS_SELECTED: '', + REACT_PASSIVE_EFFECTS_HOVER: '', + REACT_RESIZE_BAR: '', + REACT_SCHEDULE: '', + REACT_SCHEDULE_HOVER: '', + REACT_SCHEDULE_CASCADING: '', + REACT_SCHEDULE_CASCADING_HOVER: '', + REACT_SUSPEND: '', + REACT_SUSPEND_HOVER: '', + REACT_WORK_BORDER: '', + TIME_MARKER_LABEL: '', +}; + +export function updateColorsToMatchTheme(): void { + const computedStyle = getComputedStyle((document.body: any)); + + COLORS = { + BACKGROUND: computedStyle.getPropertyValue('--color-background'), + PRIORITY_BACKGROUND: computedStyle.getPropertyValue( + '--color-scheduling-profiler-priority-background', + ), + PRIORITY_BORDER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-priority-border', + ), + PRIORITY_LABEL: computedStyle.getPropertyValue('--color-text'), + FLAME_GRAPH_LABEL: computedStyle.getPropertyValue( + '--color-scheduling-profiler-flame-graph-label', + ), + USER_TIMING: computedStyle.getPropertyValue( + '--color-scheduling-profiler-user-timing', + ), + USER_TIMING_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-user-timing-hover', + ), + REACT_IDLE: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-idle', + ), + REACT_IDLE_SELECTED: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-idle-selected', + ), + REACT_IDLE_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-idle-hover', + ), + REACT_RENDER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-render', + ), + REACT_RENDER_SELECTED: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-render-selected', + ), + REACT_RENDER_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-render-hover', + ), + REACT_COMMIT: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-commit', + ), + REACT_COMMIT_SELECTED: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-commit-selected', + ), + REACT_COMMIT_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-commit-hover', + ), + REACT_LAYOUT_EFFECTS: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-layout-effects', + ), + REACT_LAYOUT_EFFECTS_SELECTED: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-layout-effects-selected', + ), + REACT_LAYOUT_EFFECTS_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-layout-effects-hover', + ), + REACT_PASSIVE_EFFECTS: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-passive-effects', + ), + REACT_PASSIVE_EFFECTS_SELECTED: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-passive-effects-selected', + ), + REACT_PASSIVE_EFFECTS_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-passive-effects-hover', + ), + REACT_RESIZE_BAR: computedStyle.getPropertyValue('--color-resize-bar'), + REACT_SCHEDULE: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-schedule', + ), + REACT_SCHEDULE_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-schedule-hover', + ), + REACT_SCHEDULE_CASCADING: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-schedule-cascading', + ), + REACT_SCHEDULE_CASCADING_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-schedule-cascading-hover', + ), + REACT_SUSPEND: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-suspend', + ), + REACT_SUSPEND_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-suspend-hover', + ), + REACT_WORK_BORDER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-work-border', + ), + TIME_MARKER_LABEL: computedStyle.getPropertyValue('--color-text'), + }; +} diff --git a/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.css b/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.css deleted file mode 100644 index 60848641f4949..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.css +++ /dev/null @@ -1,10 +0,0 @@ -.ContextMenu { - position: absolute; - border-radius: 0.125rem; - background-color: #ffffff; - border: 1px solid #ccc; - box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2); - font-size: 11px; - overflow: hidden; - z-index: 10000002; -} diff --git a/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.js b/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.js deleted file mode 100644 index 8b09ef1510dcf..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.js +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 type {RegistryContextType} from './Contexts'; - -import * as React from 'react'; -import {useContext, useEffect, useLayoutEffect, useRef, useState} from 'react'; -import {createPortal} from 'react-dom'; -import {RegistryContext} from './Contexts'; - -import styles from './ContextMenu.css'; - -function repositionToFit(element: HTMLElement, pageX: number, pageY: number) { - const ownerWindow = element.ownerDocument.defaultView; - if (element !== null) { - if (pageY + element.offsetHeight >= ownerWindow.innerHeight) { - if (pageY - element.offsetHeight > 0) { - element.style.top = `${pageY - element.offsetHeight}px`; - } else { - element.style.top = '0px'; - } - } else { - element.style.top = `${pageY}px`; - } - - if (pageX + element.offsetWidth >= ownerWindow.innerWidth) { - if (pageX - element.offsetWidth > 0) { - element.style.left = `${pageX - element.offsetWidth}px`; - } else { - element.style.left = '0px'; - } - } else { - element.style.left = `${pageX}px`; - } - } -} - -const HIDDEN_STATE = { - data: null, - isVisible: false, - pageX: 0, - pageY: 0, -}; - -type Props = {| - children: (data: Object) => React$Node, - id: string, -|}; - -export default function ContextMenu({children, id}: Props) { - const {hideMenu, registerMenu} = useContext( - RegistryContext, - ); - - const [state, setState] = useState(HIDDEN_STATE); - - const bodyAccessorRef = useRef(null); - const containerRef = useRef(null); - const menuRef = useRef(null); - - useEffect(() => { - if (!bodyAccessorRef.current) { - return; - } - const ownerDocument = bodyAccessorRef.current.ownerDocument; - containerRef.current = ownerDocument.createElement('div'); - if (ownerDocument.body) { - ownerDocument.body.appendChild(containerRef.current); - } - return () => { - if (ownerDocument.body && containerRef.current) { - ownerDocument.body.removeChild(containerRef.current); - } - }; - }, [bodyAccessorRef, containerRef]); - - useEffect(() => { - const showMenuFn = ({data, pageX, pageY}) => { - setState({data, isVisible: true, pageX, pageY}); - }; - const hideMenuFn = () => setState(HIDDEN_STATE); - return registerMenu(id, showMenuFn, hideMenuFn); - }, [id]); - - useLayoutEffect(() => { - if (!state.isVisible || !containerRef.current) { - return; - } - - const menu = menuRef.current; - if (!menu) { - return; - } - - const hideUnlessContains: MouseEventHandler & - TouchEventHandler & - KeyboardEventHandler = event => { - if (event.target instanceof HTMLElement && !menu.contains(event.target)) { - hideMenu(); - } - }; - - const ownerDocument = containerRef.current.ownerDocument; - ownerDocument.addEventListener('mousedown', hideUnlessContains); - ownerDocument.addEventListener('touchstart', hideUnlessContains); - ownerDocument.addEventListener('keydown', hideUnlessContains); - - const ownerWindow = ownerDocument.defaultView; - ownerWindow.addEventListener('resize', hideMenu); - - repositionToFit(menu, state.pageX, state.pageY); - - return () => { - ownerDocument.removeEventListener('mousedown', hideUnlessContains); - ownerDocument.removeEventListener('touchstart', hideUnlessContains); - ownerDocument.removeEventListener('keydown', hideUnlessContains); - - ownerWindow.removeEventListener('resize', hideMenu); - }; - }, [state]); - - if (!state.isVisible) { - return
; - } else { - const container = containerRef.current; - if (container !== null) { - return createPortal( -
- {children(state.data)} -
, - container, - ); - } else { - return null; - } - } -} diff --git a/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.css b/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.css deleted file mode 100644 index 19fd8284a47cb..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.css +++ /dev/null @@ -1,20 +0,0 @@ -.ContextMenuItem { - display: flex; - align-items: center; - color: #333; - padding: 0.5rem 0.75rem; - cursor: default; - border-top: 1px solid #ccc; -} -.ContextMenuItem:first-of-type { - border-top: none; -} -.ContextMenuItem:hover, -.ContextMenuItem:focus { - outline: 0; - background-color: rgba(0, 136, 250, 0.1); -} -.ContextMenuItem:active { - background-color: #0088fa; - color: #fff; -} diff --git a/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.js b/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.js deleted file mode 100644 index 5750bd90cd18f..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 type {RegistryContextType} from './Contexts'; - -import * as React from 'react'; -import {useContext} from 'react'; -import {RegistryContext} from './Contexts'; - -import styles from './ContextMenuItem.css'; - -type Props = {| - children: React$Node, - onClick: () => void, - title: string, -|}; - -export default function ContextMenuItem({children, onClick, title}: Props) { - const {hideMenu} = useContext(RegistryContext); - - const handleClick: MouseEventHandler = event => { - onClick(); - hideMenu(); - }; - - return ( -
- {children} -
- ); -} diff --git a/packages/react-devtools-scheduling-profiler/src/context/Contexts.js b/packages/react-devtools-scheduling-profiler/src/context/Contexts.js deleted file mode 100644 index 46c742e06d0b8..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/context/Contexts.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 {createContext} from 'react'; - -export type ShowFn = ({|data: Object, pageX: number, pageY: number|}) => void; -export type HideFn = () => void; -export type OnChangeFn = boolean => void; - -const idToShowFnMap = new Map(); -const idToHideFnMap = new Map(); - -let currentHideFn: ?HideFn = null; -let currentOnChange: ?OnChangeFn = null; - -function hideMenu() { - if (typeof currentHideFn === 'function') { - currentHideFn(); - - if (typeof currentOnChange === 'function') { - currentOnChange(false); - } - } - - currentHideFn = null; - currentOnChange = null; -} - -function showMenu({ - data, - id, - onChange, - pageX, - pageY, -}: {| - data: Object, - id: string, - onChange?: OnChangeFn, - pageX: number, - pageY: number, -|}) { - const showFn = idToShowFnMap.get(id); - if (typeof showFn === 'function') { - // Prevent open menus from being left hanging. - hideMenu(); - - currentHideFn = idToHideFnMap.get(id); - showFn({data, pageX, pageY}); - - if (typeof onChange === 'function') { - currentOnChange = onChange; - onChange(true); - } - } -} - -function registerMenu(id: string, showFn: ShowFn, hideFn: HideFn) { - if (idToShowFnMap.has(id)) { - throw Error(`Context menu with id "${id}" already registered.`); - } - - idToShowFnMap.set(id, showFn); - idToHideFnMap.set(id, hideFn); - - return function unregisterMenu() { - idToShowFnMap.delete(id); - idToHideFnMap.delete(id); - }; -} - -export type RegistryContextType = {| - hideMenu: typeof hideMenu, - showMenu: typeof showMenu, - registerMenu: typeof registerMenu, -|}; - -export const RegistryContext = createContext({ - hideMenu, - showMenu, - registerMenu, -}); diff --git a/packages/react-devtools-scheduling-profiler/src/context/index.js b/packages/react-devtools-scheduling-profiler/src/context/index.js deleted file mode 100644 index c903d4f886409..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/context/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 {RegistryContext} from './Contexts'; -import ContextMenu from './ContextMenu'; -import ContextMenuItem from './ContextMenuItem'; -import useContextMenu from './useContextMenu'; - -export {RegistryContext, ContextMenu, ContextMenuItem, useContextMenu}; diff --git a/packages/react-devtools-scheduling-profiler/src/context/useContextMenu.js b/packages/react-devtools-scheduling-profiler/src/context/useContextMenu.js deleted file mode 100644 index 467c138f62d87..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/context/useContextMenu.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 type {OnChangeFn, RegistryContextType} from './Contexts'; - -import {useContext, useEffect} from 'react'; -import {RegistryContext} from './Contexts'; - -export default function useContextMenu({ - data, - id, - onChange, - ref, -}: {| - data: T, - id: string, - onChange: OnChangeFn, - ref: {+current: HTMLElement | null}, -|}) { - const {showMenu} = useContext(RegistryContext); - - useEffect(() => { - if (ref.current !== null) { - const handleContextMenu = (event: MouseEvent | TouchEvent) => { - event.preventDefault(); - event.stopPropagation(); - - const pageX = - (event: any).pageX || - (event.touches && (event: any).touches[0].pageX); - const pageY = - (event: any).pageY || - (event.touches && (event: any).touches[0].pageY); - - showMenu({data, id, onChange, pageX, pageY}); - }; - - const trigger = ref.current; - trigger.addEventListener('contextmenu', handleContextMenu); - - return () => { - trigger.removeEventListener('contextmenu', handleContextMenu); - }; - } - }, [data, id, showMenu]); -} diff --git a/packages/react-devtools-scheduling-profiler/src/createDataResourceFromImportedFile.js b/packages/react-devtools-scheduling-profiler/src/createDataResourceFromImportedFile.js new file mode 100644 index 0000000000000..3c7e74326094d --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/createDataResourceFromImportedFile.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) Facebook, Inc. and its 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 {createResource} from 'react-devtools-shared/src/devtools/cache'; +import {importFile} from './import-worker'; + +import type {Resource} from 'react-devtools-shared/src/devtools/cache'; +import type {ReactProfilerData} from './types'; +import type {ImportWorkerOutputData} from './import-worker/index'; + +export type DataResource = Resource; + +export default function createDataResourceFromImportedFile( + file: File, +): DataResource { + return createResource( + () => { + return new Promise((resolve, reject) => { + const promise = ((importFile( + file, + ): any): Promise); + promise.then(data => { + switch (data.status) { + case 'SUCCESS': + resolve(data.processedData); + break; + case 'INVALID_PROFILE_ERROR': + resolve(data.error); + break; + case 'UNEXPECTED_ERROR': + reject(data.error); + break; + } + }); + }); + }, + () => file, + {useWeakMap: true}, + ); +} diff --git a/packages/react-devtools-scheduling-profiler/src/hooks.js b/packages/react-devtools-scheduling-profiler/src/hooks.js deleted file mode 100644 index a9692010bed2c..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/hooks.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 { - // $FlowFixMe - unstable_createMutableSource as createMutableSource, - useLayoutEffect, - // $FlowFixMe - unstable_useMutableSource as useMutableSource, -} from 'react'; - -import { - updateDisplayDensity, - updateThemeVariables, -} from 'react-devtools-shared/src/devtools/views/Settings/SettingsContext'; -import {enableDarkMode} from './SchedulingProfilerFeatureFlags'; - -export type BrowserTheme = 'dark' | 'light'; - -const DARK_MODE_QUERY = '(prefers-color-scheme: dark)'; - -const getSnapshot = window => - window.matchMedia(DARK_MODE_QUERY).matches ? 'dark' : 'light'; - -const darkModeMutableSource = createMutableSource( - window, - () => window.matchMedia(DARK_MODE_QUERY).matches, -); - -const subscribe = (window, callback) => { - const mediaQueryList = window.matchMedia(DARK_MODE_QUERY); - mediaQueryList.addEventListener('change', callback); - return () => { - mediaQueryList.removeEventListener('change', callback); - }; -}; - -export function useBrowserTheme(): void { - const theme = useMutableSource(darkModeMutableSource, getSnapshot, subscribe); - - useLayoutEffect(() => { - const documentElements = [((document.documentElement: any): HTMLElement)]; - if (enableDarkMode) { - switch (theme) { - case 'light': - updateThemeVariables('light', documentElements); - break; - case 'dark': - updateThemeVariables('dark', documentElements); - break; - default: - throw Error(`Unsupported theme value "${theme}"`); - } - } else { - updateThemeVariables('light', documentElements); - } - }, [theme]); -} - -export function useDisplayDensity(): void { - useLayoutEffect(() => { - const documentElements = [((document.documentElement: any): HTMLElement)]; - updateDisplayDensity('comfortable', documentElements); - }, []); -} diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/import.worker.js b/packages/react-devtools-scheduling-profiler/src/import-worker/importFile.js similarity index 65% rename from packages/react-devtools-scheduling-profiler/src/import-worker/import.worker.js rename to packages/react-devtools-scheduling-profiler/src/import-worker/importFile.js index 118490e2effe5..1e0510b8539e0 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/import.worker.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/importFile.js @@ -10,7 +10,7 @@ import 'regenerator-runtime/runtime'; import type {TimelineEvent} from '@elg/speedscope'; -import type {ReactProfilerData} from '../types'; +import type {ImportWorkerOutputData} from './index'; import preprocessData from './preprocessData'; import {readInputData} from './readInputData'; @@ -18,18 +18,7 @@ import InvalidProfileError from './InvalidProfileError'; declare var self: DedicatedWorkerGlobalScope; -type ImportWorkerInputData = {| - file: File, -|}; - -export type ImportWorkerOutputData = - | {|status: 'SUCCESS', processedData: ReactProfilerData|} - | {|status: 'INVALID_PROFILE_ERROR', error: Error|} - | {|status: 'UNEXPECTED_ERROR', error: Error|}; - -self.onmessage = async function(event: MessageEvent) { - const {file} = ((event.data: any): ImportWorkerInputData); - +export async function importFile(file: File): Promise { try { const readFile = await readInputData(file); const events: TimelineEvent[] = JSON.parse(readFile); @@ -37,21 +26,21 @@ self.onmessage = async function(event: MessageEvent) { throw new InvalidProfileError('No profiling data found in file.'); } - self.postMessage({ + return { status: 'SUCCESS', processedData: preprocessData(events), - }); + }; } catch (error) { if (error instanceof InvalidProfileError) { - self.postMessage({ + return { status: 'INVALID_PROFILE_ERROR', error, - }); + }; } else { - self.postMessage({ + return { status: 'UNEXPECTED_ERROR', error, - }); + }; } } -}; +} diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerFeatureFlags.js b/packages/react-devtools-scheduling-profiler/src/import-worker/importFile.worker.js similarity index 64% rename from packages/react-devtools-scheduling-profiler/src/SchedulingProfilerFeatureFlags.js rename to packages/react-devtools-scheduling-profiler/src/import-worker/importFile.worker.js index 7558576c31781..b5088a839aedc 100644 --- a/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerFeatureFlags.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/importFile.worker.js @@ -3,8 +3,8 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow */ -export const enableDarkMode = false; +import * as importFileModule from './importFile'; + +export const importFile = importFileModule.importFile; diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/index.js b/packages/react-devtools-scheduling-profiler/src/import-worker/index.js new file mode 100644 index 0000000000000..3ce3a0ff93da6 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/index.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// This file uses workerize to load ./importFile.worker as a webworker and instanciates it, +// exposing flow typed functions that can be used on other files. + +import * as importFileModule from './importFile'; +import WorkerizedImportFile from './importFile.worker'; + +import type {ReactProfilerData} from '../types'; + +type ImportFileModule = typeof importFileModule; + +const workerizedImportFile: ImportFileModule = window.Worker + ? WorkerizedImportFile() + : importFileModule; + +export type ImportWorkerOutputData = + | {|status: 'SUCCESS', processedData: ReactProfilerData|} + | {|status: 'INVALID_PROFILE_ERROR', error: Error|} + | {|status: 'UNEXPECTED_ERROR', error: Error|}; + +export type importFileFunction = (file: File) => ImportWorkerOutputData; + +export const importFile = (file: File) => workerizedImportFile.importFile(file); diff --git a/packages/react-devtools-scheduling-profiler/src/index.css b/packages/react-devtools-scheduling-profiler/src/index.css deleted file mode 100644 index 0e798eef50713..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/index.css +++ /dev/null @@ -1,21 +0,0 @@ -html { - height: 100%; -} - -body { - height: 100%; - margin: 0; - font-family: var(--font-family-sans); - font-size: var(--font-size-sans-normal); - background-color: var(--color-background); - color: var(--color-text); -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} - -.Container { - height: 100%; -} \ No newline at end of file diff --git a/packages/react-devtools-scheduling-profiler/src/index.js b/packages/react-devtools-scheduling-profiler/src/index.js deleted file mode 100644 index b10a2b07efd6f..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/index.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 'regenerator-runtime/runtime'; - -import * as React from 'react'; -// $FlowFixMe Flow does not yet know about createRoot() -import {createRoot} from 'react-dom'; -import nullthrows from 'nullthrows'; -import App from './App'; - -import styles from './index.css'; - -const container = document.createElement('div'); -container.className = styles.Container; -container.id = 'root'; - -const body = nullthrows(document.body, 'Expect document.body to exist'); -body.appendChild(container); - -createRoot(container).render( - - - , -); diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js index 314be68e2888b..c69b982c47cdc 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js @@ -15,6 +15,7 @@ import type { } from './useCanvasInteraction'; import type {Rect, Size} from './geometry'; +import {COLORS} from '../content-views/constants'; import nullthrows from 'nullthrows'; import {Surface} from './Surface'; import {View} from './View'; @@ -38,14 +39,11 @@ type LayoutState = $ReadOnly<{| |}>; function getColorForBarState(state: ResizeBarState): string { - // Colors obtained from Firefox Profiler switch (state) { case 'normal': - return '#ccc'; case 'hovered': - return '#bbb'; case 'dragging': - return '#aaa'; + return COLORS.REACT_RESIZE_BAR; } throw new Error(`Unknown resize bar state ${state}`); } @@ -131,6 +129,7 @@ class ResizeBar extends View { } export class ResizableSplitView extends View { + _canvasRef: {current: HTMLCanvasElement | null}; _resizingState: ResizingState | null = null; _layoutState: LayoutState; @@ -139,9 +138,12 @@ export class ResizableSplitView extends View { frame: Rect, topSubview: View, bottomSubview: View, + canvasRef: {current: HTMLCanvasElement | null}, ) { super(surface, frame, noopLayout); + this._canvasRef = canvasRef; + this.addSubview(topSubview); this.addSubview(new ResizeBar(surface, frame)); this.addSubview(bottomSubview); @@ -279,6 +281,18 @@ export class ResizableSplitView extends View { } _handleMouseMove(interaction: MouseMoveInteraction) { + const cursorLocation = interaction.payload.location; + const resizeBarFrame = this._getResizeBar().frame; + + const canvas = this._canvasRef.current; + if (canvas !== null) { + if (rectContainsPoint(cursorLocation, resizeBarFrame)) { + canvas.style.cursor = 'ns-resize'; + } else { + canvas.style.cursor = 'default'; + } + } + const {_resizingState} = this; if (_resizingState) { this._resizingState = { diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js b/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js index b08d9bbbbb466..3b374aebbee79 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js @@ -175,15 +175,16 @@ export function useCanvasInteraction( return false; }; - document.addEventListener('mousemove', onDocumentMouseMove); - document.addEventListener('mouseup', onDocumentMouseUp); + const ownerDocument = canvas.ownerDocument; + ownerDocument.addEventListener('mousemove', onDocumentMouseMove); + ownerDocument.addEventListener('mouseup', onDocumentMouseUp); canvas.addEventListener('mousedown', onCanvasMouseDown); canvas.addEventListener('wheel', onCanvasWheel); return () => { - document.removeEventListener('mousemove', onDocumentMouseMove); - document.removeEventListener('mouseup', onDocumentMouseUp); + ownerDocument.removeEventListener('mousemove', onDocumentMouseMove); + ownerDocument.removeEventListener('mouseup', onDocumentMouseUp); canvas.removeEventListener('mousedown', onCanvasMouseDown); canvas.removeEventListener('wheel', onCanvasWheel); diff --git a/packages/react-devtools-scheduling-profiler/vercel.json b/packages/react-devtools-scheduling-profiler/vercel.json deleted file mode 100644 index 25f13ebe8852d..0000000000000 --- a/packages/react-devtools-scheduling-profiler/vercel.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "react-devtools-scheduling-profiler" -} diff --git a/packages/react-devtools-scheduling-profiler/webpack.config.js b/packages/react-devtools-scheduling-profiler/webpack.config.js deleted file mode 100644 index e30d4fda13db2..0000000000000 --- a/packages/react-devtools-scheduling-profiler/webpack.config.js +++ /dev/null @@ -1,128 +0,0 @@ -'use strict'; - -const {resolve} = require('path'); -const {DefinePlugin} = require('webpack'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); -const {getVersionString} = require('./buildUtils'); - -const NODE_ENV = process.env.NODE_ENV; -if (!NODE_ENV) { - console.error('NODE_ENV not set'); - process.exit(1); -} -const __DEV__ = NODE_ENV === 'development'; - -const TARGET = process.env.TARGET; -if (!TARGET) { - console.error('TARGET not set'); - process.exit(1); -} -const shouldUseDevServer = TARGET === 'local'; - -const builtModulesDir = resolve(__dirname, '..', '..', 'build', 'node_modules'); - -const DEVTOOLS_VERSION = getVersionString(); - -const imageInlineSizeLimit = 10000; - -const babelOptions = { - configFile: resolve( - __dirname, - '..', - 'react-devtools-shared', - 'babel.config.js', - ), - plugins: shouldUseDevServer - ? [resolve(builtModulesDir, 'react-refresh/babel')] - : [], -}; - -const config = { - mode: __DEV__ ? 'development' : 'production', - devtool: __DEV__ ? 'cheap-module-eval-source-map' : 'source-map', - entry: { - app: './src/index.js', - }, - resolve: { - alias: { - react: resolve(builtModulesDir, 'react'), - 'react-dom': resolve(builtModulesDir, 'react-dom'), - 'react-refresh': resolve(builtModulesDir, 'react-refresh'), - scheduler: resolve(builtModulesDir, 'scheduler'), - }, - }, - plugins: [ - new DefinePlugin({ - __DEV__, - __PROFILE__: false, - __EXPERIMENTAL__: true, - __VARIANT__: false, - 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, - }), - new HtmlWebpackPlugin({ - title: 'React Concurrent Mode Profiler', - }), - shouldUseDevServer && new ReactRefreshWebpackPlugin(), - ].filter(Boolean), - module: { - rules: [ - { - test: /\.worker\.js$/, - use: [ - 'worker-loader', - { - loader: 'babel-loader', - options: babelOptions, - }, - ], - }, - { - test: /\.js$/, - loader: 'babel-loader', - options: babelOptions, - }, - { - test: /\.css$/, - use: [ - { - loader: 'style-loader', - }, - { - loader: 'css-loader', - options: { - sourceMap: true, - modules: { - localIdentName: '[local]___[hash:base64:5]', - }, - }, - }, - ], - }, - { - test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], - loader: 'url-loader', - options: { - limit: imageInlineSizeLimit, - name: 'static/media/[name].[hash:8].[ext]', - }, - }, - ], - }, -}; - -if (shouldUseDevServer) { - config.devServer = { - hot: true, - port: 8081, - clientLogLevel: 'warning', - stats: 'errors-only', - }; -} else { - config.output = { - path: resolve(__dirname, 'dist'), - filename: '[name].js', - }; -} - -module.exports = config; diff --git a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.css b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.css index 20af7c096f059..4a8bca7073390 100644 --- a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.css +++ b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.css @@ -1,7 +1,9 @@ .ContextMenu { position: absolute; background-color: var(--color-context-background); + box-shadow: 1px 1px 2px var(--color-shadow); border-radius: 0.25rem; overflow: hidden; z-index: 10000002; + user-select: none; } \ No newline at end of file diff --git a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js index 16a893c3cc44f..466c4fdad6aa3 100644 --- a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js +++ b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js @@ -54,7 +54,9 @@ type Props = {| |}; export default function ContextMenu({children, id}: Props) { - const {registerMenu} = useContext(RegistryContext); + const {hideMenu, registerMenu} = useContext( + RegistryContext, + ); const [state, setState] = useState(HIDDEN_STATE); @@ -75,11 +77,11 @@ export default function ContextMenu({children, id}: Props) { }, []); useEffect(() => { - const showMenu = ({data, pageX, pageY}) => { + const showMenuFn = ({data, pageX, pageY}) => { setState({data, isVisible: true, pageX, pageY}); }; - const hideMenu = () => setState(HIDDEN_STATE); - return registerMenu(id, showMenu, hideMenu); + const hideMenuFn = () => setState(HIDDEN_STATE); + return registerMenu(id, showMenuFn, hideMenuFn); }, [id]); useLayoutEffect(() => { @@ -92,21 +94,17 @@ export default function ContextMenu({children, id}: Props) { if (container !== null) { const hideUnlessContains = event => { if (!menu.contains(event.target)) { - setState(HIDDEN_STATE); + hideMenu(); } }; - const hide = event => { - setState(HIDDEN_STATE); - }; - const ownerDocument = container.ownerDocument; ownerDocument.addEventListener('mousedown', hideUnlessContains); ownerDocument.addEventListener('touchstart', hideUnlessContains); ownerDocument.addEventListener('keydown', hideUnlessContains); const ownerWindow = ownerDocument.defaultView; - ownerWindow.addEventListener('resize', hide); + ownerWindow.addEventListener('resize', hideMenu); repositionToFit(menu, state.pageX, state.pageY); @@ -115,7 +113,7 @@ export default function ContextMenu({children, id}: Props) { ownerDocument.removeEventListener('touchstart', hideUnlessContains); ownerDocument.removeEventListener('keydown', hideUnlessContains); - ownerWindow.removeEventListener('resize', hide); + ownerWindow.removeEventListener('resize', hideMenu); }; } }, [state]); diff --git a/packages/react-devtools-shared/src/devtools/ContextMenu/Contexts.js b/packages/react-devtools-shared/src/devtools/ContextMenu/Contexts.js index 0d2e55106c89f..e2caf6eefcaa6 100644 --- a/packages/react-devtools-shared/src/devtools/ContextMenu/Contexts.js +++ b/packages/react-devtools-shared/src/devtools/ContextMenu/Contexts.js @@ -11,33 +11,53 @@ import {createContext} from 'react'; export type ShowFn = ({|data: Object, pageX: number, pageY: number|}) => void; export type HideFn = () => void; +export type OnChangeFn = boolean => void; const idToShowFnMap = new Map(); const idToHideFnMap = new Map(); -let currentHideFn = null; +let currentHide: ?HideFn = null; +let currentOnChange: ?OnChangeFn = null; function hideMenu() { - if (typeof currentHideFn === 'function') { - currentHideFn(); + if (typeof currentHide === 'function') { + currentHide(); + + if (typeof currentOnChange === 'function') { + currentOnChange(false); + } } + + currentHide = null; + currentOnChange = null; } function showMenu({ data, id, + onChange, pageX, pageY, }: {| data: Object, id: string, + onChange?: OnChangeFn, pageX: number, pageY: number, |}) { const showFn = idToShowFnMap.get(id); if (typeof showFn === 'function') { - currentHideFn = idToHideFnMap.get(id); + // Prevent open menus from being left hanging. + hideMenu(); + + currentHide = idToHideFnMap.get(id); + showFn({data, pageX, pageY}); + + if (typeof onChange === 'function') { + currentOnChange = onChange; + onChange(true); + } } } @@ -56,14 +76,9 @@ function registerMenu(id: string, showFn: ShowFn, hideFn: HideFn) { } export type RegistryContextType = {| - hideMenu: () => void, - showMenu: ({| - data: Object, - id: string, - pageX: number, - pageY: number, - |}) => void, - registerMenu: (string, ShowFn, HideFn) => Function, + hideMenu: typeof hideMenu, + showMenu: typeof showMenu, + registerMenu: typeof registerMenu, |}; export const RegistryContext = createContext({ diff --git a/packages/react-devtools-shared/src/devtools/ContextMenu/useContextMenu.js b/packages/react-devtools-shared/src/devtools/ContextMenu/useContextMenu.js index 1c713bae73dd5..150cb0766fc55 100644 --- a/packages/react-devtools-shared/src/devtools/ContextMenu/useContextMenu.js +++ b/packages/react-devtools-shared/src/devtools/ContextMenu/useContextMenu.js @@ -10,17 +10,19 @@ import {useContext, useEffect} from 'react'; import {RegistryContext} from './Contexts'; -import type {RegistryContextType} from './Contexts'; +import type {OnChangeFn, RegistryContextType} from './Contexts'; import type {ElementRef} from 'react'; export default function useContextMenu({ data, id, + onChange, ref, }: {| data: Object, id: string, - ref: {current: ElementRef<'div'> | null}, + onChange?: OnChangeFn, + ref: {current: ElementRef<*> | null}, |}) { const {showMenu} = useContext(RegistryContext); @@ -37,7 +39,7 @@ export default function useContextMenu({ (event: any).pageY || (event.touches && (event: any).touches[0].pageY); - showMenu({data, id, pageX, pageY}); + showMenu({data, id, onChange, pageX, pageY}); }; const trigger = ref.current; diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 59e10a5ef72e3..16bc56ae64f07 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -62,6 +62,7 @@ type Config = {| isProfiling?: boolean, supportsNativeInspection?: boolean, supportsReloadAndProfile?: boolean, + supportsSchedulingProfiler?: boolean, supportsProfiling?: boolean, supportsTraceUpdates?: boolean, |}; @@ -159,6 +160,7 @@ export default class Store extends EventEmitter<{| _supportsNativeInspection: boolean = true; _supportsProfiling: boolean = false; _supportsReloadAndProfile: boolean = false; + _supportsSchedulingProfiler: boolean = false; _supportsTraceUpdates: boolean = false; _unsupportedBridgeProtocol: BridgeProtocol | null = null; @@ -193,6 +195,7 @@ export default class Store extends EventEmitter<{| supportsNativeInspection, supportsProfiling, supportsReloadAndProfile, + supportsSchedulingProfiler, supportsTraceUpdates, } = config; this._supportsNativeInspection = supportsNativeInspection !== false; @@ -202,6 +205,9 @@ export default class Store extends EventEmitter<{| if (supportsReloadAndProfile) { this._supportsReloadAndProfile = true; } + if (supportsSchedulingProfiler) { + this._supportsSchedulingProfiler = true; + } if (supportsTraceUpdates) { this._supportsTraceUpdates = true; } @@ -414,6 +420,10 @@ export default class Store extends EventEmitter<{| ); } + get supportsSchedulingProfiler(): boolean { + return this._supportsSchedulingProfiler; + } + get supportsTraceUpdates(): boolean { return this._supportsTraceUpdates; } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js index a7ea9178db2b9..a2ada2bf77358 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js @@ -23,6 +23,7 @@ import useContextMenu from '../../ContextMenu/useContextMenu'; import {meta} from '../../../hydration'; import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache'; import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags'; +import HookNamesContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesContext'; import type {InspectedElement} from './types'; import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks'; @@ -52,6 +53,8 @@ export function InspectedElementHooksTree({ }: HooksTreeViewProps) { const {hooks, id} = inspectedElement; + const {loadHookNames: loadHookNamesFunction} = useContext(HookNamesContext); + // Changing parseHookNames is done in a transition, because it suspends. // This value is done outside of the transition, so the UI toggle feels responsive. const [parseHookNamesOptimistic, setParseHookNamesOptimistic] = useState( @@ -82,16 +85,17 @@ export function InspectedElementHooksTree({
hooks
- {(!parseHookNames || hookParsingFailed) && ( - - - - )} + {loadHookNamesFunction !== null && + (!parseHookNames || hookParsingFailed) && ( + + + + )} diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index 812ca916d8ebe..3c768c11ab97e 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -24,6 +24,7 @@ import {TreeContextController} from './Components/TreeContext'; import ViewElementSourceContext from './Components/ViewElementSourceContext'; import HookNamesContext from './Components/HookNamesContext'; import {ProfilerContextController} from './Profiler/ProfilerContext'; +import {SchedulingProfilerContextController} from 'react-devtools-scheduling-profiler/src/SchedulingProfilerContext'; import {ModalDialogContextController} from './ModalDialog'; import ReactLogo from './ReactLogo'; import UnsupportedBridgeProtocolDialog from './UnsupportedBridgeProtocolDialog'; @@ -218,36 +219,40 @@ export default function DevTools({ -
- {showTabBar && ( -
- - - {process.env.DEVTOOLS_VERSION} - -
- +
+ {showTabBar && ( +
+ + + {process.env.DEVTOOLS_VERSION} + +
+ +
+ )} + + - )} - - -
+ diff --git a/packages/react-devtools-shared/src/devtools/views/Icon.js b/packages/react-devtools-shared/src/devtools/views/Icon.js index ffa297610bdf5..c9ae931f5ee74 100644 --- a/packages/react-devtools-shared/src/devtools/views/Icon.js +++ b/packages/react-devtools-shared/src/devtools/views/Icon.js @@ -21,6 +21,7 @@ export type IconType = | 'flame-chart' | 'profiler' | 'ranked-chart' + | 'scheduling-profiler' | 'search' | 'settings' | 'store-as-global-variable' @@ -64,6 +65,9 @@ export default function Icon({className = '', type}: Props) { case 'ranked-chart': pathData = PATH_RANKED_CHART; break; + case 'scheduling-profiler': + pathData = PATH_SCHEDULING_PROFILER; + break; case 'search': pathData = PATH_SEARCH; break; @@ -136,6 +140,11 @@ const PATH_FLAME_CHART = ` const PATH_PROFILER = 'M5 9.2h3V19H5zM10.6 5h2.8v14h-2.8zm5.6 8H19v6h-2.8z'; +const PATH_SCHEDULING_PROFILER = ` + M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 + 16H5V9h14v10zm0-12H5V5h14v2zM7 11h5v5H7z +`; + const PATH_SEARCH = ` M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js index 9fda41499871e..17027f6a0c4ee 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js @@ -16,6 +16,7 @@ import ClearProfilingDataButton from './ClearProfilingDataButton'; import CommitFlamegraph from './CommitFlamegraph'; import CommitRanked from './CommitRanked'; import RootSelector from './RootSelector'; +import {SchedulingProfiler} from 'react-devtools-scheduling-profiler/src/SchedulingProfiler'; import RecordToggle from './RecordToggle'; import ReloadAndProfileButton from './ReloadAndProfileButton'; import ProfilingImportExportButtons from './ProfilingImportExportButtons'; @@ -26,6 +27,7 @@ import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/Set 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 {StoreContext} from '../context'; import styles from './Profiler.css'; @@ -41,8 +43,12 @@ function Profiler(_: {||}) { supportsProfiling, } = useContext(ProfilerContext); + const {supportsSchedulingProfiler} = useContext(StoreContext); + + let showRightColumn = true; + let view = null; - if (didRecordCommits) { + if (didRecordCommits || selectedTabID === 'scheduling-profiler') { switch (selectedTabID) { case 'flame-chart': view = ; @@ -50,6 +56,10 @@ function Profiler(_: {||}) { case 'ranked-chart': view = ; break; + case 'scheduling-profiler': + view = ; + showRightColumn = false; + break; default: break; } @@ -101,7 +111,9 @@ function Profiler(_: {||}) { currentTab={selectedTabID} id="Profiler" selectTab={selectTab} - tabs={tabs} + tabs={ + supportsSchedulingProfiler ? tabsWithSchedulingProfiler : tabs + } type="profiler" /> @@ -119,7 +131,7 @@ function Profiler(_: {||}) {
-
{sidebar}
+ {showRightColumn &&
{sidebar}
}
@@ -141,6 +153,17 @@ const tabs = [ }, ]; +const tabsWithSchedulingProfiler = [ + ...tabs, + null, // Divider/separator + { + id: 'scheduling-profiler', + icon: 'scheduling-profiler', + label: 'Scheduling', + title: 'Scheduling Profiler', + }, +]; + const NoProfilingData = () => (
No profiling data has been recorded.
diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js index 48f9aa11eefdb..3206fcb28f74a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js @@ -19,7 +19,8 @@ import {StoreContext} from '../context'; import type {ProfilingDataFrontend} from './types'; -export type TabID = 'flame-chart' | 'ranked-chart'; +// TODO (scheduling profiler) Should this be its own context? +export type TabID = 'flame-chart' | 'ranked-chart' | 'scheduling-profiler'; export type Context = {| // Which tab is selected in the Profiler UI? diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js index d6570dd5ab34b..94cd201d45759 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js @@ -19,13 +19,17 @@ import { prepareProfilingDataFrontendFromExport, } from './utils'; import {downloadFile} from '../utils'; +import {SchedulingProfilerContext} from 'react-devtools-scheduling-profiler/src/SchedulingProfilerContext'; import styles from './ProfilingImportExportButtons.css'; import type {ProfilingDataExport} from './types'; export default function ProfilingImportExportButtons() { - const {isProfiling, profilingData, rootID} = useContext(ProfilerContext); + const {isProfiling, profilingData, rootID, selectedTabID} = useContext( + ProfilerContext, + ); + const {importSchedulingProfilerData} = useContext(SchedulingProfilerContext); const store = useContext(StoreContext); const {profilerStore} = store; @@ -64,13 +68,13 @@ export default function ProfilingImportExportButtons() { } }, [rootID, profilingData]); - const uploadData = useCallback(() => { + const clickInputElement = useCallback(() => { if (inputRef.current !== null) { inputRef.current.click(); } }, []); - const handleFiles = useCallback(() => { + const importProfilerData = useCallback(() => { const input = inputRef.current; if (input !== null && input.files.length > 0) { const fileReader = new FileReader(); @@ -104,6 +108,13 @@ export default function ProfilingImportExportButtons() { } }, [modalDialogDispatch, profilerStore]); + const importSchedulingProfilerDataWrapper = event => { + const input = inputRef.current; + if (input !== null && input.files.length > 0) { + importSchedulingProfilerData(input.files[0]); + } + }; + return (
@@ -111,18 +122,26 @@ export default function ProfilingImportExportButtons() { ref={inputRef} className={styles.Input} type="file" - onChange={handleFiles} + onChange={ + selectedTabID === 'scheduling-profiler' + ? importSchedulingProfilerDataWrapper + : importProfilerData + } tabIndex={-1} />