From ab1efcfd5b13cb9f2f887c53e4cf07449a9c500c Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 22 Jul 2021 13:55:51 -0400 Subject: [PATCH] Import scheduling profiler into DevTools Profiler tab --- .circleci/config.yml | 52 ------ .../react-devtools-core/webpack.standalone.js | 33 +++- .../react-devtools-extensions/package.json | 2 +- .../react-devtools-extensions/src/main.js | 2 + .../src/parseHookNames/index.js | 5 +- .../parseHookNames/parseHookNames.worker.js | 7 + .../webpack.config.js | 38 +++-- packages/react-devtools-inline/package.json | 4 +- .../react-devtools-inline/src/frontend.js | 1 + .../react-devtools-inline/webpack.config.js | 33 +++- .../README.md | 16 +- .../buildUtils.js | 36 ---- .../package.json | 6 +- .../src/App.css | 19 --- .../src/App.js | 34 ---- .../src/CanvasPage.css | 8 +- .../src/CanvasPage.js | 17 +- .../src/EventTooltip.css | 17 +- .../src/ImportButton.js | 5 +- .../src/SchedulingProfiler.css | 75 ++------- .../src/SchedulingProfiler.js | 146 ++++++++-------- .../src/SchedulingProfilerContext.js | 62 +++++++ .../src/assets/logo.svg | 1 - .../src/assets/profilerBrowser.png | Bin 77466 -> 0 bytes .../src/assets/reactlogo.svg | 7 - .../src/content-views/FlamechartView.js | 2 +- .../src/content-views/constants.js | 157 ++++++++++++++---- .../src/context/ContextMenu.css | 10 -- .../src/context/ContextMenu.js | 143 ---------------- .../src/context/ContextMenuItem.css | 20 --- .../src/context/ContextMenuItem.js | 40 ----- .../src/context/Contexts.js | 87 ---------- .../src/context/index.js | 15 -- .../src/context/useContextMenu.js | 52 ------ .../src/createDataResourceFromImportedFile.js | 46 +++++ .../src/hooks.js | 71 -------- .../{import.worker.js => importFile.js} | 29 +--- .../importFile.worker.js} | 6 +- .../src/import-worker/index.js | 31 ++++ .../src/index.css | 21 --- .../src/index.js | 31 ---- .../src/view-base/ResizableSplitView.js | 22 ++- .../src/view-base/useCanvasInteraction.js | 9 +- .../vercel.json | 3 - .../webpack.config.js | 128 -------------- .../src/devtools/ContextMenu/ContextMenu.css | 2 + .../src/devtools/ContextMenu/ContextMenu.js | 20 +-- .../src/devtools/ContextMenu/Contexts.js | 39 +++-- .../devtools/ContextMenu/useContextMenu.js | 8 +- .../src/devtools/store.js | 10 ++ .../Components/InspectedElementHooksTree.js | 24 +-- .../src/devtools/views/DevTools.js | 59 ++++--- .../src/devtools/views/Icon.js | 9 + .../src/devtools/views/Profiler/Profiler.js | 29 +++- .../views/Profiler/ProfilerContext.js | 3 +- .../Profiler/ProfilingImportExportButtons.js | 31 +++- .../views/Settings/SettingsContext.js | 138 ++++++++++++++- .../src/devtools/views/TabBar.css | 8 + .../src/devtools/views/TabBar.js | 15 +- .../src/devtools/views/Tooltip.css | 1 + .../src/devtools/views/root.css | 60 ++++++- .../src/hookNamesCache.js | 9 +- yarn.lock | 45 +++++ 63 files changed, 931 insertions(+), 1128 deletions(-) delete mode 100644 packages/react-devtools-scheduling-profiler/buildUtils.js delete mode 100644 packages/react-devtools-scheduling-profiler/src/App.css delete mode 100644 packages/react-devtools-scheduling-profiler/src/App.js create mode 100644 packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js delete mode 100644 packages/react-devtools-scheduling-profiler/src/assets/logo.svg delete mode 100644 packages/react-devtools-scheduling-profiler/src/assets/profilerBrowser.png delete mode 100644 packages/react-devtools-scheduling-profiler/src/assets/reactlogo.svg delete mode 100644 packages/react-devtools-scheduling-profiler/src/context/ContextMenu.css delete mode 100644 packages/react-devtools-scheduling-profiler/src/context/ContextMenu.js delete mode 100644 packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.css delete mode 100644 packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.js delete mode 100644 packages/react-devtools-scheduling-profiler/src/context/Contexts.js delete mode 100644 packages/react-devtools-scheduling-profiler/src/context/index.js delete mode 100644 packages/react-devtools-scheduling-profiler/src/context/useContextMenu.js create mode 100644 packages/react-devtools-scheduling-profiler/src/createDataResourceFromImportedFile.js delete mode 100644 packages/react-devtools-scheduling-profiler/src/hooks.js rename packages/react-devtools-scheduling-profiler/src/import-worker/{import.worker.js => importFile.js} (65%) rename packages/react-devtools-scheduling-profiler/src/{SchedulingProfilerFeatureFlags.js => import-worker/importFile.worker.js} (64%) create mode 100644 packages/react-devtools-scheduling-profiler/src/import-worker/index.js delete mode 100644 packages/react-devtools-scheduling-profiler/src/index.css delete mode 100644 packages/react-devtools-scheduling-profiler/src/index.js delete mode 100644 packages/react-devtools-scheduling-profiler/vercel.json delete mode 100644 packages/react-devtools-scheduling-profiler/webpack.config.js 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 b0282be2f68289ae83da05816100e26a1b7b35cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 77466 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

f`ZWPEhg5ob7!T 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̓{K7am(nwR z$DhnjL%vgT6bI6KcN;{CmnhAJ@{Nt4Z6;-2cyAr&bK=`BQz$FWoU!+Jo&1g&e0SWO zD!MUj(~E|YLJ|J4c|2&M=q8^dH?$?4>+q}u{ro~@2+*+qE`5s-*4bE0&?8Yd>h*2h zrF8E2cd)0(JDVA6`(}jAwL=M^aH6|rz}rNd&(`ai6pfp*C-}PUSc4l7+`2LRnaPoP zvYU^V?OkRsNx}jMqZl~17;(_&Sfdv&D3__LDua&fI?ONq-~YbI`gZ0*s-K&Sy6y7f zoQcg6%=+0+j+JQ?1H?(5>Vy6whDe>t9Ik&~L2Kbl!PjogdSXj3!NFP#MB!I{^iD1yyF z%;G{8Kq9Wx{y~7E`ceD3p6A2bk0qnOSXgc_81=F6t>Ue>qs2VuBU`aOctqo2iOE8U z)Mqcw>6ZKJgoaEHaeS~7JhF-y^iGp1EB`!S7+D8wxOxxFriFw*NcSRMP%FMvtvTp+ z*m`M|rE>oC(AnNYhBT3miNIF=^Qv$gf%)yJC#$fR_(KsT2~S!E5%RKw{%H~4 zK#AU`hdKTaxSV8;@@#?}1{YmuZv+HKnm-PmZk+~|GTG|J0l(FHO>c3Z81|Xr1|F?7 z+uIEHT8O`(f1f+wQO!i#^spAZcieP?W;|;xCmglIq!&7&Y31#{IV{mx1GQm45ZSKs z8AF32>&M>CbPXywUs6JbD9>|NFZlaaZWz6X}OsX5@s6qo#bB?$2bY{W@P|T zMw)M2L`|rAr@+?VBjP5%;rxZH<-pRf(WWBuzO=w3nxF4lCKn4VQW({T8~Z1B3+{y?|MI+^L{&@-h6u2O4fZn>t1*6-}V1r&vV^s6R=h3WL!XN7+4@# zDKN0AFb$GipN*gf$&RB2s{&mS3=b|jfopAIPjnYq^w4~HTkCBLxL9Oeqq2D@#is4z z@KG?>#5P?sXR`@z3=ms$sKEx||gYRtb6R=Rqt`!RzFtL3Y2Lr#S?GZXt6 z+Q9)ug$X%mIvN@&JvYV{Bh=sV5+)x<)l3HLSzO_sw(j$k%GHeUxuE>syy5LTPgHN$ z^Oj+pOY}(3rUB7WEN!Y2h0WZ9u9J5Y9QS;;JWmdj3;-A+;e~anf;TUYWwT4=Wrc8Pi|q2?T{EE-V2N&{Ag2i?_9d2lB#$TvYo*r>i|jBB#%M z!BZ!vmmQG@38YDp=`kIkt;JmDi4w*BxQeBU7aK?L+kc|h%(f`maAh%7P1$dLzD7TC z&fUW{becGkQW8COVyXXRI0^rtZQMB2?slEDl^*{x3d>6l#Hb|ioxe^8(cnj0Uf!i1 zQQ2uarh5t!DzQ6I5^xmBEFAi_!Wj#=?Kv>-cBIxxJyODTW&x0ph6Kzr6>53#(+f;_ zP0)IS!5{o~nAK8@FsEMrRzIy~XD?$lWCXo)XK<%Bk}1WSJ#Y2Y3jk&DM!*Dy9a8s7VZ(4jO*Kyxc@6RK+&_s8;aO+2r9y$r(Bvn>5NKQmpnVYRd zTSlUwqdteVT#7J4zFU^gBNUyUxF;0A5!7%0XV6V%w!VwSa~%oghn*_#2hA*Pj^8E4 zAK3Qo#Nds%&A1637h(0|^>?AmDMLO$@BuwS{1(HR;;7qSw@iw@gC8k4CDMy16|MXX zQ@*#`l6`U6sMF^t+8mHt9HnNAU9MwfY)LBnlCsNfO@Qi0<6qwDsYQR9%^dVINk1)w z!|1vDcnJh;rtvffo*T}azUjuj6Ky(gQ;V3ZAC{aNeses5XPkf8I}Eu9m7ynvX(rkC zQH&%|#<+3L`udsdME(eSjo{+c+t$9#sDI8LaPgdlRReLDbe(`|yNJE*#g#XW_)JXp z_r+pj*&0J3C~$q+D~K#ojb*jl-jFyLWTWFol5u%o8Cx1D!QKG{rYNIcrtoo@-gWTU1AvA`N>6Wt z^e*r-tf_hn>xog7ke_2{46%`(rd#MTt}7rtcBOZ3_=zteeBS!Lc= z%3`*Qc8X(qN}0`N?Q$V+5x`r0cNrs2xep~aeEu2}mJ1eS%I1~%x*ahY9F&=_6A*2% zVO#Pz$Er6&;ArU39gZ3F^A5Ad1i-XhJwO{mu;b6rVq&r-Pkd3dCgbj^Wlfv@B=l!_ zA%r%xV6X#fNQkw(xHEewcj?0ZL)7k;RKyLAy<(zhUqPVIrq#28XNKz!dPj!VN(C?t zW#^xCk1x*u>S1R*NA7*fim#6tzJlP7X!LmMw4;w^CE+UB!IF~~g0m*|vW2n*v7EK2 z?zc%;tzx2epTk^X2B+#bpKFagv8#w38=&&z6YWwEW0_z_qhD`ZAf_I%hPM@!Njv92 zC$T!Ut={v<__bFAu*bXh4DO_ax4Pp-(#hQ?;~%Zko1NrN`@Aq7_Kthd^hlL%HX?F? zFJ@JR)m!^OpcZmSRI68E>|GZsuTryfc9}a#U{cRR$SouIomDH6jEjlNB^1F3e(6Hr zr#E?H4ie+DN0?f+i)Jhw8|w{oqtwbG?>H~o$*R@Cs-%=0;-1^Ljbv%X8Y&ad%rTE^Z%PEvqCGgQ7-b{gO z#x*5DqMc~HAmQFA3Dy0q&y|kzKSO4+K!0P(pKDC>=gxQejQLBvm^!e0G*F^ghH1Iu`Q4I7lZ zlWnS4Kj+%l$lJ{;h#Wsi7?dMdM0Y$ad||&~hw?Q69Yp;8tO#=euQc5D-;%^8d3cqp zWC)3^uBrY#Q8&NsQ0V_;*WFxdjZH8l8H?KErkf|rR~FbD2OOG`?ity9$)h7*JArvb z-;NRp`xoJW`Boa*X_pvJ)f}eSFgZ!(t&XK}IL-V5V!+&6% zd*h?5Z^3|35G~F9R5?0UnrB))Kb~f3f>p=kuX?zUJR zn$AMPiPBwvg@f!8APr5G`rz-p!NlpMtnBe_w=MZ(Wbc@0QH(pXY`ZuzzaSr~Mcyk5 zJlo0uQMQsnXNL%5Ny8*YYA%q%38ZN!RzLkBRoB>Dp+^U+NY>U-nONAbERRETaAcuJ zzgK4=b-B3wc&WL|GgUTV`(k5aCyn~sCpB|T*_=#4M?=*qcBj@s<3gGwQc6K%WJ9?y zqO6T?m*{$!gFNHKU7_CJ6Heh@l@`z9WtR&3EpBC%l)@tF#(k`gtlZxhP5dNC=&P2> zlbTpoR-IvowtDai#66KQ7bnrok_N95tg9qiFI?Q}r#%(|hV6OtvP36?tTm)0{Th{i zdTu)uuuFT(jy3Zm;T5>X5;b&xbCub_j@mTzt*{;RKAFzqx!q`|ILflu7Ei zs)R`9{+&$7&ls0U5wT7_{$>CgRTG})ymR6$3K~Tq#VTP>EQhCC)nz{#trST{@s}?( zR-4VGc7RfLXL@d$h02I@4YC!~y0Lg!K(_v+Jw7}_ znhG~(h!Ee2yHfFrtv-X<*Ott|>Z0V$Rb`>WEK8<`0QJnoBMl9g`1H^_St4EmT(X7Z z7U~Z0vR8xm0Vc)Y!6FD3{Mq9BL_JT{Dgm%mB3;ROxEOo0+3L3QYMLeG;h4wW)3hss zd);X-bzLaKfJPuim&#zy4|URvvk}$exv+xFEGi%kto7{UrtKh&qPh9PzL+KtmrZ!O z2L5dtI!9$Xu0iRQYXf;9ZO1W^cbkCla7lIFYj39ZNNI)p_Dm@l;P9&|gsb*a-Z2D! zm(b`8ne2f*;qdD?J~qKYh$P9@eBV+6DK!3bp>eJ}ViyD9j+D4Tq_xc}R_XhC;*(?w ze7vwv6Dk+h9O!csxAUhjrD6qGa2!TZa+*2FKl{=-7F{#uHT4(M zhuS`!$EsnWEKhAPDeruGhHo`@V82{u^byVt>qtamKA1At>q6CxdC^TO@PZnV=Rt^~ zjH%3=XgRsDj3x$2nR2n$B%S>Fq}QY|h-0=swf$7^tLTk8FuwB=%QgZG{PaIsh>@^g0ezVd=Mju3dU@%iaM{ z5xkfp6waZl7`cQjtANRTrp*yu<^f!7yQkDO3Qs3rvmYLPoipnAc>O``PT-#t&`wYr zBjdU8v#sNo$jAUm_K&9#w;VXwVR$y8H-FOlGLLmvL)dE2E9H=!+Jg-QJQ_}K9i?;c zYU8wn7)hKR-eyQ}1!{bZnw~aq+;h3fDwFZYLfn-Js~5I&H)&ZvU197?+T7rV$=$Qm z;RlP)o}k7Tr4o2zbL$5v1%FJ3zE(}E)Fw|vGPjhy#asAXNr2dKhR{*pJV7%((@}ne z5H-hx2$XEkrTNs5^n19Ze$l5!bI_5Q>nzF#;u8Fhs=Io6P5RkrpjN9fSJHbi=jxqC z%eMW5od(Y(NXna3%zHIISA5VbNl7i+a(9H*dUxDN$;F2Qu2YSY(*Yg~fSX7w+Q-~< zyN^gL<-Cb*LEWVLpd13TCh6C}MyHCFuM>Y9LziPWxq$U8pmkg$<;RXlu%R6)=GKkH zo7KG6xTbByR8d4KdAyBt{MZa1>*S={32*vgF;B!Sq%rc_Gs+%bgQcm}83pWaTiGyg z!~RXXz{%FKj}f`wj9N|dp4@=>HEZmq7sQX{P(<-c*~>_dq}2%_xS0bs7TfMJlq>Xr z(|s(cEz-5eE5wHs)%{V4rD-9@aJI=%{iKzp0YCSmda8Oi@(1AF5Y}`m3iOurvupiM z75%>=^FK5t6-e)C*{G{J$8B zO>1o#-PUs00$-dymtAng9jrZSqnyT5wtGXuz|XfV8^ZAMdLto$>*zcmy zv~KXlH@%U9O|#{;99~>AcrnY;Fy|?HVO%SPwWuk()S%S+S$&T=71vcfxx&AKPA}~a z#MM~F%Ep45`*V^PtYh~`1w}=q+oC*^F_GP2Wf}f_h#UQucF2jtnyfBe*KP$4TJbWxIPn+a!2v}xslM?EayDi+-7K+}sEiRlwBy>*g~e8O zgi3kbj69EfiApzMmtarleY{Z(ZiVldN)$Y%oBpn{_cjN;2Me7ze024y^^W1XqcF2D zUN<>dJlDlz2^6eaD9GvDmnE%$TRsceKMxA94%KpzGqfEOVG2*5-8RufcQ=PhFkf-h zvky8t5k|~MGuw$)!u_u~Zwl>%*crbD``B@h4V*SGOe-T6NucKlXyr(-dU#&Lh&Uz3 zl4m}>3W@kB^Cfj-Mm7)|_)vl`HWUAm}ynYb@$2`7#j;F@Z+lH)Uf zvJn+ZK$5q72rmDJD6AzQ&Y(ZyP@l&hWEM5poaC63%it~j{6#AsBa+1iHNHi-968&M zPn&|iW!iU#Hxzk4x@dqjG}?5YIAyE&@Xd`Z7*4{S+}b*dx@l;bkM$otu-wn)1hVU` z`>|S=*6yE&f2crpGDY}_&F0J9pNdbZ<$ep4wz_Chq1$s~f@ED%)0pW|Tv_RvGADnv zY(p8Nxt%;b_LD972;suh%gQ@#%I9t2sHo$b?sr zkY%DdXK=~qWxV8+D0XOY`9b=pDHJ70Uq53h74qkm>WOmGk}%*a*+{*X1neQF+BshF z7nX7WLDfDKVXC~l4+>M*5=Yq*rYZ!KZHav$uOPsL8>bS39KNyNvu`<%bZE9+_8yFC zt6l9WeJ?%}pjEX{J2Y-b*TzmiiJi=zHT9)-KG*={k%C)uCyQ9$6;!dC&l8G+4!KUh z@W1KH{h0C2ZK*MefiFG5r{ zLojMhi_FR&kVHUK93i@l7{@$u*RItC-m@+dod?}1Wi zPP_zo9K^EDvl(BTA}E7R{_h3PZ+o*`)+Ie$ec?cZ>Kn49{Z=O!t8_kc*;)|QyyRPw z_oqrCmv>&?L4Lu72m|yC$E$A!?xHzORmDgp!z|=jpN{mcVx+FQ+4~=KG=Hz(`8_1G z<41wx=DL<64!polBsA+0U%x4Zf+C7@j*%4nVrm_jVs?T3nmV?Cq7cXhgnojU%@wND zdUl0MzV_#Uo>#RhW5dNitL{qln{%siqf6qsZtA+3zQ!1}`erVNDX@i5;b2EKW&b|5 zm;^)@@@aufdwpCZ>Gv3_|N`}RBwlcRmiirDg8C~E>EX<*b54W9NphCRHS5I z;bra05}bYj@5t#m3F_^w-y0}7pMANw5(7&lFgzX@YVemYdD87V@Gb#{dQ9k?a;onH zS@*Pk)nKYiz$GDNHZhcL(nLXyCN$d0y| zTv$I<&nN|p!w)m54?34qmkq=~5;o$=-~ayUJv=FKu>X@jYHT{H5};7|?!z?({4~|x z#xmpyk<#KwGkZo82BMQ3dl1p%Pg^z^)0G zz9V(7o%?{)q5}qf_rAYkJ1B8L;)?q-$I62Ftic<7Uc$+~0=W-M&K<_KV&YVUgpGKZ z!+}D|+Um_pYyLefy3M}Llrq`;43PzBj4A4}EEgD6TQ}!zYq&e(YuK$o&50AfXwTac z4})O&Hnc;UVD|oe%!qd%&k&zz<*s-^yvtWx&|(~N=S>P(o2o5(m-D2{$crqIOMH_W z_iw<#X@hS5V(<3iB_erQg<+hhPMem5YKyn$4aR!~FB896)XC;X7u}I~7*F>5_MSk5 z9yQU@TNmC=?LZYLN2&?l#a+Dkxej-GH87WPq`$5cU^4Npn;+1cT; z#YC%98$S&{OLgWmF7Huws+*57eY{|ZT>EmlK`Z<^+o_}WDv#>g`n105c3bp+}-_7;dz-od<&sQmcdiqU{!F z@bm5#r5IcMV@bUna3eqdUR1s0TuLDR(Eh1GkMj31X~ve@6H1WqosoyWbn?k4@y`v) zYo1p-{;YcD;^D7p%+KZfiS?;K)U*a0@VLQ(>JGbvPDw{ie~2uwvSu6Y)&<{a_wP2* zDBY5krP1T~Ef0PR2AAa7HP1Yc#eWsVVJBNyxxB)ikx8Yk#$}Xtugd9-Z5C?+3iz-oCIE#v7uiYV{Ag^Y(xm zWgS{7of&K#RC&w*zHrL)ce{|Kp~9jH2xFVPa!Q9Y^Fs}#qmj*!)MbC(uNyoE4FlbW@-K&6 zO2k|%RT17QoN>Kn70=YhYO_p=oo}vmX?zSqVzWrExD=1$#rxm1P}9VmLX!ryUP)~X zZC~p(lYxeViBr{H4FLQs=evp&v`%Tw&E#{{Tl!-N=iooO+*RM z`;KdWGM0ty)5#?%1-E^pTfLVIa(ldh{XG4@i?Y9f?|6PMf13ZMI!pb<|3yhtzxhwo z_y1ku{hvXoGeGlS%ijMpC;va@#Ppe`iplRlG)Eumo?h^SKBkG7q}cwu(d+9NKdRJz H@z4JP4@+@R 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} />