Skip to content

Commit 10cfef5

Browse files
committed
ui: Add bidirectional navigation between flamegraph and Ahat views
Adds size columns (shallow, native, retained, reachable) across all Ahat views. Adds flamegraph-to-Ahat navigation and removes legacy reference table options from the flamegraph context menu. Introduces mapCol utility for adapting shared column definitions. Change-Id: I6b3f522f5d5a82822decf247ce9b672941fe086a
1 parent 9d3feeb commit 10cfef5

File tree

26 files changed

+1702
-614
lines changed

26 files changed

+1702
-614
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: 127 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,98 @@ 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+
472+
/** Adapt a column from one row type to another via an extraction function. */
473+
export function mapCol<T, U>(
474+
col: SortableTableColumn<U>,
475+
extract: (row: T) => U,
476+
): SortableTableColumn<T> {
477+
return {
478+
label: col.label,
479+
align: col.align,
480+
minWidth: col.minWidth,
481+
sortKey: col.sortKey ? (row: T) => col.sortKey!(extract(row)) : undefined,
482+
render: (row: T, idx: number) => col.render(extract(row), idx),
483+
};
484+
}
485+
365486
// ─── Breadcrumbs ──────────────────────────────────────────────────────────────
366487

367488
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)