| // Copyright (C) 2019 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import {featureFlags} from '../core/feature_flags'; |
| import {CallsiteInfo, FlamegraphStateViewingOption, ProfileType} from './state'; |
| |
| interface ViewingOption { |
| option: FlamegraphStateViewingOption; |
| name: string; |
| } |
| |
| const SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG = featureFlags.register({ |
| id: 'showHeapGraphDominatorTree', |
| name: 'Show heap graph dominator tree', |
| description: 'Show dominated size and objects tabs in Java heap graph view.', |
| defaultValue: true, |
| }); |
| |
| export function viewingOptions(profileType: ProfileType): Array<ViewingOption> { |
| switch (profileType) { |
| case ProfileType.PERF_SAMPLE: |
| return [ |
| { |
| option: FlamegraphStateViewingOption.PERF_SAMPLES_KEY, |
| name: 'Samples', |
| }, |
| ]; |
| case ProfileType.JAVA_HEAP_GRAPH: |
| return [ |
| { |
| option: |
| FlamegraphStateViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, |
| name: 'Size', |
| }, |
| { |
| option: FlamegraphStateViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY, |
| name: 'Objects', |
| }, |
| ].concat( |
| SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG.get() |
| ? [ |
| { |
| option: |
| FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY, |
| name: 'Dominated size', |
| }, |
| { |
| option: |
| FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY, |
| name: 'Dominated objects', |
| }, |
| ] |
| : [], |
| ); |
| case ProfileType.HEAP_PROFILE: |
| return [ |
| { |
| option: |
| FlamegraphStateViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, |
| name: 'Unreleased size', |
| }, |
| { |
| option: FlamegraphStateViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY, |
| name: 'Unreleased count', |
| }, |
| { |
| option: FlamegraphStateViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY, |
| name: 'Total size', |
| }, |
| { |
| option: FlamegraphStateViewingOption.OBJECTS_ALLOCATED_KEY, |
| name: 'Total count', |
| }, |
| ]; |
| case ProfileType.NATIVE_HEAP_PROFILE: |
| return [ |
| { |
| option: |
| FlamegraphStateViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, |
| name: 'Unreleased malloc size', |
| }, |
| { |
| option: FlamegraphStateViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY, |
| name: 'Unreleased malloc count', |
| }, |
| { |
| option: FlamegraphStateViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY, |
| name: 'Total malloc size', |
| }, |
| { |
| option: FlamegraphStateViewingOption.OBJECTS_ALLOCATED_KEY, |
| name: 'Total malloc count', |
| }, |
| ]; |
| case ProfileType.JAVA_HEAP_SAMPLES: |
| return [ |
| { |
| option: FlamegraphStateViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY, |
| name: 'Total allocation size', |
| }, |
| { |
| option: FlamegraphStateViewingOption.OBJECTS_ALLOCATED_KEY, |
| name: 'Total allocation count', |
| }, |
| ]; |
| case ProfileType.MIXED_HEAP_PROFILE: |
| return [ |
| { |
| option: FlamegraphStateViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY, |
| name: 'Total allocation size (malloc + java)', |
| }, |
| { |
| option: FlamegraphStateViewingOption.OBJECTS_ALLOCATED_KEY, |
| name: 'Total allocation count (malloc + java)', |
| }, |
| ]; |
| default: |
| const exhaustiveCheck: never = profileType; |
| throw new Error(`Unhandled case: ${exhaustiveCheck}`); |
| } |
| } |
| |
| export function defaultViewingOption( |
| profileType: ProfileType, |
| ): FlamegraphStateViewingOption { |
| return viewingOptions(profileType)[0].option; |
| } |
| |
| export function expandCallsites( |
| data: CallsiteInfo[], |
| clickedCallsiteIndex: number, |
| ): CallsiteInfo[] { |
| if (clickedCallsiteIndex === -1) return data; |
| const expandedCallsites: CallsiteInfo[] = []; |
| if (clickedCallsiteIndex >= data.length || clickedCallsiteIndex < -1) { |
| return expandedCallsites; |
| } |
| const clickedCallsite = data[clickedCallsiteIndex]; |
| expandedCallsites.unshift(clickedCallsite); |
| // Adding parents |
| let parentId = clickedCallsite.parentId; |
| while (parentId > -1) { |
| expandedCallsites.unshift(data[parentId]); |
| parentId = data[parentId].parentId; |
| } |
| // Adding children |
| const parents: number[] = []; |
| parents.push(clickedCallsiteIndex); |
| for (let i = clickedCallsiteIndex + 1; i < data.length; i++) { |
| const element = data[i]; |
| if (parents.includes(element.parentId)) { |
| expandedCallsites.push(element); |
| parents.push(element.id); |
| } |
| } |
| return expandedCallsites; |
| } |
| |
| // Merge callsites that have approximately width less than |
| // MIN_PIXEL_DISPLAYED. All small callsites in the same depth and with same |
| // parent will be merged to one with total size of all merged callsites. |
| export function mergeCallsites(data: CallsiteInfo[], minSizeDisplayed: number) { |
| const mergedData: CallsiteInfo[] = []; |
| const mergedCallsites: Map<number, number> = new Map(); |
| for (let i = 0; i < data.length; i++) { |
| // When a small callsite is found, it will be merged with other small |
| // callsites of the same depth. So if the current callsite has already been |
| // merged we can skip it. |
| if (mergedCallsites.has(data[i].id)) { |
| continue; |
| } |
| const copiedCallsite = copyCallsite(data[i]); |
| copiedCallsite.parentId = getCallsitesParentHash( |
| copiedCallsite, |
| mergedCallsites, |
| ); |
| |
| let mergedAny = false; |
| // If current callsite is small, find other small callsites with same depth |
| // and parent and merge them into the current one, marking them as merged. |
| if (copiedCallsite.totalSize <= minSizeDisplayed && i + 1 < data.length) { |
| let j = i + 1; |
| let nextCallsite = data[j]; |
| while (j < data.length && copiedCallsite.depth === nextCallsite.depth) { |
| if ( |
| copiedCallsite.parentId === |
| getCallsitesParentHash(nextCallsite, mergedCallsites) && |
| nextCallsite.totalSize <= minSizeDisplayed |
| ) { |
| copiedCallsite.totalSize += nextCallsite.totalSize; |
| mergedCallsites.set(nextCallsite.id, copiedCallsite.id); |
| mergedAny = true; |
| } |
| j++; |
| nextCallsite = data[j]; |
| } |
| if (mergedAny) { |
| copiedCallsite.name = '[merged]'; |
| copiedCallsite.merged = true; |
| } |
| } |
| mergedData.push(copiedCallsite); |
| } |
| return mergedData; |
| } |
| |
| function copyCallsite(callsite: CallsiteInfo): CallsiteInfo { |
| return { |
| id: callsite.id, |
| parentId: callsite.parentId, |
| depth: callsite.depth, |
| name: callsite.name, |
| totalSize: callsite.totalSize, |
| mapping: callsite.mapping, |
| selfSize: callsite.selfSize, |
| merged: callsite.merged, |
| highlighted: callsite.highlighted, |
| location: callsite.location, |
| }; |
| } |
| |
| function getCallsitesParentHash( |
| callsite: CallsiteInfo, |
| map: Map<number, number>, |
| ): number { |
| return map.has(callsite.parentId) |
| ? +map.get(callsite.parentId)! |
| : callsite.parentId; |
| } |
| export function findRootSize(data: CallsiteInfo[]) { |
| let totalSize = 0; |
| let i = 0; |
| while (i < data.length && data[i].depth === 0) { |
| totalSize += data[i].totalSize; |
| i++; |
| } |
| return totalSize; |
| } |