Merge "[ui] Put error message in omnibox attrs, not in the selector." into main
diff --git a/src/trace_processor/sqlite/sqlite_engine.cc b/src/trace_processor/sqlite/sqlite_engine.cc
index 2a76c53..9513182 100644
--- a/src/trace_processor/sqlite/sqlite_engine.cc
+++ b/src/trace_processor/sqlite/sqlite_engine.cc
@@ -19,9 +19,11 @@
 #include <memory>
 #include <optional>
 #include <utility>
+#include <vector>
 
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/string_utils.h"
+#include "perfetto/public/compiler.h"
 #include "src/trace_processor/sqlite/db_sqlite_table.h"
 #include "src/trace_processor/sqlite/query_cache.h"
 #include "src/trace_processor/sqlite/scoped_db.h"
@@ -104,7 +106,9 @@
   }
   for (const auto& drop : drop_stmts) {
     int ret = sqlite3_exec(db(), drop.c_str(), nullptr, nullptr, nullptr);
-    PERFETTO_CHECK(ret == SQLITE_OK);
+    if (PERFETTO_UNLIKELY(ret != SQLITE_OK)) {
+      PERFETTO_FATAL("Failed to execute statement: '%s'", drop.c_str());
+    }
   }
 
   // It is important to unregister any functions that have been registered with
@@ -114,7 +118,9 @@
     int ret = sqlite3_create_function_v2(db_.get(), it.key().first.c_str(),
                                          it.key().second, SQLITE_UTF8, nullptr,
                                          nullptr, nullptr, nullptr, nullptr);
-    PERFETTO_CHECK(ret == SQLITE_OK);
+    if (PERFETTO_UNLIKELY(ret != SQLITE_OK)) {
+      PERFETTO_FATAL("Failed to drop function: '%s'", it.key().first.c_str());
+    }
   }
   fn_ctx_.Clear();
 
@@ -127,7 +133,16 @@
   saved_tables_.Clear();
 
   // The above operations should have cleared all the tables.
-  PERFETTO_CHECK(sqlite_tables_.size() == 0);
+  if (PERFETTO_UNLIKELY(sqlite_tables_.size() != 0)) {
+    std::vector<std::string> tables;
+    for (auto it = sqlite_tables_.GetIterator(); it; ++it) {
+      tables.push_back(it.key());
+    }
+    std::string joined = base::Join(tables, ",");
+    PERFETTO_FATAL(
+        "SqliteTable instances still exist: count='%zu', tables='[%s]'",
+        sqlite_tables_.size(), joined.c_str());
+  }
 }
 
 SqliteEngine::PreparedStatement SqliteEngine::PrepareStatement(SqlSource sql) {
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index cfdaf7e..dd4adf0 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -16,6 +16,7 @@
 
 import {Disposable, Trash} from '../base/disposable';
 import {assertFalse} from '../base/logging';
+import {time} from '../base/time';
 import {globals} from '../frontend/globals';
 import {
   Command,
@@ -260,6 +261,11 @@
             };
           });
         },
+
+    panToTimestamp(ts: time):
+        void {
+          globals.panToTimestamp(ts);
+        },
   };
 
   dispose(): void {
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index 025d5ff..1ce9074 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -580,7 +580,7 @@
 
     return m(Omnibox, {
       value: globals.state.omniboxState.omnibox,
-      placeholder: 'Search...',
+      placeholder: 'Search or type \'>\' for commands or \':\' for SQL mode',
       inputRef: App.OMNIBOX_INPUT_REF,
       onInput: (value, prev) => {
         if (prev === '') {
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 0affaed..f81ceb3 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -52,6 +52,7 @@
 import {BottomTabList} from './bottom_tab';
 import {FrontendLocalState} from './frontend_local_state';
 import {Router} from './router';
+import {horizontalScrollToTs} from './scroll_helper';
 import {ServiceWorkerController} from './service_worker_controller';
 import {SliceSqlId} from './sql_types';
 import {createStore, Store} from './store';
@@ -861,6 +862,10 @@
   openQuery(query: string, title: string, tag?: string) {
     assertExists(this._openQueryHandler)(query, title, tag);
   }
+
+  panToTimestamp(ts: time): void {
+    horizontalScrollToTs(ts);
+  }
 }
 
 export const globals = new Globals();
diff --git a/ui/src/plugins/dev.perfetto.CoreCommands/index.ts b/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
index 7024520..083a848 100644
--- a/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
+++ b/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {Time, time} from '../../base/time';
+import {exists} from '../../base/utils';
 import {
   Plugin,
   PluginContext,
@@ -172,9 +174,40 @@
         });
       },
     });
+
+    ctx.registerCommand({
+      id: 'dev.perfetto.CoreCommands#PanToTimestamp',
+      name: 'Pan To Timestamp',
+      callback: (tsRaw: unknown) => {
+        if (exists(tsRaw)) {
+          if (typeof tsRaw !== 'bigint') {
+            throw Error(`${tsRaw} is not a bigint`);
+          }
+          ctx.timeline.panToTimestamp(Time.fromRaw(tsRaw));
+        } else {
+          // No args passed, probably run from the command palette.
+          const ts = promptForTimestamp('Enter a timestamp');
+          if (exists(ts)) {
+            ctx.timeline.panToTimestamp(Time.fromRaw(ts));
+          }
+        }
+      },
+    });
   },
 };
 
+function promptForTimestamp(message: string): time|undefined {
+  const tsStr = window.prompt(message);
+  if (tsStr !== null) {
+    try {
+      return Time.fromRaw(BigInt(tsStr));
+    } catch {
+      window.alert(`${tsStr} is not an integer`);
+    }
+  }
+  return undefined;
+}
+
 export const plugin: PluginDescriptor = {
   pluginId: 'dev.perfetto.CoreCommands',
   plugin: coreCommands,
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index e0d7c9d..725c526 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -278,6 +278,9 @@
 
     // Retrieve a list of tracks on the timeline.
     tracks: TrackRef[];
+
+    // Bring a timestamp into view.
+    panToTimestamp(ts: time): void;
   }
 
   // Control over the bottom details pane.