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());
+    };
+  });
 }