xref: /aosp_15_r20/external/perfetto/ui/src/components/colorizer.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2019 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {hsl} from 'color-convert';
16import {hash} from '../base/hash';
17import {featureFlags} from '../core/feature_flags';
18import {Color, HSLColor, HSLuvColor} from '../public/color';
19import {ColorScheme} from '../public/color_scheme';
20import {RandState, pseudoRand} from '../base/rand';
21
22// 128 would provide equal weighting between dark and light text.
23// However, we want to prefer light text for stylistic reasons.
24// A higher value means color must be brighter before switching to dark text.
25const PERCEIVED_BRIGHTNESS_LIMIT = 180;
26
27// This file defines some opinionated colors and provides functions to access
28// random but predictable colors based on a seed, as well as standardized ways
29// to access colors for core objects such as slices and thread states.
30
31// We have, over the years, accumulated a number of different color palettes
32// which are used for different parts of the UI.
33// It would be nice to combine these into a single palette in the future, but
34// changing colors is difficult especially for slice colors, as folks get used
35// to certain slices being certain colors and are resistant to change.
36// However we do it, we should make it possible for folks to switch back the a
37// previous palette, or define their own.
38
39const USE_CONSISTENT_COLORS = featureFlags.register({
40  id: 'useConsistentColors',
41  name: 'Use common color palette for timeline elements',
42  description: 'Use the same color palette for all timeline elements.',
43  defaultValue: false,
44});
45
46const randColourState: RandState = {seed: 0};
47
48const MD_PALETTE_RAW: Color[] = [
49  new HSLColor({h: 4, s: 90, l: 58}),
50  new HSLColor({h: 340, s: 82, l: 52}),
51  new HSLColor({h: 291, s: 64, l: 42}),
52  new HSLColor({h: 262, s: 52, l: 47}),
53  new HSLColor({h: 231, s: 48, l: 48}),
54  new HSLColor({h: 207, s: 90, l: 54}),
55  new HSLColor({h: 199, s: 98, l: 48}),
56  new HSLColor({h: 187, s: 100, l: 42}),
57  new HSLColor({h: 174, s: 100, l: 29}),
58  new HSLColor({h: 122, s: 39, l: 49}),
59  new HSLColor({h: 88, s: 50, l: 53}),
60  new HSLColor({h: 66, s: 70, l: 54}),
61  new HSLColor({h: 45, s: 100, l: 51}),
62  new HSLColor({h: 36, s: 100, l: 50}),
63  new HSLColor({h: 14, s: 100, l: 57}),
64  new HSLColor({h: 16, s: 25, l: 38}),
65  new HSLColor({h: 200, s: 18, l: 46}),
66  new HSLColor({h: 54, s: 100, l: 62}),
67];
68
69const WHITE_COLOR = new HSLColor([0, 0, 100]);
70const BLACK_COLOR = new HSLColor([0, 0, 0]);
71const GRAY_COLOR = new HSLColor([0, 0, 90]);
72
73const MD_PALETTE: ColorScheme[] = MD_PALETTE_RAW.map((color): ColorScheme => {
74  const base = color.lighten(10, 60).desaturate(20);
75  const variant = base.lighten(30, 80).desaturate(20);
76
77  return {
78    base,
79    variant,
80    disabled: GRAY_COLOR,
81    textBase: WHITE_COLOR, // White text suits MD colors quite well
82    textVariant: WHITE_COLOR,
83    textDisabled: WHITE_COLOR, // Low contrast is on purpose
84  };
85});
86
87// Create a color scheme based on a single color, which defines the variant
88// color as a slightly darker and more saturated version of the base color.
89export function makeColorScheme(base: Color, variant?: Color): ColorScheme {
90  variant = variant ?? base.darken(15).saturate(15);
91
92  return {
93    base,
94    variant,
95    disabled: GRAY_COLOR,
96    textBase:
97      base.perceivedBrightness >= PERCEIVED_BRIGHTNESS_LIMIT
98        ? BLACK_COLOR
99        : WHITE_COLOR,
100    textVariant:
101      variant.perceivedBrightness >= PERCEIVED_BRIGHTNESS_LIMIT
102        ? BLACK_COLOR
103        : WHITE_COLOR,
104    textDisabled: WHITE_COLOR, // Low contrast is on purpose
105  };
106}
107
108const GRAY = makeColorScheme(new HSLColor([0, 0, 62]));
109const DESAT_RED = makeColorScheme(new HSLColor([3, 30, 49]));
110const DARK_GREEN = makeColorScheme(new HSLColor([120, 44, 34]));
111const LIME_GREEN = makeColorScheme(new HSLColor([75, 55, 47]));
112const TRANSPARENT_WHITE = makeColorScheme(new HSLColor([0, 1, 97], 0.55));
113const ORANGE = makeColorScheme(new HSLColor([36, 100, 50]));
114const INDIGO = makeColorScheme(new HSLColor([231, 48, 48]));
115
116// A piece of wisdom from a long forgotten blog post: "Don't make
117// colors you want to change something normal like grey."
118export const UNEXPECTED_PINK = makeColorScheme(new HSLColor([330, 100, 70]));
119
120// Selects a predictable color scheme from a palette of material design colors,
121// based on a string seed.
122function materialColorScheme(seed: string): ColorScheme {
123  const colorIdx = hash(seed, MD_PALETTE.length);
124  return MD_PALETTE[colorIdx];
125}
126
127const proceduralColorCache = new Map<string, ColorScheme>();
128
129// Procedurally generates a predictable color scheme based on a string seed.
130function proceduralColorScheme(seed: string): ColorScheme {
131  const colorScheme = proceduralColorCache.get(seed);
132  if (colorScheme) {
133    return colorScheme;
134  } else {
135    const hue = hash(seed, 360);
136    // Saturation 100 would give the most differentiation between colors, but
137    // it's garish.
138    const saturation = 80;
139
140    // Prefer using HSLuv, not the browser's built-in vanilla HSL handling. This
141    // is because this function chooses hue/lightness uniform at random, but HSL
142    // is not perceptually uniform.
143    // See https://www.boronine.com/2012/03/26/Color-Spaces-for-Human-Beings/.
144    const base = new HSLuvColor({
145      h: hue,
146      s: saturation,
147      l: hash(seed + 'x', 40) + 40,
148    });
149    const variant = new HSLuvColor({h: hue, s: saturation, l: 30});
150    const colorScheme = makeColorScheme(base, variant);
151
152    proceduralColorCache.set(seed, colorScheme);
153
154    return colorScheme;
155  }
156}
157
158export function colorForState(state: string): ColorScheme {
159  if (state === 'Running') {
160    return DARK_GREEN;
161  } else if (state.startsWith('Runnable')) {
162    return LIME_GREEN;
163  } else if (state.includes('Uninterruptible Sleep')) {
164    if (state.includes('non-IO')) {
165      return DESAT_RED;
166    }
167    return ORANGE;
168  } else if (state.includes('Dead')) {
169    return GRAY;
170  } else if (state.includes('Sleeping') || state.includes('Idle')) {
171    return TRANSPARENT_WHITE;
172  }
173  return INDIGO;
174}
175
176export function colorForTid(tid: number): ColorScheme {
177  return materialColorScheme(tid.toString());
178}
179
180export function colorForThread(thread?: {
181  pid?: number;
182  tid: number;
183}): ColorScheme {
184  if (thread === undefined) {
185    return GRAY;
186  }
187  const tid = thread.pid ?? thread.tid;
188  return colorForTid(tid);
189}
190
191export function colorForCpu(cpu: number): Color {
192  if (USE_CONSISTENT_COLORS.get()) {
193    return materialColorScheme(cpu.toString()).base;
194  } else {
195    const hue = (128 + 32 * cpu) % 256;
196    return new HSLColor({h: hue, s: 50, l: 50});
197  }
198}
199
200export function randomColor(): string {
201  const rand = pseudoRand(randColourState);
202  if (USE_CONSISTENT_COLORS.get()) {
203    return materialColorScheme(rand.toString()).base.cssString;
204  } else {
205    // 40 different random hues 9 degrees apart.
206    const hue = Math.floor(rand * 40) * 9;
207    return '#' + hsl.hex([hue, 90, 30]);
208  }
209}
210
211export function getColorForSlice(sliceName: string): ColorScheme {
212  const name = sliceName.replace(/( )?\d+/g, '');
213  if (USE_CONSISTENT_COLORS.get()) {
214    return materialColorScheme(name);
215  } else {
216    return proceduralColorScheme(name);
217  }
218}
219
220export function colorForFtrace(name: string): ColorScheme {
221  return materialColorScheme(name);
222}
223
224export function getColorForSample(callsiteId: number): ColorScheme {
225  if (USE_CONSISTENT_COLORS.get()) {
226    return materialColorScheme(String(callsiteId));
227  } else {
228    return proceduralColorScheme(String(callsiteId));
229  }
230}
231