perfetto-ui: Moving heap profile flamegraph to details panel
Instead of having flamegraph for heap profiles in separate track, moving it
to the details panel which is now scrollable.
Test trace: ?s=25632898bab2c18347f2c2d5cd1c5ddc9e98f1278cb96e896eda5de3adbf3
Change-Id: I0cd5be4599474a98181a54551aa8eddf0e62e01f
diff --git a/ui/src/assets/details.scss b/ui/src/assets/details.scss
index d5f4d5c..77c2c33 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -97,6 +97,57 @@
width: 50%;
}
}
+ &.heap-profile {
+ display: flex;
+ justify-content: space-between;
+ align-content: center;
+ height: 30px;
+ padding: 0px;
+ font-size: 12px;
+ * {
+ align-self: center;
+ }
+ .options {
+ display: inline-flex;
+ justify-content: space-around;
+ }
+ .details {
+ display: inline-flex;
+ justify-content: flex-end;
+ }
+ button {
+ width: fit-content;
+ height: 20px;
+ padding: 3px;
+ padding-top: 0px;
+ margin: 2px;
+ font-size: 12px;
+ opacity: 0.5;
+ &.download {
+ opacity: 1;
+ padding-top: 3px;
+ height: 21px;
+ }
+ &.chosen {
+ opacity: 1;
+ }
+ .material-icons {
+ font-size: 15px;
+ margin-right: 3px;
+ vertical-align: middle;
+ }
+ }
+ .title {
+ justify-self: start;
+ margin-left: 5px;
+ font-size: 14px;
+ margin-right: 10px;
+ }
+ .time {
+ justify-self: end;
+ margin-right: 10px;
+ }
+ }
}
table {
@@ -143,18 +194,6 @@
margin-top: 12px;
margin-left: 10px;
}
-
- .explanation {
- font-size: 14px;
- width: 35%;
- margin-top: 10px;
- padding-left: 10px;
- }
-
- .material-icons {
- vertical-align: middle;
- margin-right: 10px;
- }
}
.notes-editor-panel {
@@ -201,10 +240,6 @@
}
}
-.flame-graph-panel {
- height: 500px;
-}
-
.log-panel {
width: 100%;
height: 100%;
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 552a0b6..9b8c082 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -15,6 +15,7 @@
import {Draft} from 'immer';
import {assertExists} from '../base/logging';
+import {CallsiteInfo} from '../common/state';
import {ConvertTrace, ConvertTraceToPprof} from '../controller/trace_converter';
import {
@@ -440,6 +441,18 @@
};
},
+ expandHeapProfileFlamegraph(
+ state: StateDraft, args: {expandedCallsite?: CallsiteInfo}): void {
+ if (state.currentHeapProfileFlamegraph === null) return;
+ state.currentHeapProfileFlamegraph.expandedCallsite = args.expandedCallsite;
+ },
+
+ changeViewHeapProfileFlamegraph(
+ state: StateDraft, args: {viewingOption: string}): void {
+ if (state.currentHeapProfileFlamegraph === null) return;
+ state.currentHeapProfileFlamegraph.viewingOption = args.viewingOption;
+ },
+
selectChromeSlice(state: StateDraft, args: {id: number, trackId: string}):
void {
state.currentSelection = {
diff --git a/ui/src/tracks/heap_profile_flamegraph/controller_unittest.ts b/ui/src/common/flamegraph_unittest.ts
similarity index 98%
rename from ui/src/tracks/heap_profile_flamegraph/controller_unittest.ts
rename to ui/src/common/flamegraph_unittest.ts
index ef110dd..d9015a8 100644
--- a/ui/src/tracks/heap_profile_flamegraph/controller_unittest.ts
+++ b/ui/src/common/flamegraph_unittest.ts
@@ -1,5 +1,5 @@
-import {CallsiteInfo} from '../../frontend/globals';
-import {mergeCallsites} from './controller';
+import {mergeCallsites} from './flamegraph_util';
+import {CallsiteInfo} from './state';
test('zeroCallsitesMerged', () => {
const callsites: CallsiteInfo[] = [
diff --git a/ui/src/common/flamegraph_util.ts b/ui/src/common/flamegraph_util.ts
new file mode 100644
index 0000000..f4f4d16
--- /dev/null
+++ b/ui/src/common/flamegraph_util.ts
@@ -0,0 +1,115 @@
+// 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 {CallsiteInfo} from '../common/state';
+
+export const SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY = 'space';
+export const ALLOC_SPACE_MEMORY_ALLOCATED_KEY = 'alloc_space';
+export const OBJECTS_ALLOCATED_NOT_FREED_KEY = 'objects';
+export const OBJECTS_ALLOCATED_KEY = 'alloc_objects';
+
+export const DEFAULT_VIEWING_OPTION = SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY;
+
+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);
+
+ // 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);
+ }
+ j++;
+ nextCallsite = data[j];
+ }
+ }
+ 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
+ };
+}
+
+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;
+}
\ No newline at end of file
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index c52cb8a..74ca488 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -41,6 +41,16 @@
export type NewEngineMode = 'USE_HTTP_RPC_IF_AVAILABLE'|'FORCE_BUILTIN_WASM';
+export interface CallsiteInfo {
+ id: number;
+ parentId: number;
+ depth: number;
+ name?: string;
+ totalSize: number;
+ selfSize: number;
+ mapping: string;
+}
+
export interface TraceFileSource {
type: 'FILE';
file: File;
@@ -153,6 +163,8 @@
id: number;
upid: number;
ts: number;
+ expandedCallsite?: CallsiteInfo;
+ viewingOption?: string;
}
export interface ChromeSliceSelection {
diff --git a/ui/src/controller/globals.ts b/ui/src/controller/globals.ts
index e268932..1dde45d 100644
--- a/ui/src/controller/globals.ts
+++ b/ui/src/controller/globals.ts
@@ -20,10 +20,10 @@
import {createEmptyState, State} from '../common/state';
import {ControllerAny} from './controller';
-type PublishKinds =
- 'OverviewData'|'TrackData'|'Threads'|'QueryResult'|'LegacyTrace'|
- 'SliceDetails'|'CounterDetails'|'HeapDumpDetails'|'FileDownload'|'Loading'|
- 'Search'|'BufferUsage'|'RecordingLog'|'SearchResult';
+type PublishKinds = 'OverviewData'|'TrackData'|'Threads'|'QueryResult'|
+ 'LegacyTrace'|'SliceDetails'|'CounterDetails'|'HeapProfileDetails'|
+ 'HeapProfileFlamegraph'|'FileDownload'|'Loading'|'Search'|'BufferUsage'|
+ 'RecordingLog'|'SearchResult';
export interface App {
state: State;
diff --git a/ui/src/controller/heap_profile_controller.ts b/ui/src/controller/heap_profile_controller.ts
new file mode 100644
index 0000000..4a5d262
--- /dev/null
+++ b/ui/src/controller/heap_profile_controller.ts
@@ -0,0 +1,313 @@
+// 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 {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;
+
+ constructor(private args: HeapProfileControllerArgs) {
+ super('main');
+ }
+
+ run() {
+ const selection = globals.state.currentHeapProfileFlamegraph;
+
+ if (!selection) return;
+
+ if (selection.kind === 'HEAP_PROFILE_FLAMEGRAPH') {
+ if (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))) {
+ const selectedId = selection.id;
+ const selectedUpid = selection.upid;
+ const selectedKind = selection.kind;
+ const selectedTs = selection.ts;
+ const selectedExpandedCallsite = selection.expandedCallsite;
+ const lastSelectedViewingOption = selection.viewingOption ?
+ selection.viewingOption :
+ DEFAULT_VIEWING_OPTION;
+
+ this.lastSelectedHeapProfile = {
+ kind: selectedKind,
+ id: selectedId,
+ upid: selectedUpid,
+ ts: selectedTs,
+ expandedCallsite: selectedExpandedCallsite,
+ viewingOption: lastSelectedViewingOption
+ };
+
+ const expandedId =
+ selectedExpandedCallsite ? selectedExpandedCallsite.id : -1;
+ const rootSize = selectedExpandedCallsite === undefined ?
+ undefined :
+ selectedExpandedCallsite.totalSize;
+
+ const key = `${selectedUpid};${selectedTs}`;
+
+ // TODO(tneda): Prevent lots of flamegraph queries being queued if a
+ // user clicks lots of the markers quickly.
+ this.getFlamegraphData(
+ key, lastSelectedViewingOption, selection.ts, selectedUpid)
+ .then(flamegraphData => {
+ if (flamegraphData !== undefined && selection &&
+ selection.kind === selectedKind &&
+ selection.id === selectedId && selection.ts === selectedTs) {
+ const expandedFlamegraphData =
+ expandCallsites(flamegraphData, expandedId);
+ this.prepareAndMergeCallsites(
+ expandedFlamegraphData,
+ this.lastSelectedHeapProfile!.viewingOption,
+ rootSize,
+ this.lastSelectedHeapProfile!.expandedCallsite);
+ }
+ });
+ }
+ }
+ }
+
+ private prepareAndMergeCallsites(
+ flamegraphData: CallsiteInfo[],
+ viewingOption: string|undefined = DEFAULT_VIEWING_OPTION,
+ rootSize?: number, expandedCallsite?: CallsiteInfo) {
+ const mergedFlamegraphData = mergeCallsites(
+ flamegraphData, this.getMinSizeDisplayed(flamegraphData, rootSize));
+ globals.publish(
+ 'HeapProfileFlamegraph',
+ {flamegraph: mergedFlamegraphData, expandedCallsite, viewingOption});
+ }
+
+
+ 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(tneda): 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(tneda): 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
+ inner 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;
+ }
+}
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index 2257307..3b257d3 100644
--- a/ui/src/controller/selection_controller.ts
+++ b/ui/src/controller/selection_controller.ts
@@ -73,7 +73,7 @@
if (results !== undefined && selection &&
selection.kind === selectedKind && selection.id === selectedId) {
Object.assign(selected, results);
- globals.publish('HeapDumpDetails', selected);
+ globals.publish('HeapProfileDetails', selected);
}
});
} else if (selection.kind === 'COUNTER') {
@@ -154,13 +154,7 @@
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,
- };
+ return {ts: startTime, allocated, allocatedNotFreed, tsNs: ts, pid, upid};
}
async counterDetails(ts: number, rightTs: number, id: number) {
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 8c5fb67..e2f692e 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -44,9 +44,6 @@
import {GPU_FREQ_TRACK_KIND} from '../tracks/gpu_freq/common';
import {HEAP_PROFILE_TRACK_KIND} from '../tracks/heap_profile/common';
import {
- HEAP_PROFILE_FLAMEGRAPH_TRACK_KIND
-} from '../tracks/heap_profile_flamegraph/common';
-import {
PROCESS_SCHEDULING_TRACK_KIND
} from '../tracks/process_scheduling/common';
import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary/common';
@@ -54,6 +51,10 @@
import {Child, Children, Controller} from './controller';
import {globals} from './globals';
+import {
+ HeapProfileController,
+ HeapProfileControllerArgs
+} from './heap_profile_controller';
import {LoadingManager} from './loading_manager';
import {LogsController} from './logs_controller';
import {QueryController, QueryControllerArgs} from './query_controller';
@@ -149,6 +150,10 @@
childControllers.push(
Child('selection', SelectionController, selectionArgs));
+ const heapProfileArgs: HeapProfileControllerArgs = {engine};
+ childControllers.push(
+ Child('heapProfile', HeapProfileController, heapProfileArgs));
+
childControllers.push(Child('search', SearchController, {
engine,
app: globals,
@@ -633,14 +638,6 @@
trackGroup: pUuid,
config: {upid}
});
-
- tracksToAdd.push({
- engineId: this.engineId,
- kind: HEAP_PROFILE_FLAMEGRAPH_TRACK_KIND,
- name: `Heap Profile Flamegraph`,
- trackGroup: pUuid,
- config: {upid}
- });
}
if (upidToProcessTracks.has(upid)) {
diff --git a/ui/src/frontend/flamegraph.ts b/ui/src/frontend/flamegraph.ts
index 704d14f..472440a 100644
--- a/ui/src/frontend/flamegraph.ts
+++ b/ui/src/frontend/flamegraph.ts
@@ -15,7 +15,7 @@
import {searchSegment} from '../base/binary_search';
import {cropText} from '../common/canvas_utils';
-import {CallsiteInfo} from './globals';
+import {CallsiteInfo} from '../common/state';
interface Node {
width: number;
@@ -59,6 +59,8 @@
private hoveredCallsite?: CallsiteInfo;
private clickedCallsite?: CallsiteInfo;
+ private startingY = 0;
+
constructor(flamegraphData: CallsiteInfo[]) {
this.flamegraphData = flamegraphData;
this.findMaxDepth();
@@ -97,6 +99,7 @@
*/
updateDataIfChanged(
flamegraphData: CallsiteInfo[], clickedCallsite?: CallsiteInfo) {
+ this.clickedCallsite = clickedCallsite;
if (this.flamegraphData === flamegraphData) {
return;
}
@@ -114,6 +117,7 @@
const name = '____MMMMMMQQwwZZZZZZzzzzzznnnnnnwwwwwwWWWWWqq$$mmmmmm__';
const charWidth = ctx.measureText(name).width / name.length;
const nodeHeight = this.getNodeHeight();
+ this.startingY = y;
if (this.flamegraphData === undefined) {
return;
@@ -167,7 +171,7 @@
(isFullWidth ? 1 : value.totalSize / parentSize) * parentNode.width;
const currentX = parentNode.nextXForChildren;
- currentY = nodeHeight * (value.depth + 1);
+ currentY = y + nodeHeight * (value.depth + 1);
// Draw node.
const name = this.getCallsiteName(value);
@@ -329,17 +333,17 @@
this.hoveredCallsite = undefined;
}
- // Returns id of clicked callsite if any.
- onMouseClick({x, y}: {x: number, y: number}): number {
+ onMouseClick({x, y}: {x: number, y: number}): CallsiteInfo|undefined {
if (this.isThumbnail) {
- return -1;
+ return undefined;
}
const clickedCallsite = this.findSelectedCallsite(x, y);
- return clickedCallsite === undefined ? -1 : clickedCallsite.id;
+ return clickedCallsite;
}
private findSelectedCallsite(x: number, y: number): CallsiteInfo|undefined {
- const depth = Math.trunc(y / this.getNodeHeight()) - 1; // at 0 is root
+ const depth = Math.trunc((y - this.startingY) / this.getNodeHeight()) -
+ 1; // at 0 is root
if (depth >= 0 && this.xStartsPerDepth.has(depth)) {
const startX = this.searchSmallest(this.xStartsPerDepth.get(depth)!, x);
const result = this.graphData.get(`${depth};${startX}`);
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 81ae8f6..57389ad 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -15,7 +15,7 @@
import {assertExists} from '../base/logging';
import {DeferredAction} from '../common/actions';
import {CurrentSearchResults, SearchSummary} from '../common/search_data';
-import {createEmptyState, State} from '../common/state';
+import {CallsiteInfo, createEmptyState, State} from '../common/state';
import {FrontendLocalState} from './frontend_local_state';
import {RafScheduler} from './raf_scheduler';
@@ -45,22 +45,18 @@
duration?: number;
}
-export interface CallsiteInfo {
- id: number;
- parentId: number;
- depth: number;
- name?: string;
- totalSize: number;
- selfSize: number;
- mapping: string;
-}
-
export interface HeapProfileDetails {
+ id?: number;
ts?: number;
tsNs?: number;
allocated?: number;
allocatedNotFreed?: number;
pid?: number;
+ upid?: number;
+ flamegraph?: CallsiteInfo[];
+ expandedCallsite?: CallsiteInfo;
+ viewingOption?: string;
+ expandedId?: number;
}
export interface QuantizedLoad {
@@ -96,7 +92,7 @@
private _threadMap?: ThreadMap = undefined;
private _sliceDetails?: SliceDetails = undefined;
private _counterDetails?: CounterDetails = undefined;
- private _heapDumpDetails?: HeapProfileDetails = undefined;
+ private _heapProfileDetails?: HeapProfileDetails = undefined;
private _numQueriesQueued = 0;
private _bufferUsage?: number = undefined;
private _recordingLog?: string = undefined;
@@ -128,7 +124,7 @@
this._threadMap = new Map<number, ThreadDesc>();
this._sliceDetails = {};
this._counterDetails = {};
- this._heapDumpDetails = {};
+ this._heapProfileDetails = {};
}
get state(): State {
@@ -184,12 +180,12 @@
this._counterDetails = assertExists(click);
}
- get heapDumpDetails() {
- return assertExists(this._heapDumpDetails);
+ get heapProfileDetails() {
+ return assertExists(this._heapProfileDetails);
}
- set heapDumpDetails(click: HeapProfileDetails) {
- this._heapDumpDetails = assertExists(click);
+ set heapProfileDetails(click: HeapProfileDetails) {
+ this._heapProfileDetails = assertExists(click);
}
set numQueuedQueries(value: number) {
diff --git a/ui/src/frontend/heap_profile_panel.ts b/ui/src/frontend/heap_profile_panel.ts
index 524bb40..c862582 100644
--- a/ui/src/frontend/heap_profile_panel.ts
+++ b/ui/src/frontend/heap_profile_panel.ts
@@ -15,71 +15,139 @@
import * as m from 'mithril';
import {Actions} from '../common/actions';
+import {
+ ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
+ DEFAULT_VIEWING_OPTION,
+ OBJECTS_ALLOCATED_KEY,
+ OBJECTS_ALLOCATED_NOT_FREED_KEY,
+ SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY
+} from '../common/flamegraph_util';
import {timeToCode} from '../common/time';
+import {Flamegraph} from './flamegraph';
import {globals} from './globals';
-import {Panel} from './panel';
+import {Panel, PanelSize} from './panel';
interface HeapProfileDetailsPanelAttrs {}
+const HEADER_HEIGHT = 30;
+
export class HeapProfileDetailsPanel extends
Panel<HeapProfileDetailsPanelAttrs> {
private ts = 0;
private pid = 0;
+ private flamegraph: Flamegraph = new Flamegraph([]);
+ private currentViewingOption = DEFAULT_VIEWING_OPTION;
view() {
- const heapDumpInfo = globals.heapDumpDetails;
+ const heapDumpInfo = globals.heapProfileDetails;
if (heapDumpInfo && heapDumpInfo.ts !== undefined &&
heapDumpInfo.allocated !== undefined &&
heapDumpInfo.allocatedNotFreed !== undefined &&
- heapDumpInfo.tsNs !== undefined && heapDumpInfo.pid !== undefined) {
+ heapDumpInfo.tsNs !== undefined && heapDumpInfo.pid !== undefined &&
+ heapDumpInfo.upid !== undefined) {
this.ts = heapDumpInfo.tsNs;
this.pid = heapDumpInfo.pid;
+ if (heapDumpInfo.flamegraph) {
+ this.flamegraph.updateDataIfChanged(heapDumpInfo.flamegraph);
+ }
+ const height = heapDumpInfo.flamegraph ?
+ this.flamegraph.getHeight() + HEADER_HEIGHT :
+ 0;
return m(
'.details-panel',
- m('.details-panel-heading', m('h2', `Heap Profile Details`)),
- m(
- '.details-table',
- [m('table',
- [
- m('tr',
- m('th', `Snapshot time`),
- m('td', `${timeToCode(heapDumpInfo.ts)}`)),
- m('tr',
- m('th', `Total allocated:`),
- m('td',
- `${heapDumpInfo.allocated.toLocaleString()} bytes`)),
- m('tr',
- m('th', `Allocated not freed:`),
- m('td',
- `${
- heapDumpInfo.allocatedNotFreed
- .toLocaleString()} bytes`)),
- ])],
- ),
- m('.explanation',
- 'Heap profile support is in beta. If you need missing features, ',
- 'download and open it in ',
- m(`a[href='https://pprof.corp.google.com']`, 'pprof'),
- ' (Googlers only) or ',
- m(`a[href='https://www.speedscope.app']`, 'Speedscope'),
- '.'),
- m('button',
- {
- onclick: () => {
- this.downloadPprof();
+ {
+ onclick: (e: MouseEvent) => {
+ if (this.flamegraph !== undefined) {
+ this.onMouseClick({y: e.layerY, x: e.layerX});
}
+ return false;
},
- m('i.material-icons', 'file_download'),
- 'Download profile'),
+ onmousemove: (e: MouseEvent) => {
+ if (this.flamegraph !== undefined) {
+ this.onMouseMove({y: e.layerY, x: e.layerX});
+ globals.rafScheduler.scheduleRedraw();
+ }
+ return false;
+ },
+ onmouseout: () => {
+ if (this.flamegraph !== undefined) {
+ this.onMouseOut();
+ }
+ }
+ },
+ m('.details-panel-heading.heap-profile',
+ [
+ m('div.options',
+ [
+ m('div.title', `Heap Profile:`),
+ this.getViewingOptionButtons(),
+ ]),
+ m('div.details',
+ [
+ m('div.time',
+ `Snapshot time: ${timeToCode(heapDumpInfo.ts)}`),
+ m('button.download',
+ {
+ onclick: () => {
+ this.downloadPprof();
+ }
+ },
+ m('i.material-icons', 'file_download'),
+ 'Download profile'),
+ ]),
+ ]),
+ m(`div[style=height:${height}px]`),
);
} else {
return m(
'.details-panel',
- m('.details-panel-heading', m('h2', `Heap Profile Details`)));
+ m('.details-panel-heading', m('h2', `Heap Profile`)));
}
}
+ getButtonsClass(viewingOption = DEFAULT_VIEWING_OPTION): string {
+ return this.currentViewingOption === viewingOption ? '.chosen' : '';
+ }
+
+ getViewingOptionButtons(): m.Children {
+ return m(
+ 'div',
+ m(`button${this.getButtonsClass(SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY)}`,
+ {
+ onclick: () => {
+ this.changeViewingOption(SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY);
+ }
+ },
+ 'space'),
+ m(`button${this.getButtonsClass(ALLOC_SPACE_MEMORY_ALLOCATED_KEY)}`,
+ {
+ onclick: () => {
+ this.changeViewingOption(ALLOC_SPACE_MEMORY_ALLOCATED_KEY);
+ }
+ },
+ 'alloc_space'),
+ m(`button${this.getButtonsClass(OBJECTS_ALLOCATED_NOT_FREED_KEY)}`,
+ {
+ onclick: () => {
+ this.changeViewingOption(OBJECTS_ALLOCATED_NOT_FREED_KEY);
+ }
+ },
+ 'objects'),
+ m(`button${this.getButtonsClass(OBJECTS_ALLOCATED_KEY)}`,
+ {
+ onclick: () => {
+ this.changeViewingOption(OBJECTS_ALLOCATED_KEY);
+ }
+ },
+ 'alloc_objects'));
+ }
+
+ changeViewingOption(viewingOption: string) {
+ this.currentViewingOption = viewingOption;
+ globals.dispatch(Actions.changeViewHeapProfileFlamegraph({viewingOption}));
+ }
+
downloadPprof() {
const engine = Object.values(globals.state.engines)[0];
if (!engine) return;
@@ -89,5 +157,34 @@
Actions.convertTraceToPprof({pid: this.pid, ts1: this.ts, src}));
}
- renderCanvas() {}
+ private changeFlamegraphData() {
+ const data = globals.heapProfileDetails;
+ const flamegraphData = data.flamegraph === undefined ? [] : data.flamegraph;
+ this.flamegraph.updateDataIfChanged(flamegraphData, data.expandedCallsite);
+ }
+
+ renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
+ this.changeFlamegraphData();
+ const unit =
+ this.currentViewingOption === SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
+ this.currentViewingOption === ALLOC_SPACE_MEMORY_ALLOCATED_KEY ?
+ 'B' :
+ '';
+ this.flamegraph.draw(ctx, size.width, size.height, 0, HEADER_HEIGHT, unit);
+ }
+
+ onMouseClick({x, y}: {x: number, y: number}): boolean {
+ const expandedCallsite = this.flamegraph.onMouseClick({x, y});
+ globals.dispatch(Actions.expandHeapProfileFlamegraph({expandedCallsite}));
+ return true;
+ }
+
+ onMouseMove({x, y}: {x: number, y: number}): boolean {
+ this.flamegraph.onMouseMove({x, y});
+ return true;
+ }
+
+ onMouseOut() {
+ this.flamegraph.onMouseOut();
+ }
}
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index cce7b1e..72d21b2 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -28,9 +28,7 @@
LogExistsKey
} from '../common/logs';
import {CurrentSearchResults, SearchSummary} from '../common/search_data';
-import {
- HeapProfileFlamegraphKey
-} from '../tracks/heap_profile_flamegraph/common';
+import {CallsiteInfo} from '../common/state';
import {maybeShowErrorDialog} from './error_dialog';
import {
@@ -44,8 +42,7 @@
import {HomePage} from './home_page';
import {openBufferWithLegacyTraceViewer} from './legacy_trace_viewer';
import {postMessageHandler} from './post_message_handler';
-import {RecordPage} from './record_page';
-import {updateAvailableAdbDevices} from './record_page';
+import {RecordPage, updateAvailableAdbDevices} from './record_page';
import {Router} from './router';
import {CheckHttpRpcConnection} from './rpc_http_dialog';
import {ViewerPage} from './viewer_page';
@@ -100,8 +97,6 @@
if ([LogExistsKey, LogBoundsKey, LogEntriesKey].includes(args.id)) {
const data = globals.trackDataStore.get(LogExistsKey) as LogExists;
if (data && data.exists) globals.rafScheduler.scheduleFullRedraw();
- } else if (HeapProfileFlamegraphKey === args.id) {
- globals.rafScheduler.scheduleFullRedraw();
} else {
globals.rafScheduler.scheduleRedraw();
}
@@ -130,8 +125,19 @@
this.redraw();
}
- publishHeapDumpDetails(click: HeapProfileDetails) {
- globals.heapDumpDetails = click;
+ publishHeapProfileDetails(click: HeapProfileDetails) {
+ globals.heapProfileDetails = click;
+ this.redraw();
+ }
+
+ publishHeapProfileFlamegraph(args: {
+ flamegraph: CallsiteInfo[],
+ expandedCallsite?: CallsiteInfo,
+ viewingOption?: string
+ }) {
+ globals.heapProfileDetails.flamegraph = args.flamegraph;
+ globals.heapProfileDetails.expandedCallsite = args.expandedCallsite;
+ globals.heapProfileDetails.viewingOption = args.viewingOption;
this.redraw();
}
diff --git a/ui/src/tracks/all_controller.ts b/ui/src/tracks/all_controller.ts
index 912f34f..ca4f7c8 100644
--- a/ui/src/tracks/all_controller.ts
+++ b/ui/src/tracks/all_controller.ts
@@ -18,7 +18,6 @@
import './chrome_slices/controller';
import './counter/controller';
import './heap_profile/controller';
-import './heap_profile_flamegraph/controller';
import './cpu_freq/controller';
import './gpu_freq/controller';
import './cpu_slices/controller';
diff --git a/ui/src/tracks/all_frontend.ts b/ui/src/tracks/all_frontend.ts
index 2957edf..3ce3de0 100644
--- a/ui/src/tracks/all_frontend.ts
+++ b/ui/src/tracks/all_frontend.ts
@@ -18,7 +18,6 @@
import './chrome_slices/frontend';
import './counter/frontend';
import './heap_profile/frontend';
-import './heap_profile_flamegraph/frontend';
import './cpu_freq/frontend';
import './gpu_freq/frontend';
import './cpu_slices/frontend';
diff --git a/ui/src/tracks/heap_profile_flamegraph/common.ts b/ui/src/tracks/heap_profile_flamegraph/common.ts
deleted file mode 100644
index 4d5c0e7..0000000
--- a/ui/src/tracks/heap_profile_flamegraph/common.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-// 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 {TrackData} from '../../common/track_data';
-import {CallsiteInfo} from '../../frontend/globals';
-
-export const HEAP_PROFILE_FLAMEGRAPH_TRACK_KIND = 'HeapProfileFlamegraphTrack';
-export const HeapProfileFlamegraphKey = 'heap-profile-flamegraph';
-
-export const SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY = 'space';
-export const ALLOC_SPACE_MEMORY_ALLOCATED_KEY = 'alloc_space';
-export const OBJECTS_ALLOCATED_NOT_FREED_KEY = 'objects';
-export const OBJECTS_ALLOCATED_KEY = 'alloc_objects';
-
-export const DEFAULT_VIEWING_OPTION = SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY;
-
-export interface Data extends TrackData {
- flamegraph: CallsiteInfo[];
- clickedCallsite?: CallsiteInfo;
- // undefined means that there is no change since previous value.
- viewingOption?: string;
-}
-
-export interface Config {
- upid: number;
- ts: number;
- isMinimized: boolean;
- expandedId: number;
- viewingOption: string;
-}
diff --git a/ui/src/tracks/heap_profile_flamegraph/controller.ts b/ui/src/tracks/heap_profile_flamegraph/controller.ts
deleted file mode 100644
index d4cf389..0000000
--- a/ui/src/tracks/heap_profile_flamegraph/controller.ts
+++ /dev/null
@@ -1,462 +0,0 @@
-// 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 {Actions} from '../../common/actions';
-import {globals} from '../../controller/globals';
-import {
- TrackController,
- trackControllerRegistry
-} from '../../controller/track_controller';
-import {findRootSize} from '../../frontend/flamegraph';
-import {CallsiteInfo} from '../../frontend/globals';
-
-import {
- ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
- Config,
- Data,
- DEFAULT_VIEWING_OPTION,
- HEAP_PROFILE_FLAMEGRAPH_TRACK_KIND,
- HeapProfileFlamegraphKey,
- OBJECTS_ALLOCATED_KEY,
- OBJECTS_ALLOCATED_NOT_FREED_KEY,
- SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
-} from './common';
-
-const MIN_PIXEL_DISPLAYED = 1;
-
-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 callsite with size of the biggest merged
-// callsite.
-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);
-
- // 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);
- }
- j++;
- nextCallsite = data[j];
- }
- }
- 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
- };
-}
-
-function getCallsitesParentHash(
- callsite: CallsiteInfo, map: Map<number, number>): number {
- return map.has(callsite.parentId) ? map.get(callsite.parentId)! :
- callsite.parentId;
-}
-
-function 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;
-}
-
-class HeapProfileFlameraphTrackController extends
- TrackController<Config, Data> {
- static readonly kind = HEAP_PROFILE_FLAMEGRAPH_TRACK_KIND;
- private start = 0;
- private end = 0;
- private resolution = 0;
- private length = 0;
- private lastSelectedTs?: number;
- private lastSelectedId?: number;
- private lastExpandedId?: number;
- private lastViewingOption?: string;
-
- private flamegraphDatasets: Map<string, CallsiteInfo[]> = new Map();
-
- async onBoundsChange(start: number, end: number, resolution: number):
- Promise<Data> {
- this.start = start;
- this.end = end;
- this.resolution = resolution;
-
- return this.generateEmptyData();
- }
-
- private generateEmptyData(): Data {
- const data: Data = {
- start: -1,
- end: -1,
- resolution: this.resolution,
- length: 0,
- flamegraph: []
- };
- return data;
- }
-
- run() {
- const selection = globals.state.currentHeapProfileFlamegraph;
-
- if (selection && selection.kind === 'HEAP_PROFILE_FLAMEGRAPH') {
- if (this.lastSelectedId !== selection.id ||
- this.lastSelectedTs !== selection.ts) {
- const selectedId = selection.id;
- const selectedUpid = selection.upid;
- const selectedKind = selection.kind;
- const selectedTs = selection.ts;
- // If we opened new heap profile, we don't want to show it expanded, but
- // if we are opening trace for the first time with existing state (ie.
- // via link), we want to show it expanded.
- this.lastExpandedId = !this.lastSelectedTs && this.config.expandedId ?
- this.config.expandedId :
- -1;
- this.lastSelectedId = selection.id;
- this.lastSelectedTs = selection.ts;
- this.lastViewingOption = this.config.viewingOption ?
- this.config.viewingOption :
- DEFAULT_VIEWING_OPTION;
-
- this.config.ts = selectedTs;
- this.config.upid = selectedUpid;
- this.config.expandedId = this.lastExpandedId;
-
- const key = `${selectedUpid};${selectedTs}`;
-
- // TODO(tneda): Prevent lots of flamegraph queries being queued if a
- // user clicks lots of the markers quickly.
- this.getFlamegraphData(
- key, this.lastViewingOption, selection.ts, selectedUpid)
- .then(flamegraphData => {
- if (flamegraphData !== undefined && selection &&
- selection.kind === selectedKind &&
- selection.id === selectedId && selection.ts === selectedTs) {
- this.prepareAndMergeCallsites(
- flamegraphData, this.lastViewingOption);
- globals.dispatch(Actions.updateTrackConfig(
- {id: this.trackState.id, config: this.config}));
- }
- });
- } else if (
- this.config.expandedId &&
- this.config.expandedId !== this.lastExpandedId) {
- const key = `${this.config.upid};${this.lastSelectedTs}`;
- this.lastExpandedId = this.config.expandedId;
- this.getFlamegraphData(
- key,
- this.config.viewingOption,
- this.lastSelectedTs,
- this.config.upid)
- .then(flamegraphData => {
- this.prepareAndMergeCallsites(flamegraphData, key);
- });
- } else if (this.config.viewingOption !== this.lastViewingOption) {
- const key = `${this.config.upid};${this.lastSelectedTs}`;
- this.lastViewingOption = this.config.viewingOption;
- this.config.expandedId = -1;
- this.getFlamegraphData(
- key,
- this.config.viewingOption,
- this.lastSelectedTs,
- this.config.upid)
- .then(flamegraphData => {
- this.prepareAndMergeCallsites(
- flamegraphData, this.lastViewingOption);
- });
- globals.dispatch(Actions.updateTrackConfig(
- {id: this.trackState.id, config: this.config}));
- }
- } else {
- globals.publish(
- 'TrackData', {id: HeapProfileFlamegraphKey, data: undefined});
- }
- }
-
- private publishEmptyData() {
- globals.publish(
- 'TrackData',
- {id: HeapProfileFlamegraphKey, data: this.generateEmptyData()});
- }
-
- private prepareAndMergeCallsites(
- flamegraphData: CallsiteInfo[],
- viewingOption: string|undefined = DEFAULT_VIEWING_OPTION) {
- const expandedFlamegraphData =
- expandCallsites(flamegraphData, this.config.expandedId);
- const expandedCallsite = this.config.expandedId === -1 ?
- undefined :
- flamegraphData[this.config.expandedId];
-
- const rootSize =
- expandedCallsite === undefined ? undefined : expandedCallsite.totalSize;
-
- const mergedFlamegraphData = mergeCallsites(
- expandedFlamegraphData,
- getMinSizeDisplayed(expandedFlamegraphData, rootSize));
-
- globals.publish('TrackData', {
- id: HeapProfileFlamegraphKey,
- data: {
- start: this.start,
- end: this.end,
- resolution: this.resolution,
- length: this.length,
- flamegraph: mergedFlamegraphData,
- clickedCallsite: expandedCallsite,
- viewingOption
- }
- });
- }
-
-
- 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 {
- // Sending empty data to show Loading state before we get an actual
- // data.
- this.publishEmptyData();
-
- // 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.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_${ts}`);
- const tableNameCallsiteHashNameSize =
- this.tableName(`callsite_hash_name_size_${ts}`);
- const tableNameGroupedCallsitesForFlamegraph =
- this.tableName(`grouped_callsites_for_flamegraph${ts}`);
- // Joining the callsite table with frame table then with alloc table to get
- // the size and name for each callsite.
- // TODO(tneda): Make frame name nullable in the trace processor for
- // consistency with the other columns.
- await this.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
- inner 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.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.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;
- }
-}
-
-trackControllerRegistry.register(HeapProfileFlameraphTrackController);
diff --git a/ui/src/tracks/heap_profile_flamegraph/frontend.ts b/ui/src/tracks/heap_profile_flamegraph/frontend.ts
deleted file mode 100644
index 0a78642..0000000
--- a/ui/src/tracks/heap_profile_flamegraph/frontend.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-// 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 * as m from 'mithril';
-
-import {Actions} from '../../common/actions';
-import {TrackState} from '../../common/state';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {Flamegraph} from '../../frontend/flamegraph';
-import {globals} from '../../frontend/globals';
-import {Track} from '../../frontend/track';
-import {TrackButton, TrackButtonAttrs} from '../../frontend/track_panel';
-import {trackRegistry} from '../../frontend/track_registry';
-
-import {
- ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
- Config,
- Data,
- HEAP_PROFILE_FLAMEGRAPH_TRACK_KIND,
- HeapProfileFlamegraphKey,
- SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY
-} from './common';
-
-const MARGIN = 10;
-
-export class HeapProfileFlamegraphTrack extends Track<Config, Data> {
- static readonly kind = HEAP_PROFILE_FLAMEGRAPH_TRACK_KIND;
- private flamegraph: Flamegraph;
- private currentViewingOption = SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY;
-
- static create(trackState: TrackState): HeapProfileFlamegraphTrack {
- return new HeapProfileFlamegraphTrack(trackState);
- }
-
- constructor(trackState: TrackState) {
- super(trackState);
- this.flamegraph = new Flamegraph([]);
- this.flamegraph.enableThumbnail(this.config.isMinimized);
- }
-
- data() {
- return globals.trackDataStore.get(HeapProfileFlamegraphKey) as Data;
- }
-
- private changeFlamegraphData() {
- const data = this.data();
- if (data === undefined) {
- this.flamegraph.updateDataIfChanged([]);
- } else {
- this.flamegraph.updateDataIfChanged(
- data.flamegraph, data.clickedCallsite);
- if (data.viewingOption !== undefined) {
- this.currentViewingOption = data.viewingOption;
- }
- }
- }
-
- getHeight(): number {
- const data = this.data();
- if (data === undefined) {
- return 0;
- }
- if (this.config.isMinimized) {
- return super.getHeight();
- }
- this.changeFlamegraphData();
- const height = this.flamegraph.getHeight();
- return Math.max(height + MARGIN, super.getHeight());
- }
-
- getWidth(): number {
- const {visibleWindowTime, timeScale} = globals.frontendLocalState;
- const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start));
- const endPx = Math.ceil(timeScale.timeToPx(visibleWindowTime.end));
- return endPx - startPx;
- }
-
- renderCanvas(ctx: CanvasRenderingContext2D) {
- const data = this.data();
- if (data !== undefined && data.start === -1) {
- const {visibleWindowTime, timeScale} = globals.frontendLocalState;
- checkerboardExcept(
- ctx,
- this.getHeight(),
- timeScale.timeToPx(visibleWindowTime.start),
- timeScale.timeToPx(visibleWindowTime.end),
- timeScale.timeToPx(data.start),
- timeScale.timeToPx(data.end));
- return;
- }
- this.changeFlamegraphData();
- const unit =
- this.currentViewingOption === SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
- this.currentViewingOption === ALLOC_SPACE_MEMORY_ALLOCATED_KEY ?
- 'B' :
- '';
- this.flamegraph.draw(ctx, this.getWidth(), this.getHeight(), 0, 0, unit);
- }
-
- onMouseClick({x, y}: {x: number, y: number}): boolean {
- this.config.expandedId = this.flamegraph.onMouseClick({x, y});
- globals.dispatch(Actions.updateTrackConfig(
- {id: this.trackState.id, config: this.config}));
- return true;
- }
-
- onMouseMove({x, y}: {x: number, y: number}): boolean {
- this.flamegraph.onMouseMove({x, y});
- return true;
- }
-
- onMouseOut() {
- this.flamegraph.onMouseOut();
- }
-
- getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
- const buttons: Array<m.Vnode<TrackButtonAttrs>> = [];
- buttons.push(
- // Minimize button
- m(TrackButton, {
- action: () => {
- const newIsMinimized = !this.config.isMinimized;
- this.config.isMinimized = newIsMinimized;
- Actions.updateTrackConfig(
- {id: this.trackState.id, config: this.config});
- this.flamegraph.enableThumbnail(newIsMinimized);
- globals.rafScheduler.scheduleFullRedraw();
- },
- i: this.config.isMinimized ? 'expand_more' : 'expand_less',
- tooltip: this.config.isMinimized ? 'Maximize' : 'Minimize',
- selected: this.config.isMinimized,
- }));
- return buttons;
- }
-}
-
-trackRegistry.register(HeapProfileFlamegraphTrack);