Skip to content

Commit 71d2f28

Browse files
committed
Merge remote-tracking branch 'origin/main' into dev/lalitm/etm-refactor
2 parents 95ee1c3 + 63bf2e5 commit 71d2f28

File tree

13 files changed

+218
-349
lines changed

13 files changed

+218
-349
lines changed

ui/src/plugins/com.android.HeapDumpExplorer/heap_dump_page.ts

Lines changed: 160 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,46 @@ import OverviewView from './views/overview_view';
3131
import DominatorsView from './views/dominators_view';
3232
import ObjectView from './views/object_view';
3333
import AllObjectsView from './views/all_objects_view';
34-
import InstancesView from './views/instances_view';
3534
import BitmapGalleryView from './views/bitmap_gallery_view';
3635
import ClassesView from './views/classes_view';
3736
import StringsView from './views/strings_view';
3837
import ArraysView from './views/arrays_view';
39-
import FlamegraphObjectsView from './views/flamegraph_objects_view';
38+
import FlamegraphObjectsView, {
39+
flamegraphQuery,
40+
} from './views/flamegraph_objects_view';
41+
import {SQL_PREAMBLE} from './components';
42+
import {NUM} from '../../trace_processor/query_result';
4043

4144
// Each "Open in Heapdump Explorer" creates a closable flamegraph tab.
4245
let nextFgId = 0;
43-
const flamegraphTabs: Array<{id: number} & HeapdumpSelection> = [];
46+
const flamegraphTabs: Array<
47+
{id: number; count: number | null} & HeapdumpSelection
48+
> = [];
4449
let activeFgId = -1;
4550

46-
export function setFlamegraphSelection(sel: HeapdumpSelection): void {
51+
export function setFlamegraphSelection(
52+
sel: HeapdumpSelection,
53+
engine: Engine,
54+
): void {
55+
const existing = flamegraphTabs.find(
56+
(t) => t.pathHashes === sel.pathHashes && t.isDominator === sel.isDominator,
57+
);
58+
if (existing) {
59+
activeFgId = existing.id;
60+
navigate('flamegraph-objects');
61+
return;
62+
}
4763
const id = nextFgId++;
48-
flamegraphTabs.push({id, ...sel});
64+
const tab = {id, count: null as number | null, ...sel};
65+
flamegraphTabs.push(tab);
4966
activeFgId = id;
67+
const q = flamegraphQuery(sel.pathHashes, sel.isDominator);
68+
engine
69+
.query(`${SQL_PREAMBLE}; SELECT COUNT(*) AS c FROM (${q})`)
70+
.then((r) => {
71+
tab.count = Number(r.firstRow({c: NUM}).c);
72+
m.redraw();
73+
});
5074
}
5175

5276
export function resetFlamegraphSelection(): void {
@@ -65,14 +89,74 @@ export function resetCachedOverview(): void {
6589
overviewLoading = false;
6690
}
6791

68-
// Maps drill-down views to their parent tab.
69-
function activeTabKey(view: string): string {
70-
switch (view) {
71-
case 'object':
72-
case 'instances':
73-
return 'classes';
74-
default:
75-
return view;
92+
// Closable object tabs — clicking an object anywhere opens a new tab.
93+
interface InstanceTab {
94+
id: number;
95+
objId: number;
96+
label: string;
97+
}
98+
99+
let nextInstanceTabId = 0;
100+
const instanceTabs: InstanceTab[] = [];
101+
let activeInstanceTabId = -1;
102+
103+
function instanceTabKey(id: number): string {
104+
return `inst-${id}`;
105+
}
106+
107+
export function resetInstanceTabs(): void {
108+
instanceTabs.length = 0;
109+
nextInstanceTabId = 0;
110+
activeInstanceTabId = -1;
111+
}
112+
113+
function openInstanceTab(objId: number, label?: string): void {
114+
const existing = instanceTabs.find((t) => t.objId === objId);
115+
if (existing) {
116+
activeInstanceTabId = existing.id;
117+
return;
118+
}
119+
const displayLabel = label ?? 'Instance';
120+
const tab: InstanceTab = {
121+
id: nextInstanceTabId++,
122+
objId,
123+
label:
124+
displayLabel.length > 30
125+
? displayLabel.slice(0, 30) + '\u2026'
126+
: displayLabel,
127+
};
128+
instanceTabs.push(tab);
129+
activeInstanceTabId = tab.id;
130+
}
131+
132+
// Navigate wrapper: intercepts 'object' to open closable instance tabs.
133+
function navigateWithTabs(
134+
view: NavState['view'],
135+
params?: Record<string, unknown>,
136+
): void {
137+
if (view === 'object') {
138+
openInstanceTab(params?.id as number, params?.label as string | undefined);
139+
navigate(view, params);
140+
return;
141+
}
142+
activeInstanceTabId = -1;
143+
navigate(view, params);
144+
}
145+
146+
// When nav state points to 'object' (e.g. after browser back), ensure
147+
// the matching instance tab exists and is active. When nav moves away
148+
// from 'object', clear the active instance tab so fixed tabs are shown.
149+
function syncInstanceTabFromNav(): void {
150+
if (nav.view !== 'object') {
151+
activeInstanceTabId = -1;
152+
return;
153+
}
154+
const objId = nav.params.id;
155+
const existing = instanceTabs.find((t) => t.objId === objId);
156+
if (existing) {
157+
activeInstanceTabId = existing.id;
158+
} else {
159+
openInstanceTab(objId, nav.params.label);
76160
}
77161
}
78162

@@ -92,54 +176,53 @@ function getActiveTabKey(): string {
92176
tab ? tab.id : flamegraphTabs[flamegraphTabs.length - 1].id,
93177
);
94178
}
95-
return activeTabKey(nav.view);
179+
if (activeInstanceTabId >= 0) {
180+
return instanceTabKey(activeInstanceTabId);
181+
}
182+
return nav.view;
96183
}
97184

98185
function handleTabChange(key: string): void {
99-
const id = parseFgTabKey(key);
100-
if (id !== undefined) {
101-
activeFgId = id;
186+
const fgId = parseFgTabKey(key);
187+
if (fgId !== undefined) {
188+
activeFgId = fgId;
102189
navigate('flamegraph-objects');
190+
} else if (key.startsWith('inst-')) {
191+
activeInstanceTabId = parseInt(key.slice(5), 10);
192+
const tab = instanceTabs.find((t) => t.id === activeInstanceTabId);
193+
if (tab) {
194+
navigate('object', {id: tab.objId});
195+
}
103196
} else {
104197
activeFgId = -1;
198+
activeInstanceTabId = -1;
105199
navigate(key as NavState['view']);
106200
}
107201
}
108202

109203
function handleTabClose(key: string): void {
110-
const id = parseFgTabKey(key);
111-
if (id === undefined) return;
112-
const idx = flamegraphTabs.findIndex((t) => t.id === id);
204+
const fgId = parseFgTabKey(key);
205+
if (fgId !== undefined) {
206+
const idx = flamegraphTabs.findIndex((t) => t.id === fgId);
207+
if (idx === -1) return;
208+
flamegraphTabs.splice(idx, 1);
209+
if (activeFgId === fgId) {
210+
activeFgId = -1;
211+
navigate('overview');
212+
}
213+
return;
214+
}
215+
if (!key.startsWith('inst-')) return;
216+
const id = parseInt(key.slice(5), 10);
217+
const idx = instanceTabs.findIndex((t) => t.id === id);
113218
if (idx === -1) return;
114-
flamegraphTabs.splice(idx, 1);
115-
if (activeFgId === id) {
116-
activeFgId = -1;
219+
instanceTabs.splice(idx, 1);
220+
if (activeInstanceTabId === id) {
221+
activeInstanceTabId = -1;
117222
navigate('overview');
118223
}
119224
}
120225

121-
// Renders the content for the "classes" tab, which also hosts drill-down
122-
// views (instances, object detail) based on the current nav state.
123-
function classesTabContent(
124-
state: NavState,
125-
engine: Engine,
126-
overview: OverviewData,
127-
): m.Children {
128-
switch (state.view) {
129-
case 'object':
130-
return m(ObjectView, {
131-
engine,
132-
heaps: overview.heaps,
133-
navigate,
134-
params: state.params,
135-
});
136-
case 'instances':
137-
return m(InstancesView, {engine, navigate, params: state.params});
138-
default:
139-
return m(ClassesView, {engine, navigate});
140-
}
141-
}
142-
143226
function buildTabs(
144227
state: NavState,
145228
engine: Engine,
@@ -150,29 +233,33 @@ function buildTabs(
150233
{
151234
key: 'overview',
152235
title: 'Overview',
153-
content: m(OverviewView, {overview, navigate}),
236+
content: m(OverviewView, {overview, navigate: navigateWithTabs}),
154237
},
155238
{
156239
key: 'classes',
157240
title: 'Classes',
158-
content: classesTabContent(state, engine, overview),
241+
content: m(ClassesView, {engine, navigate: navigateWithTabs}),
159242
},
160243
{
161244
key: 'objects',
162245
title: 'Objects',
163-
content: m(AllObjectsView, {engine, navigate}),
246+
content: m(AllObjectsView, {
247+
engine,
248+
navigate: navigateWithTabs,
249+
initialClass: state.view === 'objects' ? state.params.cls : undefined,
250+
}),
164251
},
165252
{
166253
key: 'dominators',
167254
title: 'Dominators',
168-
content: m(DominatorsView, {engine, navigate}),
255+
content: m(DominatorsView, {engine, navigate: navigateWithTabs}),
169256
},
170257
{
171258
key: 'bitmaps',
172259
title: 'Bitmaps',
173260
content: m(BitmapGalleryView, {
174261
engine,
175-
navigate,
262+
navigate: navigateWithTabs,
176263
hasFieldValues: overview.hasFieldValues,
177264
filterKey:
178265
state.view === 'bitmaps' ? state.params.filterKey : undefined,
@@ -183,7 +270,7 @@ function buildTabs(
183270
title: 'Strings',
184271
content: m(StringsView, {
185272
engine,
186-
navigate,
273+
navigate: navigateWithTabs,
187274
initialQuery: state.view === 'strings' ? state.params.q : undefined,
188275
hasFieldValues: overview.hasFieldValues,
189276
}),
@@ -192,9 +279,8 @@ function buildTabs(
192279
key: 'arrays',
193280
title: 'Arrays',
194281
content: m(ArraysView, {
195-
key: state.view === 'arrays' ? state.params.arrayHash ?? '' : '',
196282
engine,
197-
navigate,
283+
navigate: navigateWithTabs,
198284
initialArrayHash:
199285
state.view === 'arrays' ? state.params.arrayHash : undefined,
200286
hasFieldValues: overview.hasFieldValues,
@@ -206,11 +292,14 @@ function buildTabs(
206292
for (const fg of flamegraphTabs) {
207293
tabs.push({
208294
key: fgTabKey(fg.id),
209-
title: 'Flamegraph',
295+
title:
296+
fg.count !== null
297+
? `Flamegraph (${fg.count.toLocaleString()})`
298+
: 'Flamegraph',
210299
closeButton: true,
211300
content: m(FlamegraphObjectsView, {
212301
engine,
213-
navigate,
302+
navigate: navigateWithTabs,
214303
pathHashes: fg.pathHashes,
215304
isDominator: fg.isDominator,
216305
onBackToTimeline: () => {
@@ -220,6 +309,21 @@ function buildTabs(
220309
});
221310
}
222311

312+
// Append closable object instance tabs.
313+
for (const obj of instanceTabs) {
314+
tabs.push({
315+
key: instanceTabKey(obj.id),
316+
title: obj.label,
317+
closeButton: true,
318+
content: m(ObjectView, {
319+
engine,
320+
heaps: overview.heaps,
321+
navigate: navigateWithTabs,
322+
params: {id: obj.objId},
323+
}),
324+
});
325+
}
326+
223327
return tabs;
224328
}
225329

@@ -260,6 +364,7 @@ export class HeapDumpPage implements m.ClassComponent<HeapDumpPageAttrs> {
260364

261365
view(vnode: m.Vnode<HeapDumpPageAttrs>) {
262366
syncFromSubpage(vnode.attrs.subpage);
367+
syncInstanceTabFromNav();
263368

264369
if (!HeapDumpPage.engine || !HeapDumpPage.hasHeapData) {
265370
return m(

ui/src/plugins/com.android.HeapDumpExplorer/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
HeapDumpPage,
2323
setFlamegraphSelection,
2424
resetFlamegraphSelection,
25+
resetInstanceTabs,
2526
resetCachedOverview,
2627
} from './heap_dump_page';
2728
import {resetBitmapDumpDataCache} from './queries';
@@ -49,12 +50,13 @@ export default class implements PerfettoPlugin {
4950
HeapDumpPage.hasHeapData = true;
5051
resetBitmapDumpDataCache();
5152
resetFlamegraphSelection();
53+
resetInstanceTabs();
5254
resetCachedOverview();
5355

5456
ctx.plugins
5557
.getPlugin(HeapProfilePlugin)
5658
.registerOnNodeSelectedListener(({pathHashes, isDominator}) =>
57-
setFlamegraphSelection({pathHashes, isDominator}),
59+
setFlamegraphSelection({pathHashes, isDominator}, ctx.engine),
5860
);
5961

6062
ctx.sidebar.addMenuItem({

0 commit comments

Comments
 (0)