mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[DRAFT] Import scheduling profiler into DevTools Profiler (#21897)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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}},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 |
+1
-1
@@ -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(
|
||||
|
||||
+126
-31
@@ -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]);
|
||||
}
|
||||
+46
@@ -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);
|
||||
}, []);
|
||||
}
|
||||
+9
-20
@@ -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
-3
@@ -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>,
|
||||
);
|
||||
+18
-4
@@ -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 = {
|
||||
|
||||
+5
-4
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+14
-10
@@ -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
@@ -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>
|
||||
|
||||
+2
-1
@@ -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?
|
||||
|
||||
+25
-6
@@ -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" />
|
||||
|
||||
+137
-1
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user