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 {featureFlags} from '../core/feature_flags'; 16import {ProfileType} from './state'; 17 18export enum FlamegraphViewingOption { 19 SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY = 'SPACE', 20 ALLOC_SPACE_MEMORY_ALLOCATED_KEY = 'ALLOC_SPACE', 21 OBJECTS_ALLOCATED_NOT_FREED_KEY = 'OBJECTS', 22 OBJECTS_ALLOCATED_KEY = 'ALLOC_OBJECTS', 23 PERF_SAMPLES_KEY = 'PERF_SAMPLES', 24 DOMINATOR_TREE_OBJ_SIZE_KEY = 'DOMINATED_OBJ_SIZE', 25 DOMINATOR_TREE_OBJ_COUNT_KEY = 'DOMINATED_OBJ_COUNT', 26} 27 28interface ViewingOption { 29 option: FlamegraphViewingOption; 30 name: string; 31} 32 33export interface CallsiteInfo { 34 id: number; 35 parentId: number; 36 depth: number; 37 name?: string; 38 totalSize: number; 39 selfSize: number; 40 mapping: string; 41 merged: boolean; 42 highlighted: boolean; 43 location?: string; 44} 45 46const SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG = featureFlags.register({ 47 id: 'showHeapGraphDominatorTree', 48 name: 'Show heap graph dominator tree', 49 description: 'Show dominated size and objects tabs in Java heap graph view.', 50 defaultValue: true, 51}); 52 53export function viewingOptions(profileType: ProfileType): Array<ViewingOption> { 54 switch (profileType) { 55 case ProfileType.PERF_SAMPLE: 56 return [ 57 { 58 option: FlamegraphViewingOption.PERF_SAMPLES_KEY, 59 name: 'Samples', 60 }, 61 ]; 62 case ProfileType.JAVA_HEAP_GRAPH: 63 return [ 64 { 65 option: FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 66 name: 'Size', 67 }, 68 { 69 option: FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY, 70 name: 'Objects', 71 }, 72 ].concat( 73 SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG.get() 74 ? [ 75 { 76 option: FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY, 77 name: 'Dominated size', 78 }, 79 { 80 option: FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY, 81 name: 'Dominated objects', 82 }, 83 ] 84 : [], 85 ); 86 case ProfileType.HEAP_PROFILE: 87 return [ 88 { 89 option: FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 90 name: 'Unreleased size', 91 }, 92 { 93 option: FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY, 94 name: 'Unreleased count', 95 }, 96 { 97 option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 98 name: 'Total size', 99 }, 100 { 101 option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY, 102 name: 'Total count', 103 }, 104 ]; 105 case ProfileType.NATIVE_HEAP_PROFILE: 106 return [ 107 { 108 option: FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 109 name: 'Unreleased malloc size', 110 }, 111 { 112 option: FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY, 113 name: 'Unreleased malloc count', 114 }, 115 { 116 option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 117 name: 'Total malloc size', 118 }, 119 { 120 option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY, 121 name: 'Total malloc count', 122 }, 123 ]; 124 case ProfileType.JAVA_HEAP_SAMPLES: 125 return [ 126 { 127 option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 128 name: 'Total allocation size', 129 }, 130 { 131 option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY, 132 name: 'Total allocation count', 133 }, 134 ]; 135 case ProfileType.MIXED_HEAP_PROFILE: 136 return [ 137 { 138 option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 139 name: 'Total allocation size (malloc + java)', 140 }, 141 { 142 option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY, 143 name: 'Total allocation count (malloc + java)', 144 }, 145 ]; 146 default: 147 const exhaustiveCheck: never = profileType; 148 throw new Error(`Unhandled case: ${exhaustiveCheck}`); 149 } 150} 151 152export function defaultViewingOption( 153 profileType: ProfileType, 154): FlamegraphViewingOption { 155 return viewingOptions(profileType)[0].option; 156} 157 158export function expandCallsites( 159 data: ReadonlyArray<CallsiteInfo>, 160 clickedCallsiteIndex: number, 161): ReadonlyArray<CallsiteInfo> { 162 if (clickedCallsiteIndex === -1) return data; 163 const expandedCallsites: CallsiteInfo[] = []; 164 if (clickedCallsiteIndex >= data.length || clickedCallsiteIndex < -1) { 165 return expandedCallsites; 166 } 167 const clickedCallsite = data[clickedCallsiteIndex]; 168 expandedCallsites.unshift(clickedCallsite); 169 // Adding parents 170 let parentId = clickedCallsite.parentId; 171 while (parentId > -1) { 172 expandedCallsites.unshift(data[parentId]); 173 parentId = data[parentId].parentId; 174 } 175 // Adding children 176 const parents: number[] = []; 177 parents.push(clickedCallsiteIndex); 178 for (let i = clickedCallsiteIndex + 1; i < data.length; i++) { 179 const element = data[i]; 180 if (parents.includes(element.parentId)) { 181 expandedCallsites.push(element); 182 parents.push(element.id); 183 } 184 } 185 return expandedCallsites; 186} 187 188// Merge callsites that have approximately width less than 189// MIN_PIXEL_DISPLAYED. All small callsites in the same depth and with same 190// parent will be merged to one with total size of all merged callsites. 191export function mergeCallsites( 192 data: ReadonlyArray<CallsiteInfo>, 193 minSizeDisplayed: number, 194) { 195 const mergedData: CallsiteInfo[] = []; 196 const mergedCallsites: Map<number, number> = new Map(); 197 for (let i = 0; i < data.length; i++) { 198 // When a small callsite is found, it will be merged with other small 199 // callsites of the same depth. So if the current callsite has already been 200 // merged we can skip it. 201 if (mergedCallsites.has(data[i].id)) { 202 continue; 203 } 204 const copiedCallsite = copyCallsite(data[i]); 205 copiedCallsite.parentId = getCallsitesParentHash( 206 copiedCallsite, 207 mergedCallsites, 208 ); 209 210 let mergedAny = false; 211 // If current callsite is small, find other small callsites with same depth 212 // and parent and merge them into the current one, marking them as merged. 213 if (copiedCallsite.totalSize <= minSizeDisplayed && i + 1 < data.length) { 214 let j = i + 1; 215 let nextCallsite = data[j]; 216 while (j < data.length && copiedCallsite.depth === nextCallsite.depth) { 217 if ( 218 copiedCallsite.parentId === 219 getCallsitesParentHash(nextCallsite, mergedCallsites) && 220 nextCallsite.totalSize <= minSizeDisplayed 221 ) { 222 copiedCallsite.totalSize += nextCallsite.totalSize; 223 mergedCallsites.set(nextCallsite.id, copiedCallsite.id); 224 mergedAny = true; 225 } 226 j++; 227 nextCallsite = data[j]; 228 } 229 if (mergedAny) { 230 copiedCallsite.name = '[merged]'; 231 copiedCallsite.merged = true; 232 } 233 } 234 mergedData.push(copiedCallsite); 235 } 236 return mergedData; 237} 238 239function copyCallsite(callsite: CallsiteInfo): CallsiteInfo { 240 return { 241 id: callsite.id, 242 parentId: callsite.parentId, 243 depth: callsite.depth, 244 name: callsite.name, 245 totalSize: callsite.totalSize, 246 mapping: callsite.mapping, 247 selfSize: callsite.selfSize, 248 merged: callsite.merged, 249 highlighted: callsite.highlighted, 250 location: callsite.location, 251 }; 252} 253 254function getCallsitesParentHash( 255 callsite: CallsiteInfo, 256 map: Map<number, number>, 257): number { 258 return map.has(callsite.parentId) 259 ? +map.get(callsite.parentId)! 260 : callsite.parentId; 261} 262export function findRootSize(data: ReadonlyArray<CallsiteInfo>) { 263 let totalSize = 0; 264 let i = 0; 265 while (i < data.length && data[i].depth === 0) { 266 totalSize += data[i].totalSize; 267 i++; 268 } 269 return totalSize; 270} 271