blob: 973a92f28273a48415129e2a9f28a2b74f5e1269 [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)
.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,
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.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): 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);
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 size > 0 order by depth, parent_hash, size desc, name`;
sizeIndex = 4;
break;
case ALLOC_SPACE_MEMORY_ALLOCATED_KEY:
orderBy =
`where alloc_size > 0 order by depth, parent_hash, alloc_size desc,
name`;
sizeIndex = 5;
break;
case OBJECTS_ALLOCATED_NOT_FREED_KEY:
orderBy =
`where count > 0 order by depth, parent_hash, count desc, name`;
sizeIndex = 6;
break;
case OBJECTS_ALLOCATED_KEY:
orderBy = `where alloc_count > 0 order by depth, parent_hash,
alloc_count desc, name`;
sizeIndex = 7;
break;
default:
break;
}
const callsites = await this.args.engine.query(
`SELECT hash, name, parent_hash, depth, size, alloc_size, count,
alloc_count, map_name, self_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):
Promise<string> {
// Creating unique names for views so we can reuse and not delete them
// for each marker.
const tableNameCallsiteNameSize =
this.tableName(`callsite_with_name_and_size`);
const tableNameCallsiteHashNameSize =
this.tableName(`callsite_hash_name_size`);
const tableNameGroupedCallsitesForFlamegraph =
this.tableName(`grouped_callsites_for_flamegraph`);
// Joining the callsite table with frame table then with alloc table to get
// the size and name for each callsite.
// TODO(taylori): Make frame name nullable in the trace processor for
// consistency with the other columns.
await this.args.engine.query(
`create view if not exists ${tableNameCallsiteNameSize} as
select id, parent_id, depth, IFNULL(DEMANGLE(name), name) as name,
map_name, size, alloc_size, count, alloc_count from (
select cs.id as id, parent_id, depth,
coalesce(symbols.name,
case when fr.name != '' then fr.name else map.name end) as name,
map.name as map_name,
SUM(IFNULL(size, 0)) as size,
SUM(IFNULL(size, 0)) as size,
SUM(case when size > 0 then size else 0 end) as alloc_size,
SUM(IFNULL(count, 0)) as count,
SUM(case when count > 0 then count else 0 end) as alloc_count
from stack_profile_callsite cs
join stack_profile_frame fr on cs.frame_id = fr.id
join stack_profile_mapping map on fr.mapping = map.id
left join (
select symbol_set_id, FIRST_VALUE(name) OVER(PARTITION BY
symbol_set_id) as name
from stack_profile_symbol GROUP BY symbol_set_id
) as symbols using(symbol_set_id)
left join heap_profile_allocation alloc on alloc.callsite_id = cs.id
and alloc.ts <= ${ts} and alloc.upid = ${upid} group by cs.id)`);
// Recursive query to compute the hash for each callsite based on names
// rather than ids.
// We get all the children of the row in question and emit a row with hash
// equal hash(name, parent.hash). Roots without the parent will have -1 as
// hash. Slices will be merged into a big slice.
await this.args.engine.query(
`create view if not exists ${tableNameCallsiteHashNameSize} as
with recursive callsite_table_names(
id, hash, name, map_name, size, alloc_size, count, alloc_count,
parent_hash, depth) AS (
select id, hash(name) as hash, name, map_name, size, alloc_size, count,
alloc_count, -1, depth
from ${tableNameCallsiteNameSize}
where depth = 0
union all
select cs.id, hash(cs.name, ctn.hash) as hash, cs.name, cs.map_name,
cs.size, cs.alloc_size, cs.count, cs.alloc_count, ctn.hash, cs.depth
from callsite_table_names ctn
inner join ${tableNameCallsiteNameSize} cs ON ctn.id = cs.parent_id
)
select hash, name, map_name, parent_hash, depth, SUM(size) as size,
SUM(case when alloc_size > 0 then alloc_size else 0 end)
as alloc_size, SUM(count) as count,
SUM(case when alloc_count > 0 then alloc_count else 0 end)
as alloc_count
from callsite_table_names
group by hash`);
// Recursive query to compute the cumulative size of each callsite.
// Base case: We get all the callsites where the size is non-zero.
// Recursive case: We get the callsite which is the parent of the current
// callsite(in terms of hashes) and emit a row with the size of the current
// callsite plus all the info of the parent.
// Grouping: For each callsite, our recursive table has n rows where n is
// the number of descendents with a non-zero self size. We need to group on
// the hash and sum all the sizes to get the cumulative size for each
// callsite hash.
await this.args.engine.query(`create temp table if not exists ${
tableNameGroupedCallsitesForFlamegraph}
as with recursive callsite_children(
hash, name, map_name, parent_hash, depth, size, alloc_size, count,
alloc_count, self_size, self_alloc_size, self_count, self_alloc_count)
as (
select hash, name, map_name, parent_hash, depth, size, alloc_size,
count, alloc_count, size as self_size, alloc_size as self_alloc_size,
count as self_count, alloc_count as self_alloc_count
from ${tableNameCallsiteHashNameSize}
union all
select chns.hash, chns.name, chns.map_name, chns.parent_hash,
chns.depth, cc.size, cc.alloc_size, cc.count, cc.alloc_count,
chns.size, chns.alloc_size, chns.count, chns.alloc_count
from ${tableNameCallsiteHashNameSize} chns
inner join callsite_children cc on chns.hash = cc.parent_hash
)
select hash, name, map_name, parent_hash, depth, SUM(size) as size,
SUM(case when alloc_size > 0 then alloc_size else 0 end)
as alloc_size, SUM(count) as count,
SUM(case when alloc_count > 0 then alloc_count else 0 end) as
alloc_count,
self_size, self_alloc_size, self_count, self_alloc_count
from callsite_children
group by hash`);
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};
}
}