Skip to content

Commit 79600d4

Browse files
committed
Add first draft live memory table page on the recording page that polls memory stats every few seconds from the live device
1 parent 8cd384d commit 79600d4

File tree

8 files changed

+197
-5
lines changed

8 files changed

+197
-5
lines changed

ui/src/components/widgets/datagrid/datagrid.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2302,7 +2302,7 @@ export class DataGrid implements m.ClassComponent<DataGridAttrs> {
23022302
}
23032303
}
23042304

2305-
export function renderCell(value: SqlValue, columnName: string) {
2305+
export function renderCell(value: SqlValue, columnName?: string) {
23062306
if (value === undefined) {
23072307
return '';
23082308
} else if (value instanceof Uint8Array) {
@@ -2312,7 +2312,7 @@ export function renderCell(value: SqlValue, columnName: string) {
23122312
icon: Icons.Download,
23132313
onclick: () =>
23142314
download({
2315-
fileName: `${columnName}.blob`,
2315+
fileName: `${columnName ?? 'untitled'}.bin`,
23162316
content: value,
23172317
}),
23182318
},
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (C) 2025 The Android Open Source Project
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {ProcessMemoryStats} from '../interfaces/recording_target';
16+
17+
/**
18+
* Parses the "Total PSS by process" section of `dumpsys meminfo` output.
19+
*
20+
* Example lines:
21+
* " 267,177K: system (pid 1493)"
22+
* " 178,498K: com.google.android.gms (pid 4915 / activities)"
23+
* " 58,498K: .dataservices (pid 2588)"
24+
*/
25+
export function parseDumpsysMeminfo(output: string): ProcessMemoryStats[] {
26+
const results: ProcessMemoryStats[] = [];
27+
28+
// Find the "Total PSS by process" section.
29+
const sectionStart = output.indexOf('Total PSS by process:');
30+
if (sectionStart === -1) return results;
31+
32+
// Extract lines from this section until the next "Total" section or EOF.
33+
const sectionText = output.substring(
34+
sectionStart + 'Total PSS by process:'.length,
35+
);
36+
const lines = sectionText.split('\n');
37+
38+
// Each process line looks like:
39+
// " 123,456K: com.example.app (pid 1234 / activities)"
40+
// " 123,456K: com.example.app (pid 1234)"
41+
const lineRegex = /^\s*([\d,]+)K:\s+(.+?)\s+\(pid\s+(\d+)/;
42+
43+
for (const line of lines) {
44+
// Stop at the next section header.
45+
if (line.match(/^Total /)) break;
46+
47+
const match = line.match(lineRegex);
48+
if (match) {
49+
const pssKb = parseInt(match[1].replace(/,/g, ''), 10);
50+
const processName = match[2];
51+
const pid = parseInt(match[3], 10);
52+
results.push({processName, pid, pssKb});
53+
}
54+
}
55+
56+
return results;
57+
}

ui/src/plugins/dev.perfetto.RecordTraceV2/adb/web_device_proxy/wdp_target.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
import protos from '../../../../protos';
1616
import {errResult, okResult, Result} from '../../../../base/result';
1717
import {PreflightCheck} from '../../interfaces/connection_check';
18-
import {RecordingTarget} from '../../interfaces/recording_target';
18+
import {
19+
ProcessMemoryStats,
20+
RecordingTarget,
21+
} from '../../interfaces/recording_target';
1922
import {ConsumerIpcTracingSession} from '../../tracing_protocol/consumer_ipc_tracing_session';
2023
import {checkAndroidTarget} from '../adb_platform_checks';
2124
import {
@@ -27,6 +30,7 @@ import {AsyncLazy} from '../../../../base/async_lazy';
2730
import {WdpDevice} from './wdp_schema';
2831
import {showPopupWindow} from '../../../../base/popup_window';
2932
import {defer} from '../../../../base/deferred';
33+
import {parseDumpsysMeminfo} from '../parse_dumpsys_meminfo';
3034

3135
export class WebDeviceProxyTarget implements RecordingTarget {
3236
readonly kind = 'LIVE_RECORDING';
@@ -144,6 +148,14 @@ export class WebDeviceProxyTarget implements RecordingTarget {
144148
return getAdbTracingServiceState(this.adbDevice.value);
145149
}
146150

151+
async pollMemoryStats(): Promise<ProcessMemoryStats[] | undefined> {
152+
const dev = this.adbDevice.value;
153+
if (dev === undefined) return undefined;
154+
const result = await dev.shell('dumpsys meminfo');
155+
if (!result.ok) return undefined;
156+
return parseDumpsysMeminfo(result.value);
157+
}
158+
147159
async startTracing(
148160
traceConfig: protos.ITraceConfig,
149161
): Promise<Result<ConsumerIpcTracingSession>> {

ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
import protos from '../../../../protos';
1616
import {errResult, okResult, Result} from '../../../../base/result';
1717
import {PreflightCheck} from '../../interfaces/connection_check';
18-
import {RecordingTarget} from '../../interfaces/recording_target';
18+
import {
19+
ProcessMemoryStats,
20+
RecordingTarget,
21+
} from '../../interfaces/recording_target';
1922
import {ConsumerIpcTracingSession} from '../../tracing_protocol/consumer_ipc_tracing_session';
2023
import {checkAndroidTarget} from '../adb_platform_checks';
2124
import {
@@ -24,6 +27,7 @@ import {
2427
} from '../adb_tracing_session';
2528
import {AdbWebsocketDevice} from './adb_websocket_device';
2629
import {AsyncLazy} from '../../../../base/async_lazy';
30+
import {parseDumpsysMeminfo} from '../parse_dumpsys_meminfo';
2731

2832
export class AdbWebsocketTarget implements RecordingTarget {
2933
readonly kind = 'LIVE_RECORDING';
@@ -85,6 +89,14 @@ export class AdbWebsocketTarget implements RecordingTarget {
8589
return getAdbTracingServiceState(this.adbDevice.value);
8690
}
8791

92+
async pollMemoryStats(): Promise<ProcessMemoryStats[] | undefined> {
93+
const dev = this.adbDevice.value;
94+
if (dev === undefined) return undefined;
95+
const result = await dev.shell('dumpsys meminfo');
96+
if (!result.ok) return undefined;
97+
return parseDumpsysMeminfo(result.value);
98+
}
99+
88100
async startTracing(
89101
traceConfig: protos.ITraceConfig,
90102
): Promise<Result<ConsumerIpcTracingSession>> {

ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
// limitations under the License.
1414

1515
import protos from '../../../../protos';
16-
import {RecordingTarget} from '../../interfaces/recording_target';
16+
import {
17+
ProcessMemoryStats,
18+
RecordingTarget,
19+
} from '../../interfaces/recording_target';
1720
import {PreflightCheck} from '../../interfaces/connection_check';
1821
import {AdbKeyManager} from './adb_key_manager';
1922
import {
@@ -26,6 +29,7 @@ import {errResult, okResult, Result} from '../../../../base/result';
2629
import {checkAndroidTarget} from '../adb_platform_checks';
2730
import {ConsumerIpcTracingSession} from '../../tracing_protocol/consumer_ipc_tracing_session';
2831
import {AsyncLazy} from '../../../../base/async_lazy';
32+
import {parseDumpsysMeminfo} from '../parse_dumpsys_meminfo';
2933

3034
export class AdbWebusbTarget implements RecordingTarget {
3135
readonly kind = 'LIVE_RECORDING';
@@ -87,6 +91,14 @@ export class AdbWebusbTarget implements RecordingTarget {
8791
return await createAdbTracingSession(adbDeviceStatus.value, traceConfig);
8892
}
8993

94+
async pollMemoryStats(): Promise<ProcessMemoryStats[] | undefined> {
95+
const dev = this.adbDevice.value;
96+
if (dev === undefined) return undefined;
97+
const result = await dev.shell('dumpsys meminfo');
98+
if (!result.ok) return undefined;
99+
return parseDumpsysMeminfo(result.value);
100+
}
101+
90102
disconnect(): void {
91103
this.adbDevice.value?.close();
92104
this.adbDevice.reset();

ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ import {PreflightCheck, WithPreflightChecks} from './connection_check';
1818
import {TargetPlatformId} from './target_platform';
1919
import {TracingSession} from './tracing_session';
2020

21+
export interface ProcessMemoryStats {
22+
readonly processName: string;
23+
readonly pid: number;
24+
readonly pssKb: number;
25+
}
26+
2127
/**
2228
* The interface that models a device that can be used for recording a trace.
2329
* This is the contract that RecordingTargetProvider(s) must implement in order
@@ -46,4 +52,8 @@ export interface RecordingTarget extends WithPreflightChecks {
4652
startTracing(
4753
traceConfig: protos.ITraceConfig,
4854
): Promise<Result<TracingSession>>;
55+
56+
// Optional: polls per-process memory stats from the device. Only supported
57+
// on ADB-connected targets via `dumpsys meminfo`.
58+
pollMemoryStats?(): Promise<ProcessMemoryStats[] | undefined>;
4959
}

ui/src/plugins/dev.perfetto.RecordTraceV2/pages/target_selection_page.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ import {getPresetsForPlatform} from '../presets';
3434
import {Icons} from '../../../base/semantic_icons';
3535
import {shareRecordConfig} from '../config/config_sharing';
3636
import {Card} from '../../../widgets/card';
37+
import {
38+
DataGrid,
39+
renderCell,
40+
} from '../../../components/widgets/datagrid/datagrid';
41+
import {SchemaRegistry} from '../../../components/widgets/datagrid/datagrid_schema';
42+
import {Row, SqlValue} from '../../../trace_processor/query_result';
3743

3844
type RecMgrAttrs = {recMgr: RecordingManager};
3945

@@ -472,11 +478,90 @@ class TargetDetails implements m.ClassComponent<TargetDetailsAttrs> {
472478
view({attrs}: m.CVnode<TargetDetailsAttrs>) {
473479
return [
474480
this.checksRenderer?.renderTable(),
481+
attrs.target.pollMemoryStats &&
482+
m(DeviceMemoryStatsRenderer, {target: attrs.target}),
475483
m(SessionMgmtRenderer, {recMgr: attrs.recMgr, target: attrs.target}),
476484
];
477485
}
478486
}
479487

488+
const MEMORY_STATS_SCHEMA: SchemaRegistry = {
489+
process: {
490+
process: {title: 'Process', columnType: 'text'},
491+
pid: {title: 'PID', columnType: 'quantitative'},
492+
pss_kb: {
493+
title: 'PSS (KB)',
494+
columnType: 'quantitative',
495+
cellRenderer: (value: SqlValue) => {
496+
if (typeof value === 'number') {
497+
return `${value.toLocaleString()} KB`;
498+
} else {
499+
return renderCell(value);
500+
}
501+
},
502+
},
503+
},
504+
};
505+
506+
type DeviceMemoryStatsAttrs = {target: RecordingTarget};
507+
class DeviceMemoryStatsRenderer
508+
implements m.ClassComponent<DeviceMemoryStatsAttrs>
509+
{
510+
private trash = new DisposableStack();
511+
private rows: Row[] = [];
512+
513+
constructor({attrs}: m.CVnode<DeviceMemoryStatsAttrs>) {
514+
this.trash.use(this.startPolling(attrs.target));
515+
}
516+
517+
private startPolling(target: RecordingTarget): Disposable {
518+
// Kick off an initial poll immediately.
519+
this.poll(target);
520+
const timerId = window.setInterval(() => this.poll(target), 3000);
521+
return {
522+
[Symbol.dispose]() {
523+
window.clearInterval(timerId);
524+
},
525+
};
526+
}
527+
528+
private async poll(target: RecordingTarget) {
529+
const result = await target.pollMemoryStats?.();
530+
if (result !== undefined) {
531+
this.rows = result.map((s) => ({
532+
process: s.processName,
533+
pid: s.pid,
534+
pss_kb: s.pssKb,
535+
}));
536+
}
537+
m.redraw();
538+
}
539+
540+
view() {
541+
return [
542+
m('header', 'Device memory'),
543+
m(DataGrid, {
544+
className: 'pf-device-memory-table',
545+
schema: MEMORY_STATS_SCHEMA,
546+
rootSchema: 'process',
547+
data: this.rows,
548+
initialColumns: [
549+
{id: 'process', field: 'process'},
550+
{id: 'pid', field: 'pid'},
551+
{id: 'pss_kb', field: 'pss_kb', sort: 'DESC'},
552+
],
553+
canAddColumns: false,
554+
canRemoveColumns: false,
555+
enablePivotControls: false,
556+
}),
557+
];
558+
}
559+
560+
onremove() {
561+
this.trash.dispose();
562+
}
563+
}
564+
480565
type SessionMgmtAttrs = {recMgr: RecordingManager; target: RecordingTarget};
481566
class SessionMgmtRenderer implements m.ClassComponent<SessionMgmtAttrs> {
482567
view({attrs}: m.CVnode<SessionMgmtAttrs>) {

ui/src/plugins/dev.perfetto.RecordTraceV2/styles.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,3 +1124,7 @@
11241124
color: var(--pf-color-text);
11251125
}
11261126
}
1127+
1128+
.pf-device-memory-table {
1129+
height: 500px;
1130+
}

0 commit comments

Comments
 (0)