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 {Engine} from '../common/engine'; 16import { 17 ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 18 DEFAULT_VIEWING_OPTION, 19 expandCallsites, 20 findRootSize, 21 mergeCallsites, 22 OBJECTS_ALLOCATED_KEY, 23 OBJECTS_ALLOCATED_NOT_FREED_KEY, 24 SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY 25} from '../common/flamegraph_util'; 26import {slowlyCountRows} from '../common/query_iterator'; 27import {CallsiteInfo, HeapProfileFlamegraph} from '../common/state'; 28import {fromNs} from '../common/time'; 29import {HeapProfileDetails} from '../frontend/globals'; 30 31import {Controller} from './controller'; 32import {globals} from './globals'; 33 34export interface HeapProfileControllerArgs { 35 engine: Engine; 36} 37const MIN_PIXEL_DISPLAYED = 1; 38 39class TablesCache { 40 private engine: Engine; 41 private cache: Map<string, string>; 42 private prefix: string; 43 private tableId: number; 44 private cacheSizeLimit: number; 45 46 constructor(engine: Engine, prefix: string) { 47 this.engine = engine; 48 this.cache = new Map<string, string>(); 49 this.prefix = prefix; 50 this.tableId = 0; 51 this.cacheSizeLimit = 10; 52 } 53 54 async getTableName(query: string): Promise<string> { 55 let tableName = this.cache.get(query); 56 if (tableName === undefined) { 57 // TODO(hjd): This should be LRU. 58 if (this.cache.size > this.cacheSizeLimit) { 59 for (const name of this.cache.values()) { 60 await this.engine.query(`drop table ${name}`); 61 } 62 this.cache.clear(); 63 } 64 tableName = `${this.prefix}_${this.tableId++}`; 65 await this.engine.query( 66 `create temp table if not exists ${tableName} as ${query}`); 67 this.cache.set(query, tableName); 68 } 69 return tableName; 70 } 71} 72 73export class HeapProfileController extends Controller<'main'> { 74 private flamegraphDatasets: Map<string, CallsiteInfo[]> = new Map(); 75 private lastSelectedHeapProfile?: HeapProfileFlamegraph; 76 private requestingData = false; 77 private queuedRequest = false; 78 private heapProfileDetails: HeapProfileDetails = {}; 79 private cache: TablesCache; 80 81 constructor(private args: HeapProfileControllerArgs) { 82 super('main'); 83 this.cache = new TablesCache(args.engine, 'grouped_callsites'); 84 } 85 86 run() { 87 const selection = globals.state.currentHeapProfileFlamegraph; 88 89 if (!selection) return; 90 91 if (this.shouldRequestData(selection)) { 92 if (this.requestingData) { 93 this.queuedRequest = true; 94 } else { 95 this.requestingData = true; 96 const selectedHeapProfile: HeapProfileFlamegraph = 97 this.copyHeapProfile(selection); 98 99 this.getHeapProfileMetadata( 100 selection.type, 101 selectedHeapProfile.ts, 102 selectedHeapProfile.upid) 103 .then(result => { 104 if (result !== undefined) { 105 Object.assign(this.heapProfileDetails, result); 106 } 107 108 // TODO(hjd): Clean this up. 109 if (this.lastSelectedHeapProfile && 110 this.lastSelectedHeapProfile.focusRegex !== 111 selection.focusRegex) { 112 this.flamegraphDatasets.clear(); 113 } 114 115 this.lastSelectedHeapProfile = this.copyHeapProfile(selection); 116 117 const expandedId = selectedHeapProfile.expandedCallsite ? 118 selectedHeapProfile.expandedCallsite.id : 119 -1; 120 const rootSize = 121 selectedHeapProfile.expandedCallsite === undefined ? 122 undefined : 123 selectedHeapProfile.expandedCallsite.totalSize; 124 125 const key = 126 `${selectedHeapProfile.upid};${selectedHeapProfile.ts}`; 127 128 this.getFlamegraphData( 129 key, 130 selectedHeapProfile.viewingOption ? 131 selectedHeapProfile.viewingOption : 132 DEFAULT_VIEWING_OPTION, 133 selection.ts, 134 selectedHeapProfile.upid, 135 selectedHeapProfile.type, 136 selectedHeapProfile.focusRegex) 137 .then(flamegraphData => { 138 if (flamegraphData !== undefined && selection && 139 selection.kind === selectedHeapProfile.kind && 140 selection.id === selectedHeapProfile.id && 141 selection.ts === selectedHeapProfile.ts) { 142 const expandedFlamegraphData = 143 expandCallsites(flamegraphData, expandedId); 144 this.prepareAndMergeCallsites( 145 expandedFlamegraphData, 146 this.lastSelectedHeapProfile!.viewingOption, 147 rootSize, 148 this.lastSelectedHeapProfile!.expandedCallsite); 149 } 150 }) 151 .finally(() => { 152 this.requestingData = false; 153 if (this.queuedRequest) { 154 this.queuedRequest = false; 155 this.run(); 156 } 157 }); 158 }); 159 } 160 } 161 } 162 163 private copyHeapProfile(heapProfile: HeapProfileFlamegraph): 164 HeapProfileFlamegraph { 165 return { 166 kind: heapProfile.kind, 167 id: heapProfile.id, 168 upid: heapProfile.upid, 169 ts: heapProfile.ts, 170 type: heapProfile.type, 171 expandedCallsite: heapProfile.expandedCallsite, 172 viewingOption: heapProfile.viewingOption, 173 focusRegex: heapProfile.focusRegex, 174 }; 175 } 176 177 private shouldRequestData(selection: HeapProfileFlamegraph) { 178 return selection.kind === 'HEAP_PROFILE_FLAMEGRAPH' && 179 (this.lastSelectedHeapProfile === undefined || 180 (this.lastSelectedHeapProfile !== undefined && 181 (this.lastSelectedHeapProfile.id !== selection.id || 182 this.lastSelectedHeapProfile.ts !== selection.ts || 183 this.lastSelectedHeapProfile.type !== selection.type || 184 this.lastSelectedHeapProfile.upid !== selection.upid || 185 this.lastSelectedHeapProfile.viewingOption !== 186 selection.viewingOption || 187 this.lastSelectedHeapProfile.focusRegex !== selection.focusRegex || 188 this.lastSelectedHeapProfile.expandedCallsite !== 189 selection.expandedCallsite))); 190 } 191 192 private prepareAndMergeCallsites( 193 flamegraphData: CallsiteInfo[], 194 viewingOption: string|undefined = DEFAULT_VIEWING_OPTION, 195 rootSize?: number, expandedCallsite?: CallsiteInfo) { 196 const mergedFlamegraphData = mergeCallsites( 197 flamegraphData, this.getMinSizeDisplayed(flamegraphData, rootSize)); 198 this.heapProfileDetails.flamegraph = mergedFlamegraphData; 199 this.heapProfileDetails.expandedCallsite = expandedCallsite; 200 this.heapProfileDetails.viewingOption = viewingOption; 201 globals.publish('HeapProfileDetails', this.heapProfileDetails); 202 } 203 204 205 async getFlamegraphData( 206 baseKey: string, viewingOption: string, ts: number, upid: number, 207 type: string, focusRegex: string): Promise<CallsiteInfo[]> { 208 let currentData: CallsiteInfo[]; 209 const key = `${baseKey}-${viewingOption}`; 210 if (this.flamegraphDatasets.has(key)) { 211 currentData = this.flamegraphDatasets.get(key)!; 212 } else { 213 // TODO(hjd): Show loading state. 214 215 // Collecting data for drawing flamegraph for selected heap profile. 216 // Data needs to be in following format: 217 // id, name, parent_id, depth, total_size 218 const tableName = 219 await this.prepareViewsAndTables(ts, upid, type, focusRegex); 220 currentData = await this.getFlamegraphDataFromTables( 221 tableName, viewingOption, focusRegex); 222 this.flamegraphDatasets.set(key, currentData); 223 } 224 return currentData; 225 } 226 227 async getFlamegraphDataFromTables( 228 tableName: string, viewingOption = DEFAULT_VIEWING_OPTION, 229 focusRegex: string) { 230 let orderBy = ''; 231 let sizeIndex = 4; 232 let selfIndex = 9; 233 // TODO(fmayer): Improve performance so this is no longer necessary. 234 // Alternatively consider collapsing frames of the same label. 235 const maxDepth = 100; 236 switch (viewingOption) { 237 case SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY: 238 orderBy = `where cumulative_size > 0 and depth < ${ 239 maxDepth} order by depth, parent_id, 240 cumulative_size desc, name`; 241 sizeIndex = 4; 242 selfIndex = 9; 243 break; 244 case ALLOC_SPACE_MEMORY_ALLOCATED_KEY: 245 orderBy = `where cumulative_alloc_size > 0 and depth < ${ 246 maxDepth} order by depth, parent_id, 247 cumulative_alloc_size desc, name`; 248 sizeIndex = 5; 249 selfIndex = 9; 250 break; 251 case OBJECTS_ALLOCATED_NOT_FREED_KEY: 252 orderBy = `where cumulative_count > 0 and depth < ${ 253 maxDepth} order by depth, parent_id, 254 cumulative_count desc, name`; 255 sizeIndex = 6; 256 selfIndex = 10; 257 break; 258 case OBJECTS_ALLOCATED_KEY: 259 orderBy = `where cumulative_alloc_count > 0 and depth < ${ 260 maxDepth} order by depth, parent_id, 261 cumulative_alloc_count desc, name`; 262 sizeIndex = 7; 263 selfIndex = 10; 264 break; 265 default: 266 break; 267 } 268 269 const callsites = await this.args.engine.query( 270 `SELECT id, IFNULL(DEMANGLE(name), name), IFNULL(parent_id, -1), depth, 271 cumulative_size, cumulative_alloc_size, cumulative_count, 272 cumulative_alloc_count, map_name, size, count from ${tableName} ${ 273 orderBy}`); 274 275 const flamegraphData: CallsiteInfo[] = new Array(); 276 const hashToindex: Map<number, number> = new Map(); 277 for (let i = 0; i < slowlyCountRows(callsites); i++) { 278 const hash = callsites.columns[0].longValues![i]; 279 let name = callsites.columns[1].stringValues![i]; 280 const parentHash = callsites.columns[2].longValues![i]; 281 const depth = +callsites.columns[3].longValues![i]; 282 const totalSize = +callsites.columns[sizeIndex].longValues![i]; 283 const mapping = callsites.columns[8].stringValues![i]; 284 const selfSize = +callsites.columns[selfIndex].longValues![i]; 285 const highlighted = focusRegex !== '' && 286 name.toLocaleLowerCase().includes(focusRegex.toLocaleLowerCase()); 287 const parentId = 288 hashToindex.has(+parentHash) ? hashToindex.get(+parentHash)! : -1; 289 if (depth === maxDepth - 1) { 290 name += ' [tree truncated]'; 291 } 292 hashToindex.set(+hash, i); 293 // Instead of hash, we will store index of callsite in this original array 294 // as an id of callsite. That way, we have quicker access to parent and it 295 // will stay unique. 296 flamegraphData.push({ 297 id: i, 298 totalSize, 299 depth, 300 parentId, 301 name, 302 selfSize, 303 mapping, 304 merged: false, 305 highlighted 306 }); 307 } 308 return flamegraphData; 309 } 310 311 private async prepareViewsAndTables( 312 ts: number, upid: number, type: string, 313 focusRegex: string): Promise<string> { 314 // Creating unique names for views so we can reuse and not delete them 315 // for each marker. 316 let whereClause = ''; 317 if (focusRegex !== '') { 318 whereClause = `where focus_str = '${focusRegex}'`; 319 } 320 321 return this.cache.getTableName( 322 `select id, name, map_name, parent_id, depth, cumulative_size, 323 cumulative_alloc_size, cumulative_count, cumulative_alloc_count, 324 size, alloc_size, count, alloc_count 325 from experimental_flamegraph(${ts}, ${upid}, '${type}') ${ 326 whereClause}`); 327 } 328 329 getMinSizeDisplayed(flamegraphData: CallsiteInfo[], rootSize?: number): 330 number { 331 const timeState = globals.state.frontendLocalState.visibleState; 332 let width = (timeState.endSec - timeState.startSec) / timeState.resolution; 333 // TODO(168048193): Remove screen size hack: 334 width = Math.max(width, 800); 335 if (rootSize === undefined) { 336 rootSize = findRootSize(flamegraphData); 337 } 338 return MIN_PIXEL_DISPLAYED * rootSize / width; 339 } 340 341 async getHeapProfileMetadata(type: string, ts: number, upid: number) { 342 // Don't do anything if selection of the marker stayed the same. 343 if ((this.lastSelectedHeapProfile !== undefined && 344 ((this.lastSelectedHeapProfile.ts === ts && 345 this.lastSelectedHeapProfile.upid === upid)))) { 346 return undefined; 347 } 348 349 // Collecting data for more information about heap profile, such as: 350 // total memory allocated, memory that is allocated and not freed. 351 const pidValue = await this.args.engine.query( 352 `select pid from process where upid = ${upid}`); 353 const pid = pidValue.columns[0].longValues![0]; 354 const startTime = fromNs(ts) - globals.state.traceTime.startSec; 355 return {ts: startTime, tsNs: ts, pid, upid, type}; 356 } 357} 358