1// Copyright (C) 2023 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 {BigintMath} from '../../base/bigint_math'; 16import {assertExists, assertTrue} from '../../base/logging'; 17import {duration, Time, time} from '../../base/time'; 18import {colorForTid} from '../../components/colorizer'; 19import {TrackData} from '../../components/tracks/track_data'; 20import {TimelineFetcher} from '../../components/tracks/track_helper'; 21import {checkerboardExcept} from '../../components/checkerboard'; 22import {Engine} from '../../trace_processor/engine'; 23import {Track} from '../../public/track'; 24import {LONG, NUM} from '../../trace_processor/query_result'; 25import {uuidv4Sql} from '../../base/uuid'; 26import {TrackRenderContext} from '../../public/track'; 27 28export const PROCESS_SUMMARY_TRACK = 'ProcessSummaryTrack'; 29 30interface Data extends TrackData { 31 starts: BigInt64Array; 32 utilizations: Float64Array; 33} 34 35export interface Config { 36 pidForColor: number; 37 upid: number | null; 38 utid: number | null; 39} 40 41const MARGIN_TOP = 5; 42const RECT_HEIGHT = 30; 43const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT; 44const SUMMARY_HEIGHT = TRACK_HEIGHT - MARGIN_TOP; 45 46export class ProcessSummaryTrack implements Track { 47 private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this)); 48 private engine: Engine; 49 private config: Config; 50 private uuid = uuidv4Sql(); 51 52 constructor(engine: Engine, config: Config) { 53 this.engine = engine; 54 this.config = config; 55 } 56 57 async onCreate(): Promise<void> { 58 let trackIdQuery: string; 59 if (this.config.upid !== null) { 60 trackIdQuery = ` 61 select tt.id as track_id 62 from thread_track as tt 63 join _thread_available_info_summary using (utid) 64 join thread using (utid) 65 where thread.upid = ${this.config.upid} 66 order by slice_count desc 67 `; 68 } else { 69 trackIdQuery = ` 70 select tt.id as track_id 71 from thread_track as tt 72 join _thread_available_info_summary using (utid) 73 where tt.utid = ${assertExists(this.config.utid)} 74 order by slice_count desc 75 `; 76 } 77 await this.engine.query(` 78 create virtual table process_summary_${this.uuid} 79 using __intrinsic_counter_mipmap(( 80 with 81 tt as materialized ( 82 ${trackIdQuery} 83 ), 84 ss as ( 85 select ts, 1.0 as value 86 from slice 87 join tt using (track_id) 88 where slice.depth = 0 89 union all 90 select ts + dur as ts, -1.0 as value 91 from slice 92 join tt using (track_id) 93 where slice.depth = 0 94 ) 95 select 96 ts, 97 sum(value) over (order by ts) / (select count() from tt) as value 98 from ss 99 order by ts 100 )); 101 `); 102 } 103 104 async onUpdate({ 105 visibleWindow, 106 resolution, 107 }: TrackRenderContext): Promise<void> { 108 await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution); 109 } 110 111 async onBoundsChange( 112 start: time, 113 end: time, 114 resolution: duration, 115 ): Promise<Data> { 116 // Resolution must always be a power of 2 for this logic to work 117 assertTrue( 118 BigintMath.popcount(resolution) === 1, 119 `${resolution} not pow of 2`, 120 ); 121 122 const queryRes = await this.engine.query(` 123 select last_ts as ts, last_value as utilization 124 from process_summary_${this.uuid}(${start}, ${end}, ${resolution}); 125 `); 126 const numRows = queryRes.numRows(); 127 const slices: Data = { 128 start, 129 end, 130 resolution, 131 length: numRows, 132 starts: new BigInt64Array(numRows), 133 utilizations: new Float64Array(numRows), 134 }; 135 const it = queryRes.iter({ 136 ts: LONG, 137 utilization: NUM, 138 }); 139 for (let row = 0; it.valid(); it.next(), row++) { 140 slices.starts[row] = it.ts; 141 slices.utilizations[row] = it.utilization; 142 } 143 return slices; 144 } 145 146 async onDestroy(): Promise<void> { 147 await this.engine.tryQuery( 148 `drop table if exists process_summary_${this.uuid};`, 149 ); 150 this.fetcher[Symbol.dispose](); 151 } 152 153 getHeight(): number { 154 return TRACK_HEIGHT; 155 } 156 157 render(trackCtx: TrackRenderContext): void { 158 const {ctx, size, timescale} = trackCtx; 159 160 const data = this.fetcher.data; 161 if (data === undefined) { 162 return; 163 } 164 165 // If the cached trace slices don't fully cover the visible time range, 166 // show a gray rectangle with a "Loading..." label. 167 checkerboardExcept( 168 ctx, 169 this.getHeight(), 170 0, 171 size.width, 172 timescale.timeToPx(data.start), 173 timescale.timeToPx(data.end), 174 ); 175 176 this.renderSummary(trackCtx, data); 177 } 178 179 private renderSummary( 180 {ctx, timescale}: TrackRenderContext, 181 data: Data, 182 ): void { 183 const startPx = 0; 184 const bottomY = TRACK_HEIGHT; 185 186 let lastX = startPx; 187 let lastY = bottomY; 188 189 const color = colorForTid(this.config.pidForColor); 190 ctx.fillStyle = color.base.cssString; 191 ctx.beginPath(); 192 ctx.moveTo(lastX, lastY); 193 for (let i = 0; i < data.utilizations.length; i++) { 194 const startTime = Time.fromRaw(data.starts[i]); 195 const utilization = data.utilizations[i]; 196 lastX = Math.floor(timescale.timeToPx(startTime)); 197 ctx.lineTo(lastX, lastY); 198 lastY = MARGIN_TOP + Math.round(SUMMARY_HEIGHT * (1 - utilization)); 199 ctx.lineTo(lastX, lastY); 200 } 201 ctx.lineTo(lastX, bottomY); 202 ctx.closePath(); 203 ctx.fill(); 204 } 205} 206