Skip to content

Commit 6e78165

Browse files
committed
ui: Add bidirectional navigation between flamegraph and Ahat views
Add size columns (shallow, native, retained, reachable) across all Ahat views and array elements. Add "View in Timeline" and "Open in Ahat" actions for bidirectional flamegraph-Ahat navigation. Remove artificial SQL limits in favor of UI-level pagination. Support both HPROF and perfetto native heap graph formats for array indexing. Change-Id: Ieafe822a46d99918fcec2c744cf59a18fa3248f0
1 parent 278e400 commit 6e78165

File tree

27 files changed

+2053
-587
lines changed

27 files changed

+2053
-587
lines changed

src/trace_processor/perfetto_sql/stdlib/export/to_svg.sql

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,9 +374,11 @@ SELECT
374374
$width_pixel,
375375
$height_pixel,
376376
_state_color($state, $io_wait),
377-
'Thread State: ' || $state || ' (' || _format_duration($dur) || ')' ||
378-
CASE WHEN $blocked_function IS NOT NULL THEN
379-
', blocked by ' || $blocked_function ELSE '' END,
377+
'Thread State: ' || $state || ' (' || _format_duration($dur) || ')' || CASE
378+
WHEN $blocked_function IS NOT NULL
379+
THEN ', blocked by ' || $blocked_function
380+
ELSE ''
381+
END,
380382
$href,
381383
NULL
382384
)

src/trace_processor/util/symbolizer/BUILD.gn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ if (enable_perfetto_trace_processor) {
4444
"../../../../include/perfetto/ext/base",
4545
]
4646
deps = [
47+
"../:build_id",
4748
"../../../../gn:default_deps",
4849
"../../../../include/perfetto/protozero",
4950
"../../../../include/perfetto/trace_processor:trace_processor",
5051
"../../../../protos/perfetto/trace:zero",
5152
"../../../../protos/perfetto/trace/profiling:zero",
52-
"../:build_id",
5353
]
5454
sources = [
5555
"symbolize_database.cc",

ui/src/components/query_flamegraph.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -296,11 +296,25 @@ async function computeFlamegraphTree(
296296
const unaggCols = unagg.map((x) => x.name);
297297

298298
const matchingColumns = ['name', ...unaggCols];
299-
const matchExpr = (x: string) =>
300-
matchingColumns.map(
299+
// Aggregatable properties using CONCAT_WITH_COMMA store comma-separated
300+
// values when nodes are merged. For exact match filters (^...$), we use
301+
// comma-delimited search to match individual elements.
302+
const concatCols = agg
303+
.filter((a) => a.mergeAggregation === 'CONCAT_WITH_COMMA')
304+
.map((a) => a.name);
305+
const matchExpr = (x: string) => {
306+
const likeFilter = sqliteString(makeSqlFilter(x));
307+
const standard = matchingColumns.map(
308+
(c) => `(IFNULL(${c}, '') like ${likeFilter} escape '\\')`,
309+
);
310+
// For comma-separated columns, wrap in delimiters for exact element match:
311+
// ',' || col || ',' LIKE '%,value,%'
312+
const csvMatch = concatCols.map(
301313
(c) =>
302-
`(IFNULL(${c}, '') like ${sqliteString(makeSqlFilter(x))} escape '\\')`,
314+
`(',' || IFNULL(${c}, '') || ',' like '%,' || ${likeFilter} || ',%' escape '\\')`,
303315
);
316+
return [...standard, ...csvMatch];
317+
};
304318

305319
const showStackFilter =
306320
showStackAndPivot.length === 0

ui/src/plugins/com.android.Ahat/components.ts

Lines changed: 113 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import m from 'mithril';
1616
import type {InstanceRow, PrimOrRef} from './types';
17+
import {fmtSize} from './format';
1718
import type {BreadcrumbEntry, NavState} from './nav_state';
1819
export type {BreadcrumbEntry};
1920

@@ -138,11 +139,11 @@ export function Section(): m.Component<SectionAttrs> {
138139

139140
// ─── SortableTable ────────────────────────────────────────────────────────────
140141

141-
interface SortableTableColumn<T> {
142+
export interface SortableTableColumn<T> {
142143
label: string;
143144
align?: string;
144145
minWidth?: string;
145-
sortKey?: (row: T) => number;
146+
sortKey?: (row: T) => number | string;
146147
render: (row: T, idx: number) => m.Children;
147148
}
148149

@@ -152,6 +153,8 @@ interface SortableTableAttrs<T> {
152153
limit?: number;
153154
rowKey?: (row: T, idx: number) => string | number;
154155
onRowClick?: (row: T) => void;
156+
/** Called with newly visible rows after sort/pagination changes. */
157+
onVisibleSlice?: (rows: T[]) => void;
155158
}
156159

157160
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -160,19 +163,25 @@ export function SortableTable(): m.Component<SortableTableAttrs<any>> {
160163
let sortAsc = false;
161164
let showCount = SHOW_LIMIT;
162165
let prevDataLen = -1;
166+
// Track previous visible slice to detect changes and fire onVisibleSlice.
167+
let prevVisibleEnd = 0;
168+
let prevSortCol: number | null = null;
169+
let prevSortAsc = false;
163170

164171
return {
165172
oninit(vnode) {
166173
showCount = vnode.attrs.limit ?? SHOW_LIMIT;
167174
prevDataLen = vnode.attrs.data.length;
168175
},
169176
view(vnode) {
170-
const {columns, data, rowKey, onRowClick} = vnode.attrs;
177+
const {columns, data, rowKey, onRowClick, onVisibleSlice} = vnode.attrs;
171178

172179
// Reset showCount when data changes (e.g. new query results).
180+
let dataChanged = false;
173181
if (data.length !== prevDataLen) {
174182
prevDataLen = data.length;
175183
showCount = vnode.attrs.limit ?? SHOW_LIMIT;
184+
dataChanged = true;
176185
}
177186

178187
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -181,13 +190,33 @@ export function SortableTable(): m.Component<SortableTableAttrs<any>> {
181190
sorted = data;
182191
} else {
183192
const key = columns[sortCol].sortKey!;
184-
sorted = [...data].sort((a, b) =>
185-
sortAsc ? key(a) - key(b) : key(b) - key(a),
186-
);
193+
sorted = [...data].sort((a, b) => {
194+
const ka = key(a);
195+
const kb = key(b);
196+
let cmp: number;
197+
if (typeof ka === 'string' && typeof kb === 'string') {
198+
cmp = ka.localeCompare(kb);
199+
} else {
200+
cmp = (ka as number) - (kb as number);
201+
}
202+
return sortAsc ? cmp : -cmp;
203+
});
187204
}
188205

189206
const visible = sorted.slice(0, showCount);
190207

208+
// Fire onVisibleSlice when sort or pagination changes.
209+
if (onVisibleSlice) {
210+
const sortChanged = sortCol !== prevSortCol || sortAsc !== prevSortAsc;
211+
const pageGrew = showCount > prevVisibleEnd;
212+
if (sortChanged || pageGrew || dataChanged) {
213+
prevSortCol = sortCol;
214+
prevSortAsc = sortAsc;
215+
prevVisibleEnd = showCount;
216+
onVisibleSlice(visible);
217+
}
218+
}
219+
191220
return m(
192221
'div',
193222
{class: 'ah-table-wrap'},
@@ -362,6 +391,84 @@ export function BitmapImage(): m.Component<BitmapImageAttrs> {
362391
};
363392
}
364393

394+
// ─── Reusable size columns for InstanceRow tables ────────────────────────────
395+
396+
export function shallowSizeCol(): SortableTableColumn<InstanceRow> {
397+
return {
398+
label: 'Shallow',
399+
align: 'right',
400+
sortKey: (r) => r.shallowJava,
401+
render: (r) => m('span', {class: 'ah-mono'}, fmtSize(r.shallowJava)),
402+
};
403+
}
404+
405+
export function nativeSizeCol(): SortableTableColumn<InstanceRow> {
406+
return {
407+
label: 'Shallow Native',
408+
align: 'right',
409+
sortKey: (r) => r.shallowNative,
410+
render: (r) => m('span', {class: 'ah-mono'}, fmtSize(r.shallowNative)),
411+
};
412+
}
413+
414+
export function retainedSizeCol(): SortableTableColumn<InstanceRow> {
415+
return {
416+
label: 'Retained',
417+
align: 'right',
418+
sortKey: (r) => {
419+
let j = 0;
420+
for (const h of r.retainedByHeap) j += h.java;
421+
return j;
422+
},
423+
render: (r) => {
424+
let j = 0;
425+
for (const h of r.retainedByHeap) j += h.java;
426+
return m('span', {class: 'ah-mono'}, fmtSize(j));
427+
},
428+
};
429+
}
430+
431+
export function retainedNativeSizeCol(): SortableTableColumn<InstanceRow> {
432+
return {
433+
label: 'Retained Native',
434+
align: 'right',
435+
sortKey: (r) => {
436+
let n = 0;
437+
for (const h of r.retainedByHeap) n += h.native_;
438+
return n;
439+
},
440+
render: (r) => {
441+
let n = 0;
442+
for (const h of r.retainedByHeap) n += h.native_;
443+
return m('span', {class: 'ah-mono'}, fmtSize(n));
444+
},
445+
};
446+
}
447+
448+
export function reachableSizeCol(): SortableTableColumn<InstanceRow> {
449+
return {
450+
label: 'Reachable',
451+
align: 'right',
452+
sortKey: (r) => r.reachableSize ?? 0,
453+
render: (r) =>
454+
r.reachableSize === null
455+
? m('span', {class: 'ah-mono ah-opacity-60'}, '\u2026')
456+
: m('span', {class: 'ah-mono'}, fmtSize(r.reachableSize)),
457+
};
458+
}
459+
460+
export function reachableNativeSizeCol(): SortableTableColumn<InstanceRow> {
461+
return {
462+
label: 'Reachable Native',
463+
align: 'right',
464+
sortKey: (r) => r.reachableNative ?? 0,
465+
render: (r) =>
466+
r.reachableNative === null
467+
? m('span', {class: 'ah-mono ah-opacity-60'}, '\u2026')
468+
: m('span', {class: 'ah-mono'}, fmtSize(r.reachableNative)),
469+
};
470+
}
471+
365472
// ─── Breadcrumbs ──────────────────────────────────────────────────────────────
366473

367474
interface BreadcrumbsAttrs {

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

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
import m from 'mithril';
1616
import type {Engine} from '../../trace_processor/engine';
17+
import type {Trace} from '../../public/trace';
1718
import {Spinner} from '../../widgets/spinner';
19+
import HeapProfilePlugin from '../dev.perfetto.HeapProfile';
1820
import type {NavState} from './nav_state';
1921
import type {OverviewData} from './types';
2022
import {Breadcrumbs} from './components';
@@ -36,6 +38,55 @@ import ObjectsView from './views/objects_view';
3638
import BitmapGalleryView from './views/bitmap_gallery_view';
3739
import AllocationsView from './views/allocations_view';
3840
import StringsView from './views/strings_view';
41+
import FlamegraphObjectsView from './views/flamegraph_objects_view';
42+
import {
43+
consumeFlamegraphAhatSelection,
44+
type FlamegraphAhatSelection,
45+
} from '../dev.perfetto.HeapProfile/flamegraph_to_ahat';
46+
47+
// ─── View in Timeline ─────────────────────────────────────────────────────────
48+
49+
async function viewInTimeline(objectId: number): Promise<void> {
50+
const trace = HeapDumpPage.trace;
51+
if (!trace) return;
52+
53+
const info = await queries.getHeapGraphTrackInfo(trace.engine, objectId);
54+
if (!info) return;
55+
56+
const filter = info.pathHash
57+
? `^${info.pathHash}$`
58+
: info.className
59+
? `^${info.className}$`
60+
: undefined;
61+
if (filter) {
62+
const hp = trace.plugins.getPlugin(HeapProfilePlugin);
63+
hp.setJavaHeapGraphFlamegraphFilter(filter);
64+
}
65+
66+
const uri = `/process_${info.upid}/java_heap_graph_heap_profile`;
67+
trace.navigate('#!/viewer');
68+
trace.selection.selectTrackEvent(uri, info.eventId);
69+
trace.scrollTo({track: {uri, expandGroup: true}});
70+
}
71+
72+
// Cached flamegraph selection consumed from HeapProfile. Consumed once on first
73+
// render of the flamegraph-objects view, then reused for subsequent renders.
74+
let cachedFlamegraphSelection: FlamegraphAhatSelection | null = null;
75+
76+
/** Reset cached selection on trace change to prevent stale cross-trace data. */
77+
export function resetCachedFlamegraphSelection(): void {
78+
cachedFlamegraphSelection = null;
79+
}
80+
81+
// Module-level overview cache. Survives component remounts (e.g. theme toggle).
82+
let cachedOverview: OverviewData | null = null;
83+
let overviewLoading = false;
84+
85+
/** Reset cached overview on trace change. */
86+
export function resetCachedOverview(): void {
87+
cachedOverview = null;
88+
overviewLoading = false;
89+
}
3990

4091
// ─── Content View Router ──────────────────────────────────────────────────────
4192

@@ -62,6 +113,7 @@ function renderContentView(
62113
heaps: overview.heaps,
63114
navigate,
64115
params: state.params,
116+
onViewInTimeline: viewInTimeline,
65117
});
66118
case 'objects':
67119
return m(ObjectsView, {engine, navigate, params: state.params});
@@ -79,6 +131,22 @@ function renderContentView(
79131
navigate,
80132
initialQuery: state.params.q,
81133
});
134+
case 'flamegraph-objects': {
135+
const pending = consumeFlamegraphAhatSelection();
136+
if (pending) cachedFlamegraphSelection = pending;
137+
const sel = cachedFlamegraphSelection;
138+
return m(FlamegraphObjectsView, {
139+
engine,
140+
navigate,
141+
nodeName: state.params.name,
142+
pathHashes: sel?.pathHashes,
143+
isDominator: sel?.isDominator,
144+
onBackToTimeline: () => {
145+
const trace = HeapDumpPage.trace;
146+
if (trace) trace.navigate('#!/viewer');
147+
},
148+
});
149+
}
82150
}
83151
}
84152

@@ -90,11 +158,9 @@ interface HeapDumpPageAttrs {
90158

91159
export class HeapDumpPage implements m.ClassComponent<HeapDumpPageAttrs> {
92160
static engine: Engine | null = null;
161+
static trace: Trace | null = null;
93162
static hasHeapData = false;
94163

95-
private overview: OverviewData | null = null;
96-
private loading = false;
97-
98164
oncreate(vnode: m.VnodeDOM<HeapDumpPageAttrs>) {
99165
setNavigateCallback((subpage) => {
100166
const href = `#!/ahat${subpage ? '/' + subpage : ''}`;
@@ -109,14 +175,14 @@ export class HeapDumpPage implements m.ClassComponent<HeapDumpPageAttrs> {
109175
}
110176

111177
private async loadOverview() {
112-
if (!HeapDumpPage.engine || this.loading || this.overview) return;
113-
this.loading = true;
178+
if (!HeapDumpPage.engine || overviewLoading || cachedOverview) return;
179+
overviewLoading = true;
114180
try {
115-
this.overview = await queries.getOverview(HeapDumpPage.engine);
181+
cachedOverview = await queries.getOverview(HeapDumpPage.engine);
116182
} catch (err) {
117183
console.error('Failed to load overview:', err);
118184
} finally {
119-
this.loading = false;
185+
overviewLoading = false;
120186
m.redraw();
121187
}
122188
}
@@ -132,7 +198,7 @@ export class HeapDumpPage implements m.ClassComponent<HeapDumpPageAttrs> {
132198
);
133199
}
134200

135-
if (!this.overview) {
201+
if (!cachedOverview) {
136202
return m(
137203
'div',
138204
{class: 'ah-page'},
@@ -151,7 +217,7 @@ export class HeapDumpPage implements m.ClassComponent<HeapDumpPageAttrs> {
151217
activeIndex: trailIndex,
152218
onNavigate: onBreadcrumbNavigate,
153219
}),
154-
renderContentView(nav, HeapDumpPage.engine, this.overview),
220+
renderContentView(nav, HeapDumpPage.engine, cachedOverview),
155221
),
156222
);
157223
}

0 commit comments

Comments
 (0)