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
}