TraceProcessor RPC: Add API version to handle version mismatch

This CL adds a notion of API version to the TraceProcessor
RPC protocol. This is to deal with cases where we introduce
a new TP feature (a new table, a new operator) that the UI
depends on.
Today the UI breaks in unexpected ways when the user is using
a version of trace_processor_shell --http that is too old.
This change makes it more explicit, telling the user what to do.

Preview: https://imgur.com/a/Jvap5K2
Bug: 159142289
Change-Id: Ic2d68f94c7eb7bd5b95eabd842dae71fe61222d4
diff --git a/CHANGELOG b/CHANGELOG
index d1bba4c..a7aa8fd 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -7,7 +7,10 @@
   Trace Processor:
     *
   UI:
-    *
+    * Added warning dialog when trying to use a trace_processor_shell --httpd
+      which is too old.
+    * Added warning dialog when trying to use a trace_processor_shell --httpd
+      RPC instance from more than one tab.
   SDK:
     *
 
diff --git a/protos/perfetto/trace_processor/trace_processor.proto b/protos/perfetto/trace_processor/trace_processor.proto
index b47bd3f..fbff9d7 100644
--- a/protos/perfetto/trace_processor/trace_processor.proto
+++ b/protos/perfetto/trace_processor/trace_processor.proto
@@ -35,6 +35,15 @@
 //    In this case these messages are used to {,un}marshall HTTP requests and
 //    response made through src/trace_processor/rpc/httpd.cc .
 
+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).
+  // See also TraceProcessorVersion (below).
+  TRACE_PROCESSOR_CURRENT_API_VERSION = 1;
+}
+
 // At lowest level, the wire-format of the RPC procol 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
@@ -70,6 +79,7 @@
     TPM_RESTORE_INITIAL_TABLES = 7;
     TPM_ENABLE_METATRACE = 8;
     TPM_DISABLE_AND_READ_METATRACE = 9;
+    TPM_GET_STATUS = 10;
   }
 
   oneof type {
@@ -113,6 +123,8 @@
     DescriptorSet metric_descriptors = 206;
     // For TPM_DISABLE_AND_READ_METATRACE.
     DisableAndReadMetatraceResult metatrace = 209;
+    // For TPM_GET_STATUS.
+    StatusResult status = 210;
   }
 }
 
@@ -230,6 +242,15 @@
   // when using the HTTP+RPC mode nad passing a trace file to the shell, via
   // trace_processor_shell -D trace_file.pftrace .
   optional string loaded_trace_name = 1;
+
+  // Typically something like "v11.0.123", but could be just "v11" or "unknown",
+  // for binaries built from Bazel or other build configurations. This is for
+  // human presentation only, don't attempt to parse and reason on it.
+  optional string human_readable_version = 2;
+
+  // 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;
 }
 
 // Input for the /compute_metric endpoint.
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index f2358ac..23625b3 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -356,6 +356,7 @@
       ":lib",
       "../../gn:default_deps",
       "../../gn:protobuf_full",
+      "../../protos/perfetto/trace_processor:zero",
       "../../src/profiling:deobfuscator",
       "../../src/profiling/symbolizer",
       "../../src/profiling/symbolizer:symbolize_database",
diff --git a/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor b/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor
index 476c0b9..e3e9b24 100644
--- a/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor
+++ b/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor
Binary files differ
diff --git a/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor.sha1 b/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor.sha1
index 1b19c08..6e01bd0 100644
--- a/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor.sha1
+++ b/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor.sha1
@@ -2,5 +2,5 @@
 // SHA1(tools/gen_binary_descriptors)
 // 9fc6d77de57ec76a80b76aa282f4c7cf5ce55eec
 // SHA1(protos/perfetto/trace_processor/trace_processor.proto)
-// f0ba693bdd1111c81e9240c966e16babd489bb03
+// b4135d9bc2551939bfdaa4d03423cf34a8fbcffb
   
\ No newline at end of file
diff --git a/src/trace_processor/rpc/httpd.cc b/src/trace_processor/rpc/httpd.cc
index da690fc..2cdf80f 100644
--- a/src/trace_processor/rpc/httpd.cc
+++ b/src/trace_processor/rpc/httpd.cc
@@ -401,12 +401,9 @@
   }
 
   if (req.uri == "/status") {
-    protozero::HeapBuffered<protos::pbzero::StatusResult> res;
-    res->set_loaded_trace_name(
-        trace_processor_rpc_.GetCurrentTraceName().c_str());
-    std::vector<uint8_t> buf = res.SerializeAsArray();
-    return HttpReply(client->sock.get(), "200 OK", headers, buf.data(),
-                     buf.size());
+    auto status = trace_processor_rpc_.GetStatus();
+    return HttpReply(client->sock.get(), "200 OK", headers, status.data(),
+                     status.size());
   }
 
   if (req.uri == "/compute_metric") {
diff --git a/src/trace_processor/rpc/rpc.cc b/src/trace_processor/rpc/rpc.cc
index 17df0be..a58cd7c 100644
--- a/src/trace_processor/rpc/rpc.cc
+++ b/src/trace_processor/rpc/rpc.cc
@@ -23,6 +23,7 @@
 #include "perfetto/base/logging.h"
 #include "perfetto/base/time.h"
 #include "perfetto/ext/base/utils.h"
+#include "perfetto/ext/base/version.h"
 #include "perfetto/protozero/scattered_heap_buffer.h"
 #include "perfetto/protozero/scattered_stream_writer.h"
 #include "perfetto/trace_processor/trace_processor.h"
@@ -241,6 +242,13 @@
       resp.Send(rpc_response_fn_);
       break;
     }
+    case RpcProto::TPM_GET_STATUS: {
+      Response resp(tx_seq_id_++, req_type);
+      std::vector<uint8_t> status = GetStatus();
+      resp->set_status()->AppendRawProtoBytes(status.data(), status.size());
+      resp.Send(rpc_response_fn_);
+      break;
+    }
     default: {
       // This can legitimately happen if the client is newer. We reply with a
       // generic "unkown request" response, so the client can do feature
@@ -453,10 +461,6 @@
   PERFETTO_DLOG("[RPC] RawQuery > %d rows (err: %d)", rows, !status.ok());
 }
 
-std::string Rpc::GetCurrentTraceName() {
-  return trace_processor_->GetCurrentTraceName();
-}
-
 void Rpc::RestoreInitialTables() {
   trace_processor_->RestoreInitialTables();
 }
@@ -534,5 +538,13 @@
   }
 }
 
+std::vector<uint8_t> Rpc::GetStatus() {
+  protozero::HeapBuffered<protos::pbzero::StatusResult> status;
+  status->set_loaded_trace_name(trace_processor_->GetCurrentTraceName());
+  status->set_human_readable_version(base::GetVersionString());
+  status->set_api_version(protos::pbzero::TRACE_PROCESSOR_CURRENT_API_VERSION);
+  return status.SerializeAsArray();
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/rpc/rpc.h b/src/trace_processor/rpc/rpc.h
index 9006154..8501b56 100644
--- a/src/trace_processor/rpc/rpc.h
+++ b/src/trace_processor/rpc/rpc.h
@@ -106,6 +106,7 @@
   std::vector<uint8_t> ComputeMetric(const uint8_t* data, size_t len);
   void EnableMetatrace();
   std::vector<uint8_t> DisableAndReadMetatrace();
+  std::vector<uint8_t> GetStatus();
 
   // Creates a new RPC session by deleting all tables and views that have been
   // created (by the UI or user) after the trace was loaded; built-in
diff --git a/src/trace_processor/trace_processor_shell.cc b/src/trace_processor/trace_processor_shell.cc
index 9db56c4..6925948 100644
--- a/src/trace_processor/trace_processor_shell.cc
+++ b/src/trace_processor/trace_processor_shell.cc
@@ -45,6 +45,8 @@
 #include "src/trace_processor/util/proto_to_json.h"
 #include "src/trace_processor/util/status_macros.h"
 
+#include "protos/perfetto/trace_processor/trace_processor.pbzero.h"
+
 #if PERFETTO_BUILDFLAG(PERFETTO_TP_HTTPD)
 #include "src/trace_processor/rpc/httpd.h"
 #endif
@@ -756,6 +758,8 @@
 
     if (option == 'v') {
       printf("%s\n", base::GetVersionString());
+      printf("Trace Processor RPC API version: %d\n",
+             protos::pbzero::TRACE_PROCESSOR_CURRENT_API_VERSION);
       exit(0);
     }
 
diff --git a/ui/src/common/http_rpc_engine.ts b/ui/src/common/http_rpc_engine.ts
index 498d086..d404ea9 100644
--- a/ui/src/common/http_rpc_engine.ts
+++ b/ui/src/common/http_rpc_engine.ts
@@ -23,7 +23,7 @@
 
 export interface HttpRpcState {
   connected: boolean;
-  loadedTraceName?: string;
+  status?: StatusResult;
   failure?: string;
 }
 
@@ -98,11 +98,8 @@
         httpRpcState.failure = `${resp.status} - ${resp.statusText}`;
       } else {
         const buf = new Uint8Array(await resp.arrayBuffer());
-        const status = StatusResult.decode(buf);
         httpRpcState.connected = true;
-        if (status.loadedTraceName) {
-          httpRpcState.loadedTraceName = status.loadedTraceName;
-        }
+        httpRpcState.status = StatusResult.decode(buf);
       }
     } catch (err) {
       httpRpcState.failure = `${err}`;
diff --git a/ui/src/frontend/rpc_http_dialog.ts b/ui/src/frontend/rpc_http_dialog.ts
index 40e9036..77ee0fe 100644
--- a/ui/src/frontend/rpc_http_dialog.ts
+++ b/ui/src/frontend/rpc_http_dialog.ts
@@ -13,14 +13,22 @@
 // limitations under the License.
 
 import '../tracks/all_frontend';
+
 import * as m from 'mithril';
 
+import {assertExists} from '../base/logging';
 import {Actions} from '../common/actions';
 import {HttpRpcEngine, RPC_URL} from '../common/http_rpc_engine';
+import {StatusResult} from '../common/protos';
+import * as version from '../gen/perfetto_version';
+import {perfetto} from '../gen/protos';
 
 import {globals} from './globals';
 import {showModal} from './modal';
 
+const CURRENT_API_VERSION = perfetto.protos.TraceProcessorApiVersion
+                                .TRACE_PROCESSOR_CURRENT_API_VERSION;
+
 const PROMPT = `Trace Processor Native Accelerator detected on ${RPC_URL} with:
 $loadedTraceName
 
@@ -43,6 +51,28 @@
 too old. Get the latest version from get.perfetto.dev/trace_processor.
 `;
 
+
+const MSG_TOO_OLD = `The Trace Processor instance on ${RPC_URL} is too old.
+
+This UI requires TraceProcessor features that are not present in the
+Trace Processor native accelerator you are currently running.
+If you continue, this is almost surely going to cause UI failures.
+
+Please update your local Trace Processor binary:
+
+curl -LO https://get.perfetto.dev/trace_processor
+chmod +x ./trace_processor
+./trace_processor --httpd
+
+UI version: ${version.VERSION}
+TraceProcessor RPC API required: ${CURRENT_API_VERSION} or higher
+
+TraceProcessor version: $tpVersion
+RPC API: $tpApi
+`;
+
+let forceUseOldVersion = false;
+
 // Try to connect to the external Trace Processor HTTP RPC accelerator (if
 // available, often it isn't). If connected it will populate the
 // |httpRpcState| in the frontend local state. In turn that will show the UI
@@ -53,41 +83,81 @@
 export async function CheckHttpRpcConnection(): Promise<void> {
   const state = await HttpRpcEngine.checkConnection();
   globals.frontendLocalState.setHttpRpcState(state);
+  if (!state.connected) return;
+  const tpStatus = assertExists(state.status);
 
-  // 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.
-  if (state.connected && state.loadedTraceName) {
-    showModal({
-      title: 'Use Trace Processor Native Acceleration?',
-      content:
-          m('.modal-pre',
-            PROMPT.replace('$loadedTraceName', state.loadedTraceName)),
-      buttons: [
-        {
-          text: 'YES, use loaded trace',
-          primary: true,
-          id: 'rpc_load',
-          action: () => {
-            globals.dispatch(Actions.openTraceFromHttpRpc({}));
-          }
-        },
-        {
-          text: 'YES, but reset state',
-          primary: false,
-          id: 'rpc_reset',
-          action: () => {}
-        },
-        {
-          text: 'NO, Use builtin WASM',
-          primary: false,
-          id: 'rpc_force_wasm',
-          action: () => {
-            globals.dispatch(
-                Actions.setNewEngineMode({mode: 'FORCE_BUILTIN_WASM'}));
-          }
-        },
-      ],
-    });
+  if (tpStatus.apiVersion < CURRENT_API_VERSION) {
+    await showDialogTraceProcessorTooOld(tpStatus);
+    if (!forceUseOldVersion) return;
   }
+
+  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);
+  }
+}
+
+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}`)),
+    buttons: [
+      {
+        text: 'Use builtin Wasm',
+        primary: true,
+        id: 'tp_old_wasm',
+        action: () => {
+          globals.dispatch(
+              Actions.setNewEngineMode({mode: 'FORCE_BUILTIN_WASM'}));
+        }
+      },
+      {
+        text: 'Use old version regardless (might crash)',
+        primary: false,
+        id: 'tp_old_cont',
+        action: () => {
+          forceUseOldVersion = true;
+        }
+      },
+    ],
+  });
+}
+
+async function showDialogToUsePreloadedTrace(tpStatus: StatusResult) {
+  return showModal({
+    title: 'Use Trace Processor Native Acceleration?',
+    content:
+        m('.modal-pre',
+          PROMPT.replace('$loadedTraceName', tpStatus.loadedTraceName)),
+    buttons: [
+      {
+        text: 'YES, use loaded trace',
+        primary: true,
+        id: 'rpc_load',
+        action: () => {
+          globals.dispatch(Actions.openTraceFromHttpRpc({}));
+        }
+      },
+      {
+        text: 'YES, but reset state',
+        primary: false,
+        id: 'rpc_reset',
+        action: () => {}
+      },
+      {
+        text: 'NO, Use builtin Wasm',
+        primary: false,
+        id: 'rpc_force_wasm',
+        action: () => {
+          globals.dispatch(
+              Actions.setNewEngineMode({mode: 'FORCE_BUILTIN_WASM'}));
+        }
+      },
+    ],
+  });
 }