xref: /aosp_15_r20/external/perfetto/ui/src/trace_processor/http_rpc_engine.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2019 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 protos from '../protos';
16import {fetchWithTimeout} from '../base/http_utils';
17import {assertExists} from '../base/logging';
18import {EngineBase} from '../trace_processor/engine';
19
20const RPC_CONNECT_TIMEOUT_MS = 2000;
21
22export interface HttpRpcState {
23  connected: boolean;
24  status?: protos.StatusResult;
25  failure?: string;
26}
27
28export class HttpRpcEngine extends EngineBase {
29  readonly mode = 'HTTP_RPC';
30  readonly id: string;
31  private requestQueue = new Array<Uint8Array>();
32  private websocket?: WebSocket;
33  private connected = false;
34  private disposed = false;
35
36  // Can be changed by frontend/index.ts when passing ?rpc_port=1234 .
37  static rpcPort = '9001';
38
39  constructor(id: string) {
40    super();
41    this.id = id;
42  }
43
44  rpcSendRequestBytes(data: Uint8Array): void {
45    if (this.websocket === undefined) {
46      if (this.disposed) return;
47      const wsUrl = `ws://${HttpRpcEngine.hostAndPort}/websocket`;
48      this.websocket = new WebSocket(wsUrl);
49      this.websocket.onopen = () => this.onWebsocketConnected();
50      this.websocket.onmessage = (e) => this.onWebsocketMessage(e);
51      this.websocket.onclose = (e) => this.onWebsocketClosed(e);
52      this.websocket.onerror = (e) =>
53        super.fail(
54          `WebSocket error rs=${(e.target as WebSocket)?.readyState} (ERR:ws)`,
55        );
56    }
57
58    if (this.connected) {
59      this.websocket.send(data);
60    } else {
61      this.requestQueue.push(data); // onWebsocketConnected() will flush this.
62    }
63  }
64
65  private onWebsocketConnected() {
66    for (;;) {
67      const queuedMsg = this.requestQueue.shift();
68      if (queuedMsg === undefined) break;
69      assertExists(this.websocket).send(queuedMsg);
70    }
71    this.connected = true;
72  }
73
74  private onWebsocketClosed(e: CloseEvent) {
75    if (this.disposed) return;
76    if (e.code === 1006 && this.connected) {
77      // On macbooks the act of closing the lid / suspending often causes socket
78      // disconnections. Try to gracefully re-connect.
79      console.log('Websocket closed, reconnecting');
80      this.websocket = undefined;
81      this.connected = false;
82      this.rpcSendRequestBytes(new Uint8Array()); // Triggers a reconnection.
83    } else {
84      super.fail(`Websocket closed (${e.code}: ${e.reason}) (ERR:ws)`);
85    }
86  }
87
88  private onWebsocketMessage(e: MessageEvent) {
89    assertExists(e.data as Blob)
90      .arrayBuffer()
91      .then((buf) => {
92        super.onRpcResponseBytes(new Uint8Array(buf));
93      });
94  }
95
96  static async checkConnection(): Promise<HttpRpcState> {
97    const RPC_URL = `http://${HttpRpcEngine.hostAndPort}/`;
98    const httpRpcState: HttpRpcState = {connected: false};
99    console.info(
100      `It's safe to ignore the ERR_CONNECTION_REFUSED on ${RPC_URL} below. ` +
101        `That might happen while probing the external native accelerator. The ` +
102        `error is non-fatal and unlikely to be the culprit for any UI bug.`,
103    );
104    try {
105      const resp = await fetchWithTimeout(
106        RPC_URL + 'status',
107        {method: 'post', cache: 'no-cache'},
108        RPC_CONNECT_TIMEOUT_MS,
109      );
110      if (resp.status !== 200) {
111        httpRpcState.failure = `${resp.status} - ${resp.statusText}`;
112      } else {
113        const buf = new Uint8Array(await resp.arrayBuffer());
114        httpRpcState.connected = true;
115        httpRpcState.status = protos.StatusResult.decode(buf);
116      }
117    } catch (err) {
118      httpRpcState.failure = `${err}`;
119    }
120    return httpRpcState;
121  }
122
123  static get hostAndPort() {
124    return `127.0.0.1:${HttpRpcEngine.rpcPort}`;
125  }
126
127  [Symbol.dispose]() {
128    this.disposed = true;
129    const websocket = this.websocket;
130    this.websocket = undefined;
131    websocket?.close();
132  }
133}
134