blob: 2f5d5f7eb3aae2dba3fe12fe2a89d5302cfd29f5 [file] [log] [blame]
// 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 {Engine} from '../common/engine';
import {
ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
DEFAULT_VIEWING_OPTION,
expandCallsites,
findRootSize,
mergeCallsites,
OBJECTS_ALLOCATED_KEY,
OBJECTS_ALLOCATED_NOT_FREED_KEY,
SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY
} from '../common/flamegraph_util';
import {CallsiteInfo, HeapProfileFlamegraph} from '../common/state';
import {fromNs} from '../common/time';
import {HeapProfileDetails} from '../frontend/globals';
import {Controller} from './controller';
import {globals} from './globals';
export interface HeapProfileControllerArgs {
engine: Engine;
}
const MIN_PIXEL_DISPLAYED = 1;
export class HeapProfileController extends Controller<'main'> {
private flamegraphDatasets: Map<string, CallsiteInfo[]> = new Map();
private lastSelectedHeapProfile?: HeapProfileFlamegraph;
private requestingData = false;
private queuedRequest = false;
private heapProfileDetails: HeapProfileDetails = {};
constructor(private args: HeapProfileControllerArgs) {
super('main');
}
run() {
const selection = globals.state.currentHeapProfileFlamegraph;
if (!selection) return;
if (this.shouldRequestData(selection)) {
if (this.requestingData) {
this.queuedRequest = true;
} else {
this.requestingData = true;
const selectedHeapProfile: HeapProfileFlamegraph =
this.copyHeapProfile(selection);
this.getHeapProfileMetadata(
selectedHeapProfile.ts, selectedHeapProfile.upid)
.then(result => {
if (result !== undefined) {
Object.assign(this.heapProfileDetails, result);
}
this.lastSelectedHeapProfile = this.copyHeapProfile(selection);
const expandedId = selectedHeapProfile.expandedCallsite ?
selectedHeapProfile.expandedCallsite.id :
-1;
const rootSize =
selectedHeapProfile.expandedCallsite === undefined ?
undefined :
selectedHeapProfile.expandedCallsite.totalSize;
const key =
`${selectedHeapProfile.upid};${selectedHeapProfile.ts}`;
this.getFlamegraphData(
key,
selectedHeapProfile.viewingOption ?
selectedHeapProfile.viewingOption :
DEFAULT_VIEWING_OPTION,
selection.ts,
selectedHeapProfile.upid,
selectedHeapProfile.type)
.then(flamegraphData => {
if (flamegraphData !== undefined && selection &&
selection.kind === selectedHeapProfile.kind &&
selection.id === selectedHeapProfile.id &&
selection.ts === selectedHeapProfile.ts) {
const expandedFlamegraphData =
expandCallsites(flamegraphData, expandedId);
this.prepareAndMergeCallsites(
expandedFlamegraphData,
this.lastSelectedHeapProfile!.viewingOption,
rootSize,
this.lastSelectedHeapProfile!.expandedCallsite);
}
})
.finally(() => {
this.requestingData = false;
if (this.queuedRequest) {
this.queuedRequest = false;
this.run();
}
});
});
}
}
}
private copyHeapProfile(heapProfile: HeapProfileFlamegraph):
HeapProfileFlamegraph {
return {
kind: heapProfile.kind,
id: heapProfile.id,
upid: heapProfile.upid,
ts: heapProfile.ts,
type: heapProfile.type,
expandedCallsite: heapProfile.expandedCallsite,
viewingOption: heapProfile.viewingOption
};
}
private shouldRequestData(selection: HeapProfileFlamegraph) {
return selection.kind === 'HEAP_PROFILE_FLAMEGRAPH' &&
(this.lastSelectedHeapProfile === undefined ||
(this.lastSelectedHeapProfile !== undefined &&
(this.lastSelectedHeapProfile.id !== selection.id ||
this.lastSelectedHeapProfile.ts !== selection.ts ||
this.lastSelectedHeapProfile.type !== selection.type ||
this.lastSelectedHeapProfile.upid !== selection.upid ||
this.lastSelectedHeapProfile.viewingOption !==
selection.viewingOption ||
this.lastSelectedHeapProfile.expandedCallsite !==
selection.expandedCallsite)));
}
private prepareAndMergeCallsites(
flamegraphData: CallsiteInfo[],
viewingOption: string|undefined = DEFAULT_VIEWING_OPTION,
rootSize?: number, expandedCallsite?: CallsiteInfo) {
const mergedFlamegraphData = mergeCallsites(
flamegraphData, this.getMinSizeDisplayed(flamegraphData, rootSize));
this.heapProfileDetails.flamegraph = mergedFlamegraphData;
this.heapProfileDetails.expandedCallsite = expandedCallsite;
this.heapProfileDetails.viewingOption = viewingOption;
globals.publish('HeapProfileDetails', this.heapProfileDetails);
}
async getFlamegraphData(
baseKey: string, viewingOption: string, ts: number, upid: number,
type: string): Promise<CallsiteInfo[]> {
let currentData: CallsiteInfo[];
const key = `${baseKey}-${viewingOption}`;
if (this.flamegraphDatasets.has(key)) {
currentData = this.flamegraphDatasets.get(key)!;
} else {
// TODO(taylori): Show loading state.
// Collecting data for drawing flamegraph for selected heap profile.
// Data needs to be in following format:
// id, name, parent_id, depth, total_size
const tableName = await this.prepareViewsAndTables(ts, upid, type);
currentData =
await this.getFlamegraphDataFromTables(tableName, viewingOption);
this.flamegraphDatasets.set(key, currentData);
}
return currentData;
}
async getFlamegraphDataFromTables(
tableName: string, viewingOption = DEFAULT_VIEWING_OPTION) {
let orderBy = '';
let sizeIndex = 4;
switch (viewingOption) {
case SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY:
orderBy = `where cumulative_size > 0 order by depth, parent_id,
cumulative_size desc, name`;
sizeIndex = 4;
break;
case ALLOC_SPACE_MEMORY_ALLOCATED_KEY:
orderBy = `where cumulative_alloc_size > 0 order by depth, parent_id,
cumulative_alloc_size desc, name`;
sizeIndex = 5;
break;
case OBJECTS_ALLOCATED_NOT_FREED_KEY:
orderBy = `where cumulative_count > 0 order by depth, parent_id,
cumulative_count desc, name`;
sizeIndex = 6;
break;
case OBJECTS_ALLOCATED_KEY:
orderBy = `where cumulative_alloc_count > 0 order by depth, parent_id,
cumulative_alloc_count desc, name`;
sizeIndex = 7;
break;
default:
break;
}
const callsites = await this.args.engine.query(
`SELECT id, name, IFNULL(parent_id, -1), depth, cumulative_size,
cumulative_alloc_size, cumulative_count,
cumulative_alloc_count, map_name, size from ${tableName} ${orderBy}`);
const flamegraphData: CallsiteInfo[] = new Array();
const hashToindex: Map<number, number> = new Map();
for (let i = 0; i < callsites.numRecords; i++) {
const hash = callsites.columns[0].longValues![i];
const name = callsites.columns[1].stringValues![i];
const parentHash = callsites.columns[2].longValues![i];
const depth = +callsites.columns[3].longValues![i];
const totalSize = +callsites.columns[sizeIndex].longValues![i];
const mapping = callsites.columns[8].stringValues![i];
const selfSize = +callsites.columns[9].longValues![i];
const parentId =
hashToindex.has(+parentHash) ? hashToindex.get(+parentHash)! : -1;
hashToindex.set(+hash, i);
// Instead of hash, we will store index of callsite in this original array
// as an id of callsite. That way, we have quicker access to parent and it
// will stay unique.
flamegraphData.push(
{id: i, totalSize, depth, parentId, name, selfSize, mapping});
}
return flamegraphData;
}
private async prepareViewsAndTables(ts: number, upid: number, type: string):
Promise<string> {
// Creating unique names for views so we can reuse and not delete them
// for each marker.
const tableNameGroupedCallsitesForFlamegraph =
this.tableName(`grouped_callsites_for_flamegraph`);
await this.args.engine.query(`create temp table if not exists ${
tableNameGroupedCallsitesForFlamegraph} as
select id, name, map_name, parent_id, depth, cumulative_size,
cumulative_alloc_size, cumulative_count, cumulative_alloc_count,
size, alloc_size, count, alloc_count
from experimental_flamegraph(${ts}, ${upid}, '${type}')`);
return tableNameGroupedCallsitesForFlamegraph;
}
tableName(name: string): string {
const selection = globals.state.currentHeapProfileFlamegraph;
if (!selection) return name;
return `${name}_${selection.upid}_${selection.ts}`;
}
getMinSizeDisplayed(flamegraphData: CallsiteInfo[], rootSize?: number):
number {
const timeState = globals.state.frontendLocalState.visibleState;
const width =
(timeState.endSec - timeState.startSec) / timeState.resolution;
if (rootSize === undefined) {
rootSize = findRootSize(flamegraphData);
}
return MIN_PIXEL_DISPLAYED * rootSize / width;
}
async getHeapProfileMetadata(ts: number, upid: number) {
// Don't do anything if selection of the marker stayed the same.
if ((this.lastSelectedHeapProfile !== undefined &&
((this.lastSelectedHeapProfile.ts === ts &&
this.lastSelectedHeapProfile.upid === upid)))) {
return undefined;
}
// Collecting data for more information about heap profile, such as:
// total memory allocated, memory that is allocated and not freed.
const pidValue = await this.args.engine.query(
`select pid from process where upid = ${upid}`);
const pid = pidValue.columns[0].longValues![0];
const allocatedMemory = await this.args.engine.query(
`select sum(size) from heap_profile_allocation where ts <= ${
ts} and size > 0 and upid = ${upid}`);
const allocated = allocatedMemory.columns[0].longValues![0];
const allocatedNotFreedMemory = await this.args.engine.query(
`select sum(size) from heap_profile_allocation where ts <= ${
ts} and upid = ${upid}`);
const allocatedNotFreed = allocatedNotFreedMemory.columns[0].longValues![0];
const startTime = fromNs(ts) - globals.state.traceTime.startSec;
return {ts: startTime, allocated, allocatedNotFreed, tsNs: ts, pid, upid};
}
}