Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/react-devtools-scheduling-profiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"url-loader": "^4.1.0",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
"webpack-dev-server": "^3.11.0",
"worker-loader": "^3.0.2"
}
}
14 changes: 6 additions & 8 deletions packages/react-devtools-scheduling-profiler/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,21 @@ import '@reach/tooltip/styles.css';

import * as React from 'react';

import {ModalDialogContextController} from 'react-devtools-shared/src/devtools/views/ModalDialog';
import {SchedulingProfiler} from './SchedulingProfiler';
import {useBrowserTheme} from './hooks';
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 (
<ModalDialogContextController>
<div className={styles.DevTools}>
<div className={styles.TabContent}>
<SchedulingProfiler />
</div>
<div className={styles.DevTools}>
<div className={styles.TabContent}>
<SchedulingProfiler />
</div>
</ModalDialogContextController>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,3 @@
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
}

.ErrorMessage {
margin: 0.5rem 0;
color: var(--color-dim);
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-normal);
}
41 changes: 6 additions & 35 deletions packages/react-devtools-scheduling-profiler/src/ImportButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,61 +7,32 @@
* @flow
*/

import type {TimelineEvent} from '@elg/speedscope';
import type {ReactProfilerData} from './types';

import * as React from 'react';
import {useCallback, useContext, useRef} 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 {ModalDialogContext} from 'react-devtools-shared/src/devtools/views/ModalDialog';

import preprocessData from './utils/preprocessData';
import {readInputData} from './utils/readInputData';

import styles from './ImportButton.css';

type Props = {|
onDataImported: (profilerData: ReactProfilerData) => void,
onFileSelect: (file: File) => void,
|};

export default function ImportButton({onDataImported}: Props) {
export default function ImportButton({onFileSelect}: Props) {
const inputRef = useRef<HTMLInputElement | null>(null);
const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);

const handleFiles = useCallback(async () => {
const handleFiles = useCallback(() => {
const input = inputRef.current;
if (input === null) {
return;
}

if (input.files.length > 0) {
try {
const readFile = await readInputData(input.files[0]);
const events: TimelineEvent[] = JSON.parse(readFile);
if (events.length > 0) {
onDataImported(preprocessData(events));
}
} catch (error) {
modalDialogDispatch({
type: 'SHOW',
title: 'Import failed',
content: (
<>
<div>The profiling data you selected cannot be imported.</div>
{error !== null && (
<div className={styles.ErrorMessage}>{error.message}</div>
)}
</>
),
});
}
onFileSelect(input.files[0]);
}

// Reset input element to allow the same file to be re-imported
input.value = '';
}, [onDataImported, modalDialogDispatch]);
}, [onFileSelect]);

const uploadData = useCallback(() => {
if (inputRef.current !== null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
text-align: center;
}

.ErrorMessage {
margin: 0.5rem 0;
color: var(--color-dim);
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-normal);
}

.Row {
display: flex;
flex-direction: row;
Expand Down
115 changes: 94 additions & 21 deletions packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,53 +7,87 @@
* @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 * as React from 'react';
import {useState} from 'react';

import ImportButton from './ImportButton';
import {ModalDialog} from 'react-devtools-shared/src/devtools/views/ModalDialog';
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 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';

export function SchedulingProfiler(_: {||}) {
const [profilerData, setProfilerData] = useState<ReactProfilerData | null>(
null,
);
type DataResource = Resource<void, File, ReactProfilerData | Error>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also not sure if storing data + import errors in a resource is the cleanest approach. I'm doing this to handle these expected/intentional errors without throwing them and using error boundaries. Any idea how I can improve this?


function createDataResourceFromImportedFile(file: File): DataResource {
return createResource(
() => {
return new Promise<ReactProfilerData | Error>((resolve, reject) => {
const worker: Worker = new (ImportWorker: any)();

const view = profilerData ? (
<CanvasPage profilerData={profilerData} />
) : (
<Welcome onDataImported={setProfilerData} />
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 handleFileSelect = useCallback((file: File) => {
setDataResource(createDataResourceFromImportedFile(file));
}, []);

return (
<div className={styles.SchedulingProfiler}>
<div className={styles.Toolbar}>
<ReactLogo />
<span className={styles.AppName}>Concurrent Mode Profiler</span>
<div className={styles.VRule} />
<ImportButton onDataImported={setProfilerData} />
<ImportButton onFileSelect={handleFileSelect} />
<div className={styles.Spacer} />
</div>
<div className={styles.Content}>
{view}
<ModalDialog />
{dataResource ? (
<Suspense fallback={<ProcessingData />}>
<DataResourceComponent
dataResource={dataResource}
onFileSelect={handleFileSelect}
/>
</Suspense>
) : (
<Welcome onFileSelect={handleFileSelect} />
)}
</div>
</div>
);
}

type WelcomeProps = {|
onDataImported: (profilerData: ReactProfilerData) => void,
|};

const Welcome = ({onDataImported}: WelcomeProps) => (
const Welcome = ({onFileSelect}: {|onFileSelect: (file: File) => void|}) => (
<div className={styles.EmptyStateContainer}>
<div className={styles.ScreenshotWrapper}>
<img
Expand All @@ -65,8 +99,47 @@ const Welcome = ({onDataImported}: WelcomeProps) => (
<div className={styles.Header}>Welcome!</div>
<div className={styles.Row}>
Click the import button
<ImportButton onDataImported={onDataImported} /> to import a Chrome
<ImportButton onFileSelect={onFileSelect} /> to import a Chrome
performance profile.
</div>
</div>
);

const ProcessingData = () => (
<div className={styles.EmptyStateContainer}>
<div className={styles.Header}>Processing data...</div>
<div className={styles.Row}>This should only take a minute.</div>
</div>
);

const CouldNotLoadProfile = ({error, onFileSelect}) => (
<div className={styles.EmptyStateContainer}>
<div className={styles.Header}>Could not load profile</div>
{error.message && (
<div className={styles.Row}>
<div className={styles.ErrorMessage}>{error.message}</div>
</div>
)}
<div className={styles.Row}>
Try importing
<ImportButton onFileSelect={onFileSelect} />
another Chrome performance profile.
</div>
</div>
);

const DataResourceComponent = ({
dataResource,
onFileSelect,
}: {|
dataResource: DataResource,
onFileSelect: (file: File) => void,
|}) => {
const dataOrError = dataResource.read();
if (dataOrError instanceof Error) {
return (
<CouldNotLoadProfile error={dataOrError} onFileSelect={onFileSelect} />
);
}
return <CanvasPage profilerData={dataOrError} />;
};
12 changes: 11 additions & 1 deletion packages/react-devtools-scheduling-profiler/src/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
useLayoutEffect,
} from 'react';

import {updateThemeVariables} from 'react-devtools-shared/src/devtools/views/Settings/SettingsContext';
import {
updateDisplayDensity,
updateThemeVariables,
} from 'react-devtools-shared/src/devtools/views/Settings/SettingsContext';
import {enableDarkMode} from './SchedulingProfilerFeatureFlags';

export type BrowserTheme = 'dark' | 'light';
Expand Down Expand Up @@ -57,3 +60,10 @@ export function useBrowserTheme(): void {
}
}, [theme]);
}

export function useDisplayDensity(): void {
useLayoutEffect(() => {
const documentElements = [((document.documentElement: any): HTMLElement)];
updateDisplayDensity('comfortable', documentElements);
}, []);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* 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
*/

/**
* An error thrown when an invalid profile could not be processed.
*/
export default class InvalidProfileError extends Error {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* 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 type {TimelineEvent} from '@elg/speedscope';
import type {ReactProfilerData} from '../types';

import preprocessData from './preprocessData';
import {readInputData} from './readInputData';
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);

try {
const readFile = await readInputData(file);
const events: TimelineEvent[] = JSON.parse(readFile);
if (events.length === 0) {
throw new InvalidProfileError('No profiling data found in file.');
}

self.postMessage({
status: 'SUCCESS',
processedData: preprocessData(events),
});
} catch (error) {
if (error instanceof InvalidProfileError) {
self.postMessage({
status: 'INVALID_PROFILE_ERROR',
error,
});
} else {
self.postMessage({
status: 'UNEXPECTED_ERROR',
error,
});
}
}
};
Loading