Implemented ranked chart
Brian Vaughn committed
Mar 16, 2019 at 14:06 UTC
01ad9f1da6d6db997b1b2c865b2eb36de577538d
16 files changed
+484
-132
src/devtools/ProfilingCache.js
+99
-76
@@ -6,6 +6,10 @@ import {
6
getCommitTree,
7
invalidateCommitTrees,
8
} from 'src/devtools/views/Profiler/CommitTreeBuilder';
9
+import {
10
+ getChartData as getRankedChartData,
11
+ invalidateChartData as invalidateRankedChartData,
12
+} from 'src/devtools/views/Profiler/RankedChartBuilder';
13
14
import type { Resource } from './cache';
15
import type { Bridge } from '../types';
@@ -15,9 +19,10 @@ import type {
19
} from 'src/backend/types';
20
import type {
21
CommitDetails as CommitDetailsFrontend,
18
- CommitTree,
22
+ CommitTree as CommitTreeFrontend,
23
ProfilingSummary as ProfilingSummaryFrontend,
24
} from 'src/devtools/views/Profiler/types';
25
+import type { ChartData as RankedChartData } from 'src/devtools/views/Profiler/RankedChartBuilder';
26
27
type CommitDetailsParams = {|
28
commitIndex: number,
@@ -25,7 +30,7 @@ type CommitDetailsParams = {|
30
rendererID: number,
31
|};
32
28
-type CommitTreeParams = {|
33
+type GetCommitTreeParams = {|
34
commitIndex: number,
35
profilingSummary: ProfilingSummaryFrontend,
36
rendererID: number,
@@ -38,6 +43,9 @@ type ProfilingSummaryParams = {|
43
|};
44
45
export default class ProfilingCache {
46
+ _bridge: Bridge;
47
+ _store: Store;
48
+
49
_pendingCommitDetailsMap: Map<
50
string,
51
(commitDetails: CommitDetailsFrontend) => void
@@ -48,90 +56,105 @@ export default class ProfilingCache {
56
(profilingSummary: ProfilingSummaryFrontend) => void
57
> = new Map();
58
51
- CommitDetails: Resource<CommitDetailsParams, CommitDetailsFrontend>;
52
- CommitTree: Resource<CommitTreeParams, CommitTree>;
53
- ProfilingSummary: Resource<ProfilingSummaryParams, ProfilingSummaryFrontend>;
59
+ CommitDetails: Resource<
60
+ CommitDetailsParams,
61
+ CommitDetailsFrontend
62
+ > = createResource(
63
+ ({ commitIndex, rendererID, rootID }: CommitDetailsParams) => {
64
+ return new Promise(resolve => {
65
+ if (!this._store.profilingOperations.has(rootID)) {
66
+ // If no profiling data was recorded for this root, skip the round trip.
67
+ resolve({
68
+ actualDurations: new Map(),
69
+ interactions: [],
70
+ });
71
+ } else {
72
+ this._pendingCommitDetailsMap.set(
73
+ `${rootID}-${commitIndex}`,
74
+ resolve
75
+ );
76
+ this._bridge.send('getCommitDetails', {
77
+ commitIndex,
78
+ rendererID,
79
+ rootID,
80
+ });
81
+ }
82
+ });
83
+ },
84
+ ({ commitIndex, rendererID, rootID }: CommitDetailsParams) =>
85
+ `${rootID}-${commitIndex}`
86
+ );
87
+
88
+ ProfilingSummary: Resource<
89
+ ProfilingSummaryParams,
90
+ ProfilingSummaryFrontend
91
+ > = createResource(
92
+ ({ rendererID, rootID }: ProfilingSummaryParams) => {
93
+ return new Promise(resolve => {
94
+ if (!this._store.profilingOperations.has(rootID)) {
95
+ // If no profiling data was recorded for this root, skip the round trip.
96
+ resolve({
97
+ commitDurations: [],
98
+ commitTimes: [],
99
+ initialTreeBaseDurations: new Map(),
100
+ interactionCount: 0,
101
+ });
102
+ } else {
103
+ this._pendingProfileSummaryMap.set(rootID, resolve);
104
+ this._bridge.send('getProfilingSummary', { rendererID, rootID });
105
+ }
106
+ });
107
+ },
108
+ ({ rendererID, rootID }: ProfilingSummaryParams) => rootID
109
+ );
110
111
constructor(bridge: Bridge, store: Store) {
56
- this.CommitDetails = createResource(
57
- ({ commitIndex, rendererID, rootID }: CommitDetailsParams) => {
58
- return new Promise(resolve => {
59
- if (!store.profilingOperations.has(rootID)) {
60
- // If no profiling data was recorded for this root, skip the round trip.
61
- resolve({
62
- actualDurations: new Map(),
63
- interactions: [],
64
- });
65
- } else {
66
- this._pendingCommitDetailsMap.set(
67
- `${rootID}-${commitIndex}`,
68
- resolve
69
- );
70
- bridge.send('getCommitDetails', {
71
- commitIndex,
72
- rendererID,
73
- rootID,
74
- });
75
- }
76
- });
77
- },
78
- ({ commitIndex, rendererID, rootID }: CommitDetailsParams) =>
79
- `${rootID}-${commitIndex}`
80
- );
81
-
82
- this.CommitTree = createResource(
83
- ({
84
- commitIndex,
85
- profilingSummary,
86
- rendererID,
87
- rootID,
88
- }: CommitTreeParams) =>
89
- new Promise(resolve =>
90
- resolve(
91
- getCommitTree({
92
- commitIndex,
93
- profilingSummary,
94
- rendererID,
95
- rootID,
96
- store,
97
- })
98
- )
99
- ),
100
- ({
101
- commitIndex,
102
- profilingSummary,
103
- rendererID,
104
- rootID,
105
- }: CommitTreeParams) => `${rootID}-${commitIndex}`
106
- );
107
-
108
- this.ProfilingSummary = createResource(
109
- ({ rendererID, rootID }: ProfilingSummaryParams) => {
110
- return new Promise(resolve => {
111
- if (!store.profilingOperations.has(rootID)) {
112
- // If no profiling data was recorded for this root, skip the round trip.
113
- resolve({
114
- commitDurations: [],
115
- commitTimes: [],
116
- initialTreeBaseDurations: new Map(),
117
- interactionCount: 0,
118
- });
119
- } else {
120
- this._pendingProfileSummaryMap.set(rootID, resolve);
121
- bridge.send('getProfilingSummary', { rendererID, rootID });
122
- }
123
- });
124
- },
125
- ({ rendererID, rootID }: ProfilingSummaryParams) => rootID
126
- );
112
+ this._bridge = bridge;
113
+ this._store = store;
114
115
bridge.addListener('commitDetails', this.onCommitDetails);
116
bridge.addListener('profilingSummary', this.onProfileSummary);
117
}
118
119
+ getCommitTree = ({
120
+ commitIndex,
121
+ profilingSummary,
122
+ rendererID,
123
+ rootID,
124
+ }: GetCommitTreeParams) =>
125
+ getCommitTree({
126
+ commitIndex,
127
+ profilingSummary,
128
+ rendererID,
129
+ rootID,
130
+ store: this._store,
131
+ });
132
+
133
+ getRankedChartData = ({
134
+ commitDetails,
135
+ commitIndex,
136
+ commitTree,
137
+ rootID,
138
+ }: {|
139
+ commitDetails: CommitDetailsFrontend,
140
+ commitIndex: number,
141
+ commitTree: CommitTreeFrontend,
142
+ rootID: number,
143
+ |}): RankedChartData =>
144
+ getRankedChartData({
145
+ commitDetails,
146
+ commitIndex,
147
+ commitTree,
148
+ rootID,
149
+ });
150
+
151
invalidate() {
152
+ // Invalidate Susepnse caches.
153
invalidateResources();
154
+
155
+ // Invalidate non-Suspense caches too.
156
invalidateCommitTrees();
157
+ invalidateRankedChartData();
158
159
this._pendingCommitDetailsMap.clear();
160
this._pendingProfileSummaryMap.clear();
src/devtools/views/Profiler/ChartNode.css
new
+31
@@ -0,0 +1,31 @@
1
+.Group {
2
+ transition: all ease-in-out 250ms;
3
+}
4
+
5
+.Rect {
6
+ cursor: pointer;
7
+ stroke: var(--color-background);
8
+ transition: all ease-in-out 250ms;
9
+}
10
+
11
+.ForeignObject {
12
+ pointer-events: none;
13
+ transition: all ease-in-out 250ms;
14
+}
15
+
16
+.Div {
17
+ pointer-events: none;
18
+ white-space: nowrap;
19
+ text-overflow: ellipsis;
20
+ overflow: hidden;
21
+ font-size: var(--font-family-sans);
22
+ font-family: var(--font-size-sans-normal);
23
+ margin-left: 4px;
24
+ margin-right: 4px;
25
+ line-height: 1.5;
26
+ padding: 0 0 0;
27
+ font-weight: 400;
28
+ color: var(--color-commit-gradient-text);
29
+ text-align: left;
30
+ transition: all ease-in-out 250ms;
31
+}
src/devtools/views/Profiler/ChartNode.js
new
+67
@@ -0,0 +1,67 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { textHeight } from './constants';
5
+
6
+import styles from './ChartNode.css';
7
+
8
+type Props = {|
9
+ color: string,
10
+ height: number,
11
+ isDimmed?: boolean,
12
+ label: string,
13
+ onClick: Function,
14
+ onDoubleClick?: Function,
15
+ placeLabelAboveNode?: boolean,
16
+ title: string,
17
+ width: number,
18
+ x: number,
19
+ y: number,
20
+|};
21
+
22
+const minWidthToDisplay = 35;
23
+
24
+export default function ChartNode({
25
+ color,
26
+ height,
27
+ isDimmed = false,
28
+ label,
29
+ onClick,
30
+ onDoubleClick,
31
+ title,
32
+ width,
33
+ x,
34
+ y,
35
+}: Props) {
36
+ return (
37
+ <g className={styles.Group} transform={`translate(${x},${y})`}>
38
+ <title>{title}</title>
39
+ <rect
40
+ width={width}
41
+ height={height}
42
+ fill={color}
43
+ onClick={onClick}
44
+ onDoubleClick={onDoubleClick}
45
+ className={styles.Rect}
46
+ style={{
47
+ opacity: isDimmed ? 0.5 : 1,
48
+ }}
49
+ />
50
+ {width >= minWidthToDisplay && (
51
+ <foreignObject
52
+ width={width}
53
+ height={height}
54
+ className={styles.ForeignObject}
55
+ style={{
56
+ paddingLeft: x < 0 ? -x : 0,
57
+ opacity: isDimmed ? 0.75 : 1,
58
+ display: width < minWidthToDisplay ? 'none' : 'block',
59
+ }}
60
+ y={height < textHeight ? -textHeight : 0}
61
+ >
62
+ <div className={styles.Div}>{label}</div>
63
+ </foreignObject>
64
+ )}
65
+ </g>
66
+ );
67
+}
src/devtools/views/Profiler/CommitFlamegraph.js
+1
-1
@@ -24,7 +24,7 @@ export default function CommitFlamegraph(_: {||}) {
24
rootID: ((rootID: any): number),
25
});
26
27
- const commitTree = profilingCache.CommitTree.read({
27
+ const commitTree = profilingCache.getCommitTree({
28
commitIndex: ((selectedCommitIndex: any): number),
29
profilingSummary,
30
rendererID: ((rendererID: any): number),
src/devtools/views/Profiler/CommitRanked.css
+5
@@ -0,0 +1,5 @@
1
+.Container {
2
+ width: 100%;
3
+ height: 100%;
4
+ padding: 0.5rem;
5
+}
src/devtools/views/Profiler/CommitRanked.js
+94
-55
@@ -1,18 +1,55 @@
1
// @flow
2
3
-import React, { useContext } from 'react';
3
+import React, { useCallback, useContext, useMemo } from 'react';
4
+import AutoSizer from 'react-virtualized-auto-sizer';
5
+import { FixedSizeList } from 'react-window';
6
import { ProfilerContext } from './ProfilerContext';
5
-import { calculateSelfDuration } from './utils';
7
+import NoCommitData from './NoCommitData';
8
+import CommitRankedListItem from './CommitRankedListItem';
9
+import { barHeight } from './constants';
10
+import { scale } from './utils';
11
import { StoreContext } from '../context';
12
13
import styles from './CommitRanked.css';
14
10
-import type { CommitDetails, CommitTree, Node } from './types';
15
+import type { ChartData } from './RankedChartBuilder';
16
12
-export default function CommitRanked(_: {||}) {
13
- const { rendererID, rootID, selectedCommitIndex } = useContext(
14
- ProfilerContext
17
+export type ItemData = {|
18
+ chartData: ChartData,
19
+ scaleX: (value: number, fallbackValue: number) => number,
20
+ selectedFiberID: number | null,
21
+ selectedFiberIndex: number,
22
+ selectFiber: (id: number | null) => void,
23
+ width: number,
24
+|};
25
+
26
+export default function CommitRankedAutoSizer(_: {||}) {
27
+ const { selectFiber } = useContext(ProfilerContext);
28
+ const deselectCurrentFiber = useCallback(
29
+ event => {
30
+ event.stopPropagation();
31
+ selectFiber(null);
32
+ },
33
+ [selectFiber]
34
+ );
35
+
36
+ return (
37
+ <div className={styles.Container} onClick={deselectCurrentFiber}>
38
+ <AutoSizer>
39
+ {({ height, width }) => <CommitRanked height={height} width={width} />}
40
+ </AutoSizer>
41
+ </div>
42
);
43
+}
44
+
45
+function CommitRanked({ height, width }: {| height: number, width: number |}) {
46
+ const {
47
+ rendererID,
48
+ rootID,
49
+ selectedCommitIndex,
50
+ selectedFiberID,
51
+ selectFiber,
52
+ } = useContext(ProfilerContext);
53
54
const { profilingCache } = useContext(StoreContext);
55
@@ -27,64 +64,66 @@ export default function CommitRanked(_: {||}) {
64
rootID: ((rootID: any): number),
65
});
66
30
- const commitTree = profilingCache.CommitTree.read({
67
+ const commitTree = profilingCache.getCommitTree({
68
commitIndex: ((selectedCommitIndex: any): number),
69
profilingSummary,
70
rendererID: ((rendererID: any): number),
71
rootID: ((rootID: any): number),
72
});
73
37
- const chartData = generateChartData(commitTree, commitDetails);
38
-
39
- return 'Coming soon: Ranked';
40
-}
41
-
42
-type ChartNode = {|
43
- id: number,
44
- label: string,
45
- name: string,
46
- title: string,
47
- value: number,
48
-|};
49
-
50
-type ChartData = {|
51
- maxValue: number,
52
- nodes: Array<ChartNode>,
53
-|};
74
+ const chartData = profilingCache.getRankedChartData({
75
+ commitDetails,
76
+ commitIndex: ((selectedCommitIndex: any): number),
77
+ commitTree,
78
+ rootID: ((rootID: any): number),
79
+ });
80
55
-const generateChartData = (
56
- commitTree: CommitTree,
57
- commitDetails: CommitDetails
58
-): ChartData => {
59
- const { nodes } = commitTree;
81
+ const selectedFiberIndex = useMemo(
82
+ () => getNodeIndex(chartData, selectedFiberID),
83
+ [chartData, selectedFiberID]
84
+ );
85
61
- let maxSelfDuration = 0;
86
+ const itemData = useMemo<ItemData>(
87
+ () => ({
88
+ chartData,
89
+ scaleX: scale(0, chartData.nodes[selectedFiberIndex].value, 0, width),
90
+ selectedFiberID,
91
+ selectedFiberIndex,
92
+ selectFiber,
93
+ width,
94
+ }),
95
+ [chartData, selectedFiberID, selectedFiberIndex, selectFiber, width]
96
+ );
97
63
- const chartNodes: Array<ChartNode> = [];
64
- commitDetails.actualDurations.forEach((actualDuration, id) => {
65
- const node = ((nodes.get(id): any): Node);
98
+ // If a commit contains no fibers with an actualDuration > 0,
99
+ // Display a fallback message.
100
+ if (chartData.nodes.length === 0) {
101
+ return <NoCommitData />;
102
+ }
103
+
104
+ return (
105
+ <FixedSizeList
106
+ height={height}
107
+ innerTagName="svg"
108
+ itemCount={chartData.nodes.length}
109
+ itemData={itemData}
110
+ itemSize={barHeight}
111
+ width={width}
112
+ >
113
+ {CommitRankedListItem}
114
+ </FixedSizeList>
115
+ );
116
+}
117
67
- // Don't show the root node in this chart.
68
- if (node.parentID === 0) {
69
- return;
118
+const getNodeIndex = (chartData: ChartData, id: number | null): number => {
119
+ if (id === null) {
120
+ return 0;
121
+ }
122
+ const { nodes } = chartData;
123
+ for (let index = 0; index < nodes.length; index++) {
124
+ if (nodes[index].id === id) {
125
+ return index;
126
}
71
-
72
- const selfDuration = calculateSelfDuration(id, commitTree, commitDetails);
73
- maxSelfDuration = Math.max(maxSelfDuration, selfDuration);
74
-
75
- const name = node.displayName || 'Unknown';
76
- const label = `${name} (${selfDuration.toFixed(1)}ms)`;
77
- chartNodes.push({
78
- id,
79
- label,
80
- name,
81
- title: label,
82
- value: selfDuration,
83
- });
84
- });
85
-
86
- return {
87
- maxValue: maxSelfDuration,
88
- nodes: chartNodes.sort((a, b) => b.value - a.value),
89
- };
127
+ }
128
+ return 0;
129
};
src/devtools/views/Profiler/CommitRankedListItem.js
new
+51
@@ -0,0 +1,51 @@
1
+// @flow
2
+
3
+import React, { useCallback } from 'react';
4
+import { barHeight, minBarWidth } from './constants';
5
+import { getGradientColor } from './utils';
6
+import ChartNode from './ChartNode';
7
+
8
+import type { ItemData } from './CommitRanked';
9
+
10
+export default function CommitRankedListItem({
11
+ data,
12
+ index,
13
+ style,
14
+}: {
15
+ data: ItemData,
16
+ index: number,
17
+ style: Object,
18
+}) {
19
+ const { chartData, scaleX, selectedFiberIndex, selectFiber, width } = data;
20
+
21
+ const node = chartData.nodes[index];
22
+
23
+ const handleClick = useCallback(
24
+ event => {
25
+ event.stopPropagation();
26
+ selectFiber(node.id);
27
+ },
28
+ [node, selectFiber]
29
+ );
30
+
31
+ // List items are absolutely positioned using the CSS "top" attribute.
32
+ // The "left" value will always be 0.
33
+ // Since height is fixed, and width is based on the node's duration,
34
+ // We can ignore those values as well.
35
+ const top = parseInt(style.top, 10);
36
+
37
+ return (
38
+ <ChartNode
39
+ color={getGradientColor(node.value / chartData.maxValue)}
40
+ height={barHeight}
41
+ isDimmed={index < selectedFiberIndex}
42
+ key={node.id}
43
+ label={node.label}
44
+ onClick={handleClick}
45
+ title={node.title}
46
+ width={Math.max(minBarWidth, scaleX(node.value, width))}
47
+ x={0}
48
+ y={top}
49
+ />
50
+ );
51
+}
src/devtools/views/Profiler/NoCommitData.css
new
+11
@@ -0,0 +1,11 @@
1
+.NoCommitData {
2
+ width: 100%;
3
+ height: 100%;
4
+ display: flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+}
8
+
9
+.Header {
10
+ font-size: var(--font-size-sans-large);
11
+}
src/devtools/views/Profiler/NoCommitData.js
new
+20
@@ -0,0 +1,20 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import styles from './NoCommitData.css';
6
+
7
+export default function NoCommitData() {
8
+ return (
9
+ <div className={styles.NoCommitData}>
10
+ <p className={styles.Header}>
11
+ There is no timing data to display for the currently selected commit.
12
+ </p>
13
+ <p>
14
+ This can indicate that a render occurred too quickly for the timing API
15
+ to measure. Try selecting another commit in the upper, right-hand
16
+ corner.
17
+ </p>
18
+ </div>
19
+ );
20
+}
src/devtools/views/Profiler/ProfilerContext.js
+12
@@ -44,6 +44,10 @@ type Context = {|
44
// It impacts the flame graph and ranked charts.
45
selectedCommitIndex: number | null,
46
setSelectedCommitIndex: (value: number | null) => void,
47
+
48
+ // Which fiber is currently selected in the Ranked or Flamegraph charts?
49
+ selectedFiberID: number | null,
50
+ selectFiber: (id: number | null) => void,
51
|};
52
53
const ProfilerContext = createContext<Context>(((null: any): Context));
@@ -118,6 +122,8 @@ function ProfilerContextController({ children }: Props) {
122
null
123
);
124
125
+ const [selectedFiberID, selectFiber] = useState<number | null>(null);
126
+
127
const value = useMemo(
128
() => ({
129
hasProfilingData,
@@ -136,6 +142,9 @@ function ProfilerContextController({ children }: Props) {
142
143
selectedCommitIndex,
144
setSelectedCommitIndex,
145
+
146
+ selectedFiberID,
147
+ selectFiber,
148
}),
149
[
150
hasProfilingData,
@@ -154,6 +163,9 @@ function ProfilerContextController({ children }: Props) {
163
164
selectedCommitIndex,
165
setSelectedCommitIndex,
166
+
167
+ selectedFiberID,
168
+ selectFiber,
169
]
170
);
171
src/devtools/views/Profiler/RankedChartBuilder.js
new
+78
@@ -0,0 +1,78 @@
1
+// @flow
2
+
3
+import { calculateSelfDuration } from './utils';
4
+
5
+import type { CommitDetails, CommitTree, Node } from './types';
6
+
7
+export type ChartNode = {|
8
+ id: number,
9
+ label: string,
10
+ name: string,
11
+ title: string,
12
+ value: number,
13
+|};
14
+
15
+export type ChartData = {|
16
+ maxValue: number,
17
+ nodes: Array<ChartNode>,
18
+|};
19
+
20
+const cachedChartData: Map<string, ChartData> = new Map();
21
+
22
+export function getChartData({
23
+ commitDetails,
24
+ commitIndex,
25
+ commitTree,
26
+ rootID,
27
+}: {|
28
+ commitDetails: CommitDetails,
29
+ commitIndex: number,
30
+ commitTree: CommitTree,
31
+ rootID: number,
32
+|}): ChartData {
33
+ const key = `${rootID}-${commitIndex}`;
34
+
35
+ if (cachedChartData.has(key)) {
36
+ return ((cachedChartData.get(key): any): ChartData);
37
+ }
38
+
39
+ const { nodes } = commitTree;
40
+
41
+ let maxSelfDuration = 0;
42
+
43
+ const chartNodes: Array<ChartNode> = [];
44
+ commitDetails.actualDurations.forEach((actualDuration, id) => {
45
+ const node = ((nodes.get(id): any): Node);
46
+
47
+ // Don't show the root node in this chart.
48
+ if (node.parentID === 0) {
49
+ return;
50
+ }
51
+
52
+ const selfDuration = calculateSelfDuration(id, commitTree, commitDetails);
53
+ maxSelfDuration = Math.max(maxSelfDuration, selfDuration);
54
+
55
+ const name = node.displayName || 'Unknown';
56
+ const label = `${name} (${selfDuration.toFixed(1)}ms)`;
57
+ chartNodes.push({
58
+ id,
59
+ label,
60
+ name,
61
+ title: label,
62
+ value: selfDuration,
63
+ });
64
+ });
65
+
66
+ const chartData = {
67
+ maxValue: maxSelfDuration,
68
+ nodes: chartNodes.sort((a, b) => b.value - a.value),
69
+ };
70
+
71
+ cachedChartData.set(key, chartData);
72
+
73
+ return chartData;
74
+}
75
+
76
+export function invalidateChartData(): void {
77
+ cachedChartData.clear();
78
+}
src/devtools/views/Profiler/SnapshotSelector.css
+1
@@ -21,6 +21,7 @@
21
flex: 0 0 auto;
22
white-space: nowrap;
23
font-family: var(--font-family-monospace);
24
+ margin-left: 0.25rem;
25
}
26
27
.NoCommits {
src/devtools/views/Profiler/constants.js
+1
@@ -1,5 +1,6 @@
1
// @flow
2
3
+export const barHeight = 20;
4
export const maxBarWidth = 30;
5
export const minBarHeight = 5;
6
export const minBarWidth = 5;
src/devtools/views/Profiler/utils.js
+10
@@ -58,3 +58,13 @@ export const formatPercentage = (percentage: number) =>
58
Math.round(percentage * 100);
59
export const formatTime = (timestamp: number) =>
60
Math.round(Math.round(timestamp) / 100) / 10;
61
+
62
+export const scale = (
63
+ minValue: number,
64
+ maxValue: number,
65
+ minRange: number,
66
+ maxRange: number
67
+) => (value: number, fallbackValue: number) =>
68
+ maxValue - minValue === 0
69
+ ? fallbackValue
70
+ : ((value - minValue) / (maxValue - minValue)) * (maxRange - minRange);
src/devtools/views/Settings/SettingsContext.js
+1
@@ -145,6 +145,7 @@ function updateThemeVariables(theme: Theme): void {
145
updateStyleHelper(theme, 'color-commit-gradient-7');
146
updateStyleHelper(theme, 'color-commit-gradient-8');
147
updateStyleHelper(theme, 'color-commit-gradient-9');
148
+ updateStyleHelper(theme, 'color-commit-gradient-text');
149
updateStyleHelper(theme, 'color-component-name');
150
updateStyleHelper(theme, 'color-component-name-inverted');
151
updateStyleHelper(theme, 'color-dim');
src/devtools/views/root.css
+2
@@ -27,6 +27,7 @@
27
--light-color-commit-gradient-7: #dfba57;
28
--light-color-commit-gradient-8: #efbb49;
29
--light-color-commit-gradient-9: #febc38;
30
+ --light-color-commit-gradient-text: #000000;
31
--light-color-component-name: #8155cb;
32
--light-color-component-name-inverted: #ffffff;
33
--light-color-dim: #777d88;
@@ -68,6 +69,7 @@
69
--dark-color-commit-gradient-7: #dfba57;
70
--dark-color-commit-gradient-8: #efbb49;
71
--dark-color-commit-gradient-9: #febc38;
72
+ --dark-color-commit-gradient-text: #000000;
73
--dark-color-component-name: #61dafb;
74
--dark-color-component-name-inverted: ##282828;
75
--dark-color-dim: #8f949d;