Add focus regex for heapprofiles
Plumb a regex for filtering which slices appear in the flamechart.
Also fix a bug where clicking on the UI at top of the flamechart
reset slice selection.
Bug: 149833691
Change-Id: I777e7764616afbf80cfceb5a6980b6dc9679b88e
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index ffd33eb..f4272e2 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -453,7 +453,7 @@
id: args.id,
upid: args.upid,
ts: args.ts,
- type: args.type
+ type: args.type,
};
},
@@ -467,6 +467,7 @@
ts: args.ts,
type: args.type,
viewingOption: DEFAULT_VIEWING_OPTION,
+ focusRegex: '',
};
},
@@ -483,6 +484,12 @@
state.currentHeapProfileFlamegraph.viewingOption = args.viewingOption;
},
+ changeFocusHeapProfileFlamegraph(
+ state: StateDraft, args: {focusRegex: string}): void {
+ if (state.currentHeapProfileFlamegraph === null) return;
+ state.currentHeapProfileFlamegraph.focusRegex = args.focusRegex;
+ },
+
selectChromeSlice(state: StateDraft, args: {id: number, trackId: string}):
void {
state.currentSelection = {
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 5bd331a..16ac056 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -186,6 +186,7 @@
ts: number;
type: string;
viewingOption: HeapProfileFlamegraphViewingOption;
+ focusRegex: string;
expandedCallsite?: CallsiteInfo;
}
diff --git a/ui/src/controller/heap_profile_controller.ts b/ui/src/controller/heap_profile_controller.ts
index 50afa42..d6a22c1 100644
--- a/ui/src/controller/heap_profile_controller.ts
+++ b/ui/src/controller/heap_profile_controller.ts
@@ -35,15 +35,51 @@
}
const MIN_PIXEL_DISPLAYED = 1;
+class TablesCache {
+ private engine: Engine;
+ private cache: Map<string, string>;
+ private prefix: string;
+ private tableId: number;
+ private cacheSizeLimit: number;
+
+ constructor(engine: Engine, prefix: string) {
+ this.engine = engine;
+ this.cache = new Map<string, string>();
+ this.prefix = prefix;
+ this.tableId = 0;
+ this.cacheSizeLimit = 10;
+ }
+
+ async getTableName(query: string): Promise<string> {
+ let tableName = this.cache.get(query);
+ if (tableName === undefined) {
+ // TODO(hjd): This should be LRU.
+ if (this.cache.size > this.cacheSizeLimit) {
+ for (const name of this.cache.values()) {
+ await this.engine.query(`drop table ${name}`);
+ }
+ this.cache.clear();
+ }
+ tableName = `${this.prefix}_${this.tableId++}`;
+ await this.engine.query(
+ `create temp table if not exists ${tableName} as ${query}`);
+ this.cache.set(query, tableName);
+ }
+ return tableName;
+ }
+}
+
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 = {};
+ private cache: TablesCache;
constructor(private args: HeapProfileControllerArgs) {
super('main');
+ this.cache = new TablesCache(args.engine, 'grouped_callsites');
}
run() {
@@ -66,6 +102,13 @@
Object.assign(this.heapProfileDetails, result);
}
+ // TODO(hjd): Clean this up.
+ if (this.lastSelectedHeapProfile &&
+ this.lastSelectedHeapProfile.focusRegex !==
+ selection.focusRegex) {
+ this.flamegraphDatasets.clear();
+ }
+
this.lastSelectedHeapProfile = this.copyHeapProfile(selection);
const expandedId = selectedHeapProfile.expandedCallsite ?
@@ -86,7 +129,8 @@
DEFAULT_VIEWING_OPTION,
selection.ts,
selectedHeapProfile.upid,
- selectedHeapProfile.type)
+ selectedHeapProfile.type,
+ selectedHeapProfile.focusRegex)
.then(flamegraphData => {
if (flamegraphData !== undefined && selection &&
selection.kind === selectedHeapProfile.kind &&
@@ -122,7 +166,8 @@
ts: heapProfile.ts,
type: heapProfile.type,
expandedCallsite: heapProfile.expandedCallsite,
- viewingOption: heapProfile.viewingOption
+ viewingOption: heapProfile.viewingOption,
+ focusRegex: heapProfile.focusRegex,
};
}
@@ -136,6 +181,7 @@
this.lastSelectedHeapProfile.upid !== selection.upid ||
this.lastSelectedHeapProfile.viewingOption !==
selection.viewingOption ||
+ this.lastSelectedHeapProfile.focusRegex !== selection.focusRegex ||
this.lastSelectedHeapProfile.expandedCallsite !==
selection.expandedCallsite)));
}
@@ -155,7 +201,7 @@
async getFlamegraphData(
baseKey: string, viewingOption: string, ts: number, upid: number,
- type: string): Promise<CallsiteInfo[]> {
+ type: string, focusRegex: string): Promise<CallsiteInfo[]> {
let currentData: CallsiteInfo[];
const key = `${baseKey}-${viewingOption}`;
if (this.flamegraphDatasets.has(key)) {
@@ -166,7 +212,8 @@
// 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);
+ const tableName =
+ await this.prepareViewsAndTables(ts, upid, type, focusRegex);
currentData =
await this.getFlamegraphDataFromTables(tableName, viewingOption);
this.flamegraphDatasets.set(key, currentData);
@@ -254,26 +301,22 @@
return flamegraphData;
}
- private async prepareViewsAndTables(ts: number, upid: number, type: string):
- Promise<string> {
+ private async prepareViewsAndTables(
+ ts: number, upid: number, type: string,
+ focusRegex: 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`);
+ let whereClause = '';
+ if (focusRegex !== '') {
+ whereClause = `where focus_str = '${focusRegex}'`;
+ }
- await this.args.engine.query(`create temp table if not exists ${
- tableNameGroupedCallsitesForFlamegraph} as
- select id, name, map_name, parent_id, depth, cumulative_size,
+ return this.cache.getTableName(
+ `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}`;
+ from experimental_flamegraph(${ts}, ${upid}, '${type}') ${
+ whereClause}`);
}
getMinSizeDisplayed(flamegraphData: CallsiteInfo[], rootSize?: number):
diff --git a/ui/src/frontend/frontend_local_state.ts b/ui/src/frontend/frontend_local_state.ts
index a966a45..8bc6f42 100644
--- a/ui/src/frontend/frontend_local_state.ts
+++ b/ui/src/frontend/frontend_local_state.ts
@@ -26,6 +26,7 @@
import {randomColor} from './colorizer';
import {Tab} from './details_panel';
import {globals} from './globals';
+import {debounce, ratelimit} from './rate_limiters';
import {TimeScale} from './time_scale';
interface Range {
@@ -40,36 +41,6 @@
return current;
}
-// Returns a wrapper around |f| which calls f at most once every |ms|ms.
-function ratelimit(f: Function, ms: number): Function {
- let inProgess = false;
- return () => {
- if (inProgess) {
- return;
- }
- inProgess = true;
- window.setTimeout(() => {
- f();
- inProgess = false;
- }, ms);
- };
-}
-
-// Returns a wrapper around |f| which waits for a |ms|ms pause in calls
-// before calling |f|.
-function debounce(f: Function, ms: number): Function {
- let timerId: undefined|number;
- return () => {
- if (timerId) {
- window.clearTimeout(timerId);
- }
- timerId = window.setTimeout(() => {
- f();
- timerId = undefined;
- }, ms);
- };
-}
-
// Calculate the space a scrollbar takes up so that we can subtract it from
// the canvas width.
function calculateScrollbarWidth() {
diff --git a/ui/src/frontend/heap_profile_panel.ts b/ui/src/frontend/heap_profile_panel.ts
index 85b8b7a..8c92778 100644
--- a/ui/src/frontend/heap_profile_panel.ts
+++ b/ui/src/frontend/heap_profile_panel.ts
@@ -27,6 +27,7 @@
import {Flamegraph} from './flamegraph';
import {globals} from './globals';
import {Panel, PanelSize} from './panel';
+import {debounce} from './rate_limiters';
interface HeapProfileDetailsPanelAttrs {}
@@ -37,6 +38,11 @@
private ts = 0;
private pid = 0;
private flamegraph: Flamegraph = new Flamegraph([]);
+ private focusRegex = '';
+ private updateFocusRegexDebounced = debounce(() => {
+ this.updateFocusRegex();
+ }, 20);
+
view() {
const heapDumpInfo = globals.heapProfileDetails;
@@ -76,6 +82,7 @@
}
},
m('.details-panel-heading.heap-profile',
+ {onclick: (e: MouseEvent) => e.stopPropagation()},
[
m('div.options',
[
@@ -86,6 +93,15 @@
[
m('div.time',
`Snapshot time: ${timeToCode(heapDumpInfo.ts)}`),
+ m('input[type=text][placeholder=Focus]', {
+ oninput: (e: Event) => {
+ const target = (e.target as HTMLInputElement);
+ this.focusRegex = target.value;
+ this.updateFocusRegexDebounced();
+ },
+ // Required to stop hot-key handling:
+ onkeydown: (e: Event) => e.stopPropagation(),
+ }),
m('button.download',
{
onclick: () => {
@@ -105,6 +121,12 @@
}
}
+ private updateFocusRegex() {
+ globals.dispatch(Actions.changeFocusHeapProfileFlamegraph({
+ focusRegex: this.focusRegex,
+ }));
+ }
+
getButtonsClass(button: HeapProfileFlamegraphViewingOption): string {
if (globals.state.currentHeapProfileFlamegraph === null) return '';
return globals.state.currentHeapProfileFlamegraph.viewingOption === button ?
diff --git a/ui/src/frontend/rate_limiters.ts b/ui/src/frontend/rate_limiters.ts
new file mode 100644
index 0000000..ae212d4
--- /dev/null
+++ b/ui/src/frontend/rate_limiters.ts
@@ -0,0 +1,43 @@
+// Copyright (C) 2020 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.
+
+// Returns a wrapper around |f| which calls f at most once every |ms|ms.
+export function ratelimit(f: Function, ms: number): Function {
+ let inProgess = false;
+ return () => {
+ if (inProgess) {
+ return;
+ }
+ inProgess = true;
+ window.setTimeout(() => {
+ f();
+ inProgess = false;
+ }, ms);
+ };
+}
+
+// Returns a wrapper around |f| which waits for a |ms|ms pause in calls
+// before calling |f|.
+export function debounce(f: Function, ms: number): Function {
+ let timerId: undefined|number;
+ return () => {
+ if (timerId) {
+ window.clearTimeout(timerId);
+ }
+ timerId = window.setTimeout(() => {
+ f();
+ timerId = undefined;
+ }, ms);
+ };
+}