Reduce chance of UI/TP version skew

Bug: 326044799
Change-Id: Ie67aca2e18a321137f91574c92c91b6d9c3d3cd1
diff --git a/include/perfetto/ext/base/version.h b/include/perfetto/ext/base/version.h
index 212424a..ad78372 100644
--- a/include/perfetto/ext/base/version.h
+++ b/include/perfetto/ext/base/version.h
@@ -20,9 +20,27 @@
 namespace perfetto {
 namespace base {
 
-// The returned pointer is a static string is safe to pass around.
+// The returned pointer is a static string and safe to pass around.
+// Returns a human readable string currently of the approximate form:
+// Perfetto v42.1-deadbeef0 (deadbeef03c641e4b4ea9cf38e9b5696670175a9)
+// However you should not depend on the format of this string.
+// It maybe not be possible to determine the version. In which case the
+// string will be of the approximate form:
+// Perfetto v0.0 (unknown)
 const char* GetVersionString();
 
+// The returned pointer is a static string and safe to pass around.
+// Returns the short code used to identity the version:
+// v42.1-deadbeef0
+// It maybe not be possible to determine the version. In which case
+// this returns nullptr.
+// This can be compared with equality to other
+// version codes to detect matched builds (for example to see if
+// trace_processor_shell and the UI were built at the same revision)
+// but you should not attempt to parse it as the format may change
+// without warning.
+const char* GetVersionCode();
+
 }  // namespace base
 }  // namespace perfetto
 
diff --git a/protos/perfetto/trace_processor/trace_processor.proto b/protos/perfetto/trace_processor/trace_processor.proto
index c96caa0..1e579ca 100644
--- a/protos/perfetto/trace_processor/trace_processor.proto
+++ b/protos/perfetto/trace_processor/trace_processor.proto
@@ -38,9 +38,18 @@
 
 enum TraceProcessorApiVersion {
   // This variable has been introduced in v15 and is used to deal with API
-  // mismatches between UI and trace_processor_shell --httpd. Increment this
-  // every time a new feature that the UI depends on is being introduced (e.g.
-  // new tables, new SQL operators, metrics that are required by the UI).
+  // mismatches between UI and trace_processor_shell --httpd.
+  //
+  // Prior to API version 11 this was incremented every time a new
+  // feature that the UI depended on was introduced (e.g. new tables,
+  // new SQL operators, metrics that are required by the UI, etc).
+  // This:
+  // a. Tended to be forgotten
+  // b. Still led to issues when the TP dropped *backwards*
+  //    compatibility of a feature (since we checked TP >= UI
+  //    TRACE_PROCESSOR_CURRENT_API_VERSION).
+  // Now the UI attempts to redirect the user to the matched version
+  // of the UI if one exists.
   // See also StatusResult.api_version (below).
   // Changes:
   // 7. Introduce GUESS_CPU_SIZE
@@ -48,10 +57,12 @@
   // 9. Add get_thread_state_summary_for_interval.
   // 10. Add 'slice_is_ancestor' to stdlib.
   // 11. Removal of experimental module from stdlib.
-  TRACE_PROCESSOR_CURRENT_API_VERSION = 11;
+  // 12. Changed UI to be more aggresive about version matching.
+  //     Added version_code.
+  TRACE_PROCESSOR_CURRENT_API_VERSION = 12;
 }
 
-// At lowest level, the wire-format of the RPC procol is a linear sequence of
+// At lowest level, the wire-format of the RPC protocol is a linear sequence of
 // TraceProcessorRpc messages on each side of the byte pipe
 // Each message is prefixed by a tag (field = 1, type = length delimited) and a
 // varint encoding its size (this is so the whole stream can also be read /
@@ -236,6 +247,15 @@
   // The API version is incremented every time a change that the UI depends
   // on is introduced (e.g. adding a new table that the UI queries).
   optional int32 api_version = 3;
+
+  // Typically something like "v42.1-deadbeef0", but could be just
+  // "v42", "v0.0", or unset for binaries built from Bazel or other
+  // build configurations. This can be compared with equality to other
+  // version codes to detect matched builds (for example to see if
+  // trace_processor_shell and the UI were built at the same revision)
+  // but you should not attempt to parse it as the format may change
+  // without warning.
+  optional string version_code = 4;
 }
 
 // Input for the /compute_metric endpoint.
diff --git a/python/perfetto/trace_processor/trace_processor.descriptor b/python/perfetto/trace_processor/trace_processor.descriptor
index 261ab0b..ce0bc66 100644
--- a/python/perfetto/trace_processor/trace_processor.descriptor
+++ b/python/perfetto/trace_processor/trace_processor.descriptor
Binary files differ
diff --git a/src/base/version.cc b/src/base/version.cc
index 18569d3..e989534 100644
--- a/src/base/version.cc
+++ b/src/base/version.cc
@@ -23,18 +23,26 @@
 #if PERFETTO_BUILDFLAG(PERFETTO_VERSION_GEN)
 #include "perfetto_version.gen.h"
 #else
-#define PERFETTO_VERSION_STRING() "v0.0"
+#define PERFETTO_VERSION_STRING() nullptr
 #define PERFETTO_VERSION_SCM_REVISION() "unknown"
 #endif
 
 namespace perfetto {
 namespace base {
 
+const char* GetVersionCode() {
+  return PERFETTO_VERSION_STRING();
+}
+
 const char* GetVersionString() {
   static const char* version_str = [] {
     static constexpr size_t kMaxLen = 256;
+    const char* version_code = PERFETTO_VERSION_STRING();
+    if (version_code == nullptr) {
+      version_code = "v0.0";
+    }
     char* version = new char[kMaxLen + 1];
-    snprintf(version, kMaxLen, "Perfetto %s (%s)", PERFETTO_VERSION_STRING(),
+    snprintf(version, kMaxLen, "Perfetto %s (%s)", version_code,
              PERFETTO_VERSION_SCM_REVISION());
     return version;
   }();
diff --git a/src/trace_processor/rpc/rpc.cc b/src/trace_processor/rpc/rpc.cc
index 68e0732..9ca121e 100644
--- a/src/trace_processor/rpc/rpc.cc
+++ b/src/trace_processor/rpc/rpc.cc
@@ -491,6 +491,10 @@
   protozero::HeapBuffered<protos::pbzero::StatusResult> status;
   status->set_loaded_trace_name(trace_processor_->GetCurrentTraceName());
   status->set_human_readable_version(base::GetVersionString());
+  const char* version_code = base::GetVersionCode();
+  if (version_code) {
+    status->set_version_code(version_code);
+  }
   status->set_api_version(protos::pbzero::TRACE_PROCESSOR_CURRENT_API_VERSION);
   return status.SerializeAsArray();
 }
diff --git a/ui/src/assets/modal.scss b/ui/src/assets/modal.scss
index f7ed990..25e7706 100644
--- a/ui/src/assets/modal.scss
+++ b/ui/src/assets/modal.scss
@@ -127,7 +127,7 @@
     background-color: #e6e6e6;
     color: rgba(0, 0, 0, 0.8);
     border: 2px solid transparent;
-    border-radius: 4px;
+    border-radius: $pf-border-radius;
     cursor: pointer;
     text-transform: none;
     overflow: visible;
diff --git a/ui/src/frontend/router.ts b/ui/src/frontend/router.ts
index 29028b0..c6c316b 100644
--- a/ui/src/frontend/router.ts
+++ b/ui/src/frontend/router.ts
@@ -308,4 +308,40 @@
       throw new Error('History rewriting livelock');
     }
   }
+
+  static getUrlForVersion(versionCode: string): string {
+    const url = `${window.location.origin}/${versionCode}/`;
+    return url;
+  }
+
+  static async isVersionAvailable(versionCode: string):
+        Promise<string|undefined> {
+    if (versionCode === '') {
+      return undefined;
+    }
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 1000);
+    const url = Router.getUrlForVersion(versionCode);
+    let r;
+    try {
+      r = await fetch(url, {signal: controller.signal});
+    } catch (e) {
+      console.error(`No UI version for ${versionCode} at ${url}. This is an error if ${versionCode} is a released Perfetto version`);
+      return undefined;
+    } finally {
+      clearTimeout(timeoutId);
+    }
+    if (!r.ok) {
+      return undefined;
+    }
+    return url;
+  }
+
+  static navigateToVersion(versionCode: string): void {
+    const url = Router.getUrlForVersion(versionCode);
+    if (url === undefined) {
+      throw new Error(`No URL known for UI version ${versionCode}.`);
+    }
+    window.location.replace(url);
+  }
 }
diff --git a/ui/src/frontend/rpc_http_dialog.ts b/ui/src/frontend/rpc_http_dialog.ts
index f29c846..97c186e 100644
--- a/ui/src/frontend/rpc_http_dialog.ts
+++ b/ui/src/frontend/rpc_http_dialog.ts
@@ -20,6 +20,7 @@
 import {StatusResult, TraceProcessorApiVersion} from '../protos';
 import {HttpRpcEngine} from '../trace_processor/http_rpc_engine';
 import {showModal} from '../widgets/modal';
+import {Router} from './router';
 
 import {globals} from './globals';
 import {publishHttpRpcState} from './publish';
@@ -27,9 +28,9 @@
 const CURRENT_API_VERSION =
     TraceProcessorApiVersion.TRACE_PROCESSOR_CURRENT_API_VERSION;
 
-const PROMPT = () => `Trace Processor Native Accelerator detected on ` +
-`${HttpRpcEngine.hostAndPort} with:
-$loadedTraceName
+function getPromptMessage(tpStatus: StatusResult): string {
+  return `Trace Processor Native Accelerator detected on ${HttpRpcEngine.hostAndPort} with:
+${tpStatus.loadedTraceName}
 
 YES, use loaded trace:
 Will load from the current state of Trace Processor. If you did run
@@ -46,13 +47,11 @@
 Using the native accelerator has some minor caveats:
 - Only one tab can be using the accelerator.
 - Sharing, downloading and conversion-to-legacy aren't supported.
-- You may encounter UI errors if the Trace Processor version you are using is
-too old. Get the latest version from get.perfetto.dev/trace_processor.
 `;
+}
 
-
-const MSG_TOO_OLD = () => `The Trace Processor instance on ` +
-`${HttpRpcEngine.hostAndPort} is too old.
+function getIncompatibleRpcMessage(tpStatus: StatusResult): string {
+  return `The Trace Processor instance on ${HttpRpcEngine.hostAndPort} is too old.
 
 This UI requires TraceProcessor features that are not present in the
 Trace Processor native accelerator you are currently running.
@@ -64,14 +63,88 @@
 chmod +x ./trace_processor
 ./trace_processor --httpd
 
-UI version: ${VERSION}
-TraceProcessor RPC API required: ${CURRENT_API_VERSION} or higher
+UI version code: ${VERSION}
+UI RPC API: ${CURRENT_API_VERSION}
 
-TraceProcessor version: $tpVersion
-RPC API: $tpApi
+Trace processor version: ${tpStatus.humanReadableVersion}
+Trace processor version code: ${tpStatus.versionCode}
+Trace processor RPC API: ${tpStatus.apiVersion}
 `;
+}
 
-let forceUseOldVersion = false;
+function getVersionMismatchMessage(tpStatus: StatusResult): string {
+  return `The trace processor instance on ${HttpRpcEngine.hostAndPort} is a different build from the UI.
+
+This may cause problems. Where possible it is better to use the matched version of the UI.
+You can do this by clicking the button below.
+
+UI version code: ${VERSION}
+UI RPC API: ${CURRENT_API_VERSION}
+
+Trace processor version: ${tpStatus.humanReadableVersion}
+Trace processor version code: ${tpStatus.versionCode}
+Trace processor RPC API: ${tpStatus.apiVersion}
+`;
+}
+
+// The flow is fairly complicated:
+// +-----------------------------------+
+// |        User loads the UI          |
+// +-----------------+-----------------+
+//                   |
+// +-----------------+-----------------+
+// |   Is trace_processor present at   |
+// |   HttpRpcEngine.hostAndPort?      |
+// +--------------------------+--------+
+//    |No                     |Yes
+//    |        +--------------+-------------------------------+
+//    |        |  Does version code of UI and TP match?       |
+//    |        +--------------+----------------------------+--+
+//    |                       |No                          |Yes
+//    |                       |                            |
+//    |                       |                            |
+//    |         +-------------+-------------+              |
+//    |         |Is a build of the UI at the|              |
+//    |         |TP version code existant   |              |
+//    |         |and reachable?             |              |
+//    |         +---+----------------+------+              |
+//    |             | No             | Yes                 |
+//    |             |                |                     |
+//    |             |       +--------+-------+             |
+//    |             |       |Dialog: Mismatch|             |
+//    |             |       |Load matched UI +-------------------------------+
+//    |             |       |Continue        +-+           |                 |
+//    |             |       +----------------+ |           |                 |
+//    |             |                          |           |                 |
+//    |      +------+--------------------------+----+      |                 |
+//    |      |TP RPC version >= UI RPC version      |      |                 |
+//    |      +----+-------------------+-------------+      |                 |
+//    |           | No                |Yes                 |                 |
+//    |      +----+--------------+    |                    |                 |
+//    |      |Dialog: Bad RPC    |    |                    |                 |
+//    |  +---+Use built-in WASM  |    |                    |                 |
+//    |  |   |Continue anyway    +----|                    |                 |
+//    |  |   +-------------------+    |        +-----------+-----------+     |
+//    |  |                            +--------+TP has preloaded trace?|     |
+//    |  |                                     +-+---------------+-----+     |
+//    |  |                                       |No             |Yes        |
+//    |  |                                       |  +---------------------+  |
+//    |  |                                       |  | Dialog: Preloaded?  |  |
+//    |  |                                       +--+ YES, use loaded trace  |
+//    |  |                                 +--------| YES, but reset state|  |
+//    |  |  +---------------------------------------| NO, Use builtin Wasm|  |
+//    |  |  |                              |     |  +---------------------+  |
+//    |  |  |                              |     |                           |
+//    |  |  |                           Reset TP |                           |
+//    |  |  |                              |     |                           |
+//    |  |  |                              |     |                           |
+//  Show the UI                         Show the UI                  Link to
+//  (WASM mode)                         (RPC mode)                   matched UI
+
+// There are three options in the end:
+// - Show the UI (WASM mode)
+// - Show the UI (RPC mode)
+// - Redirect to a matched version of the UI
 
 // Try to connect to the external Trace Processor HTTP RPC accelerator (if
 // available, often it isn't). If connected it will populate the
@@ -83,73 +156,184 @@
 export async function CheckHttpRpcConnection(): Promise<void> {
   const state = await HttpRpcEngine.checkConnection();
   publishHttpRpcState(state);
-  if (!state.connected) return;
+  if (!state.connected) {
+    // No RPC = exit immediately to the WASM UI.
+    return;
+  }
   const tpStatus = assertExists(state.status);
 
-  if (tpStatus.apiVersion < CURRENT_API_VERSION) {
-    await showDialogTraceProcessorTooOld(tpStatus);
-    if (!forceUseOldVersion) return;
+  function forceWasm() {
+    globals.dispatch(Actions.setNewEngineMode({mode: 'FORCE_BUILTIN_WASM'}));
   }
 
+  // Check short version:
+  if (tpStatus.versionCode !== '' && tpStatus.versionCode !== VERSION) {
+    const url = await Router.isVersionAvailable(tpStatus.versionCode);
+    if (url !== undefined) {
+      // If matched UI available show a dialog asking the user to
+      // switch.
+      const result = await showDialogVersionMismatch(tpStatus, url);
+      switch (result) {
+      case MismatchedVersionDialog.Dismissed:
+      case MismatchedVersionDialog.UseMatchingUi:
+        Router.navigateToVersion(tpStatus.versionCode);
+        return;
+      case MismatchedVersionDialog.UseMismatchedRpc:
+        break;
+      case MismatchedVersionDialog.UseWasm:
+        forceWasm();
+        return;
+      default:
+        const x: never = result;
+        throw new Error(`Unsupported result ${x}`);
+      }
+    }
+  }
+
+  // Check the RPC version:
+  if (tpStatus.apiVersion < CURRENT_API_VERSION) {
+    const result = await showDialogIncompatibleRPC(tpStatus);
+    switch (result) {
+    case IncompatibleRpcDialogResult.Dismissed:
+    case IncompatibleRpcDialogResult.UseWasm:
+      forceWasm();
+      return;
+    case IncompatibleRpcDialogResult.UseIncompatibleRpc:
+      break;
+    default:
+      const x: never = result;
+      throw new Error(`Unsupported result ${x}`);
+    }
+  }
+
+  // Check if pre-loaded:
   if (tpStatus.loadedTraceName) {
     // If a trace is already loaded in the trace processor (e.g., the user
     // launched trace_processor_shell -D trace_file.pftrace), prompt the user to
     // initialize the UI with the already-loaded trace.
-    return showDialogToUsePreloadedTrace(tpStatus);
+    const result = await showDialogToUsePreloadedTrace(tpStatus);
+    switch (result) {
+    case PreloadedDialogResult.Dismissed:
+    case PreloadedDialogResult.UseRpcWithPreloadedTrace:
+      globals.dispatch(Actions.openTraceFromHttpRpc({}));
+      return;
+    case PreloadedDialogResult.UseRpc:
+      // Resetting state is the default.
+      return;
+    case PreloadedDialogResult.UseWasm:
+      forceWasm();
+      return;
+    default:
+      const x: never = result;
+      throw new Error(`Unsupported result ${x}`);
+    }
   }
 }
 
-async function showDialogTraceProcessorTooOld(tpStatus: StatusResult) {
-  return showModal({
-    title: 'Your Trace Processor binary is outdated',
-    content:
-        m('.modal-pre',
-          MSG_TOO_OLD().replace('$tpVersion', tpStatus.humanReadableVersion)
-            .replace('$tpApi', `${tpStatus.apiVersion}`)),
+enum MismatchedVersionDialog {
+  UseMatchingUi = 'useMatchingUi',
+  UseWasm = 'useWasm',
+  UseMismatchedRpc = 'useMismatchedRpc',
+  Dismissed = 'dismissed',
+}
+
+async function showDialogVersionMismatch(tpStatus: StatusResult,
+  url: string):
+            Promise<MismatchedVersionDialog> {
+  let result = MismatchedVersionDialog.Dismissed;
+  await showModal({
+    title: 'Version mismatch',
+    content: m('.modal-pre', getVersionMismatchMessage(tpStatus)),
+    buttons: [
+      {
+        primary: true,
+        text: `Open ${url}`,
+        action: () => {
+          result = MismatchedVersionDialog.UseMatchingUi;
+        },
+      },
+      {
+        text: 'Use builtin Wasm',
+        action: () => {
+          result = MismatchedVersionDialog.UseWasm;
+        },
+      },
+      {
+        text: 'Use mismatched version regardless (might crash)',
+        action: () => {
+          result = MismatchedVersionDialog.UseMismatchedRpc;
+        },
+      },
+    ],
+  });
+  return result;
+}
+
+enum IncompatibleRpcDialogResult {
+  UseWasm = 'useWasm',
+  UseIncompatibleRpc = 'useIncompatibleRpc',
+  Dismissed = 'dismissed',
+}
+
+async function showDialogIncompatibleRPC(tpStatus: StatusResult):
+      Promise<IncompatibleRpcDialogResult> {
+  let result = IncompatibleRpcDialogResult.Dismissed;
+  await showModal({
+    title: 'Incompatible RPC version',
+    content: m('.modal-pre', getIncompatibleRpcMessage(tpStatus)),
     buttons: [
       {
         text: 'Use builtin Wasm',
         primary: true,
         action: () => {
-          globals.dispatch(
-            Actions.setNewEngineMode({mode: 'FORCE_BUILTIN_WASM'}));
+          result = IncompatibleRpcDialogResult.UseWasm;
         },
       },
       {
-        text: 'Use old version regardless (might crash)',
-        primary: false,
+        text: 'Use old version regardless (will crash)',
         action: () => {
-          forceUseOldVersion = true;
+          result = IncompatibleRpcDialogResult.UseIncompatibleRpc;
         },
       },
     ],
   });
+  return result;
 }
 
-async function showDialogToUsePreloadedTrace(tpStatus: StatusResult) {
-  return showModal({
-    title: 'Use Trace Processor Native Acceleration?',
-    content:
-        m('.modal-pre',
-          PROMPT().replace('$loadedTraceName', tpStatus.loadedTraceName)),
+enum PreloadedDialogResult {
+  UseRpcWithPreloadedTrace = 'useRpcWithPreloadedTrace',
+  UseRpc = 'useRpc',
+  UseWasm = 'useWasm',
+  Dismissed = 'dismissed',
+}
+
+async function showDialogToUsePreloadedTrace(tpStatus: StatusResult):
+    Promise<PreloadedDialogResult> {
+  let result = PreloadedDialogResult.Dismissed;
+  await showModal({
+    title: 'Use trace processor native acceleration?',
+    content: m('.modal-pre', getPromptMessage(tpStatus)),
     buttons: [
       {
         text: 'YES, use loaded trace',
         primary: true,
         action: () => {
-          globals.dispatch(Actions.openTraceFromHttpRpc({}));
+          result = PreloadedDialogResult.UseRpcWithPreloadedTrace;
         },
       },
       {
         text: 'YES, but reset state',
+        action: () => {
+          result = PreloadedDialogResult.UseRpc;
+        },
       },
       {
-        text: 'NO, Use builtin Wasm',
+        text: 'NO, Use builtin WASM',
         action: () => {
-          globals.dispatch(
-            Actions.setNewEngineMode({mode: 'FORCE_BUILTIN_WASM'}));
+          result = PreloadedDialogResult.UseWasm;
         },
       },
     ],
   });
+  return result;
 }
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
index cbd5c9d..eb596c2 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
@@ -110,7 +110,7 @@
               sum(stall_backend_mem) as total_stall_backend_mem,
               sum(l3_cache_miss) as total_l3_cache_miss
             FROM target_thread_ipc_slice WHERE ts IS NOT NULL`,
-          'target thread ipc statistic'
+          'target thread ipc statistic',
         );
       },
     });