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