blob: 461a6e494edcd4601c96b45e0a86a619bb160f2a [file]
// Copyright (C) 2026 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 protos from '../../../protos';
import type {AdbDevice} from '../../dev.perfetto.RecordTraceV2/adb/adb_device';
import {createAdbTracingSession} from '../../dev.perfetto.RecordTraceV2/adb/adb_tracing_session';
import type {TracingSession} from '../../dev.perfetto.RecordTraceV2/interfaces/tracing_session';
import {TracedWebsocketTarget} from '../../dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_target';
const DUMP_INTERVAL_MS = 10_000;
const PROC_STATS_BUFFER_SIZE_KB = 4 * 1024;
const HEAPPROFD_BUFFER_SIZE_KB = 128 * 1024;
const JAVA_HPROF_BUFFER_SIZE_KB = 256 * 1024;
const STATS_POLL_INTERVAL_MS = 3000;
export type ProfileState = 'recording' | 'stopping' | 'finished' | 'error';
export class ProfileSession {
readonly pid: number;
readonly processName: string;
readonly startX: number;
private inner?: TracingSession;
private intervalHandle?: ReturnType<typeof setInterval>;
private _state: ProfileState = 'recording';
private _error?: string;
private _bufferUsagePct?: number;
private constructor(pid: number, processName: string, startX: number) {
this.pid = pid;
this.processName = processName;
this.startX = startX;
}
static async start(
targetOrDevice: TracedWebsocketTarget | AdbDevice,
pid: number,
processName: string,
startX: number,
): Promise<ProfileSession> {
const self = new ProfileSession(pid, processName, startX);
const config = buildProcessProfileConfig(pid);
const result =
targetOrDevice instanceof TracedWebsocketTarget
? await targetOrDevice.startTracing(config)
: await createAdbTracingSession(targetOrDevice, config);
if (!result.ok) {
self._state = 'error';
self._error = `Failed to start profile: ${result.error}`;
return self;
}
self.inner = result.value;
self.intervalHandle = setInterval(async () => {
self._bufferUsagePct = await self.inner!.getBufferUsagePct();
}, STATS_POLL_INTERVAL_MS);
self.inner.onSessionUpdate.addListener(() => {
const s = self.inner!.state;
if (s === 'FINISHED') {
self._state = 'finished';
} else if (s === 'ERRORED') {
self._state = 'error';
self._error = self
.inner!.logs.filter((l) => l.isError)
.map((l) => l.message)
.join('; ');
}
});
return self;
}
get state(): ProfileState {
return this._state;
}
get error(): string | undefined {
return this._error;
}
get bufferUsagePct(): number | undefined {
return this._bufferUsagePct;
}
/** Stops recording and waits for the trace data to be ready. */
async stop(): Promise<void> {
if (this._state !== 'recording' || this.inner === undefined) return;
clearInterval(this.intervalHandle);
this._state = 'stopping';
await this.inner.stop();
if (this.inner.state !== 'FINISHED') {
await new Promise<void>((resolve) => {
const sub = this.inner!.onSessionUpdate.addListener(() => {
const s = this.inner!.state;
if (s === 'FINISHED' || s === 'ERRORED') {
sub[Symbol.dispose]();
resolve();
}
});
});
}
this._state = this.inner.state === 'FINISHED' ? 'finished' : 'error';
}
/** Cancels recording and discards trace data. */
async cancel(): Promise<void> {
if (this._state !== 'recording' || this.inner === undefined) return;
clearInterval(this.intervalHandle);
this._state = 'error';
await this.inner.cancel();
}
/** Returns the trace buffer once state is 'finished'. */
getTraceData(): ArrayBuffer | undefined {
return this.inner?.getTraceData();
}
}
function buildProcessProfileConfig(pid: number): protos.ITraceConfig {
return {
compressionType:
protos.TraceConfig.CompressionType.COMPRESSION_TYPE_DEFLATE,
buffers: [
{
name: 'process_stats',
sizeKb: PROC_STATS_BUFFER_SIZE_KB,
fillPolicy: protos.TraceConfig.BufferConfig.FillPolicy.DISCARD,
},
{
name: 'heapprofd',
sizeKb: HEAPPROFD_BUFFER_SIZE_KB,
fillPolicy: protos.TraceConfig.BufferConfig.FillPolicy.RING_BUFFER,
},
{
name: 'java_hprof',
sizeKb: JAVA_HPROF_BUFFER_SIZE_KB,
fillPolicy: protos.TraceConfig.BufferConfig.FillPolicy.RING_BUFFER,
},
],
dataSources: [
{
config: {
name: 'linux.process_stats',
targetBufferName: 'process_stats',
processStatsConfig: {
scanAllProcessesOnStart: true, // Necessary for track names.
},
},
},
{
config: {
name: 'android.heapprofd',
targetBufferName: 'heapprofd',
heapprofdConfig: {
pid: [pid],
samplingIntervalBytes: 32 * 1024, // Slightly larger than default to reduce overhead.
shmemSizeBytes: 16 * 1024 * 1024,
blockClient: true, // Important for trace integrity.
continuousDumpConfig: {
dumpIntervalMs: DUMP_INTERVAL_MS, // Important for getting regular heap snapshots to see how memory usage evolves over time.
},
},
},
},
{
config: {
name: 'android.java_hprof',
targetBufferName: 'java_hprof',
javaHprofConfig: {
pid: [pid],
continuousDumpConfig: {
dumpIntervalMs: DUMP_INTERVAL_MS, // Required for Java profiles.
},
},
},
},
],
};
}