xref: /aosp_15_r20/external/perfetto/ui/src/frontend/rpc_http_dialog.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2018 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 m from 'mithril';
16import protos from '../protos';
17import {assertExists} from '../base/logging';
18import {VERSION} from '../gen/perfetto_version';
19import {HttpRpcEngine} from '../trace_processor/http_rpc_engine';
20import {showModal} from '../widgets/modal';
21import {AppImpl} from '../core/app_impl';
22
23const CURRENT_API_VERSION =
24  protos.TraceProcessorApiVersion.TRACE_PROCESSOR_CURRENT_API_VERSION;
25
26function getPromptMessage(tpStatus: protos.StatusResult): string {
27  return `Trace Processor detected on ${HttpRpcEngine.hostAndPort} with:
28${tpStatus.loadedTraceName}
29
30YES, use loaded trace:
31Will load from the current state of Trace Processor. If you did run
32trace_processor_shell --httpd file.pftrace this is likely what you want.
33
34YES, but reset state:
35Use this if you want to open another trace but still use the
36accelerator. This is the equivalent of killing and restarting
37trace_processor_shell --httpd.
38
39NO, Use builtin WASM:
40Will not use the accelerator in this tab.
41
42Using the native accelerator has some minor caveats:
43- Only one tab can be using the accelerator.
44- Sharing, downloading and conversion-to-legacy aren't supported.
45`;
46}
47
48function getIncompatibleRpcMessage(tpStatus: protos.StatusResult): string {
49  return `The Trace Processor instance on ${HttpRpcEngine.hostAndPort} is too old.
50
51This UI requires TraceProcessor features that are not present in the
52Trace Processor native accelerator you are currently running.
53If you continue, this is almost surely going to cause UI failures.
54
55Please update your local Trace Processor binary:
56
57curl -LO https://get.perfetto.dev/trace_processor
58chmod +x ./trace_processor
59./trace_processor --httpd
60
61UI version code: ${VERSION}
62UI RPC API: ${CURRENT_API_VERSION}
63
64Trace processor version: ${tpStatus.humanReadableVersion}
65Trace processor version code: ${tpStatus.versionCode}
66Trace processor RPC API: ${tpStatus.apiVersion}
67`;
68}
69
70function getVersionMismatchMessage(tpStatus: protos.StatusResult): string {
71  return `The Trace Processor instance on ${HttpRpcEngine.hostAndPort} is a different build from the UI.
72
73This may cause problems. Where possible it is better to use the matched version of the UI.
74You can do this by clicking the button below.
75
76UI version code: ${VERSION}
77UI RPC API: ${CURRENT_API_VERSION}
78
79Trace processor version: ${tpStatus.humanReadableVersion}
80Trace processor version code: ${tpStatus.versionCode}
81Trace processor RPC API: ${tpStatus.apiVersion}
82`;
83}
84
85// The flow is fairly complicated:
86// +-----------------------------------+
87// |        User loads the UI          |
88// +-----------------+-----------------+
89//                   |
90// +-----------------+-----------------+
91// |   Is trace_processor present at   |
92// |   HttpRpcEngine.hostAndPort?      |
93// +--------------------------+--------+
94//    |No                     |Yes
95//    |        +--------------+-------------------------------+
96//    |        |  Does version code of UI and TP match?       |
97//    |        +--------------+----------------------------+--+
98//    |                       |No                          |Yes
99//    |                       |                            |
100//    |                       |                            |
101//    |         +-------------+-------------+              |
102//    |         |Is a build of the UI at the|              |
103//    |         |TP version code existant   |              |
104//    |         |and reachable?             |              |
105//    |         +---+----------------+------+              |
106//    |             | No             | Yes                 |
107//    |             |                |                     |
108//    |             |       +--------+-------+             |
109//    |             |       |Dialog: Mismatch|             |
110//    |             |       |Load matched UI +-------------------------------+
111//    |             |       |Continue        +-+           |                 |
112//    |             |       +----------------+ |           |                 |
113//    |             |                          |           |                 |
114//    |      +------+--------------------------+----+      |                 |
115//    |      |TP RPC version >= UI RPC version      |      |                 |
116//    |      +----+-------------------+-------------+      |                 |
117//    |           | No                |Yes                 |                 |
118//    |      +----+--------------+    |                    |                 |
119//    |      |Dialog: Bad RPC    |    |                    |                 |
120//    |  +---+Use built-in WASM  |    |                    |                 |
121//    |  |   |Continue anyway    +----|                    |                 |
122//    |  |   +-------------------+    |        +-----------+-----------+     |
123//    |  |                            +--------+TP has preloaded trace?|     |
124//    |  |                                     +-+---------------+-----+     |
125//    |  |                                       |No             |Yes        |
126//    |  |                                       |  +---------------------+  |
127//    |  |                                       |  | Dialog: Preloaded?  |  |
128//    |  |                                       |  + YES, use loaded trace  |
129//    |  |                                 +--------| YES, but reset state|  |
130//    |  |  +---------------------------------------| NO, Use builtin Wasm|  |
131//    |  |  |                              |     |  +---------------------+  |
132//    |  |  |                              |     |                           |
133//    |  |  |                           Reset TP |                           |
134//    |  |  |                              |     |                           |
135//    |  |  |                              |     |                           |
136//  Show the UI                         Show the UI                  Link to
137//  (WASM mode)                         (RPC mode)                   matched UI
138
139// There are three options in the end:
140// - Show the UI (WASM mode)
141// - Show the UI (RPC mode)
142// - Redirect to a matched version of the UI
143
144// Try to connect to the external Trace Processor HTTP RPC accelerator (if
145// available, often it isn't). If connected it will populate the
146// |httpRpcState| in the frontend local state. In turn that will show the UI
147// chip in the sidebar. trace_controller.ts will repeat this check before
148// trying to load a new trace. We do this ahead of time just to have a
149// consistent UX (i.e. so that the user can tell if the RPC is working without
150// having to open a trace).
151export async function CheckHttpRpcConnection(): Promise<void> {
152  const state = await HttpRpcEngine.checkConnection();
153  AppImpl.instance.httpRpc.httpRpcAvailable = state.connected;
154  if (!state.connected) {
155    // No RPC = exit immediately to the WASM UI.
156    return;
157  }
158  const tpStatus = assertExists(state.status);
159
160  function forceWasm() {
161    AppImpl.instance.httpRpc.newEngineMode = 'FORCE_BUILTIN_WASM';
162  }
163
164  // Check short version:
165  if (tpStatus.versionCode !== '' && tpStatus.versionCode !== VERSION) {
166    const url = await isVersionAvailable(tpStatus.versionCode);
167    if (url !== undefined) {
168      // If matched UI available show a dialog asking the user to
169      // switch.
170      const result = await showDialogVersionMismatch(tpStatus, url);
171      switch (result) {
172        case MismatchedVersionDialog.Dismissed:
173        case MismatchedVersionDialog.UseMatchingUi:
174          navigateToVersion(tpStatus.versionCode);
175          return;
176        case MismatchedVersionDialog.UseMismatchedRpc:
177          break;
178        case MismatchedVersionDialog.UseWasm:
179          forceWasm();
180          return;
181        default:
182          const x: never = result;
183          throw new Error(`Unsupported result ${x}`);
184      }
185    }
186  }
187
188  // Check the RPC version:
189  if (tpStatus.apiVersion < CURRENT_API_VERSION) {
190    const result = await showDialogIncompatibleRPC(tpStatus);
191    switch (result) {
192      case IncompatibleRpcDialogResult.Dismissed:
193      case IncompatibleRpcDialogResult.UseWasm:
194        forceWasm();
195        return;
196      case IncompatibleRpcDialogResult.UseIncompatibleRpc:
197        break;
198      default:
199        const x: never = result;
200        throw new Error(`Unsupported result ${x}`);
201    }
202  }
203
204  // Check if pre-loaded:
205  if (tpStatus.loadedTraceName) {
206    // If a trace is already loaded in the trace processor (e.g., the user
207    // launched trace_processor_shell -D trace_file.pftrace), prompt the user to
208    // initialize the UI with the already-loaded trace.
209    const result = await showDialogToUsePreloadedTrace(tpStatus);
210    switch (result) {
211      case PreloadedDialogResult.Dismissed:
212      case PreloadedDialogResult.UseRpcWithPreloadedTrace:
213        AppImpl.instance.openTraceFromHttpRpc();
214        return;
215      case PreloadedDialogResult.UseRpc:
216        // Resetting state is the default.
217        return;
218      case PreloadedDialogResult.UseWasm:
219        forceWasm();
220        return;
221      default:
222        const x: never = result;
223        throw new Error(`Unsupported result ${x}`);
224    }
225  }
226}
227
228enum MismatchedVersionDialog {
229  UseMatchingUi = 'useMatchingUi',
230  UseWasm = 'useWasm',
231  UseMismatchedRpc = 'useMismatchedRpc',
232  Dismissed = 'dismissed',
233}
234
235async function showDialogVersionMismatch(
236  tpStatus: protos.StatusResult,
237  url: string,
238): Promise<MismatchedVersionDialog> {
239  let result = MismatchedVersionDialog.Dismissed;
240  await showModal({
241    title: 'Version mismatch',
242    content: m('.modal-pre', getVersionMismatchMessage(tpStatus)),
243    buttons: [
244      {
245        primary: true,
246        text: `Open ${url}`,
247        action: () => {
248          result = MismatchedVersionDialog.UseMatchingUi;
249        },
250      },
251      {
252        text: 'Use builtin Wasm',
253        action: () => {
254          result = MismatchedVersionDialog.UseWasm;
255        },
256      },
257      {
258        text: 'Use mismatched version regardless (might crash)',
259        action: () => {
260          result = MismatchedVersionDialog.UseMismatchedRpc;
261        },
262      },
263    ],
264  });
265  return result;
266}
267
268enum IncompatibleRpcDialogResult {
269  UseWasm = 'useWasm',
270  UseIncompatibleRpc = 'useIncompatibleRpc',
271  Dismissed = 'dismissed',
272}
273
274async function showDialogIncompatibleRPC(
275  tpStatus: protos.StatusResult,
276): Promise<IncompatibleRpcDialogResult> {
277  let result = IncompatibleRpcDialogResult.Dismissed;
278  await showModal({
279    title: 'Incompatible RPC version',
280    content: m('.modal-pre', getIncompatibleRpcMessage(tpStatus)),
281    buttons: [
282      {
283        text: 'Use builtin Wasm',
284        primary: true,
285        action: () => {
286          result = IncompatibleRpcDialogResult.UseWasm;
287        },
288      },
289      {
290        text: 'Use old version regardless (will crash)',
291        action: () => {
292          result = IncompatibleRpcDialogResult.UseIncompatibleRpc;
293        },
294      },
295    ],
296  });
297  return result;
298}
299
300enum PreloadedDialogResult {
301  UseRpcWithPreloadedTrace = 'useRpcWithPreloadedTrace',
302  UseRpc = 'useRpc',
303  UseWasm = 'useWasm',
304  Dismissed = 'dismissed',
305}
306
307async function showDialogToUsePreloadedTrace(
308  tpStatus: protos.StatusResult,
309): Promise<PreloadedDialogResult> {
310  let result = PreloadedDialogResult.Dismissed;
311  await showModal({
312    title: 'Use trace processor native acceleration?',
313    content: m('.modal-pre', getPromptMessage(tpStatus)),
314    buttons: [
315      {
316        text: 'YES, use loaded trace',
317        primary: true,
318        action: () => {
319          result = PreloadedDialogResult.UseRpcWithPreloadedTrace;
320        },
321      },
322      {
323        text: 'YES, but reset state',
324        action: () => {
325          result = PreloadedDialogResult.UseRpc;
326        },
327      },
328      {
329        text: 'NO, Use builtin WASM',
330        action: () => {
331          result = PreloadedDialogResult.UseWasm;
332        },
333      },
334    ],
335  });
336  return result;
337}
338
339function getUrlForVersion(versionCode: string): string {
340  const url = `${window.location.origin}/${versionCode}/`;
341  return url;
342}
343
344async function isVersionAvailable(
345  versionCode: string,
346): Promise<string | undefined> {
347  if (versionCode === '') {
348    return undefined;
349  }
350  const controller = new AbortController();
351  const timeoutId = setTimeout(() => controller.abort(), 1000);
352  const url = getUrlForVersion(versionCode);
353  let r;
354  try {
355    r = await fetch(url, {signal: controller.signal});
356  } catch (e) {
357    console.error(
358      `No UI version for ${versionCode} at ${url}. ` +
359        `This is an error if ${versionCode} is a released Perfetto version`,
360    );
361    return undefined;
362  } finally {
363    clearTimeout(timeoutId);
364  }
365  if (!r.ok) {
366    return undefined;
367  }
368  return url;
369}
370
371function navigateToVersion(versionCode: string): void {
372  const url = getUrlForVersion(versionCode);
373  if (url === undefined) {
374    throw new Error(`No URL known for UI version ${versionCode}.`);
375  }
376  window.location.replace(url);
377}
378