@samitouri / QOS-React / commits / e8f84dd5c4

Iterating on Profiling tab suspense. Stashing changes but planning to refactor immediately.

Brian Vaughn committed Mar 15, 2019 at 08:34 UTC e8f84dd5c4d02ca8278d57e0d1406ff075a7ffb1
12 files changed +308 -293
src/devtools/store.js
+9 -1
@@ -275,16 +275,24 @@ export default class Store extends EventEmitter {
275 }
276
277 startProfiling(): void {
278 + this._bridge.send('startProfiling');
279 +
280 // Invalidate suspense cache if profiling data is being (re-)recorded.
281 + // Note that we clear now because any existing data is "stale".
282 this._profilingCache.invalidate();
283
281 - this._bridge.send('startProfiling');
284 this._isProfiling = false;
285 this.emit('isProfiling');
286 }
287
288 stopProfiling(): void {
289 this._bridge.send('stopProfiling');
290 +
291 + // Invalidate suspense cache if profiling data is being (re-)recorded.
292 + // Note that we clear again, in case any views read from the cache while profiling.
293 + // (That would have resolved a now-stale value without any profiling data.)
294 + this._profilingCache.invalidate();
295 +
296 this._isProfiling = false;
297 this.emit('isProfiling');
298 }
src/devtools/views/Profiler/FilterModal.js
+13 -12
@@ -1,8 +1,7 @@
1 // @flow
2
3 -import React, { useCallback, useContext, useEffect, useRef } from 'react';
4 -import { useModalDismissSignal } from '../hooks';
5 -import { ProfilerContext } from './ProfilerContext';
3 +import React, { useCallback, useEffect, useRef } from 'react';
4 +import { useLocalStorage, useModalDismissSignal } from '../hooks';
5
6 import styles from './FilterModal.css';
7
@@ -11,12 +10,14 @@ type Props = {|
10 |};
11
12 export default function FilterModal({ dismissModal }: Props) {
14 - const {
15 - isMinCommitDurationEnabled,
16 - minCommitDuration,
17 - setMinCommitDuration,
18 - setIsMinCommitDurationEnabled,
19 - } = useContext(ProfilerContext);
13 + const [
14 + isCommitFilterEnabled,
15 + setIsCommitFilterEnabled,
16 + ] = useLocalStorage<boolean>('isCommitFilterEnabled', false);
17 + const [minCommitDuration, setMinCommitDuration] = useLocalStorage<number>(
18 + 'minCommitDuration',
19 + 0
20 + );
21
22 const handleNumberChange = useCallback(
23 ({ currentTarget }) => {
@@ -30,14 +31,14 @@ export default function FilterModal({ dismissModal }: Props) {
31
32 const handleEnabledChange = useCallback(
33 ({ currentTarget }) => {
33 - setIsMinCommitDurationEnabled(currentTarget.checked);
34 + setIsCommitFilterEnabled(currentTarget.checked);
35 if (currentTarget.checked) {
36 if (inputRef.current !== null) {
37 inputRef.current.focus();
38 }
39 }
40 },
40 - [setIsMinCommitDurationEnabled]
41 + [setIsCommitFilterEnabled]
42 );
43
44 const inputRef = useRef<HTMLInputElement | null>(null);
@@ -56,7 +57,7 @@ export default function FilterModal({ dismissModal }: Props) {
57 <div className={styles.Modal} ref={modalRef}>
58 <label>
59 <input
59 - checked={isMinCommitDurationEnabled}
60 + checked={isCommitFilterEnabled}
61 onChange={handleEnabledChange}
62 type="checkbox"
63 />{' '}
src/devtools/views/Profiler/Profiler.js
+19 -6
@@ -1,7 +1,11 @@
1 // @flow
2
3 -import React, { useCallback, useContext, useState } from 'react';
4 -import { ProfilerContext, ProfilerContextController } from './ProfilerContext';
3 +import React, { Suspense, useCallback, useContext, useState } from 'react';
4 +import { ProfilerDataContextController } from './ProfilerDataContext';
5 +import {
6 + ProfilerStatusContext,
7 + ProfilerStatusContextController,
8 +} from './ProfilerStatusContext';
9 import Button from '../Button';
10 import ButtonIcon from '../ButtonIcon';
11 import TabBar from '../TabBar';
@@ -15,14 +19,23 @@ export type Props = {||};
19
20 export default function ProfilerOuter(_: Props) {
21 return (
18 - <ProfilerContextController>
19 - <ProfilerInner />
20 - </ProfilerContextController>
22 + <ProfilerStatusContextController>
23 + <Suspense fallback={<ProfilerFallback />}>
24 + <ProfilerDataContextController>
25 + <ProfilerInner />
26 + </ProfilerDataContextController>
27 + </Suspense>
28 + </ProfilerStatusContextController>
29 );
30 }
31
32 +function ProfilerFallback() {
33 + // TODO (profiling) Real fallback UI
34 + return null;
35 +}
36 +
37 function ProfilerInner(_: Props) {
25 - const { hasProfilingData, isProfiling } = useContext(ProfilerContext);
38 + const { hasProfilingData, isProfiling } = useContext(ProfilerStatusContext);
39
40 const showProfilingControls = !isProfiling && hasProfilingData;
41
src/devtools/views/Profiler/ProfilerContext.js deleted
-149
@@ -1,149 +0,0 @@
1 -// @flow
2 -
3 -import React, {
4 - createContext,
5 - useCallback,
6 - useContext,
7 - useMemo,
8 - useState,
9 -} from 'react';
10 -import { useSubscription } from '../hooks';
11 -import { TreeContext } from 'src/devtools/views/Elements/TreeContext';
12 -import { StoreContext } from '../context';
13 -import { useLocalStorage } from '../hooks';
14 -import Store from '../../store';
15 -
16 -type Context = {|
17 - commitIndex: number,
18 - hasProfilingData: boolean,
19 - isMinCommitDurationEnabled: boolean,
20 - isProfiling: boolean,
21 - minCommitDuration: number,
22 - rendererID: number | null,
23 - rootID: number | null,
24 - setCommitIndex: (value: number) => void,
25 - setMinCommitDuration: (value: number) => void,
26 - setIsMinCommitDurationEnabled: (value: boolean) => void,
27 - startProfiling(value: boolean): void,
28 - stopProfiling(value: boolean): void,
29 -|};
30 -
31 -const ProfilerContext = createContext<Context>(((null: any): Context));
32 -ProfilerContext.displayName = 'ProfilerContext';
33 -
34 -type StoreProfilingState = {|
35 - hasProfilingData: boolean,
36 - isProfiling: boolean,
37 -|};
38 -
39 -type Props = {|
40 - children: React$Node,
41 -|};
42 -
43 -function ProfilerContextController({ children }: Props) {
44 - const store = useContext(StoreContext);
45 -
46 - const { isProfiling, hasProfilingData } = useSubscription<
47 - StoreProfilingState,
48 - Store
49 - >(
50 - useMemo(
51 - () => ({
52 - source: store,
53 - getCurrentValue: (store: Store) => ({
54 - hasProfilingData: store.hasProfilingData,
55 - isProfiling: store.isProfiling,
56 - }),
57 - subscribe: (store: Store, callback: Function) => {
58 - store.addListener('isProfiling', callback);
59 - return () => store.removeListener('isProfiling', callback);
60 - },
61 - }),
62 - [store]
63 - )
64 - );
65 -
66 - const startProfiling = useCallback(() => store.startProfiling(), [store]);
67 - const stopProfiling = useCallback(() => store.stopProfiling(), [store]);
68 -
69 - // TODO (profiling) The browser extension is a multi-root app,
70 - // so ti won't work for the "Profiling" root to depend on a value that's set by the "Elements" root.
71 - // We'll either need to lift that state up into the (shared) Store,
72 - // or use a portal to share the contexts themselves between Chrome tabs.
73 - const { selectedElementID } = useContext(TreeContext);
74 -
75 - // If no root is selected, assume the first root.
76 - // Many React apps are single root anyway.
77 - let rendererID = null;
78 - let rootID = null;
79 - if (selectedElementID) {
80 - rendererID = store.getRendererIDForElement(
81 - ((selectedElementID: any): number)
82 - );
83 - rootID = store.getRootIDForElement(((selectedElementID: any): number));
84 - } else if (store.roots.length > 0) {
85 - rootID = store.roots[0];
86 - rendererID = store.getRendererIDForElement(((rootID: any): number));
87 - }
88 -
89 - const [commitIndex, setCommitIndex] = useState(0);
90 - const [prevRootID, setPrevRootID] = useState(rootID);
91 - if (prevRootID !== rootID) {
92 - setPrevRootID(rootID);
93 - setCommitIndex(0);
94 - }
95 -
96 - const [prevIsProfiling, setPrevIsProfiling] = useState(isProfiling);
97 - if (prevIsProfiling !== isProfiling) {
98 - setPrevIsProfiling(isProfiling);
99 - setCommitIndex(0);
100 - }
101 -
102 - const [
103 - isMinCommitDurationEnabled,
104 - setIsMinCommitDurationEnabled,
105 - ] = useLocalStorage<boolean>('isMinCommitDurationEnabled', false);
106 - const [minCommitDuration, setMinCommitDuration] = useLocalStorage<number>(
107 - 'minCommitDuration',
108 - 0
109 - );
110 -
111 - const value = useMemo(
112 - () => ({
113 - commitIndex,
114 - hasProfilingData,
115 - isMinCommitDurationEnabled,
116 - isProfiling,
117 - minCommitDuration,
118 - rendererID,
119 - rootID,
120 - setMinCommitDuration,
121 - setIsMinCommitDurationEnabled,
122 - setCommitIndex,
123 - startProfiling,
124 - stopProfiling,
125 - }),
126 - [
127 - commitIndex,
128 - hasProfilingData,
129 - isMinCommitDurationEnabled,
130 - isProfiling,
131 - minCommitDuration,
132 - rendererID,
133 - rootID,
134 - setMinCommitDuration,
135 - setIsMinCommitDurationEnabled,
136 - setCommitIndex,
137 - startProfiling,
138 - stopProfiling,
139 - ]
140 - );
141 -
142 - return (
143 - <ProfilerContext.Provider value={value}>
144 - {children}
145 - </ProfilerContext.Provider>
146 - );
147 -}
148 -
149 -export { ProfilerContext, ProfilerContextController };
src/devtools/views/Profiler/ProfilerDataContext.js new
+94
@@ -0,0 +1,94 @@
1 +// @flow
2 +
3 +import React, { createContext, useContext, useMemo, useState } from 'react';
4 +import { TreeContext } from 'src/devtools/views/Elements/TreeContext';
5 +import { StoreContext } from '../context';
6 +import { useLocalStorage } from '../hooks';
7 +import { ProfilerStatusContext } from './ProfilerStatusContext';
8 +
9 +type Context = {|
10 + commitIndex: number | null,
11 + filteredCommitIndices: Array<number>,
12 + rendererID: number | null,
13 + rootID: number | null,
14 + setCommitIndex: (value: number) => void,
15 +|};
16 +
17 +const ProfilerDataContext = createContext<Context>(((null: any): Context));
18 +ProfilerDataContext.displayName = 'ProfilerDataContext';
19 +
20 +type Props = {|
21 + children: React$Node,
22 +|};
23 +
24 +function ProfilerDataContextController({ children }: Props) {
25 + const store = useContext(StoreContext);
26 +
27 + // TODO (profiling) The browser extension is a multi-root app,
28 + // so it won't work for the "Profiling" root to depend on a value that's set by the "Elements" root.
29 + // We'll either need to lift that state up into the (shared) Store,
30 + // or use a portal to share the contexts themselves between Chrome tabs.
31 + const { selectedElementID } = useContext(TreeContext);
32 +
33 + // If no root is selected, assume the first root; many React apps are single root anyway.
34 + let rendererID = null;
35 + let rootID = null;
36 + if (selectedElementID) {
37 + rendererID = store.getRendererIDForElement(
38 + ((selectedElementID: any): number)
39 + );
40 + rootID = store.getRootIDForElement(((selectedElementID: any): number));
41 + } else if (store.roots.length > 0) {
42 + rootID = store.roots[0];
43 + rendererID = store.getRendererIDForElement(((rootID: any): number));
44 + }
45 +
46 + // This value is important because it ensure we re-render after our suspense cache has been cleared.
47 + const { isProfiling } = useContext(ProfilerStatusContext);
48 +
49 + const profilingSummary = store.profilingCache.ProfilingSummary.read({
50 + rendererID: ((rendererID: any): number),
51 + rootID: ((rootID: any): number),
52 + });
53 +
54 + const [isCommitFilterEnabled] = useLocalStorage<boolean>(
55 + 'isCommitFilterEnabled',
56 + false
57 + );
58 + const [minCommitDuration] = useLocalStorage<number>('minCommitDuration', 0);
59 + const { commitDurations } = profilingSummary;
60 + const filteredCommitIndices = useMemo(() => {
61 + const array = [];
62 + if (!isProfiling) {
63 + for (let i = 0; i < commitDurations.length; i++) {
64 + if (!isCommitFilterEnabled || commitDurations[i] >= minCommitDuration) {
65 + array.push(i);
66 + }
67 + }
68 + }
69 + return array;
70 + }, [commitDurations, isCommitFilterEnabled, isProfiling, minCommitDuration]);
71 +
72 + const [commitIndex, setCommitIndex] = useState<number | null>(
73 + commitDurations.length > 0 ? 0 : null
74 + );
75 +
76 + const value = useMemo(
77 + () => ({
78 + commitIndex,
79 + filteredCommitIndices,
80 + rendererID,
81 + rootID,
82 + setCommitIndex,
83 + }),
84 + [commitIndex, filteredCommitIndices, rendererID, rootID, setCommitIndex]
85 + );
86 +
87 + return (
88 + <ProfilerDataContext.Provider value={value}>
89 + {children}
90 + </ProfilerDataContext.Provider>
91 + );
92 +}
93 +
94 +export { ProfilerDataContext, ProfilerDataContextController };
src/devtools/views/Profiler/ProfilerStatusContext.js new
+69
@@ -0,0 +1,69 @@
1 +// @flow
2 +
3 +import React, { createContext, useCallback, useContext, useMemo } from 'react';
4 +import { useSubscription } from '../hooks';
5 +import { StoreContext } from '../context';
6 +import Store from '../../store';
7 +
8 +type Context = {|
9 + hasProfilingData: boolean,
10 + isProfiling: boolean,
11 + startProfiling(value: boolean): void,
12 + stopProfiling(value: boolean): void,
13 +|};
14 +
15 +const ProfilerStatusContext = createContext<Context>(((null: any): Context));
16 +ProfilerStatusContext.displayName = 'ProfilerStatusContext';
17 +
18 +type StoreProfilingState = {|
19 + hasProfilingData: boolean,
20 + isProfiling: boolean,
21 +|};
22 +
23 +type Props = {|
24 + children: React$Node,
25 +|};
26 +
27 +function ProfilerStatusContextController({ children }: Props) {
28 + const store = useContext(StoreContext);
29 +
30 + const subscription = useMemo(
31 + () => ({
32 + getCurrentValue: () => ({
33 + hasProfilingData: store.hasProfilingData,
34 + isProfiling: store.isProfiling,
35 + }),
36 + subscribe: (callback: Function) => {
37 + store.addListener('isProfiling', callback);
38 + return () => store.removeListener('isProfiling', callback);
39 + },
40 + }),
41 + [store]
42 + );
43 +
44 + const { isProfiling, hasProfilingData } = useSubscription<
45 + StoreProfilingState,
46 + Store
47 + >(subscription);
48 +
49 + const startProfiling = useCallback(() => store.startProfiling(), [store]);
50 + const stopProfiling = useCallback(() => store.stopProfiling(), [store]);
51 +
52 + const value = useMemo(
53 + () => ({
54 + hasProfilingData,
55 + isProfiling,
56 + startProfiling,
57 + stopProfiling,
58 + }),
59 + [hasProfilingData, isProfiling, startProfiling, stopProfiling]
60 + );
61 +
62 + return (
63 + <ProfilerStatusContext.Provider value={value}>
64 + {children}
65 + </ProfilerStatusContext.Provider>
66 + );
67 +}
68 +
69 +export { ProfilerStatusContext, ProfilerStatusContextController };
src/devtools/views/Profiler/RecordToggle.js
+2 -2
@@ -3,7 +3,7 @@
3 import React, { useContext } from 'react';
4 import Button from '../Button';
5 import ButtonIcon from '../ButtonIcon';
6 -import { ProfilerContext } from './ProfilerContext';
6 +import { ProfilerStatusContext } from './ProfilerStatusContext';
7
8 import styles from './RecordToggle.css';
9
@@ -11,7 +11,7 @@ export type Props = {||};
11
12 export default function RecordToggle(_: Props) {
13 const { isProfiling, startProfiling, stopProfiling } = useContext(
14 - ProfilerContext
14 + ProfilerStatusContext
15 );
16
17 return (
src/devtools/views/Profiler/SnapshotCommitList.js
+36 -36
@@ -2,6 +2,7 @@
2
3 import React, {
4 useCallback,
5 + useContext,
6 useEffect,
7 useMemo,
8 useRef,
@@ -11,67 +12,64 @@ import AutoSizer from 'react-virtualized-auto-sizer';
12 import { FixedSizeList } from 'react-window';
13 import SnapshotCommitListItem from './SnapshotCommitListItem';
14 import { minBarWidth } from './constants';
15 +import { ProfilerDataContext } from './ProfilerDataContext';
16 +import { StoreContext } from '../context';
17
18 export type ItemData = {|
19 commitDurations: Array<number>,
20 + commitIndex: number | null,
21 commitTimes: Array<number>,
22 + filteredCommitIndices: Array<number>,
23 isMouseDown: boolean,
24 maxDuration: number,
20 - selectedCommitIndex: number,
25 setCommitIndex: (index: number) => void,
26 |};
27
24 -type Props = {|
25 - commitDurations: Array<number>,
26 - commitTimes: Array<number>,
27 - selectedCommitIndex: number,
28 - setCommitIndex: (index: number) => void,
29 - viewNextCommit: () => void,
30 - viewPrevCommit: () => void,
31 -|};
28 +type Props = {||};
29
33 -export default function SnapshotCommitList(props: Props) {
30 +export default function SnapshotCommitList(_: Props) {
31 return (
32 <AutoSizer>
36 - {({ height, width }) => <List height={height} width={width} {...props} />}
33 + {({ height, width }) => <List height={height} width={width} />}
34 </AutoSizer>
35 );
36 }
37
38 type ListProps = {|
39 height: number,
43 - commitDurations: Array<number>,
44 - commitTimes: Array<number>,
45 - selectedCommitIndex: number,
46 - setCommitIndex: (index: number) => void,
47 - viewNextCommit: () => void,
48 - viewPrevCommit: () => void,
40 width: number,
41 |};
42
52 -function List({
53 - height,
54 - commitDurations,
55 - commitTimes,
56 - selectedCommitIndex,
57 - setCommitIndex,
58 - viewNextCommit,
59 - viewPrevCommit,
60 - width,
61 -}: ListProps) {
43 +function List({ height, width }: ListProps) {
44 const listRef = useRef<FixedSizeList<ItemData> | null>(null);
45 const [isMouseDown, setIsMouseDown] = useState(false);
64 - const prevSelectedCommitIndexRef = useRef<number>(-1);
46 + const prevCommitIndexRef = useRef<number | null>(null);
47 +
48 + const { profilingCache } = useContext(StoreContext);
49 + const {
50 + commitIndex,
51 + filteredCommitIndices,
52 + rendererID,
53 + rootID,
54 + setCommitIndex,
55 + } = useContext(ProfilerDataContext);
56 +
57 + const { commitDurations, commitTimes } = profilingCache.ProfilingSummary.read(
58 + {
59 + rendererID: ((rendererID: any): number),
60 + rootID: ((rootID: any): number),
61 + }
62 + );
63
64 // Make sure any newly selected snapshot is visible within the list.
65 useEffect(() => {
68 - if (selectedCommitIndex !== prevSelectedCommitIndexRef.current) {
69 - prevSelectedCommitIndexRef.current = selectedCommitIndex;
70 - if (listRef.current !== null) {
71 - listRef.current.scrollToItem(selectedCommitIndex);
66 + if (commitIndex !== prevCommitIndexRef.current) {
67 + prevCommitIndexRef.current = commitIndex;
68 + if (commitIndex !== null && listRef.current !== null) {
69 + listRef.current.scrollToItem(commitIndex);
70 }
71 }
74 - }, [listRef, selectedCommitIndex]);
72 + }, [listRef, commitIndex]);
73
74 const handleMouseDown = useCallback(() => {
75 setIsMouseDown(true);
@@ -104,18 +102,20 @@ function List({
102 const itemData = useMemo<ItemData>(
103 () => ({
104 commitDurations,
105 + commitIndex,
106 commitTimes,
107 + filteredCommitIndices,
108 isMouseDown,
109 maxDuration,
110 - selectedCommitIndex,
110 setCommitIndex,
111 }),
112 [
113 commitDurations,
114 + commitIndex,
115 commitTimes,
116 + filteredCommitIndices,
117 isMouseDown,
118 maxDuration,
118 - selectedCommitIndex,
119 setCommitIndex,
120 ]
121 );
@@ -130,7 +130,7 @@ function List({
130 <FixedSizeList
131 direction="horizontal"
132 height={height}
133 - itemCount={commitDurations.length}
133 + itemCount={filteredCommitIndices.length}
134 itemData={itemData}
135 itemSize={itemSize}
136 ref={(listRef: any) /* Flow bug? */}
src/devtools/views/Profiler/SnapshotCommitListItem.js
+5 -2
@@ -17,13 +17,16 @@ type Props = {
17 function SnapshotCommitListItem({ data: itemData, index, style }: Props) {
18 const {
19 commitDurations,
20 + commitIndex,
21 commitTimes,
22 + filteredCommitIndices,
23 isMouseDown,
24 maxDuration,
23 - selectedCommitIndex,
25 setCommitIndex,
26 } = itemData;
27
28 + index = filteredCommitIndices[index];
29 +
30 const commitDuration = commitDurations[index];
31 const commitTime = commitTimes[index];
32
@@ -35,7 +38,7 @@ function SnapshotCommitListItem({ data: itemData, index, style }: Props) {
38 // Guard against commits with duration 0
39 const percentage =
40 Math.min(1, Math.max(0, commitDuration / maxDuration)) || 0;
38 - const isSelected = selectedCommitIndex === index;
41 + const isSelected = commitIndex === index;
42
43 // Leave a 1px gap between snapshots
44 const width = parseFloat(style.width) - 1;
src/devtools/views/Profiler/SnapshotSelector.js
+29 -68
@@ -1,106 +1,72 @@
1 // @flow
2
3 -import React, { Fragment, Suspense, useContext } from 'react';
3 +import React, { Fragment, useCallback, useContext } from 'react';
4 import Button from '../Button';
5 import ButtonIcon from '../ButtonIcon';
6 -import { StoreContext } from '../context';
7 -import { ProfilerContext } from './ProfilerContext';
6 +import { ProfilerDataContext } from './ProfilerDataContext';
7 import SnapshotCommitList from './SnapshotCommitList';
8
9 import styles from './SnapshotSelector.css';
10
11 export type Props = {||};
12
14 -export default function SnapshotSelectorSuspense(_: Props) {
15 - return (
16 - <Suspense fallback={<SnapshotSelectorFallback />}>
17 - <SnapshotSelector />
18 - </Suspense>
19 - );
20 -}
21 -
22 -function SnapshotSelector(_: Props) {
23 - const { profilingCache } = useContext(StoreContext);
13 +export default function SnapshotSelector(_: Props) {
14 const {
15 commitIndex,
26 - isMinCommitDurationEnabled,
27 - minCommitDuration,
16 + filteredCommitIndices,
17 rendererID,
18 rootID,
19 setCommitIndex,
31 - } = useContext(ProfilerContext);
20 + } = useContext(ProfilerDataContext);
21
33 - if (rendererID === null || rootID === null) {
34 - return null;
22 + const numCommits = filteredCommitIndices.length;
23 + let currentCommitNumber = '-';
24 + if (numCommits > 0) {
25 + currentCommitNumber = `${commitIndex + 1}`.padStart(
26 + `${numCommits}`.length,
27 + '0'
28 + );
29 }
30
37 - const { commitDurations, commitTimes } = profilingCache.ProfilingSummary.read(
38 - {
39 - rendererID: ((rendererID: any): number),
40 - rootID: ((rootID: any): number),
41 - }
42 - );
31 + const viewNextCommit = useCallback(() => {
32 + const nextCommitIndex = Math.min(
33 + ((commitIndex: any): number) + 1,
34 + filteredCommitIndices.length - 1
35 + );
36 + setCommitIndex(filteredCommitIndices[nextCommitIndex]);
37 + }, [commitIndex, filteredCommitIndices, setCommitIndex]);
38 + const viewPrevCommit = useCallback(() => {
39 + const nextCommitIndex = Math.max(((commitIndex: any): number) - 1, 0);
40 + setCommitIndex(filteredCommitIndices[nextCommitIndex]);
41 + }, [commitIndex, filteredCommitIndices, setCommitIndex]);
42
44 - // TODO (profiling) This is not sufficient; index here doesn't map to a meaningful index in the profiling data.
45 - let filteredCommitDurations = commitDurations;
46 - let filteredCommitTimes = commitTimes;
47 - if (isMinCommitDurationEnabled) {
48 - filteredCommitDurations = [];
49 - filteredCommitTimes = [];
50 - for (let i = 0; i < commitDurations.length; i++) {
51 - if (commitDurations[i] >= minCommitDuration) {
52 - filteredCommitDurations.push(commitDurations[i]);
53 - filteredCommitTimes.push(commitTimes[i]);
54 - }
55 - }
43 + if (rendererID === null || rootID === null) {
44 + return null;
45 }
46
58 - const numCommits = filteredCommitDurations.length;
59 - const currentCommitNumber = `${
60 - numCommits > 0 ? commitIndex + 1 : '-'
61 - }`.padStart(`${numCommits}`.length, '0');
62 -
63 - // TODO (profiler) We need to guard commit index and share filterd statuses in a better way.
64 -
65 - const viewNextCommit = () => {
66 - setCommitIndex(Math.min(commitIndex + 1, numCommits - 1));
67 - };
68 - const viewPrevCommit = () => {
69 - setCommitIndex(Math.max(commitIndex - 1, 0));
70 - };
71 -
47 return (
48 <Fragment>
49 <div className={styles.VRule} />
50 <div className={styles.SnapshotSelector}>
51 <span className={styles.Number}>
77 - {currentCommitNumber} / {numCommits}
52 + {numCommits > 0 ? `${currentCommitNumber} / ${numCommits}` : '-'}
53 </span>
54 <Button
55 className={styles.Button}
81 - disabled={numCommits === 0 || commitIndex <= 0}
56 + disabled={commitIndex === null || commitIndex <= 0}
57 onClick={viewPrevCommit}
58 >
59 <ButtonIcon type="previous" />
60 </Button>
61 <div className={styles.Commits}>
87 - {numCommits > 0 && (
88 - <SnapshotCommitList
89 - commitDurations={filteredCommitDurations}
90 - commitTimes={filteredCommitTimes}
91 - selectedCommitIndex={commitIndex}
92 - setCommitIndex={setCommitIndex}
93 - viewNextCommit={viewNextCommit}
94 - viewPrevCommit={viewPrevCommit}
95 - />
96 - )}
62 + {numCommits > 0 && <SnapshotCommitList />}
63 {numCommits === 0 && (
64 <div className={styles.NoCommits}>No commits</div>
65 )}
66 </div>
67 <Button
68 className={styles.Button}
103 - disabled={commitIndex >= numCommits - 1}
69 + disabled={commitIndex === null || commitIndex >= numCommits - 1}
70 onClick={viewNextCommit}
71 >
72 <ButtonIcon type="next" />
@@ -109,8 +75,3 @@ function SnapshotSelector(_: Props) {
75 </Fragment>
76 );
77 }
112 -
113 -function SnapshotSelectorFallback() {
114 - // TODO (profiling) Better loading UI
115 - return <div className={styles.SnapshotSelector}>Loading...</div>;
116 -}
src/devtools/views/TabBar.css
+5 -2
@@ -66,11 +66,14 @@
66 .TabLabelSmall {
67 display: none;
68 }
69 +
70 + .IconSizeSmall {
71 + margin-right: 0;
72 + }
73 }
74
75 @media screen and (max-width: 600px) {
72 - .IconSizeLarge,
73 - .IconSizeSmall {
76 + .IconSizeLarge {
77 margin-right: 0;
78 }
79
src/devtools/views/hooks.js
+27 -15
@@ -2,9 +2,15 @@
2
3 import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
4
5 +type LocalStorageKey =
6 + | 'displayDensity'
7 + | 'isCommitFilterEnabled'
8 + | 'minCommitDuration'
9 + | 'theme';
10 +
11 // Forked from https://usehooks.com/useLocalStorage/
12 export function useLocalStorage<T>(
7 - key: string,
13 + key: LocalStorageKey,
14 initialValue: T
15 ): [T, (value: T | (() => T)) => void] {
16 const getValueFromLocalStorage = useCallback(() => {
@@ -85,24 +91,27 @@ export function useModalDismissSignal(
91 }
92
93 // Copied from https://github.com/facebook/react/pull/15022
88 -export function useSubscription<Value, Source>({
89 - source,
94 +export function useSubscription<Value>({
95 getCurrentValue,
96 subscribe,
97 }: {|
93 - source: Source,
94 - getCurrentValue: (source: Source) => Value,
95 - subscribe: (source: Source, callback: Function) => () => void,
98 + getCurrentValue: () => Value,
99 + subscribe: (callback: Function) => () => void,
100 |}): Value {
101 const [state, setState] = useState({
98 - source,
99 - value: getCurrentValue(source),
102 + getCurrentValue,
103 + subscribe,
104 + value: getCurrentValue(),
105 });
106
102 - if (state.source !== source) {
107 + if (
108 + state.getCurrentValue !== getCurrentValue ||
109 + state.subscribe !== subscribe
110 + ) {
111 setState({
104 - source,
105 - value: getCurrentValue(source),
112 + getCurrentValue,
113 + subscribe,
114 + value: getCurrentValue(),
115 });
116 }
117
@@ -115,11 +124,14 @@ export function useSubscription<Value, Source>({
124 }
125
126 setState(prevState => {
118 - if (prevState.source !== source) {
127 + if (
128 + prevState.getCurrentValue !== getCurrentValue ||
129 + prevState.subscribe !== subscribe
130 + ) {
131 return prevState;
132 }
133
122 - const value = getCurrentValue(source);
134 + const value = getCurrentValue();
135 if (prevState.value === value) {
136 return prevState;
137 }
@@ -127,7 +139,7 @@ export function useSubscription<Value, Source>({
139 return { ...prevState, value };
140 });
141 };
130 - const unsubscribe = subscribe(source, checkForUpdates);
142 + const unsubscribe = subscribe(checkForUpdates);
143
144 checkForUpdates();
145
@@ -135,7 +147,7 @@ export function useSubscription<Value, Source>({
147 didUnsubscribe = true;
148 unsubscribe();
149 };
138 - }, [getCurrentValue, source, subscribe]);
150 + }, [getCurrentValue, subscribe]);
151
152 return state.value;
153 }