Move cloning logic to the tracing session object - Rename cloneSession() to snapshot(). - Move to the tracing session object.
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_tracing_session.ts index 5c6b6f1..76dcbb8 100644 --- a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_tracing_session.ts +++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_tracing_session.ts
@@ -24,18 +24,24 @@ tracedSocket = socket; } -export async function createAdbTracingSession( +export function createAdbTracingSession( adbDevice: AdbDevice, traceConfig: protos.ITraceConfig, ): Promise<Result<ConsumerIpcTracingSession>> { + return ConsumerIpcTracingSession.create( + () => openAdbConsumerIpc(adbDevice), + traceConfig, + ); +} + +async function openAdbConsumerIpc( + adbDevice: AdbDevice, +): Promise<Result<TracingProtocol>> { const streamStatus = await adbDevice.createStream( getTracedConsumerSocketAddressForAdb(), ); if (!streamStatus.ok) return streamStatus; - const stream = streamStatus.value; - const consumerIpc = await TracingProtocol.create(stream); - const session = new ConsumerIpcTracingSession(consumerIpc, traceConfig); - return okResult(session); + return okResult(await TracingProtocol.create(streamStatus.value)); } export async function getAdbTracingServiceState( @@ -58,71 +64,6 @@ return okResult(resp.serviceState); } -// Clones an in-progress tracing session identified by `uniqueSessionName` and -// returns the cloned trace as a single concatenated byte array. -// -// Opens a fresh consumer connection rather than reusing an existing one: the -// CloneSession RPC attaches the consumer to the cloned session, so sharing a -// connection would detach any in-progress recording on the original session. -// The connection is closed before this function returns. -export async function cloneAdbTracingSession( - adbDevice: AdbDevice, - uniqueSessionName: string, -): Promise<Result<Uint8Array>> { - // Create a new connection for the clone operation - const streamStatus = await adbDevice.createStream( - getTracedConsumerSocketAddressForAdb(), - ); - if (!streamStatus.ok) return streamStatus; - const stream = streamStatus.value; - const consumerIpc = await TracingProtocol.create(stream); - - try { - // Clone the session by name - const cloneResp = await consumerIpc.invoke( - 'CloneSession', - new protos.CloneSessionRequest({uniqueSessionName}), - ); - - if (!cloneResp.success) { - consumerIpc.close(); - return errResult(cloneResp.error || 'CloneSession failed'); - } - - // Read the cloned trace data - const traceData = await readClonedData(consumerIpc); - consumerIpc.close(); - return okResult(traceData); - } catch (e) { - consumerIpc.close(); - return errResult(`CloneSession error: ${e}`); - } -} - -function readClonedData(consumerIpc: TracingProtocol): Promise<Uint8Array> { - return new Promise((resolve) => { - const chunks: Uint8Array[] = []; - const stream = consumerIpc.invokeStreaming( - 'ReadBuffers', - new protos.ReadBuffersRequest({}), - ); - stream.onTraceData = (data: Uint8Array, hasMore: boolean) => { - chunks.push(data); - if (!hasMore) { - // Concatenate all chunks - const totalLen = chunks.reduce((sum, c) => sum + c.length, 0); - const result = new Uint8Array(totalLen); - let offset = 0; - for (const chunk of chunks) { - result.set(chunk, offset); - offset += chunk.length; - } - resolve(result); - } - }; - }); -} - // Return the fully formed ADB socket address according to the settings // The address is of the form <type>:<address> function getTracedConsumerSocketAddressForAdb() {
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/web_device_proxy/wdp_target.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/web_device_proxy/wdp_target.ts index d2ade5f..39b000d 100644 --- a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/web_device_proxy/wdp_target.ts +++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/web_device_proxy/wdp_target.ts
@@ -19,7 +19,6 @@ import {ConsumerIpcTracingSession} from '../../tracing_protocol/consumer_ipc_tracing_session'; import {checkAndroidTarget} from '../adb_platform_checks'; import { - cloneAdbTracingSession, createAdbTracingSession, getAdbTracingServiceState, } from '../adb_tracing_session'; @@ -152,10 +151,4 @@ if (!adbDeviceStatus.ok) return adbDeviceStatus; return await createAdbTracingSession(adbDeviceStatus.value, traceConfig); } - - async cloneSession(uniqueSessionName: string): Promise<Result<Uint8Array>> { - const adbDeviceStatus = await this.connectIfNeeded(); - if (!adbDeviceStatus.ok) return adbDeviceStatus; - return cloneAdbTracingSession(adbDeviceStatus.value, uniqueSessionName); - } }
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target.ts index 4b33b8a..5abe373 100644 --- a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target.ts +++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target.ts
@@ -19,7 +19,6 @@ import {ConsumerIpcTracingSession} from '../../tracing_protocol/consumer_ipc_tracing_session'; import {checkAndroidTarget} from '../adb_platform_checks'; import { - cloneAdbTracingSession, createAdbTracingSession, getAdbTracingServiceState, } from '../adb_tracing_session'; @@ -93,10 +92,4 @@ if (!adbDeviceStatus.ok) return adbDeviceStatus; return await createAdbTracingSession(adbDeviceStatus.value, traceConfig); } - - async cloneSession(uniqueSessionName: string): Promise<Result<Uint8Array>> { - const adbDeviceStatus = await this.connectIfNeeded(); - if (!adbDeviceStatus.ok) return adbDeviceStatus; - return cloneAdbTracingSession(adbDeviceStatus.value, uniqueSessionName); - } }
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target.ts index 2e19845..e9c5322 100644 --- a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target.ts +++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target.ts
@@ -17,7 +17,6 @@ import {PreflightCheck} from '../../interfaces/connection_check'; import {AdbKeyManager} from './adb_key_manager'; import { - cloneAdbTracingSession, createAdbTracingSession, getAdbTracingServiceState, } from '../adb_tracing_session'; @@ -88,12 +87,6 @@ return await createAdbTracingSession(adbDeviceStatus.value, traceConfig); } - async cloneSession(uniqueSessionName: string): Promise<Result<Uint8Array>> { - const adbDeviceStatus = await this.connectIfNeeded(); - if (!adbDeviceStatus.ok) return adbDeviceStatus; - return cloneAdbTracingSession(adbDeviceStatus.value, uniqueSessionName); - } - disconnect(): void { this.adbDevice.value?.close(); this.adbDevice.reset();
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_tracing_session.ts index 400bf3a..f08deb8 100644 --- a/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_tracing_session.ts +++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_tracing_session.ts
@@ -23,6 +23,7 @@ } from '../interfaces/tracing_session'; import {ChromeExtensionTarget} from './chrome_extension_target'; import {defer, Deferred} from '../../../base/deferred'; +import {errResult, Result} from '../../../base/result'; export class ChromeExtensionTracingSession implements TracingSession { private _state: TracingSessionState = 'RECORDING'; @@ -132,6 +133,10 @@ return this._state; } + async snapshot(): Promise<Result<Uint8Array>> { + return errResult('snapshot() is not supported for chrome tracing sessions'); + } + private setState(newState: TracingSessionState) { this._state = newState; this.onSessionUpdate.notify();
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target.ts index 30dd7b5..458db1e 100644 --- a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target.ts +++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target.ts
@@ -46,8 +46,4 @@ startTracing( traceConfig: protos.ITraceConfig, ): Promise<Result<TracingSession>>; - - // Optional: clone an active tracing session by its unique name and return - // the snapshot data. Creates a new connection for the clone operation. - cloneSession?(uniqueSessionName: string): Promise<Result<Uint8Array>>; }
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/tracing_session.ts index e404608..5310144 100644 --- a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/tracing_session.ts +++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/tracing_session.ts
@@ -13,6 +13,7 @@ // limitations under the License. import {Evt} from '../../../base/events'; +import {Result} from '../../../base/result'; import {RecordingTarget} from './recording_target'; /** @@ -34,6 +35,13 @@ /** Returns the trace file captured once state === 'FINISHED'. */ getTraceData(): Uint8Array | undefined; + + /** + * Take a snapshot of this in-progress session and return its trace bytes. + * The original session keeps recording. Requires that the session was + * started with a non-empty `unique_session_name` in its TraceConfig. + */ + snapshot(): Promise<Result<Uint8Array>>; } export type TracingSessionState =
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_target.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_target.ts index 0299e4b..3b8393d 100644 --- a/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_target.ts +++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_target.ts
@@ -114,65 +114,10 @@ async startTracing( traceConfig: protos.ITraceConfig, ): Promise<Result<ConsumerIpcTracingSession>> { - const ipcStatus = await this.createConsumerIpcChannel(); - if (!ipcStatus.ok) return ipcStatus; - const consumerIpc = ipcStatus.value; - const session = new ConsumerIpcTracingSession(consumerIpc, traceConfig); - return okResult(session); - } - - async cloneSession(uniqueSessionName: string): Promise<Result<Uint8Array>> { - // Create a new connection specifically for the clone operation. - // This is needed because CloneSession attaches the consumer to the clone, - // and we want to keep the original session running on its own connection. - const ipcStatus = await this.createConsumerIpcChannel(); - if (!ipcStatus.ok) return ipcStatus; - const consumerIpc = ipcStatus.value; - - try { - // Clone the session by name - const cloneResp = await consumerIpc.invoke( - 'CloneSession', - new protos.CloneSessionRequest({uniqueSessionName}), - ); - - if (!cloneResp.success) { - consumerIpc.close(); - return errResult(cloneResp.error || 'CloneSession failed'); - } - - // Read the cloned trace data - const traceData = await this.readClonedData(consumerIpc); - consumerIpc.close(); - return okResult(traceData); - } catch (e) { - consumerIpc.close(); - return errResult(`CloneSession error: ${e}`); - } - } - - private readClonedData(consumerIpc: TracingProtocol): Promise<Uint8Array> { - return new Promise((resolve) => { - const chunks: Uint8Array[] = []; - const stream = consumerIpc.invokeStreaming( - 'ReadBuffers', - new protos.ReadBuffersRequest({}), - ); - stream.onTraceData = (data: Uint8Array, hasMore: boolean) => { - chunks.push(data); - if (!hasMore) { - // Concatenate all chunks - const totalLen = chunks.reduce((sum, c) => sum + c.length, 0); - const result = new Uint8Array(totalLen); - let offset = 0; - for (const chunk of chunks) { - result.set(chunk, offset); - offset += chunk.length; - } - resolve(result); - } - }; - }); + return ConsumerIpcTracingSession.create( + () => this.createConsumerIpcChannel(), + traceConfig, + ); } private async createConsumerIpcChannel(): Promise<Result<TracingProtocol>> {
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/consumer_ipc_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/consumer_ipc_tracing_session.ts index 4f2b246..d377146 100644 --- a/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/consumer_ipc_tracing_session.ts +++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/consumer_ipc_tracing_session.ts
@@ -21,6 +21,7 @@ TracingSessionState, } from '../interfaces/tracing_session'; import {TracingProtocol} from './tracing_protocol'; +import {errResult, okResult, Result} from '../../../base/result'; /** * A concrete implementation of {@link TracingSession} over a @@ -28,24 +29,61 @@ * are able to obtain, in a way or another, a byte stream to talk to the traced * consumer socket. */ +// Factory for opening a fresh consumer-side TracingProtocol channel. Used by +// snapshot() since CloneSession requires a separate consumer connection. +export type ConsumerIpcFactory = () => Promise<Result<TracingProtocol>>; + export class ConsumerIpcTracingSession implements TracingSession { private consumerIpc: TracingProtocol; private _state: TracingSessionState = 'RECORDING'; readonly logs = new Array<TracingSessionLogEntry>(); private traceBuf = new ResizableArrayBuffer(64 * 1024); readonly onSessionUpdate = new EvtSource<void>(); + private readonly uniqueSessionName?: string; + private readonly ipcFactory: ConsumerIpcFactory; - constructor(consumerIpc: TracingProtocol, traceConfig: protos.ITraceConfig) { + /** + * Starts a fresh tracing session: opens a consumer connection, sends + * EnableTracing, and drives the lifecycle through to FINISHED. + * + * @param ipcFactory Opens a consumer-side TracingProtocol channel. Called + * once here for the recording itself, and again later by snapshot() if + * invoked, since CloneSession requires a separate consumer connection. + * @param traceConfig The TraceConfig to start tracing with. Its + * `uniqueSessionName` (if any) is captured so a later snapshot() call + * knows which session to snapshot. + */ + static async create( + ipcFactory: ConsumerIpcFactory, + traceConfig: protos.ITraceConfig, + ): Promise<Result<ConsumerIpcTracingSession>> { + const ipcStatus = await ipcFactory(); + if (!ipcStatus.ok) return ipcStatus; + const session = new ConsumerIpcTracingSession( + ipcStatus.value, + ipcFactory, + traceConfig.uniqueSessionName ?? undefined, + ); + session.startLive(traceConfig); + return okResult(session); + } + + private constructor( + consumerIpc: TracingProtocol, + ipcFactory: ConsumerIpcFactory, + uniqueSessionName?: string, + ) { this.consumerIpc = consumerIpc; this.consumerIpc.onClose = this.onProtocolClose.bind(this); - this.start(traceConfig); + this.uniqueSessionName = uniqueSessionName; + this.ipcFactory = ipcFactory; } get state(): TracingSessionState { return this._state; } - private async start(traceConfig: protos.ITraceConfig): Promise<void> { + private async startLive(traceConfig: protos.ITraceConfig): Promise<void> { const req = new protos.EnableTracingRequest({traceConfig}); this.log(`Starting trace, durationMs: ${traceConfig.durationMs}`); const resp = await this.consumerIpc.invoke('EnableTracing', req); @@ -95,7 +133,6 @@ } // There is nothing more to do if we arrive here via cancel() or an error. if (!['STOPPING', 'RECORDING'].includes(this._state)) return; - // We reach this point either: // 1. In state == 'RECORDING', if the durationMs expired and the // EnableTracing request is resolved. @@ -147,4 +184,46 @@ this.setState('ERRORED'); this.consumerIpc.close(); } + + async snapshot(): Promise<Result<Uint8Array>> { + const uniqueSessionName = this.uniqueSessionName; + if (!uniqueSessionName) { + return errResult( + 'snapshot requires a non-empty unique_session_name in the ' + + 'original TraceConfig', + ); + } + const ipcStatus = await this.ipcFactory(); + if (!ipcStatus.ok) return ipcStatus; + const consumerIpc = ipcStatus.value; + try { + const cloneResp = await consumerIpc.invoke( + 'CloneSession', + new protos.CloneSessionRequest({uniqueSessionName}), + ); + if (!cloneResp.success) { + return errResult(cloneResp.error || 'CloneSession failed'); + } + const bytes = await readAllTraceBytes(consumerIpc); + return okResult(bytes); + } catch (e) { + return errResult(`snapshot error: ${e}`); + } finally { + consumerIpc.close(); + } + } +} + +function readAllTraceBytes(consumerIpc: TracingProtocol): Promise<Uint8Array> { + return new Promise((resolve) => { + const buf = new ResizableArrayBuffer(64 * 1024); + const stream = consumerIpc.invokeStreaming( + 'ReadBuffers', + new protos.ReadBuffersRequest({}), + ); + stream.onTraceData = (data: Uint8Array, hasMore: boolean) => { + buf.append(data); + if (!hasMore) resolve(buf.get()); + }; + }); }