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 {Actions} from '../common/actions'; 16import {Engine} from '../common/engine'; 17import { 18 ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 19 DEFAULT_VIEWING_OPTION, 20 expandCallsites, 21 findRootSize, 22 mergeCallsites, 23 OBJECTS_ALLOCATED_KEY, 24 OBJECTS_ALLOCATED_NOT_FREED_KEY, 25 PERF_SAMPLES_KEY, 26 SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 27} from '../common/flamegraph_util'; 28import {NUM, STR} from '../common/query_result'; 29import {CallsiteInfo, FlamegraphState, ProfileType} from '../common/state'; 30import {tpDurationToSeconds, TPTime} from '../common/time'; 31import {FlamegraphDetails, globals} from '../frontend/globals'; 32import {publishFlamegraphDetails} from '../frontend/publish'; 33import { 34 Config as PerfSampleConfig, 35 PERF_SAMPLES_PROFILE_TRACK_KIND, 36} from '../tracks/perf_samples_profile'; 37 38import {AreaSelectionHandler} from './area_selection_handler'; 39import {Controller} from './controller'; 40 41export function profileType(s: string): ProfileType { 42 if (isProfileType(s)) { 43 return s; 44 } 45 if (s.startsWith('heap_profile')) { 46 return ProfileType.HEAP_PROFILE; 47 } 48 throw new Error('Unknown type ${s}'); 49} 50 51function isProfileType(s: string): s is ProfileType { 52 return Object.values(ProfileType).includes(s as ProfileType); 53} 54 55function getFlamegraphType(type: ProfileType) { 56 switch (type) { 57 case ProfileType.HEAP_PROFILE: 58 case ProfileType.NATIVE_HEAP_PROFILE: 59 case ProfileType.JAVA_HEAP_SAMPLES: 60 return 'native'; 61 case ProfileType.JAVA_HEAP_GRAPH: 62 return 'graph'; 63 case ProfileType.PERF_SAMPLE: 64 return 'perf'; 65 default: 66 throw new Error(`Unexpected profile type ${profileType}`); 67 } 68} 69 70export interface FlamegraphControllerArgs { 71 engine: Engine; 72} 73const MIN_PIXEL_DISPLAYED = 1; 74 75class TablesCache { 76 private engine: Engine; 77 private cache: Map<string, string>; 78 private prefix: string; 79 private tableId: number; 80 private cacheSizeLimit: number; 81 82 constructor(engine: Engine, prefix: string) { 83 this.engine = engine; 84 this.cache = new Map<string, string>(); 85 this.prefix = prefix; 86 this.tableId = 0; 87 this.cacheSizeLimit = 10; 88 } 89 90 async getTableName(query: string): Promise<string> { 91 let tableName = this.cache.get(query); 92 if (tableName === undefined) { 93 // TODO(hjd): This should be LRU. 94 if (this.cache.size > this.cacheSizeLimit) { 95 for (const name of this.cache.values()) { 96 await this.engine.query(`drop table ${name}`); 97 } 98 this.cache.clear(); 99 } 100 tableName = `${this.prefix}_${this.tableId++}`; 101 await this.engine.query( 102 `create temp table if not exists ${tableName} as ${query}`); 103 this.cache.set(query, tableName); 104 } 105 return tableName; 106 } 107} 108 109export class FlamegraphController extends Controller<'main'> { 110 private flamegraphDatasets: Map<string, CallsiteInfo[]> = new Map(); 111 private lastSelectedFlamegraphState?: FlamegraphState; 112 private requestingData = false; 113 private queuedRequest = false; 114 private flamegraphDetails: FlamegraphDetails = {}; 115 private areaSelectionHandler: AreaSelectionHandler; 116 private cache: TablesCache; 117 118 constructor(private args: FlamegraphControllerArgs) { 119 super('main'); 120 this.cache = new TablesCache(args.engine, 'grouped_callsites'); 121 this.areaSelectionHandler = new AreaSelectionHandler(); 122 } 123 124 run() { 125 const [hasAreaChanged, area] = this.areaSelectionHandler.getAreaChange(); 126 if (hasAreaChanged) { 127 const upids = []; 128 if (!area) { 129 this.checkCompletionAndPublishFlamegraph( 130 {...globals.flamegraphDetails, isInAreaSelection: false}); 131 return; 132 } 133 for (const trackId of area.tracks) { 134 const trackState = globals.state.tracks[trackId]; 135 if (!trackState || 136 trackState.kind !== PERF_SAMPLES_PROFILE_TRACK_KIND) { 137 continue; 138 } 139 upids.push((trackState.config as PerfSampleConfig).upid); 140 } 141 if (upids.length === 0) { 142 this.checkCompletionAndPublishFlamegraph( 143 {...globals.flamegraphDetails, isInAreaSelection: false}); 144 return; 145 } 146 globals.dispatch(Actions.openFlamegraph({ 147 upids, 148 start: area.start, 149 end: area.end, 150 type: ProfileType.PERF_SAMPLE, 151 viewingOption: PERF_SAMPLES_KEY, 152 })); 153 } 154 const selection = globals.state.currentFlamegraphState; 155 if (!selection || !this.shouldRequestData(selection)) { 156 return; 157 } 158 if (this.requestingData) { 159 this.queuedRequest = true; 160 return; 161 } 162 this.requestingData = true; 163 164 this.assembleFlamegraphDetails(selection, area !== undefined); 165 } 166 167 private async assembleFlamegraphDetails( 168 selection: FlamegraphState, isInAreaSelection: boolean) { 169 const selectedFlamegraphState = {...selection}; 170 const flamegraphMetadata = await this.getFlamegraphMetadata( 171 selection.type, 172 selectedFlamegraphState.start, 173 selectedFlamegraphState.end, 174 selectedFlamegraphState.upids); 175 if (flamegraphMetadata !== undefined) { 176 Object.assign(this.flamegraphDetails, flamegraphMetadata); 177 } 178 179 // TODO(hjd): Clean this up. 180 if (this.lastSelectedFlamegraphState && 181 this.lastSelectedFlamegraphState.focusRegex !== selection.focusRegex) { 182 this.flamegraphDatasets.clear(); 183 } 184 185 this.lastSelectedFlamegraphState = {...selection}; 186 187 const expandedId = selectedFlamegraphState.expandedCallsite ? 188 selectedFlamegraphState.expandedCallsite.id : 189 -1; 190 const rootSize = selectedFlamegraphState.expandedCallsite === undefined ? 191 undefined : 192 selectedFlamegraphState.expandedCallsite.totalSize; 193 194 const key = `${selectedFlamegraphState.upids};${ 195 selectedFlamegraphState.start};${selectedFlamegraphState.end}`; 196 197 try { 198 const flamegraphData = await this.getFlamegraphData( 199 key, 200 selectedFlamegraphState.viewingOption ? 201 selectedFlamegraphState.viewingOption : 202 DEFAULT_VIEWING_OPTION, 203 selection.start, 204 selection.end, 205 selectedFlamegraphState.upids, 206 selectedFlamegraphState.type, 207 selectedFlamegraphState.focusRegex); 208 if (flamegraphData !== undefined && selection && 209 selection.kind === selectedFlamegraphState.kind && 210 selection.start === selectedFlamegraphState.start && 211 selection.end === selectedFlamegraphState.end) { 212 const expandedFlamegraphData = 213 expandCallsites(flamegraphData, expandedId); 214 this.prepareAndMergeCallsites( 215 expandedFlamegraphData, 216 this.lastSelectedFlamegraphState.viewingOption, 217 isInAreaSelection, 218 rootSize, 219 this.lastSelectedFlamegraphState.expandedCallsite); 220 } 221 } finally { 222 this.requestingData = false; 223 if (this.queuedRequest) { 224 this.queuedRequest = false; 225 this.run(); 226 } 227 } 228 } 229 230 private shouldRequestData(selection: FlamegraphState) { 231 return selection.kind === 'FLAMEGRAPH_STATE' && 232 (this.lastSelectedFlamegraphState === undefined || 233 (this.lastSelectedFlamegraphState.start !== selection.start || 234 this.lastSelectedFlamegraphState.end !== selection.end || 235 this.lastSelectedFlamegraphState.type !== selection.type || 236 !FlamegraphController.areArraysEqual( 237 this.lastSelectedFlamegraphState.upids, selection.upids) || 238 this.lastSelectedFlamegraphState.viewingOption !== 239 selection.viewingOption || 240 this.lastSelectedFlamegraphState.focusRegex !== 241 selection.focusRegex || 242 this.lastSelectedFlamegraphState.expandedCallsite !== 243 selection.expandedCallsite)); 244 } 245 246 private prepareAndMergeCallsites( 247 flamegraphData: CallsiteInfo[], 248 viewingOption: string|undefined = DEFAULT_VIEWING_OPTION, 249 isInAreaSelection: boolean, rootSize?: number, 250 expandedCallsite?: CallsiteInfo) { 251 this.flamegraphDetails.flamegraph = mergeCallsites( 252 flamegraphData, this.getMinSizeDisplayed(flamegraphData, rootSize)); 253 this.flamegraphDetails.expandedCallsite = expandedCallsite; 254 this.flamegraphDetails.viewingOption = viewingOption; 255 this.flamegraphDetails.isInAreaSelection = isInAreaSelection; 256 this.checkCompletionAndPublishFlamegraph(this.flamegraphDetails); 257 } 258 259 private async checkCompletionAndPublishFlamegraph(flamegraphDetails: 260 FlamegraphDetails) { 261 flamegraphDetails.graphIncomplete = 262 (await this.args.engine.query(`select value from stats 263 where severity = 'error' and name = 'heap_graph_non_finalized_graph'`)) 264 .firstRow({value: NUM}) 265 .value > 0; 266 publishFlamegraphDetails(flamegraphDetails); 267 } 268 269 async getFlamegraphData( 270 baseKey: string, viewingOption: string, start: TPTime, end: TPTime, 271 upids: number[], type: ProfileType, 272 focusRegex: string): Promise<CallsiteInfo[]> { 273 let currentData: CallsiteInfo[]; 274 const key = `${baseKey}-${viewingOption}`; 275 if (this.flamegraphDatasets.has(key)) { 276 currentData = this.flamegraphDatasets.get(key)!; 277 } else { 278 // TODO(hjd): Show loading state. 279 280 // Collecting data for drawing flamegraph for selected profile. 281 // Data needs to be in following format: 282 // id, name, parent_id, depth, total_size 283 const tableName = 284 await this.prepareViewsAndTables(start, end, upids, type, focusRegex); 285 currentData = await this.getFlamegraphDataFromTables( 286 tableName, viewingOption, focusRegex); 287 this.flamegraphDatasets.set(key, currentData); 288 } 289 return currentData; 290 } 291 292 async getFlamegraphDataFromTables( 293 tableName: string, viewingOption = DEFAULT_VIEWING_OPTION, 294 focusRegex: string) { 295 let orderBy = ''; 296 let totalColumnName: 'cumulativeSize'|'cumulativeAllocSize'| 297 'cumulativeCount'|'cumulativeAllocCount' = 'cumulativeSize'; 298 let selfColumnName: 'size'|'count' = 'size'; 299 // TODO(fmayer): Improve performance so this is no longer necessary. 300 // Alternatively consider collapsing frames of the same label. 301 const maxDepth = 100; 302 switch (viewingOption) { 303 case ALLOC_SPACE_MEMORY_ALLOCATED_KEY: 304 orderBy = `where cumulative_alloc_size > 0 and depth < ${ 305 maxDepth} order by depth, parent_id, 306 cumulative_alloc_size desc, name`; 307 totalColumnName = 'cumulativeAllocSize'; 308 selfColumnName = 'size'; 309 break; 310 case OBJECTS_ALLOCATED_NOT_FREED_KEY: 311 orderBy = `where cumulative_count > 0 and depth < ${ 312 maxDepth} order by depth, parent_id, 313 cumulative_count desc, name`; 314 totalColumnName = 'cumulativeCount'; 315 selfColumnName = 'count'; 316 break; 317 case OBJECTS_ALLOCATED_KEY: 318 orderBy = `where cumulative_alloc_count > 0 and depth < ${ 319 maxDepth} order by depth, parent_id, 320 cumulative_alloc_count desc, name`; 321 totalColumnName = 'cumulativeAllocCount'; 322 selfColumnName = 'count'; 323 break; 324 case PERF_SAMPLES_KEY: 325 case SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY: 326 orderBy = `where cumulative_size > 0 and depth < ${ 327 maxDepth} order by depth, parent_id, 328 cumulative_size desc, name`; 329 totalColumnName = 'cumulativeSize'; 330 selfColumnName = 'size'; 331 break; 332 default: 333 break; 334 } 335 336 const callsites = await this.args.engine.query(` 337 SELECT 338 id as hash, 339 IFNULL(IFNULL(DEMANGLE(name), name), '[NULL]') as name, 340 IFNULL(parent_id, -1) as parentHash, 341 depth, 342 cumulative_size as cumulativeSize, 343 cumulative_alloc_size as cumulativeAllocSize, 344 cumulative_count as cumulativeCount, 345 cumulative_alloc_count as cumulativeAllocCount, 346 map_name as mapping, 347 size, 348 count, 349 IFNULL(source_file, '') as sourceFile, 350 IFNULL(line_number, -1) as lineNumber 351 from ${tableName} ${orderBy}`); 352 353 const flamegraphData: CallsiteInfo[] = []; 354 const hashToindex: Map<number, number> = new Map(); 355 const it = callsites.iter({ 356 hash: NUM, 357 name: STR, 358 parentHash: NUM, 359 depth: NUM, 360 cumulativeSize: NUM, 361 cumulativeAllocSize: NUM, 362 cumulativeCount: NUM, 363 cumulativeAllocCount: NUM, 364 mapping: STR, 365 sourceFile: STR, 366 lineNumber: NUM, 367 size: NUM, 368 count: NUM, 369 }); 370 for (let i = 0; it.valid(); ++i, it.next()) { 371 const hash = it.hash; 372 let name = it.name; 373 const parentHash = it.parentHash; 374 const depth = it.depth; 375 const totalSize = it[totalColumnName]; 376 const selfSize = it[selfColumnName]; 377 const mapping = it.mapping; 378 const highlighted = focusRegex !== '' && 379 name.toLocaleLowerCase().includes(focusRegex.toLocaleLowerCase()); 380 const parentId = 381 hashToindex.has(+parentHash) ? hashToindex.get(+parentHash)! : -1; 382 383 let location: string|undefined; 384 if (/[a-zA-Z]/i.test(it.sourceFile)) { 385 location = it.sourceFile; 386 if (it.lineNumber !== -1) { 387 location += `:${it.lineNumber}`; 388 } 389 } 390 391 if (depth === maxDepth - 1) { 392 name += ' [tree truncated]'; 393 } 394 // Instead of hash, we will store index of callsite in this original array 395 // as an id of callsite. That way, we have quicker access to parent and it 396 // will stay unique: 397 hashToindex.set(hash, i); 398 399 flamegraphData.push({ 400 id: i, 401 totalSize, 402 depth, 403 parentId, 404 name, 405 selfSize, 406 mapping, 407 merged: false, 408 highlighted, 409 location, 410 }); 411 } 412 return flamegraphData; 413 } 414 415 private async prepareViewsAndTables( 416 start: TPTime, end: TPTime, upids: number[], type: ProfileType, 417 focusRegex: string): Promise<string> { 418 // Creating unique names for views so we can reuse and not delete them 419 // for each marker. 420 let focusRegexConditional = ''; 421 if (focusRegex !== '') { 422 focusRegexConditional = `and focus_str = '${focusRegex}'`; 423 } 424 const flamegraphType = getFlamegraphType(type); 425 426 /* 427 * TODO(octaviant) this branching should be eliminated for simplicity. 428 */ 429 if (type === ProfileType.PERF_SAMPLE) { 430 let upidConditional = `upid = ${upids[0]}`; 431 if (upids.length > 1) { 432 upidConditional = 433 `upid_group = '${FlamegraphController.serializeUpidGroup(upids)}'`; 434 } 435 return this.cache.getTableName( 436 `select id, name, map_name, parent_id, depth, cumulative_size, 437 cumulative_alloc_size, cumulative_count, cumulative_alloc_count, 438 size, alloc_size, count, alloc_count, source_file, line_number 439 from experimental_flamegraph 440 where profile_type = '${flamegraphType}' and ${start} <= ts and 441 ts <= ${end} and ${upidConditional} 442 ${focusRegexConditional}`); 443 } 444 return this.cache.getTableName( 445 `select id, name, map_name, parent_id, depth, cumulative_size, 446 cumulative_alloc_size, cumulative_count, cumulative_alloc_count, 447 size, alloc_size, count, alloc_count, source_file, line_number 448 from experimental_flamegraph 449 where profile_type = '${flamegraphType}' 450 and ts = ${end} 451 and upid = ${upids[0]} 452 ${focusRegexConditional}`); 453 } 454 455 getMinSizeDisplayed(flamegraphData: CallsiteInfo[], rootSize?: number): 456 number { 457 const timeState = globals.state.frontendLocalState.visibleState; 458 const dur = globals.stateVisibleTime().duration; 459 let width = tpDurationToSeconds(dur / timeState.resolution); 460 // TODO(168048193): Remove screen size hack: 461 width = Math.max(width, 800); 462 if (rootSize === undefined) { 463 rootSize = findRootSize(flamegraphData); 464 } 465 return MIN_PIXEL_DISPLAYED * rootSize / width; 466 } 467 468 async getFlamegraphMetadata( 469 type: ProfileType, start: TPTime, end: TPTime, 470 upids: number[]): Promise<FlamegraphDetails|undefined> { 471 // Don't do anything if selection of the marker stayed the same. 472 if ((this.lastSelectedFlamegraphState !== undefined && 473 ((this.lastSelectedFlamegraphState.start === start && 474 this.lastSelectedFlamegraphState.end === end && 475 FlamegraphController.areArraysEqual( 476 this.lastSelectedFlamegraphState.upids, upids))))) { 477 return undefined; 478 } 479 480 // Collecting data for more information about profile, such as: 481 // total memory allocated, memory that is allocated and not freed. 482 const upidGroup = FlamegraphController.serializeUpidGroup(upids); 483 484 const result = await this.args.engine.query( 485 `select pid from process where upid in (${upidGroup})`); 486 const it = result.iter({pid: NUM}); 487 const pids = []; 488 for (let i = 0; it.valid(); ++i, it.next()) { 489 pids.push(it.pid); 490 } 491 return {start, dur: end - start, pids, upids, type}; 492 } 493 494 private static areArraysEqual(a: number[], b: number[]) { 495 if (a.length !== b.length) { 496 return false; 497 } 498 for (let i = 0; i < a.length; i++) { 499 if (a[i] !== b[i]) { 500 return false; 501 } 502 } 503 return true; 504 } 505 506 private static serializeUpidGroup(upids: number[]) { 507 return new Array(upids).join(); 508 } 509} 510