• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2022 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
15import {defer, Deferred} from '../../base/deferred';
16import {assertExists, assertTrue} from '../../base/logging';
17import {binaryDecode, binaryEncode} from '../../base/string_utils';
18import {
19  ChromeExtensionMessage,
20  isChromeExtensionError,
21  isChromeExtensionStatus,
22  isGetCategoriesResponse,
23} from '../../controller/chrome_proxy_record_controller';
24import {
25  isDisableTracingResponse,
26  isEnableTracingResponse,
27  isFreeBuffersResponse,
28  isGetTraceStatsResponse,
29  isReadBuffersResponse,
30} from '../../controller/consumer_port_types';
31import {
32  EnableTracingRequest,
33  IBufferStats,
34  ISlice,
35  TraceConfig,
36} from '../protos';
37import {RecordingError} from './recording_error_handling';
38import {
39  TracingSession,
40  TracingSessionListener,
41} from './recording_interfaces_v2';
42import {
43  BUFFER_USAGE_INCORRECT_FORMAT,
44  BUFFER_USAGE_NOT_ACCESSIBLE,
45  EXTENSION_ID,
46  MALFORMED_EXTENSION_MESSAGE,
47} from './recording_utils';
48
49// This class implements the protocol described in
50// https://perfetto.dev/docs/design-docs/api-and-abi#tracing-protocol-abi
51// However, with the Chrome extension we communicate using JSON messages.
52export class ChromeTracedTracingSession implements TracingSession {
53  // Needed for ReadBufferResponse: all the trace packets are split into
54  // several slices. |partialPacket| is the buffer for them. Once we receive a
55  // slice with the flag |lastSliceForPacket|, a new packet is created.
56  private partialPacket: ISlice[] = [];
57
58  // For concurrent calls to 'GetCategories', we return the same value.
59  private pendingGetCategoriesMessage?: Deferred<string[]>;
60
61  private pendingStatsMessages = new Array<Deferred<IBufferStats[]>>();
62
63  // Port through which we communicate with the extension.
64  private chromePort: chrome.runtime.Port;
65  // True when Perfetto is connected via the port to the tracing session.
66  private isPortConnected: boolean;
67
68  constructor(private tracingSessionListener: TracingSessionListener) {
69    this.chromePort = chrome.runtime.connect(EXTENSION_ID);
70    this.isPortConnected = true;
71  }
72
73  start(config: TraceConfig): void {
74    if (!this.isPortConnected) return;
75    const duration = config.durationMs;
76    this.tracingSessionListener.onStatus(`Recording in progress${
77        duration ? ' for ' + duration.toString() + ' ms' : ''}...`);
78
79    const enableTracingRequest = new EnableTracingRequest();
80    enableTracingRequest.traceConfig = config;
81    const enableTracingRequestProto = binaryEncode(
82        EnableTracingRequest.encode(enableTracingRequest).finish());
83    this.chromePort.postMessage(
84        {method: 'EnableTracing', requestData: enableTracingRequestProto});
85  }
86
87  // The 'cancel' method will end the tracing session and will NOT return the
88  // trace. Therefore, we do not need to keep the connection open.
89  cancel(): void {
90    if (!this.isPortConnected) return;
91    this.terminateConnection();
92  }
93
94  // The 'stop' method will end the tracing session and cause the trace to be
95  // returned via a callback. We maintain the connection to the target so we can
96  // extract the trace.
97  // See 'DisableTracing' in:
98  // https://perfetto.dev/docs/design-docs/life-of-a-tracing-session
99  stop(): void {
100    if (!this.isPortConnected) return;
101    this.chromePort.postMessage({method: 'DisableTracing'});
102  }
103
104  getCategories(): Promise<string[]> {
105    if (!this.isPortConnected) {
106      throw new RecordingError(
107          'Attempting to get categories from a ' +
108          'disconnected tracing session.');
109    }
110    if (this.pendingGetCategoriesMessage) {
111      return this.pendingGetCategoriesMessage;
112    }
113
114    this.chromePort.postMessage({method: 'GetCategories'});
115    return this.pendingGetCategoriesMessage = defer<string[]>();
116  }
117
118  async getTraceBufferUsage(): Promise<number> {
119    if (!this.isPortConnected) return 0;
120    const bufferStats = await this.getBufferStats();
121    let percentageUsed = -1;
122    for (const buffer of bufferStats) {
123      const used = assertExists(buffer.bytesWritten);
124      const total = assertExists(buffer.bufferSize);
125      if (total >= 0) {
126        percentageUsed = Math.max(percentageUsed, used / total);
127      }
128    }
129
130    if (percentageUsed === -1) {
131      throw new RecordingError(BUFFER_USAGE_INCORRECT_FORMAT);
132    }
133    return percentageUsed;
134  }
135
136  initConnection(): void {
137    this.chromePort.onMessage.addListener((message: ChromeExtensionMessage) => {
138      this.handleExtensionMessage(message);
139    });
140  }
141
142  private getBufferStats(): Promise<IBufferStats[]> {
143    this.chromePort.postMessage({method: 'GetTraceStats'});
144
145    const statsMessage = defer<IBufferStats[]>();
146    this.pendingStatsMessages.push(statsMessage);
147    return statsMessage;
148  }
149
150  private terminateConnection(): void {
151    this.chromePort.postMessage({method: 'FreeBuffers'});
152    this.clearState();
153  }
154
155  private clearState() {
156    this.chromePort.disconnect();
157    this.isPortConnected = false;
158    for (const statsMessage of this.pendingStatsMessages) {
159      statsMessage.reject(new RecordingError(BUFFER_USAGE_NOT_ACCESSIBLE));
160    }
161    this.pendingStatsMessages = [];
162    this.pendingGetCategoriesMessage = undefined;
163  }
164
165  private handleExtensionMessage(message: ChromeExtensionMessage) {
166    if (isChromeExtensionError(message)) {
167      this.terminateConnection();
168      this.tracingSessionListener.onError(message.error);
169    } else if (isChromeExtensionStatus(message)) {
170      this.tracingSessionListener.onStatus(message.status);
171    } else if (isReadBuffersResponse(message)) {
172      if (!message.slices) {
173        return;
174      }
175      for (const messageSlice of message.slices) {
176        // The extension sends the binary data as a string.
177        // see http://shortn/_oPmO2GT6Vb
178        if (typeof messageSlice.data !== 'string') {
179          throw new RecordingError(MALFORMED_EXTENSION_MESSAGE);
180        }
181        const decodedSlice = {
182          data: binaryDecode(messageSlice.data),
183        };
184        this.partialPacket.push(decodedSlice);
185        if (messageSlice.lastSliceForPacket) {
186          let bufferSize = 0;
187          for (const slice of this.partialPacket) {
188            bufferSize += slice.data!.length;
189          }
190
191          const completeTrace = new Uint8Array(bufferSize);
192          let written = 0;
193          for (const slice of this.partialPacket) {
194            const data = slice.data!;
195            completeTrace.set(data, written);
196            written += data.length;
197          }
198          // The trace already comes encoded as a proto.
199          this.tracingSessionListener.onTraceData(completeTrace);
200          this.terminateConnection();
201        }
202      }
203    } else if (isGetCategoriesResponse(message)) {
204      assertExists(this.pendingGetCategoriesMessage)
205          .resolve(message.categories);
206      this.pendingGetCategoriesMessage = undefined;
207    } else if (isEnableTracingResponse(message)) {
208      // Once the service notifies us that a tracing session is enabled,
209      // we can start streaming the response using 'ReadBuffers'.
210      this.chromePort.postMessage({method: 'ReadBuffers'});
211    } else if (isGetTraceStatsResponse(message)) {
212      const maybePendingStatsMessage = this.pendingStatsMessages.shift();
213      if (maybePendingStatsMessage) {
214        maybePendingStatsMessage.resolve(
215            message?.traceStats?.bufferStats || []);
216      }
217    } else if (isFreeBuffersResponse(message)) {
218      // No action required. If we successfully read a whole trace,
219      // we close the connection. Alternatively, if the tracing finishes
220      // with an exception or if the user cancels it, we also close the
221      // connection.
222    } else {
223      assertTrue(isDisableTracingResponse(message));
224      // No action required. Same reasoning as for FreeBuffers.
225    }
226  }
227}
228