[DRAFT] Import scheduling profiler into DevTools Profiler (#21897)

This commit is contained in:
Brian Vaughn
2021-07-22 13:58:57 -04:00
committed by GitHub
parent ae5d26154b
commit f4161c3ec7
63 changed files with 935 additions and 1132 deletions
-52
View File
@@ -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:
@@ -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$/,
@@ -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",
+2
View File
@@ -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;
@@ -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';
@@ -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;
@@ -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}},
},
],
},
};
+3 -1
View File
@@ -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"
}
}
+1
View File
@@ -24,6 +24,7 @@ export function createStore(bridge: FrontendBridge): Store {
return new Store(bridge, {
checkBridgeProtocolCompatibility: true,
supportsTraceUpdates: true,
supportsSchedulingProfiler: true,
});
}
@@ -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$/,
@@ -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`.
@@ -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,
};
@@ -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",
@@ -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);
}
-34
View File
@@ -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 (
<div className={styles.DevTools}>
<div className={styles.TabContent}>
<SchedulingProfiler />
</div>
</div>
);
}
@@ -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;
}
@@ -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,
});
@@ -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;
@@ -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<HTMLInputElement | null>(null);
const handleFiles = useCallback(() => {
@@ -51,6 +51,7 @@ export default function ImportButton({onFileSelect}: Props) {
/>
<Button onClick={uploadData} title="Load profile...">
<ButtonIcon type="import" />
{children}
</Button>
</>
);
@@ -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);
.WelcomeInstructionsList {
}
.VRule {
height: 20px;
width: 1px;
border-left: 1px solid var(--color-border);
padding-left: 0.25rem;
.WelcomeInstructionsListItem {
display: flex;
align-items: center;
line-height: 1.5rem;
counter-increment: li;
}
.WelcomeInstructionsListItem::before {
content: counter(li);
margin-right: 0.5rem;
}
.WelcomeInstructionsListItemLink {
color: var(--color-link);
margin-left: 0.25rem;
}
.Spacer {
flex: 1;
}
.Link {
color: var(--color-button);
}
.ScreenshotWrapper {
max-width: 30rem;
padding: 0 1rem;
margin-bottom: 2rem;
}
.Screenshot {
width: 100%;
border-radius: 0.4em;
border: 2px solid var(--color-border);
}
.AppName {
font-size: var(--font-size-sans-large);
margin-right: 0.5rem;
user-select: none;
}
@media screen and (max-width: 350px) {
.AppName {
display: none;
}
}
@media screen and (max-height: 600px) {
.ScreenshotWrapper {
display: none;
}
.ImportButtonLabel {
margin-left: 0.25rem;
}
@@ -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<void, File, ReactProfilerData | Error>;
function createDataResourceFromImportedFile(file: File): DataResource {
return createResource(
() => {
return new Promise<ReactProfilerData | Error>((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<DataResource | null>(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<string>(theme);
useLayoutEffect(() => {
updateColorsToMatchTheme();
setKey(deferredTheme);
}, [deferredTheme]);
return (
<div className={styles.SchedulingProfiler}>
<div className={styles.Toolbar}>
<ReactLogo />
<span className={styles.AppName}>Concurrent Mode Profiler</span>
<div className={styles.VRule} />
<ImportButton onFileSelect={handleFileSelect} />
<div className={styles.Spacer} />
</div>
<div className={styles.Content}>
{dataResource ? (
<Suspense fallback={<ProcessingData />}>
<DataResourceComponent
dataResource={dataResource}
onFileSelect={handleFileSelect}
/>
</Suspense>
) : (
<Welcome onFileSelect={handleFileSelect} />
)}
</div>
<div className={styles.Content}>
{schedulingProfilerData ? (
<Suspense fallback={<ProcessingData />}>
<DataResourceComponent
dataResource={schedulingProfilerData}
key={key}
onFileSelect={importSchedulingProfilerData}
/>
</Suspense>
) : (
<Welcome onFileSelect={importSchedulingProfilerData} />
)}
</div>
);
}
const Welcome = ({onFileSelect}: {|onFileSelect: (file: File) => void|}) => (
<div className={styles.EmptyStateContainer}>
<div className={styles.ScreenshotWrapper}>
<img
src={profilerBrowser}
className={styles.Screenshot}
alt="Profiler screenshot"
/>
</div>
<div className={styles.Header}>Welcome!</div>
<div className={styles.Row}>
Click the import button
<ImportButton onFileSelect={onFileSelect} /> to import a Chrome
performance profile.
</div>
</div>
<ol className={styles.WelcomeInstructionsList}>
<li className={styles.WelcomeInstructionsListItem}>
Open a website that's built with the
<a
className={styles.WelcomeInstructionsListItemLink}
href="https://reactjs.org/link/profiling"
rel="noopener noreferrer"
target="_blank">
profiling build of ReactDOM
</a>
.
</li>
<li className={styles.WelcomeInstructionsListItem}>
Open the "Performance" tab in Chrome and record some performance data.
</li>
<li className={styles.WelcomeInstructionsListItem}>
Click the "Save profile..." button in Chrome to export the data.
</li>
<li className={styles.WelcomeInstructionsListItem}>
Import the data into the profiler:
<br />
<ImportButton onFileSelect={onFileSelect}>
<span className={styles.ImportButtonLabel}>Import</span>
</ImportButton>
</li>
</ol>
);
const ProcessingData = () => (
@@ -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<Context>(
((null: any): Context),
);
SchedulingProfilerContext.displayName = 'SchedulingProfilerContext';
type Props = {|
children: React$Node,
|};
function SchedulingProfilerContextController({children}: Props) {
const [
schedulingProfilerData,
setSchedulingProfilerData,
] = useState<DataResource | null>(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 (
<SchedulingProfilerContext.Provider value={value}>
{children}
</SchedulingProfilerContext.Provider>
);
}
export {SchedulingProfilerContext, SchedulingProfilerContextController};
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

@@ -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(
@@ -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'),
};
}
@@ -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;
}
@@ -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<RegistryContextType>(
RegistryContext,
);
const [state, setState] = useState(HIDDEN_STATE);
const bodyAccessorRef = useRef<HTMLDivElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const menuRef = useRef<HTMLDivElement | null>(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 <div ref={bodyAccessorRef} />;
} else {
const container = containerRef.current;
if (container !== null) {
return createPortal(
<div ref={menuRef} className={styles.ContextMenu}>
{children(state.data)}
</div>,
container,
);
} else {
return 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;
}
@@ -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<RegistryContextType>(RegistryContext);
const handleClick: MouseEventHandler = event => {
onClick();
hideMenu();
};
return (
<div
className={styles.ContextMenuItem}
onClick={handleClick}
onTouchEnd={handleClick}>
{children}
</div>
);
}
@@ -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<string, ShowFn>();
const idToHideFnMap = new Map<string, HideFn>();
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<RegistryContextType>({
hideMenu,
showMenu,
registerMenu,
});
@@ -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};
@@ -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<T>({
data,
id,
onChange,
ref,
}: {|
data: T,
id: string,
onChange: OnChangeFn,
ref: {+current: HTMLElement | null},
|}) {
const {showMenu} = useContext<RegistryContextType>(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]);
}
@@ -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<void, File, ReactProfilerData | Error>;
export default function createDataResourceFromImportedFile(
file: File,
): DataResource {
return createResource(
() => {
return new Promise<ReactProfilerData | Error>((resolve, reject) => {
const promise = ((importFile(
file,
): any): Promise<ImportWorkerOutputData>);
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},
);
}
@@ -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);
}, []);
}
@@ -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<ImportWorkerOutputData> {
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,
});
};
}
}
};
}
@@ -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;
@@ -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);
@@ -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%;
}
@@ -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(
<React.StrictMode>
<App />
</React.StrictMode>,
);
@@ -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 = {
@@ -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);
@@ -1,3 +0,0 @@
{
"name": "react-devtools-scheduling-profiler"
}
@@ -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;
@@ -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;
}
@@ -54,7 +54,9 @@ type Props = {|
|};
export default function ContextMenu({children, id}: Props) {
const {registerMenu} = useContext<RegistryContextType>(RegistryContext);
const {hideMenu, registerMenu} = useContext<RegistryContextType>(
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]);
@@ -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<string, ShowFn>();
const idToHideFnMap = new Map<string, HideFn>();
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<RegistryContextType>({
@@ -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<RegistryContextType>(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;
+10
View File
@@ -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;
}
@@ -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({
<div className={styles.HooksTreeView}>
<div className={styles.HeaderRow}>
<div className={styles.Header}>hooks</div>
{(!parseHookNames || hookParsingFailed) && (
<Toggle
className={hookParsingFailed ? styles.ToggleError : null}
isChecked={parseHookNamesOptimistic}
isDisabled={parseHookNamesOptimistic || hookParsingFailed}
onChange={handleChange}
title={toggleTitle}>
<ButtonIcon type="parse-hook-names" />
</Toggle>
)}
{loadHookNamesFunction !== null &&
(!parseHookNames || hookParsingFailed) && (
<Toggle
className={hookParsingFailed ? styles.ToggleError : null}
isChecked={parseHookNamesOptimistic}
isDisabled={parseHookNamesOptimistic || hookParsingFailed}
onChange={handleChange}
title={toggleTitle}>
<ButtonIcon type="parse-hook-names" />
</Toggle>
)}
<Button onClick={handleCopy} title="Copy to clipboard">
<ButtonIcon type="copy" />
</Button>
+32 -27
View File
@@ -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({
<HookNamesContext.Provider value={hookNamesContext}>
<TreeContextController>
<ProfilerContextController>
<div className={styles.DevTools} ref={devToolsRef}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={setTab}
tabs={tabs}
type="navigation"
<SchedulingProfilerContextController>
<div className={styles.DevTools} ref={devToolsRef}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={setTab}
tabs={tabs}
type="navigation"
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={componentsPortalContainer}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler
portalContainer={profilerPortalContainer}
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={componentsPortalContainer}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler portalContainer={profilerPortalContainer} />
</div>
</div>
</SchedulingProfilerContextController>
</ProfilerContextController>
</TreeContextController>
</HookNamesContext.Provider>
@@ -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
@@ -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 = <CommitFlamegraph />;
@@ -50,6 +56,10 @@ function Profiler(_: {||}) {
case 'ranked-chart':
view = <CommitRanked />;
break;
case 'scheduling-profiler':
view = <SchedulingProfiler />;
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"
/>
<RootSelector />
@@ -119,7 +131,7 @@ function Profiler(_: {||}) {
<ModalDialog />
</div>
</div>
<div className={styles.RightColumn}>{sidebar}</div>
{showRightColumn && <div className={styles.RightColumn}>{sidebar}</div>}
<SettingsModal />
</div>
</SettingsModalContextController>
@@ -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 = () => (
<div className={styles.Column}>
<div className={styles.Header}>No profiling data has been recorded.</div>
@@ -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?
@@ -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 (
<Fragment>
<div className={styles.VRule} />
@@ -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}
/>
<a ref={downloadRef} className={styles.Input} />
<Button
disabled={isProfiling}
onClick={uploadData}
onClick={clickInputElement}
title="Load profile...">
<ButtonIcon type="import" />
</Button>
<Button
disabled={isProfiling || !profilerStore.didRecordCommits}
disabled={
isProfiling ||
!profilerStore.didRecordCommits ||
selectedTabID === 'scheduling-profiler'
}
onClick={downloadData}
title="Save profile...">
<ButtonIcon type="export" />
@@ -411,11 +411,16 @@ export function updateThemeVariables(
updateStyleHelper(theme, 'color-record-active', documentElements);
updateStyleHelper(theme, 'color-record-hover', documentElements);
updateStyleHelper(theme, 'color-record-inactive', documentElements);
updateStyleHelper(theme, 'color-resize-bar', documentElements);
updateStyleHelper(theme, 'color-color-scroll-thumb', documentElements);
updateStyleHelper(theme, 'color-color-scroll-track', documentElements);
updateStyleHelper(theme, 'color-search-match', documentElements);
updateStyleHelper(theme, 'color-shadow', documentElements);
updateStyleHelper(theme, 'color-search-match-current', documentElements);
updateStyleHelper(
theme,
'color-scheduling-profiler-flame-graph-label',
documentElements,
);
updateStyleHelper(
theme,
'color-selected-tree-highlight-active',
@@ -426,6 +431,137 @@ export function updateThemeVariables(
'color-selected-tree-highlight-inactive',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-priority-background',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-priority-border',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-user-timing',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-user-timing-hover',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-idle',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-idle-selected',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-idle-hover',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-render',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-render-selected',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-render-hover',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-commit',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-commit-selected',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-commit-hover',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-layout-effects',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-layout-effects-selected',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-layout-effects-hover',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-passive-effects',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-passive-effects-selected',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-passive-effects-hover',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-schedule',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-schedule-hover',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-schedule-cascading',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-schedule-cascading-hover',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-suspend',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-suspend-hover',
documentElements,
);
updateStyleHelper(
theme,
'color-scheduling-profiler-react-work-border',
documentElements,
);
updateStyleHelper(theme, 'color-shadow', documentElements);
updateStyleHelper(theme, 'color-tab-selected-border', documentElements);
updateStyleHelper(theme, 'color-text', documentElements);
updateStyleHelper(theme, 'color-text-invalid', documentElements);
@@ -85,6 +85,14 @@
.TabLabelSettings {
}
.VRule {
height: 20px;
width: 1px;
border-left: 1px solid var(--color-border);
padding-left: 0.25rem;
margin-left: 0.25rem;
}
@media screen and (max-width: 525px) {
.IconSizeNavigation {
margin-right: 0;
+11 -4
View File
@@ -29,7 +29,7 @@ export type Props = {|
disabled?: boolean,
id: string,
selectTab: (tabID: any) => void,
tabs: Array<TabInfo>,
tabs: Array<TabInfo | null>,
type: 'navigation' | 'profiler' | 'settings',
|};
@@ -41,8 +41,9 @@ export default function TabBar({
tabs,
type,
}: Props) {
if (!tabs.some(tab => tab.id === currentTab)) {
selectTab(tabs[0].id);
if (!tabs.some(tab => tab !== null && tab.id === currentTab)) {
const firstTab = ((tabs.find(tab => tab !== null): any): TabInfo);
selectTab(firstTab.id);
}
const onChange = useCallback(
@@ -88,7 +89,13 @@ export default function TabBar({
return (
<Fragment>
{tabs.map(({icon, id, label, title}) => {
{tabs.map(tab => {
if (tab === null) {
return <div className={styles.VRule} />;
}
const {icon, id, label, title} = tab;
let button = (
<label
className={[
@@ -6,6 +6,7 @@
font-size: 12px;
background-color: var(--color-tooltip-background);
color: var(--color-tooltip-text);
box-shadow: 1px 1px 2px var(--color-shadow);
/* Make sure this is above the DevTools, which are above the Overlay */
z-index: 10000002;
@@ -77,6 +77,34 @@
--light-color-record-active: #fc3a4b;
--light-color-record-hover: #3578e5;
--light-color-record-inactive: #0088fa;
--light-color-resize-bar: #cccccc;
--light-color-scheduling-profiler-flame-graph-label: #000000;
--light-color-scheduling-profiler-priority-background: #ededf0;
--light-color-scheduling-profiler-priority-border: #d7d7db;
--light-color-scheduling-profiler-user-timing: #c9cacd;
--light-color-scheduling-profiler-user-timing-hover:#93959a;
--light-color-scheduling-profiler-react-idle: #edf6ff;
--light-color-scheduling-profiler-react-idle-selected:#EDF6FF;
--light-color-scheduling-profiler-react-idle-hover:#EDF6FF;
--light-color-scheduling-profiler-react-render: #9fc3f3;
--light-color-scheduling-profiler-react-render-selected:#64A9F5;
--light-color-scheduling-profiler-react-render-hover:#2683E2;
--light-color-scheduling-profiler-react-commit: #ff718e;
--light-color-scheduling-profiler-react-commit-selected:#FF5277;
--light-color-scheduling-profiler-react-commit-hover:#ed0030;
--light-color-scheduling-profiler-react-layout-effects:#c88ff0;
--light-color-scheduling-profiler-react-layout-effects-selected:#934FC1;
--light-color-scheduling-profiler-react-layout-effects-hover:#601593;
--light-color-scheduling-profiler-react-passive-effects:#c88ff0;
--light-color-scheduling-profiler-react-passive-effects-selected:#934FC1;
--light-color-scheduling-profiler-react-passive-effects-hover:#601593;
--light-color-scheduling-profiler-react-schedule: #9fc3f3;
--light-color-scheduling-profiler-react-schedule-hover:#2683E2;
--light-color-scheduling-profiler-react-schedule-cascading:#ff718e;
--light-color-scheduling-profiler-react-schedule-cascading-hover:#ed0030;
--light-color-scheduling-profiler-react-suspend: #a6e59f;
--light-color-scheduling-profiler-react-suspend-hover:#13bc00;
--light-color-scheduling-profiler-react-work-border:#ffffff;
--light-color-scroll-thumb: #c2c2c2;
--light-color-scroll-track: #fafafa;
--light-color-search-match: yellow;
@@ -146,7 +174,7 @@
--dark-color-console-warning-border: #665500;
--dark-color-console-warning-icon: #f4bd00;
--dark-color-console-warning-text: #f5f2ed;
--dark-color-context-background: rgba(255,255,255,.9);
--dark-color-context-background: rgba(255,255,255,.95);
--dark-color-context-background-hover: rgba(0, 136, 250, 0.1);
--dark-color-context-background-selected: #0088fa;
--dark-color-context-border: #eeeeee;
@@ -169,6 +197,34 @@
--dark-color-record-active: #fc3a4b;
--dark-color-record-hover: #a2e9fc;
--dark-color-record-inactive: #61dafb;
--dark-color-resize-bar: #3d424a;
--dark-color-scheduling-profiler-flame-graph-label: #000000;
--dark-color-scheduling-profiler-priority-background: #1d2129;
--dark-color-scheduling-profiler-priority-border: #282c34;
--dark-color-scheduling-profiler-user-timing: #c9cacd;
--dark-color-scheduling-profiler-user-timing-hover:#93959a;
--dark-color-scheduling-profiler-react-idle: #3d485b;
--dark-color-scheduling-profiler-react-idle-selected:#465269;
--dark-color-scheduling-profiler-react-idle-hover:#465269;
--dark-color-scheduling-profiler-react-render: #9fc3f3;
--dark-color-scheduling-profiler-react-render-selected:#64A9F5;
--dark-color-scheduling-profiler-react-render-hover:#2683E2;
--dark-color-scheduling-profiler-react-commit: #ff718e;
--dark-color-scheduling-profiler-react-commit-selected:#FF5277;
--dark-color-scheduling-profiler-react-commit-hover:#ed0030;
--dark-color-scheduling-profiler-react-layout-effects:#c88ff0;
--dark-color-scheduling-profiler-react-layout-effects-selected:#934FC1;
--dark-color-scheduling-profiler-react-layout-effects-hover:#601593;
--dark-color-scheduling-profiler-react-passive-effects:#c88ff0;
--dark-color-scheduling-profiler-react-passive-effects-selected:#934FC1;
--dark-color-scheduling-profiler-react-passive-effects-hover:#601593;
--dark-color-scheduling-profiler-react-schedule: #9fc3f3;
--dark-color-scheduling-profiler-react-schedule-hover:#2683E2;
--dark-color-scheduling-profiler-react-schedule-cascading:#ff718e;
--dark-color-scheduling-profiler-react-schedule-cascading-hover:#ed0030;
--dark-color-scheduling-profiler-react-suspend: #a6e59f;
--dark-color-scheduling-profiler-react-suspend-hover:#13bc00;
--dark-color-scheduling-profiler-react-work-border:#ffffff;
--dark-color-scroll-thumb: #afb3b9;
--dark-color-scroll-track: #313640;
--dark-color-search-match: yellow;
@@ -184,7 +240,7 @@
--dark-color-toggle-background-on: #178fb9;
--dark-color-toggle-background-off: #777d88;
--dark-color-toggle-text: #ffffff;
--dark-color-tooltip-background: rgba(255, 255, 255, 0.9);
--dark-color-tooltip-background: rgba(255, 255, 255, 0.95);
--dark-color-tooltip-text: #000000;
/* Font smoothing */
+8 -1
View File
@@ -103,7 +103,14 @@ export function loadHookNames(
let didTimeout = false;
loadHookNamesFunction(hooksTree).then(
const response = loadHookNamesFunction(hooksTree);
console.log(
'loadHookNamesFunction:',
loadHookNamesFunction,
'->',
response,
);
response.then(
function onSuccess(hookNames) {
if (didTimeout) {
return;
+45
View File
@@ -2236,6 +2236,11 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
"@types/json-schema@^7.0.8":
version "7.0.8"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.8.tgz#edf1bf1dbf4e04413ca8e5b17b3b7d7d54b59818"
integrity sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==
"@types/minimatch@*", "@types/minimatch@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@@ -2841,6 +2846,11 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1:
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da"
integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==
ajv-keywords@^3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
ajv-merge-patch@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/ajv-merge-patch/-/ajv-merge-patch-4.1.0.tgz#cd580e5860ac53431d6aa901fa3d5e2eb2b74a6c"
@@ -2869,6 +2879,16 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^6.12.5:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^6.5.5, ajv@^6.9.1:
version "6.10.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52"
@@ -6768,6 +6788,14 @@ file-loader@^6.0.0:
loader-utils "^2.0.0"
schema-utils "^2.6.5"
file-loader@^6.1.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d"
integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==
dependencies:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
file-uri-to-path@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@@ -12631,6 +12659,15 @@ schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0:
ajv "^6.12.2"
ajv-keywords "^3.4.1"
schema-utils@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281"
integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==
dependencies:
"@types/json-schema" "^7.0.8"
ajv "^6.12.5"
ajv-keywords "^3.5.2"
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -14877,6 +14914,14 @@ worker-loader@^3.0.2:
loader-utils "^2.0.0"
schema-utils "^2.7.0"
worker-loader@^3.0.3:
version "3.0.8"
resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-3.0.8.tgz#5fc5cda4a3d3163d9c274a4e3a811ce8b60dbb37"
integrity sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==
dependencies:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
workerize-loader@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/workerize-loader/-/workerize-loader-1.3.0.tgz#4995cf2ff2b45dd6dc60e4411e63f5ae2c704d36"