Skip to content

Commit

Permalink
Scheduling Profiler: Move preprocessing to web worker and add loading…
Browse files Browse the repository at this point in the history
… indicator (#19759)

* Move preprocessData into a web worker
* Add UI feedback for loading/import error states
* Terminate worker when done handling profile
* Add display density CSS variables
  • Loading branch information
taneliang authored Sep 4, 2020
1 parent 38a512a commit eabd18c
Show file tree
Hide file tree
Showing 16 changed files with 245 additions and 93 deletions.
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>;

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

0 comments on commit eabd18c

Please sign in to comment.