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 {search, searchEq} from '../../base/binary_search'; 16import {Actions} from '../../common/actions'; 17import {cropText} from '../../common/canvas_utils'; 18import {TrackState} from '../../common/state'; 19import {translateState} from '../../common/thread_state'; 20import {checkerboardExcept} from '../../frontend/checkerboard'; 21import {Color, colorForState} from '../../frontend/colorizer'; 22import {globals} from '../../frontend/globals'; 23import {Track} from '../../frontend/track'; 24import {trackRegistry} from '../../frontend/track_registry'; 25 26import { 27 Config, 28 Data, 29 StatePercent, 30 THREAD_STATE_TRACK_KIND, 31} from './common'; 32 33const MARGIN_TOP = 4; 34const RECT_HEIGHT = 14; 35const EXCESS_WIDTH = 10; 36 37function groupBusyStates(resolution: number) { 38 return resolution >= 0.0001; 39} 40 41function getSummarizedSliceText(breakdownMap: StatePercent) { 42 let result = 'Various ('; 43 const sorted = 44 new Map([...breakdownMap.entries()].sort((a, b) => b[1] - a[1])); 45 for (const [state, value] of sorted.entries()) { 46 result += `${state}: ${Math.round(value * 100)}%, `; 47 } 48 return result.slice(0, result.length - 2) + ')'; 49} 50 51class ThreadStateTrack extends Track<Config, Data> { 52 static readonly kind = THREAD_STATE_TRACK_KIND; 53 static create(trackState: TrackState): ThreadStateTrack { 54 return new ThreadStateTrack(trackState); 55 } 56 57 constructor(trackState: TrackState) { 58 super(trackState); 59 } 60 61 getHeight(): number { 62 return 2 * MARGIN_TOP + RECT_HEIGHT; 63 } 64 65 renderCanvas(ctx: CanvasRenderingContext2D): void { 66 const {timeScale, visibleWindowTime} = globals.frontendLocalState; 67 const data = this.data(); 68 const charWidth = ctx.measureText('dbpqaouk').width / 8; 69 70 if (data === undefined) return; // Can't possibly draw anything. 71 72 checkerboardExcept( 73 ctx, 74 this.getHeight(), 75 timeScale.timeToPx(visibleWindowTime.start), 76 timeScale.timeToPx(visibleWindowTime.end), 77 timeScale.timeToPx(data.start), 78 timeScale.timeToPx(data.end), 79 ); 80 81 const shouldGroupBusyStates = groupBusyStates(data.resolution); 82 83 ctx.textAlign = 'center'; 84 ctx.font = '10px Roboto Condensed'; 85 86 for (let i = 0; i < data.starts.length; i++) { 87 const tStart = data.starts[i]; 88 const tEnd = data.ends[i]; 89 const state = data.strings[data.state[i]]; 90 if (tEnd <= visibleWindowTime.start || tStart >= visibleWindowTime.end) { 91 continue; 92 } 93 if (tStart && tEnd) { 94 // Don't display a slice for Task Dead. 95 if (state === 'x') continue; 96 const rectStart = timeScale.timeToPx(tStart); 97 const rectEnd = timeScale.timeToPx(tEnd); 98 let rectWidth = rectEnd - rectStart; 99 const color = colorForState(state); 100 let text = translateState(state); 101 const breakdown = data.summarisedStateBreakdowns.get(i); 102 if (breakdown) { 103 colorSummarizedSlice(breakdown, rectStart, rectEnd); 104 text = getSummarizedSliceText(breakdown); 105 } else { 106 let colorStr = `hsl(${color.h},${color.s}%,${color.l}%)`; 107 if (color.a) { 108 colorStr = `hsla(${color.h},${color.s}%,${color.l}%, ${color.a})`; 109 } 110 ctx.fillStyle = colorStr; 111 } 112 if (shouldGroupBusyStates && rectWidth < 1) { 113 rectWidth = 1; 114 } 115 ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT); 116 117 // Don't render text when we have less than 10px to play with. 118 if (rectWidth < 10 || text === 'Sleeping') continue; 119 const title = cropText(text, charWidth, rectWidth); 120 const rectXCenter = rectStart + rectWidth / 2; 121 ctx.fillStyle = color.l > 80 || breakdown ? '#404040' : '#fff'; 122 ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 3); 123 } 124 } 125 126 const selection = globals.state.currentSelection; 127 if (selection !== null && selection.kind === 'THREAD_STATE' && 128 selection.utid === this.config.utid) { 129 const [startIndex, endIndex] = searchEq(data.starts, selection.ts); 130 if (startIndex !== endIndex) { 131 const tStart = data.starts[startIndex]; 132 const tEnd = data.ends[startIndex]; 133 const state = data.strings[data.state[startIndex]]; 134 135 // If we try to draw too far off the end of the canvas (+/-4m~), 136 // the line is not drawn. Instead limit drawing to the canvas 137 // boundaries, but allow some excess to ensure that the start and end 138 // of the rect are not shown unless that is truly when it starts/ends. 139 const rectStart = 140 Math.max(0 - EXCESS_WIDTH, timeScale.timeToPx(tStart)); 141 const rectEnd = Math.min( 142 timeScale.timeToPx(visibleWindowTime.end) + EXCESS_WIDTH, 143 timeScale.timeToPx(tEnd)); 144 const color = colorForState(state); 145 ctx.strokeStyle = `hsl(${color.h},${color.s}%,${color.l * 0.7}%)`; 146 ctx.beginPath(); 147 ctx.lineWidth = 3; 148 ctx.strokeRect( 149 rectStart, MARGIN_TOP - 1.5, rectEnd - rectStart, RECT_HEIGHT + 3); 150 ctx.closePath(); 151 } 152 } 153 154 // Make a gradient ordered most common to least based on the colors of the 155 // states within the summarized slice. 156 function colorSummarizedSlice( 157 breakdownMap: StatePercent, rectStart: number, rectEnd: number) { 158 const gradient = 159 ctx.createLinearGradient(rectStart, MARGIN_TOP, rectEnd, MARGIN_TOP); 160 // Sometimes multiple states have the same color e.g R, R+ 161 const colorMap = new Map<Color, number>(); 162 for (const [state, value] of breakdownMap.entries()) { 163 const color = colorForState(state); 164 const currentColorValue = colorMap.get(color); 165 if (currentColorValue === undefined) { 166 colorMap.set(color, value); 167 } else { 168 colorMap.set(color, currentColorValue + value); 169 } 170 } 171 172 const sorted = 173 new Map([...colorMap.entries()].sort((a, b) => b[1] - a[1])); 174 let colorStop = 0; 175 for (const [color, value] of sorted.entries()) { 176 const colorString = `hsl(${color.h},${color.s}%,${color.l}%)`; 177 colorStop = Math.max(0, Math.min(1, colorStop + value)); 178 gradient.addColorStop(colorStop, colorString); 179 } 180 ctx.fillStyle = gradient; 181 } 182 } 183 184 onMouseClick({x}: {x: number}) { 185 const data = this.data(); 186 if (data === undefined) return false; 187 const {timeScale} = globals.frontendLocalState; 188 const time = timeScale.pxToTime(x); 189 const index = search(data.starts, time); 190 const ts = index === -1 ? undefined : data.starts[index]; 191 const tsEnd = index === -1 ? undefined : data.ends[index]; 192 const state = index === -1 ? undefined : data.strings[data.state[index]]; 193 const cpu = index === -1 ? undefined : data.cpu[index]; 194 const utid = this.config.utid; 195 if (ts && state && tsEnd && cpu !== undefined) { 196 globals.makeSelection(Actions.selectThreadState({ 197 utid, 198 ts, 199 dur: tsEnd - ts, 200 state, 201 cpu, 202 trackId: this.trackState.id 203 })); 204 return true; 205 } 206 return false; 207 } 208} 209 210trackRegistry.register(ThreadStateTrack); 211