@samitouri / QOS-React-2 / commits / 278661715c

Show import error dialog if profiling data import fails

Brian Vaughn committed May 7, 2019 at 14:38 UTC 278661715c28b01b9c32b775679e9636baad2b18
14 files changed +197 -44
src/__tests__/__snapshots__/profiling-test.js.snap
+4 -4
@@ -266,7 +266,7 @@ Object {
266 "interactionCount": 0,
267 "rootID": 1,
268 },
269 - "version": 1,
269 + "version": 2,
270 }
271 `;
272
@@ -516,7 +516,7 @@ Object {
516 "interactionCount": 0,
517 "rootID": 1,
518 },
519 - "version": 1,
519 + "version": 2,
520 }
521 `;
522
@@ -714,7 +714,7 @@ Object {
714 "interactionCount": 2,
715 "rootID": 1,
716 },
717 - "version": 1,
717 + "version": 2,
718 }
719 `;
720
@@ -915,6 +915,6 @@ Object {
915 "interactionCount": 0,
916 "rootID": 1,
917 },
918 - "version": 1,
918 + "version": 2,
919 }
920 `;
src/__tests__/profiling-test.js
+13
@@ -69,6 +69,19 @@ describe('profiling', () => {
69 TestRenderer = utils.requireTestRenderer();
70 });
71
72 + it('should throw if importing older/unsupported data', () => {
73 + const {
74 + prepareProfilingImport,
75 + } = require('src/devtools/views/Profiler/utils');
76 + expect(() =>
77 + prepareProfilingImport(
78 + JSON.stringify({
79 + version: 0,
80 + })
81 + )
82 + ).toThrow('Unsupported profiler export version "0"');
83 + });
84 +
85 describe('ProfilingSummary', () => {
86 it('should be collected for each commit', async done => {
87 const Parent = ({ count }) => {
src/backend/renderer.js
+2 -1
@@ -18,6 +18,7 @@ import {
18 ElementTypeRoot,
19 ElementTypeSuspense,
20 } from 'src/types';
21 +import { PROFILER_EXPORT_VERSION } from 'src/constants';
22 import {
23 getDisplayName,
24 getSavedComponentFilters,
@@ -2058,7 +2059,7 @@ export function attach(
2059 }
2060 }
2061 return {
2061 - version: 1,
2062 + version: PROFILER_EXPORT_VERSION,
2063 profilingSummary: getProfilingSummary(rootID),
2064 commitDetails,
2065 interactions: getInteractions(rootID),
src/constants.js
+2
@@ -15,3 +15,5 @@ export const SESSION_STORAGE_LAST_SELECTION_KEY =
15 'React::DevTools::lastSelection';
16
17 export const __DEBUG__ = false;
18 +
19 +export const PROFILER_EXPORT_VERSION = 2;
src/devtools/views/Profiler/CommitFilterModal.css renamed
src/devtools/views/Profiler/CommitFilterModal.js renamed
+8 -6
@@ -1,16 +1,13 @@
1 // @flow
2
3 import React, { useCallback, useContext, useEffect, useRef } from 'react';
4 +import { CommitFilterModalContext } from './CommitFilterModalContext';
5 import { ProfilerContext } from './ProfilerContext';
6 import { useModalDismissSignal } from '../hooks';
7
7 -import styles from './FilterModal.css';
8 +import styles from './CommitFilterModal.css';
9
9 -type Props = {|
10 - dismissModal: () => void,
11 -|};
12 -
13 -export default function FilterModal({ dismissModal }: Props) {
10 +export default function FilterModal(_: {||}) {
11 const {
12 isCommitFilterEnabled,
13 minCommitDuration,
@@ -18,6 +15,11 @@ export default function FilterModal({ dismissModal }: Props) {
15 setMinCommitDuration,
16 } = useContext(ProfilerContext);
17
18 + const { setIsModalShowing } = useContext(CommitFilterModalContext);
19 + const dismissModal = useCallback(() => setIsModalShowing(false), [
20 + setIsModalShowing,
21 + ]);
22 +
23 const handleNumberChange = useCallback(
24 (event: SyntheticEvent<HTMLInputElement>) => {
25 const newValue = parseFloat(event.currentTarget.value);
src/devtools/views/Profiler/CommitFilterModalContext.js
+6 -6
@@ -3,8 +3,8 @@
3 import React, { createContext, useMemo, useState } from 'react';
4
5 type Context = {|
6 - isFilterModalShowing: boolean,
7 - setIsFilterModalShowing: (value: boolean) => void,
6 + isModalShowing: boolean,
7 + setIsModalShowing: (value: boolean) => void,
8 |};
9
10 const CommitFilterModalContext = createContext<Context>(((null: any): Context));
@@ -15,14 +15,14 @@ type Props = {|
15 |};
16
17 function CommitFilterModalContextController({ children }: Props) {
18 - const [isFilterModalShowing, setIsFilterModalShowing] = useState(false);
18 + const [isModalShowing, setIsModalShowing] = useState(false);
19
20 const value = useMemo(
21 () => ({
22 - isFilterModalShowing,
23 - setIsFilterModalShowing,
22 + isModalShowing,
23 + setIsModalShowing,
24 }),
25 - [isFilterModalShowing]
25 + [isModalShowing]
26 );
27
28 return (
src/devtools/views/Profiler/ImportFailedModal.css new
+39
@@ -0,0 +1,39 @@
1 +.Background {
2 + position: absolute;
3 + width: 100%;
4 + height: 100%;
5 + display: flex;
6 + align-items: flex-start;
7 + justify-content: center;
8 + padding: 1rem;
9 + background-color: var(--color-modal-background);
10 + overflow: auto;
11 +}
12 +
13 +.Dialog {
14 + position: relative;
15 + z-index: 3;
16 + min-width: 20rem;
17 + max-width: 100%;
18 + display: inline-block;
19 + background-color: var(--color-background);
20 + padding: 0.5rem;
21 + border: 1px solid var(--color-border);
22 + border-radius: 0.25rem;
23 +}
24 +
25 +.Header {
26 + font-size: var(--font-size-sans-large);
27 + margin-bottom: 0.5rem;
28 +}
29 +
30 +.Buttons {
31 + text-align: right;
32 +}
33 +
34 +.ErrorMessage {
35 + margin: 0.5rem 0;
36 + color: var(--color-dim);
37 + font-family: var(--font-family-monospace);
38 + font-size: var(--font-size-monospace-normal);
39 +}
src/devtools/views/Profiler/ImportFailedModal.js new
+35
@@ -0,0 +1,35 @@
1 +// @flow
2 +
3 +import React, { useCallback, useContext, useRef } from 'react';
4 +import { ImportFailedModalContext } from './ImportFailedModalContext';
5 +import Button from '../Button';
6 +import { useModalDismissSignal } from '../hooks';
7 +
8 +import styles from './ImportFailedModal.css';
9 +
10 +export default function ImportFailedModal(_: {||}) {
11 + const { importError, setImportError } = useContext(ImportFailedModalContext);
12 + const dismissModal = useCallback(() => setImportError(null), [
13 + setImportError,
14 + ]);
15 + const modalRef = useRef<HTMLDivElement | null>(null);
16 +
17 + useModalDismissSignal(modalRef, dismissModal);
18 +
19 + return (
20 + <div className={styles.Background}>
21 + <div className={styles.Dialog} ref={modalRef}>
22 + <div className={styles.Header}>Import failed</div>
23 + <div>The profiling data you selected cannot be imported.</div>
24 + {importError !== null && (
25 + <div className={styles.ErrorMessage}>{importError.message}</div>
26 + )}
27 + <div className={styles.Buttons}>
28 + <Button autoFocus onClick={dismissModal}>
29 + Okay
30 + </Button>
31 + </div>
32 + </div>
33 + </div>
34 + );
35 +}
src/devtools/views/Profiler/ImportFailedModalContext.js new
+37
@@ -0,0 +1,37 @@
1 +// @flow
2 +
3 +import React, { createContext, useMemo, useState } from 'react';
4 +
5 +type Context = {|
6 + importError: Error | null,
7 + isModalShowing: boolean,
8 + setImportError: (error: Error | null) => void,
9 +|};
10 +
11 +const ImportFailedModalContext = createContext<Context>(((null: any): Context));
12 +ImportFailedModalContext.displayName = 'ImportFailedModalContext';
13 +
14 +type Props = {|
15 + children: React$Node,
16 +|};
17 +
18 +function ImportFailedModalContextController({ children }: Props) {
19 + const [importError, setImportError] = useState<Error | null>(null);
20 +
21 + const value = useMemo(
22 + () => ({
23 + importError,
24 + isModalShowing: !!importError,
25 + setImportError,
26 + }),
27 + [importError]
28 + );
29 +
30 + return (
31 + <ImportFailedModalContext.Provider value={value}>
32 + {children}
33 + </ImportFailedModalContext.Provider>
34 + );
35 +}
36 +
37 +export { ImportFailedModalContext, ImportFailedModalContextController };
src/devtools/views/Profiler/Profiler.js
+32 -17
@@ -1,16 +1,21 @@
1 // @flow
2
3 -import React, { Suspense, useCallback, useContext } from 'react';
3 +import React, { Suspense, useContext } from 'react';
4 import {
5 CommitFilterModalContext,
6 CommitFilterModalContextController,
7 } from './CommitFilterModalContext';
8 +import {
9 + ImportFailedModalContext,
10 + ImportFailedModalContextController,
11 +} from './ImportFailedModalContext';
12 import { ProfilerContext } from './ProfilerContext';
13 import TabBar from '../TabBar';
14 import ClearProfilingDataButton from './ClearProfilingDataButton';
15 import CommitFlamegraph from './CommitFlamegraph';
16 import CommitRanked from './CommitRanked';
13 -import FilterModal from './FilterModal';
17 +import CommitFilterModal from './CommitFilterModal';
18 +import ImportFailedModal from './ImportFailedModal';
19 import Interactions from './Interactions';
20 import RecordToggle from './RecordToggle';
21 import ReloadAndProfileButton from './ReloadAndProfileButton';
@@ -35,16 +40,20 @@ function Profiler({ supportsProfiling }: Props) {
40
41 if (isProfiling || !rootHasProfilingData) {
42 return (
38 - <NonSuspendingProfiler
39 - hasProfilingData={hasProfilingData}
40 - isProfiling={isProfiling}
41 - supportsProfiling={supportsProfiling}
42 - />
43 + <ImportFailedModalContextController>
44 + <NonSuspendingProfiler
45 + hasProfilingData={hasProfilingData}
46 + isProfiling={isProfiling}
47 + supportsProfiling={supportsProfiling}
48 + />
49 + </ImportFailedModalContextController>
50 );
51 } else {
52 return (
53 <CommitFilterModalContextController>
47 - <SuspendingProfiler />
54 + <ImportFailedModalContextController>
55 + <SuspendingProfiler />
56 + </ImportFailedModalContextController>
57 </CommitFilterModalContextController>
58 );
59 }
@@ -74,6 +83,10 @@ function NonSuspendingProfiler({
83 view = <NoProfilingDataForRoot />;
84 }
85
86 + const { isModalShowing: isImportFailedModalShowing } = useContext(
87 + ImportFailedModalContext
88 + );
89 +
90 return (
91 <div className={styles.Profiler}>
92 <div className={styles.LeftColumn}>
@@ -92,7 +105,10 @@ function NonSuspendingProfiler({
105 tabs={tabs}
106 />
107 </div>
95 - <div className={styles.Content}>{view}</div>
108 + <div className={styles.Content}>
109 + {view}
110 + {isImportFailedModalShowing && <ImportFailedModal />}
111 + </div>
112 </div>
113 </div>
114 );
@@ -113,13 +129,13 @@ function SuspendingProfiler() {
129 const { selectedFiberID, selectedTabID, selectTab } = useContext(
130 ProfilerContext
131 );
116 - const { isFilterModalShowing, setIsFilterModalShowing } = useContext(
132 +
133 + const { isModalShowing: isFilterModalShowing } = useContext(
134 CommitFilterModalContext
135 );
119 -
120 - const dismissFilterModal = useCallback(() => setIsFilterModalShowing(false), [
121 - setIsFilterModalShowing,
122 - ]);
136 + const { isModalShowing: isImportFailedModalShowing } = useContext(
137 + ImportFailedModalContext
138 + );
139
140 let view = null;
141 switch (selectedTabID) {
@@ -178,9 +194,8 @@ function SuspendingProfiler() {
194 </div>
195 <div className={styles.Content}>
196 <Suspense fallback={<ContentFallback />}>{view}</Suspense>
181 - {isFilterModalShowing && (
182 - <FilterModal dismissModal={dismissFilterModal} />
183 - )}
197 + {isFilterModalShowing && <CommitFilterModal />}
198 + {isImportFailedModalShowing && <ImportFailedModal />}
199 </div>
200 </div>
201 <div className={styles.RightColumn}>
src/devtools/views/Profiler/ProfilingImportExportButtons.js
+11 -6
@@ -2,6 +2,7 @@
2
3 import React, { Fragment, useContext, useCallback, useRef } from 'react';
4 import { ProfilerContext } from './ProfilerContext';
5 +import { ImportFailedModalContext } from './ImportFailedModalContext';
6 import Button from '../Button';
7 import ButtonIcon from '../ButtonIcon';
8 import { BridgeContext, StoreContext } from '../context';
@@ -20,6 +21,8 @@ export default function ProfilingImportExportButtons() {
21
22 const inputRef = useRef<HTMLInputElement | null>(null);
23
24 + const { setImportError } = useContext(ImportFailedModalContext);
25 +
26 const downloadData = useCallback(() => {
27 if (rendererID === null || rootID === null) {
28 return;
@@ -53,16 +56,18 @@ export default function ProfilingImportExportButtons() {
56 if (input !== null && input.files.length > 0) {
57 const fileReader = new FileReader();
58 fileReader.addEventListener('load', () => {
56 - const raw = ((fileReader.result: any): string);
57 - const data = prepareProfilingImport(raw);
58 -
59 - // TODO (profiling) Catch possible version check error and show dialog.
59 + try {
60 + const raw = ((fileReader.result: any): string);
61 + const data = prepareProfilingImport(raw);
62
61 - store.importedProfilingData = ((data: any): ImportedProfilingData);
63 + store.importedProfilingData = ((data: any): ImportedProfilingData);
64 + } catch (error) {
65 + setImportError(error);
66 + }
67 });
68 fileReader.readAsText(input.files[0]);
69 }
65 - }, [store]);
70 + }, [store, setImportError]);
71
72 return (
73 <Fragment>
src/devtools/views/Profiler/ToggleCommitFilterModalButton.js
+3 -3
@@ -6,10 +6,10 @@ import Button from '../Button';
6 import ButtonIcon from '../ButtonIcon';
7
8 export default function ToggleCommitFilterModalButton() {
9 - const { setIsFilterModalShowing } = useContext(CommitFilterModalContext);
9 + const { setIsModalShowing } = useContext(CommitFilterModalContext);
10
11 - const showFilterModal = useCallback(() => setIsFilterModalShowing(true), [
12 - setIsFilterModalShowing,
11 + const showFilterModal = useCallback(() => setIsModalShowing(true), [
12 + setIsModalShowing,
13 ]);
14
15 return (
src/devtools/views/Profiler/utils.js
+5 -1
@@ -1,5 +1,7 @@
1 // @flow
2
3 +import { PROFILER_EXPORT_VERSION } from 'src/constants';
4 +
5 import type {
6 CommitDetailsFrontend,
7 CommitTreeFrontend,
@@ -82,7 +84,9 @@ export const prepareProfilingExport = (
84 export const prepareProfilingImport = (raw: string) => {
85 const parsed = JSON.parse(raw);
86
85 - // TODO (profiling) Version check; throw if older version.
87 + if (parsed.version !== PROFILER_EXPORT_VERSION) {
88 + throw Error(`Unsupported profiler export version "${parsed.version}".`);
89 + }
90
91 const entries = [];
92 Object.values(parsed.profilingSnapshots).forEach(snapshot => {