Skip to content

Commit 085ebb7

Browse files
committed
Add profiling support to memscope
1 parent 212831c commit 085ebb7

10 files changed

Lines changed: 969 additions & 16 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (C) 2026 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+
.pf-progress {
16+
display: grid;
17+
grid-template-columns: auto 1fr auto;
18+
align-items: center;
19+
gap: 10px;
20+
font-size: var(--pf-font-size-s);
21+
color: var(--pf-color-text-muted);
22+
}
23+
24+
.pf-progress__label {
25+
font-weight: 600;
26+
}
27+
28+
.pf-progress__track {
29+
height: 6px;
30+
border-radius: 3px;
31+
background: var(--pf-color-background);
32+
overflow: hidden;
33+
}
34+
35+
.pf-progress__fill {
36+
height: 100%;
37+
background: var(--pf-color-primary);
38+
transition: width 0.3s ease;
39+
}
40+
41+
.pf-progress__value {
42+
font-variant-numeric: tabular-nums;
43+
font-weight: 600;
44+
color: var(--pf-color-text);
45+
white-space: nowrap;
46+
}
47+
48+
.pf-progress__suffix {
49+
color: var(--pf-color-text-muted);
50+
font-weight: 400;
51+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (C) 2026 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 m from 'mithril';
16+
import {clamp} from '../../../base/math_utils';
17+
18+
export interface ProgressBarAttrs {
19+
// Percentage 0..100. Values outside the range are clamped.
20+
readonly pct: number;
21+
// Optional label shown to the left of the track.
22+
readonly label?: m.Children;
23+
// Optional value shown to the right. If omitted, "<pct>%" is shown.
24+
readonly value?: m.Children;
25+
// Optional muted suffix appended after the value (e.g. " / 640 MB").
26+
readonly suffix?: m.Children;
27+
}
28+
29+
export class ProgressBar implements m.ClassComponent<ProgressBarAttrs> {
30+
view({attrs}: m.Vnode<ProgressBarAttrs>) {
31+
const clamped = clamp(attrs.pct, 0, 100);
32+
const value = attrs.value ?? `${clamped.toFixed(1)}%`;
33+
return m(
34+
'.pf-progress',
35+
attrs.label !== undefined && m('.pf-progress__label', attrs.label),
36+
m(
37+
'.pf-progress__track',
38+
m('.pf-progress__fill', {style: {width: `${clamped}%`}}),
39+
),
40+
m(
41+
'.pf-progress__value',
42+
value,
43+
attrs.suffix !== undefined &&
44+
m('span.pf-progress__suffix', attrs.suffix),
45+
),
46+
);
47+
}
48+
}

ui/src/plugins/dev.perfetto.Memscope/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export default class implements PerfettoPlugin {
5252
} else {
5353
return m(ConnectionPage, {
5454
onConnected: (result) => {
55-
session = new LiveSession(result);
55+
session = new LiveSession(app, result);
5656
session.onSnapshot(() => m.redraw());
5757
},
5858
});

ui/src/plugins/dev.perfetto.Memscope/sessions/live_session.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,18 @@ import {AdbDevice} from '../../dev.perfetto.RecordTraceV2/adb/adb_device';
2121
import {createAdbTracingSession} from '../../dev.perfetto.RecordTraceV2/adb/adb_tracing_session';
2222
import {TracingSession} from '../../dev.perfetto.RecordTraceV2/interfaces/tracing_session';
2323
import {TracedWebsocketTarget} from '../../dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_target';
24+
import {App} from '../../../public/app';
2425
import {ConnectionResult} from '../views/connection';
26+
import {ProfileSession, type ProfileState} from './profile_session';
27+
28+
export interface ProfileView {
29+
readonly pid: number;
30+
readonly processName: string;
31+
readonly state: ProfileState;
32+
readonly bufferUsagePct?: number;
33+
/** Wall-clock timestamp (ms) at which profiling started. */
34+
readonly startMs: number;
35+
}
2536

2637
const SNAPSHOT_INTERVAL_MS = 3_000; // How over to take a snapshot of the runnign trace and extract data.
2738
const INITIAL_SNAPSHOT_INTERVAL_MS = 1_000; // Use a shorter interval for the first snapshot to get data on screen faster.
@@ -83,6 +94,7 @@ export type OnSnapshotCallback = (data: SnapshotData) => void;
8394
* notifies registered callbacks.
8495
*/
8596
export class LiveSession {
97+
private readonly app: App;
8698
private session?: TracingSession;
8799
private engine?: WasmEngineProxy;
88100
private readonly sessionName: string;
@@ -113,7 +125,23 @@ export class LiveSession {
113125
// True when the last snapshot took longer than the configured interval.
114126
snapshotOverrun = false;
115127

116-
constructor(conn: ConnectionResult) {
128+
// Active process profile (if any). Undefined when no profile is running.
129+
private profileImpl?: {session: ProfileSession; startMs: number};
130+
131+
get profile(): ProfileView | undefined {
132+
const p = this.profileImpl;
133+
if (p === undefined) return undefined;
134+
return {
135+
pid: p.session.pid,
136+
processName: p.session.processName,
137+
state: p.session.state,
138+
bufferUsagePct: p.session.bufferUsagePct,
139+
startMs: p.startMs,
140+
};
141+
}
142+
143+
constructor(app: App, conn: ConnectionResult) {
144+
this.app = app;
117145
this.device = conn.device;
118146
this.linuxTarget = conn.linuxTarget;
119147
this.deviceName = conn.deviceName;
@@ -151,6 +179,43 @@ export class LiveSession {
151179
}
152180
}
153181

182+
/** Starts a heap profiling session for a single process. */
183+
async startProfile(pid: number, processName: string): Promise<void> {
184+
if (this.profileImpl) {
185+
await this.profileImpl.session.cancel();
186+
}
187+
const session = await ProfileSession.start(
188+
this.linuxTarget ?? this.device!,
189+
pid,
190+
processName,
191+
this.data?.xMax ?? 0,
192+
);
193+
this.profileImpl = {session, startMs: Date.now()};
194+
}
195+
196+
/** Stops the active profile and opens the trace in the main UI. */
197+
async stopAndOpenProfile(): Promise<void> {
198+
const profile = this.profileImpl?.session;
199+
if (!profile) return;
200+
const processName = profile.processName;
201+
const pid = profile.pid;
202+
await profile.stop();
203+
const traceData = profile.getTraceData();
204+
this.profileImpl = undefined;
205+
if (traceData) {
206+
const fileName = `heap-${processName}-${pid}.perfetto-trace`;
207+
const buffer = traceData.buffer as ArrayBuffer;
208+
this.app.openTraceFromBuffer({buffer, title: fileName, fileName});
209+
}
210+
}
211+
212+
/** Cancels the active profile and discards data. */
213+
async cancelProfile(): Promise<void> {
214+
if (!this.profileImpl) return;
215+
await this.profileImpl.session.cancel();
216+
this.profileImpl = undefined;
217+
}
218+
154219
/** Stops the tracing session, polling, and disposes of the engine. */
155220
async dispose(): Promise<void> {
156221
// Mark disposed first so any in-flight poll() bails out before touching
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// Copyright (C) 2026 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 protos from '../../../protos';
16+
import {AdbDevice} from '../../dev.perfetto.RecordTraceV2/adb/adb_device';
17+
import {createAdbTracingSession} from '../../dev.perfetto.RecordTraceV2/adb/adb_tracing_session';
18+
import {TracingSession} from '../../dev.perfetto.RecordTraceV2/interfaces/tracing_session';
19+
import {TracedWebsocketTarget} from '../../dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_target';
20+
21+
const DUMP_INTERVAL_MS = 10_000;
22+
const PROC_STATS_BUFFER_SIZE_KB = 4 * 1024;
23+
const HEAPPROFD_BUFFER_SIZE_KB = 128 * 1024;
24+
const JAVA_HPROF_BUFFER_SIZE_KB = 256 * 1024;
25+
const STATS_POLL_INTERVAL_MS = 3000;
26+
27+
export type ProfileState = 'recording' | 'stopping' | 'finished' | 'error';
28+
29+
export class ProfileSession {
30+
readonly pid: number;
31+
readonly processName: string;
32+
readonly startX: number;
33+
34+
private inner?: TracingSession;
35+
private intervalHandle?: ReturnType<typeof setInterval>;
36+
private _state: ProfileState = 'recording';
37+
private _error?: string;
38+
private _bufferUsagePct?: number;
39+
40+
private constructor(pid: number, processName: string, startX: number) {
41+
this.pid = pid;
42+
this.processName = processName;
43+
this.startX = startX;
44+
}
45+
46+
static async start(
47+
targetOrDevice: TracedWebsocketTarget | AdbDevice,
48+
pid: number,
49+
processName: string,
50+
startX: number,
51+
): Promise<ProfileSession> {
52+
const self = new ProfileSession(pid, processName, startX);
53+
const config = buildProcessProfileConfig(pid);
54+
const result =
55+
targetOrDevice instanceof TracedWebsocketTarget
56+
? await targetOrDevice.startTracing(config)
57+
: await createAdbTracingSession(targetOrDevice, config);
58+
if (!result.ok) {
59+
self._state = 'error';
60+
self._error = `Failed to start profile: ${result.error}`;
61+
return self;
62+
}
63+
self.inner = result.value;
64+
self.intervalHandle = setInterval(async () => {
65+
self._bufferUsagePct = await self.inner!.getBufferUsagePct();
66+
}, STATS_POLL_INTERVAL_MS);
67+
self.inner.onSessionUpdate.addListener(() => {
68+
const s = self.inner!.state;
69+
if (s === 'FINISHED') {
70+
self._state = 'finished';
71+
} else if (s === 'ERRORED') {
72+
self._state = 'error';
73+
self._error = self
74+
.inner!.logs.filter((l) => l.isError)
75+
.map((l) => l.message)
76+
.join('; ');
77+
}
78+
});
79+
return self;
80+
}
81+
82+
get state(): ProfileState {
83+
return this._state;
84+
}
85+
86+
get error(): string | undefined {
87+
return this._error;
88+
}
89+
90+
get bufferUsagePct(): number | undefined {
91+
return this._bufferUsagePct;
92+
}
93+
94+
/** Stops recording and waits for the trace data to be ready. */
95+
async stop(): Promise<void> {
96+
if (this._state !== 'recording' || this.inner === undefined) return;
97+
clearInterval(this.intervalHandle);
98+
this._state = 'stopping';
99+
await this.inner.stop();
100+
if (this.inner.state !== 'FINISHED') {
101+
await new Promise<void>((resolve) => {
102+
const sub = this.inner!.onSessionUpdate.addListener(() => {
103+
const s = this.inner!.state;
104+
if (s === 'FINISHED' || s === 'ERRORED') {
105+
sub[Symbol.dispose]();
106+
resolve();
107+
}
108+
});
109+
});
110+
}
111+
this._state = this.inner.state === 'FINISHED' ? 'finished' : 'error';
112+
}
113+
114+
/** Cancels recording and discards trace data. */
115+
async cancel(): Promise<void> {
116+
if (this._state !== 'recording' || this.inner === undefined) return;
117+
clearInterval(this.intervalHandle);
118+
this._state = 'error';
119+
await this.inner.cancel();
120+
}
121+
122+
/** Returns the trace buffer once state is 'finished'. */
123+
getTraceData(): Uint8Array | undefined {
124+
return this.inner?.getTraceData();
125+
}
126+
}
127+
128+
function buildProcessProfileConfig(pid: number): protos.ITraceConfig {
129+
return {
130+
compressionType:
131+
protos.TraceConfig.CompressionType.COMPRESSION_TYPE_DEFLATE,
132+
buffers: [
133+
{
134+
name: 'process_stats',
135+
sizeKb: PROC_STATS_BUFFER_SIZE_KB,
136+
fillPolicy: protos.TraceConfig.BufferConfig.FillPolicy.DISCARD,
137+
},
138+
{
139+
name: 'heapprofd',
140+
sizeKb: HEAPPROFD_BUFFER_SIZE_KB,
141+
fillPolicy: protos.TraceConfig.BufferConfig.FillPolicy.RING_BUFFER,
142+
},
143+
{
144+
name: 'java_hprof',
145+
sizeKb: JAVA_HPROF_BUFFER_SIZE_KB,
146+
fillPolicy: protos.TraceConfig.BufferConfig.FillPolicy.RING_BUFFER,
147+
},
148+
],
149+
dataSources: [
150+
{
151+
config: {
152+
name: 'linux.process_stats',
153+
targetBufferName: 'process_stats',
154+
processStatsConfig: {
155+
scanAllProcessesOnStart: true, // Necessary for track names.
156+
},
157+
},
158+
},
159+
{
160+
config: {
161+
name: 'android.heapprofd',
162+
targetBufferName: 'heapprofd',
163+
heapprofdConfig: {
164+
pid: [pid],
165+
samplingIntervalBytes: 32 * 1024, // Slightly larger than default to reduce overhead.
166+
shmemSizeBytes: 16 * 1024 * 1024, // Arbitrary, could use default.
167+
blockClient: true, // Important for trace integrity.
168+
continuousDumpConfig: {
169+
dumpIntervalMs: DUMP_INTERVAL_MS, // Important for getting regular heap snapshots to see how memory usage evolves over time.
170+
},
171+
},
172+
},
173+
},
174+
{
175+
config: {
176+
name: 'android.java_hprof',
177+
targetBufferName: 'java_hprof',
178+
javaHprofConfig: {
179+
pid: [pid],
180+
continuousDumpConfig: {
181+
dumpIntervalMs: DUMP_INTERVAL_MS, // Required for Java profiles.
182+
},
183+
},
184+
},
185+
},
186+
],
187+
};
188+
}

0 commit comments

Comments
 (0)