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|}) => (
-
-
-
-
-
Welcome!
-
- Click the import button
- to import a Chrome
- performance profile.
-
-
+
+ -
+ Open a website that's built with the
+
+ profiling build of ReactDOM
+
+ .
+
+ -
+ Open the "Performance" tab in Chrome and record some performance data.
+
+ -
+ Click the "Save profile..." button in Chrome to export the data.
+
+ -
+ Import the data into the profiler:
+
+
+ Import
+
+
+
);
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}
/>