Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ui/src/components/widgets/datagrid/datagrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2302,7 +2302,7 @@ export class DataGrid implements m.ClassComponent<DataGridAttrs> {
}
}

export function renderCell(value: SqlValue, columnName: string) {
export function renderCell(value: SqlValue, columnName?: string) {
if (value === undefined) {
return '';
} else if (value instanceof Uint8Array) {
Expand All @@ -2312,7 +2312,7 @@ export function renderCell(value: SqlValue, columnName: string) {
icon: Icons.Download,
onclick: () =>
download({
fileName: `${columnName}.blob`,
fileName: `${columnName ?? 'untitled'}.bin`,
content: value,
}),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (C) 2025 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.

import {ProcessMemoryStats} from '../interfaces/recording_target';

/**
* Parses the "Total PSS by process" section of `dumpsys meminfo` output.
*
* Example lines:
* " 267,177K: system (pid 1493)"
* " 178,498K: com.google.android.gms (pid 4915 / activities)"
* " 58,498K: .dataservices (pid 2588)"
*/
export function parseDumpsysMeminfo(output: string): ProcessMemoryStats[] {
const results: ProcessMemoryStats[] = [];

// Find the "Total PSS by process" section.
const sectionStart = output.indexOf('Total PSS by process:');
if (sectionStart === -1) return results;

// Extract lines from this section until the next "Total" section or EOF.
const sectionText = output.substring(
sectionStart + 'Total PSS by process:'.length,
);
const lines = sectionText.split('\n');

// Each process line looks like:
// " 123,456K: com.example.app (pid 1234 / activities)"
// " 123,456K: com.example.app (pid 1234)"
const lineRegex = /^\s*([\d,]+)K:\s+(.+?)\s+\(pid\s+(\d+)/;

for (const line of lines) {
// Stop at the next section header.
if (line.match(/^Total /)) break;

const match = line.match(lineRegex);
if (match) {
const pssKb = parseInt(match[1].replace(/,/g, ''), 10);
const processName = match[2];
const pid = parseInt(match[3], 10);
results.push({processName, pid, pssKb});
}
}

return results;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
import protos from '../../../../protos';
import {errResult, okResult, Result} from '../../../../base/result';
import {PreflightCheck} from '../../interfaces/connection_check';
import {RecordingTarget} from '../../interfaces/recording_target';
import {
ProcessMemoryStats,
RecordingTarget,
} from '../../interfaces/recording_target';
import {ConsumerIpcTracingSession} from '../../tracing_protocol/consumer_ipc_tracing_session';
import {checkAndroidTarget} from '../adb_platform_checks';
import {
Expand All @@ -27,6 +30,7 @@ import {AsyncLazy} from '../../../../base/async_lazy';
import {WdpDevice} from './wdp_schema';
import {showPopupWindow} from '../../../../base/popup_window';
import {defer} from '../../../../base/deferred';
import {parseDumpsysMeminfo} from '../parse_dumpsys_meminfo';

export class WebDeviceProxyTarget implements RecordingTarget {
readonly kind = 'LIVE_RECORDING';
Expand Down Expand Up @@ -144,6 +148,14 @@ export class WebDeviceProxyTarget implements RecordingTarget {
return getAdbTracingServiceState(this.adbDevice.value);
}

async pollMemoryStats(): Promise<ProcessMemoryStats[] | undefined> {
const dev = this.adbDevice.value;
if (dev === undefined) return undefined;
const result = await dev.shell('dumpsys meminfo');
if (!result.ok) return undefined;
return parseDumpsysMeminfo(result.value);
}

async startTracing(
traceConfig: protos.ITraceConfig,
): Promise<Result<ConsumerIpcTracingSession>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
import protos from '../../../../protos';
import {errResult, okResult, Result} from '../../../../base/result';
import {PreflightCheck} from '../../interfaces/connection_check';
import {RecordingTarget} from '../../interfaces/recording_target';
import {
ProcessMemoryStats,
RecordingTarget,
} from '../../interfaces/recording_target';
import {ConsumerIpcTracingSession} from '../../tracing_protocol/consumer_ipc_tracing_session';
import {checkAndroidTarget} from '../adb_platform_checks';
import {
Expand All @@ -24,6 +27,7 @@ import {
} from '../adb_tracing_session';
import {AdbWebsocketDevice} from './adb_websocket_device';
import {AsyncLazy} from '../../../../base/async_lazy';
import {parseDumpsysMeminfo} from '../parse_dumpsys_meminfo';

export class AdbWebsocketTarget implements RecordingTarget {
readonly kind = 'LIVE_RECORDING';
Expand Down Expand Up @@ -85,6 +89,14 @@ export class AdbWebsocketTarget implements RecordingTarget {
return getAdbTracingServiceState(this.adbDevice.value);
}

async pollMemoryStats(): Promise<ProcessMemoryStats[] | undefined> {
const dev = this.adbDevice.value;
if (dev === undefined) return undefined;
const result = await dev.shell('dumpsys meminfo');
if (!result.ok) return undefined;
return parseDumpsysMeminfo(result.value);
}

async startTracing(
traceConfig: protos.ITraceConfig,
): Promise<Result<ConsumerIpcTracingSession>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
// limitations under the License.

import protos from '../../../../protos';
import {RecordingTarget} from '../../interfaces/recording_target';
import {
ProcessMemoryStats,
RecordingTarget,
} from '../../interfaces/recording_target';
import {PreflightCheck} from '../../interfaces/connection_check';
import {AdbKeyManager} from './adb_key_manager';
import {
Expand All @@ -26,6 +29,7 @@ import {errResult, okResult, Result} from '../../../../base/result';
import {checkAndroidTarget} from '../adb_platform_checks';
import {ConsumerIpcTracingSession} from '../../tracing_protocol/consumer_ipc_tracing_session';
import {AsyncLazy} from '../../../../base/async_lazy';
import {parseDumpsysMeminfo} from '../parse_dumpsys_meminfo';

export class AdbWebusbTarget implements RecordingTarget {
readonly kind = 'LIVE_RECORDING';
Expand Down Expand Up @@ -87,6 +91,14 @@ export class AdbWebusbTarget implements RecordingTarget {
return await createAdbTracingSession(adbDeviceStatus.value, traceConfig);
}

async pollMemoryStats(): Promise<ProcessMemoryStats[] | undefined> {
const dev = this.adbDevice.value;
if (dev === undefined) return undefined;
const result = await dev.shell('dumpsys meminfo');
if (!result.ok) return undefined;
return parseDumpsysMeminfo(result.value);
}

disconnect(): void {
this.adbDevice.value?.close();
this.adbDevice.reset();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import {PreflightCheck, WithPreflightChecks} from './connection_check';
import {TargetPlatformId} from './target_platform';
import {TracingSession} from './tracing_session';

export interface ProcessMemoryStats {
readonly processName: string;
readonly pid: number;
readonly pssKb: number;
}

/**
* The interface that models a device that can be used for recording a trace.
* This is the contract that RecordingTargetProvider(s) must implement in order
Expand Down Expand Up @@ -46,4 +52,8 @@ export interface RecordingTarget extends WithPreflightChecks {
startTracing(
traceConfig: protos.ITraceConfig,
): Promise<Result<TracingSession>>;

// Optional: polls per-process memory stats from the device. Only supported
// on ADB-connected targets via `dumpsys meminfo`.
pollMemoryStats?(): Promise<ProcessMemoryStats[] | undefined>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ import {getPresetsForPlatform} from '../presets';
import {Icons} from '../../../base/semantic_icons';
import {shareRecordConfig} from '../config/config_sharing';
import {Card} from '../../../widgets/card';
import {
DataGrid,
renderCell,
} from '../../../components/widgets/datagrid/datagrid';
import {SchemaRegistry} from '../../../components/widgets/datagrid/datagrid_schema';
import {Row, SqlValue} from '../../../trace_processor/query_result';

type RecMgrAttrs = {recMgr: RecordingManager};

Expand Down Expand Up @@ -472,11 +478,90 @@ class TargetDetails implements m.ClassComponent<TargetDetailsAttrs> {
view({attrs}: m.CVnode<TargetDetailsAttrs>) {
return [
this.checksRenderer?.renderTable(),
attrs.target.pollMemoryStats &&
m(DeviceMemoryStatsRenderer, {target: attrs.target}),
m(SessionMgmtRenderer, {recMgr: attrs.recMgr, target: attrs.target}),
];
}
}

const MEMORY_STATS_SCHEMA: SchemaRegistry = {
process: {
process: {title: 'Process', columnType: 'text'},
pid: {title: 'PID', columnType: 'quantitative'},
pss_kb: {
title: 'PSS (KB)',
columnType: 'quantitative',
cellRenderer: (value: SqlValue) => {
if (typeof value === 'number') {
return `${value.toLocaleString()} KB`;
} else {
return renderCell(value);
}
},
},
},
};

type DeviceMemoryStatsAttrs = {target: RecordingTarget};
class DeviceMemoryStatsRenderer
implements m.ClassComponent<DeviceMemoryStatsAttrs>
{
private trash = new DisposableStack();
private rows: Row[] = [];

constructor({attrs}: m.CVnode<DeviceMemoryStatsAttrs>) {
this.trash.use(this.startPolling(attrs.target));
}

private startPolling(target: RecordingTarget): Disposable {
// Kick off an initial poll immediately.
this.poll(target);
const timerId = window.setInterval(() => this.poll(target), 3000);
return {
[Symbol.dispose]() {
window.clearInterval(timerId);
},
};
}

private async poll(target: RecordingTarget) {
const result = await target.pollMemoryStats?.();
if (result !== undefined) {
this.rows = result.map((s) => ({
process: s.processName,
pid: s.pid,
pss_kb: s.pssKb,
}));
}
m.redraw();
}

view() {
return [
m('header', 'Device memory'),
m(DataGrid, {
className: 'pf-device-memory-table',
schema: MEMORY_STATS_SCHEMA,
rootSchema: 'process',
data: this.rows,
initialColumns: [
{id: 'process', field: 'process'},
{id: 'pid', field: 'pid'},
{id: 'pss_kb', field: 'pss_kb', sort: 'DESC'},
],
canAddColumns: false,
canRemoveColumns: false,
enablePivotControls: false,
}),
];
}

onremove() {
this.trash.dispose();
}
}

type SessionMgmtAttrs = {recMgr: RecordingManager; target: RecordingTarget};
class SessionMgmtRenderer implements m.ClassComponent<SessionMgmtAttrs> {
view({attrs}: m.CVnode<SessionMgmtAttrs>) {
Expand Down
4 changes: 4 additions & 0 deletions ui/src/plugins/dev.perfetto.RecordTraceV2/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1124,3 +1124,7 @@
color: var(--pf-color-text);
}
}

.pf-device-memory-table {
height: 500px;
}