Merge "[ui] Add a simple plugins management page." into main
diff --git a/Android.bp b/Android.bp
index cebcd7b..6cb6274 100644
--- a/Android.bp
+++ b/Android.bp
@@ -10948,6 +10948,11 @@
     ],
 }
 
+// GN: //src/trace_processor/db:compare
+filegroup {
+    name: "perfetto_src_trace_processor_db_compare",
+}
+
 // GN: //src/trace_processor/db:db
 filegroup {
     name: "perfetto_src_trace_processor_db_db",
@@ -13813,6 +13818,7 @@
         ":perfetto_src_shared_lib_unittests",
         ":perfetto_src_trace_processor_containers_containers",
         ":perfetto_src_trace_processor_containers_unittests",
+        ":perfetto_src_trace_processor_db_compare",
         ":perfetto_src_trace_processor_db_db",
         ":perfetto_src_trace_processor_db_storage_fake_storage",
         ":perfetto_src_trace_processor_db_storage_storage",
diff --git a/gn/standalone/libc++/BUILD.gn b/gn/standalone/libc++/BUILD.gn
index 49d91ba..c6ae0c1 100644
--- a/gn/standalone/libc++/BUILD.gn
+++ b/gn/standalone/libc++/BUILD.gn
@@ -21,10 +21,11 @@
       "_LIBCXXABI_DISABLE_VISIBILITY_ANNOTATIONS",
     ]
     if (is_debug) {
-      # libc++ has two levels of debug mode. Setting _LIBCPP_DEBUG to zero
-      # enables most assertions. Setting it to one additionally enables iterator
-      # debugging, but that seems to require some extra link-time dependencies.
-      # See https://libcxx.llvm.org/docs/DesignDocs/DebugMode.html
+      # Enable "lightweight" assertions in libc++ (e.g. bounds checking, empty
+      # optional dereferencing etc) with _LIBCPP_ENABLE_ASSERTIONS but do not
+      # enable iterator debugging with _LIBCPP_DEBUG (which can be very
+      # expensive).
+      defines += [ "_LIBCPP_ENABLE_ASSERTIONS=1" ]
       defines += [ "_LIBCPP_DEBUG=0" ]
     }
     cflags_cc = [
diff --git a/include/perfetto/ext/tracing/core/shared_memory_abi.h b/include/perfetto/ext/tracing/core/shared_memory_abi.h
index 4e05599..dd146ea 100644
--- a/include/perfetto/ext/tracing/core/shared_memory_abi.h
+++ b/include/perfetto/ext/tracing/core/shared_memory_abi.h
@@ -153,8 +153,10 @@
   // See PageLayout below.
   static constexpr size_t kMaxChunksPerPage = 14;
 
-  // Each TracePacket in the Chunk is prefixed by a 4 bytes redundant VarInt
-  // (see proto_utils.h) stating its size.
+  // Each TracePacket fragment in the Chunk is prefixed by a VarInt stating its
+  // size that is up to 4 bytes long. Since the size is often known after the
+  // fragment has been filled, the VarInt is often redundantly encoded (see
+  // proto_utils.h) to be exactly 4 bytes.
   static constexpr size_t kPacketHeaderSize = 4;
 
   // TraceWriter specifies this invalid packet/fragment size to signal to the
@@ -416,19 +418,6 @@
       return packets.count;
     }
 
-    // Increases |packets.count| to the given |packet_count|, but only if
-    // |packet_count| is larger than the current value of |packets.count|.
-    // Returns the new packet count. Same atomicity guarantees as
-    // IncrementPacketCount().
-    uint16_t IncreasePacketCountTo(uint16_t packet_count) {
-      ChunkHeader* chunk_header = header();
-      auto packets = chunk_header->packets.load(std::memory_order_relaxed);
-      if (packets.count < packet_count)
-        packets.count = packet_count & ChunkHeader::Packets::kMaxCount;
-      chunk_header->packets.store(packets, std::memory_order_release);
-      return packets.count;
-    }
-
     // Flags are cleared by TryAcquireChunk(), by passing the new header for
     // the chunk, or through ClearNeedsPatchingFlag.
     void SetFlag(ChunkHeader::Flags flag) {
diff --git a/python/perfetto/prebuilts/manifests/trace_processor_shell.py b/python/perfetto/prebuilts/manifests/trace_processor_shell.py
index 0196c4b..6fe447b 100755
--- a/python/perfetto/prebuilts/manifests/trace_processor_shell.py
+++ b/python/perfetto/prebuilts/manifests/trace_processor_shell.py
@@ -1,15 +1,15 @@
-# This file has been generated by: tools/roll-prebuilts v40.0
+# This file has been generated by: tools/roll-prebuilts 3e53e144bee271ec558363df2e561a77d7e0b789
 TRACE_PROCESSOR_SHELL_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9978200,
+        10142120,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-amd64/trace_processor_shell',
     'sha256':
-        'f3e21eb29fb51cb2ea9b81b69132c5ae93ce3276c57ccd27fcf7c675306b4e41',
+        '44585789d420d0bc38edc3dd6fbade4c4a718dc535fb68ac7a2449bf1a251f30',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -19,11 +19,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8493976,
+        8659128,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-arm64/trace_processor_shell',
     'sha256':
-        '84f35765141374b8d883813ac533e0c004cf72d1c6f05aef0c973364ff541eb9',
+        'bccca60b99fb2c587503a6430e0b15204ebeddb97607006d54f203e64ac923af',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -33,11 +33,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9830856,
+        9987880,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-amd64/trace_processor_shell',
     'sha256':
-        'b3dc0a9c641b84a57fa5d59637921ae2237e4f05b1778341a691df220faf0cd7',
+        '3510cfc89e627a95ede2d06e77d1ddd70d31ef99d0d213d1fbaba4f438030e4c',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -47,11 +47,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        7231096,
+        7389416,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm/trace_processor_shell',
     'sha256':
-        'a21252830fb1bbb7b3fd9665ce6e70920cffa6b1e72c16589c90896c002c3348',
+        '2a87d3587e9a756ea486746ba08e7535dacf8872b1d2081e9381a612668d154d',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9238056,
+        9396856,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm64/trace_processor_shell',
     'sha256':
-        'f77519ec19743ec2c22ed78fe3a20106a482a28d77c4154378af108c5f7bdd4a',
+        '42738a5aa187fe7644d87361fcf9854459f3d46a301026c9ac76ac4bf0ec9d3e',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -75,55 +75,55 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        6870968,
+        7018688,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm/trace_processor_shell',
     'sha256':
-        '2c7055fb44085ec60ad8bb970d495c9c88070fce08902f11fcd44e0ae3369876'
+        '9d8247c09b82835dc5019fed2f41e8844162a8cbf520e4243111852d6398911b'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8414568,
+        8578936,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm64/trace_processor_shell',
     'sha256':
-        'd8ca0dc2bab7ea604a6721f0ac0e2b433b43261f247c6c98c510dc17aafe5a72'
+        '91405004e1a47b1170eb32315f3d0a5b287bc71cb856e7403fad0f02e8b4dfd1'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9328508,
+        9492612,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x86/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x86/trace_processor_shell',
     'sha256':
-        'de6a6ea45769888e59a1678d37b6e355b27b834d34a0b9e4980a942d333b88cc'
+        '01364fc6fb485b20d838326462239d14c8f2daf1e7dde524b2cba4fd5acb8a73'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9577896,
+        9742264,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x64/trace_processor_shell',
     'sha256':
-        'cd4b16c5f78a060934204737ba8b312e824ff7cc28f3732daf7d64e733a727f9'
+        '43b456dcd0238f52f5730c6f5b9f8249f096df718d3fefa443c3246d66df4bd6'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'trace_processor_shell.exe',
     'file_size':
-        9248256,
+        9408000,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/windows-amd64/trace_processor_shell.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/windows-amd64/trace_processor_shell.exe',
     'sha256':
-        '26584b4bbab40f8b0ad991a869e7483f92d7223e1473b879a6ceafa49b76390a',
+        'b4dc6a7968673373265344e8e5249ccc31f7d12bb9df527370065dff71b1e74a',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/python/perfetto/prebuilts/manifests/tracebox.py b/python/perfetto/prebuilts/manifests/tracebox.py
index 21698c1..a694d8a 100755
--- a/python/perfetto/prebuilts/manifests/tracebox.py
+++ b/python/perfetto/prebuilts/manifests/tracebox.py
@@ -1,15 +1,15 @@
-# This file has been generated by: tools/roll-prebuilts v40.0
+# This file has been generated by: tools/roll-prebuilts 3e53e144bee271ec558363df2e561a77d7e0b789
 TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'tracebox',
     'file_size':
-        1498816,
+        1515200,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-amd64/tracebox',
     'sha256':
-        '185014447d35357edbd20e7ce9924842a0d5c6576bd2257abae2ed48b65fd3b8',
+        'b451e873b1f6c8bd2fc3f1e12adc381c77a5b6dce9ec28ad8788e6f7f5efd348',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -21,9 +21,9 @@
     'file_size':
         1392776,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-arm64/tracebox',
     'sha256':
-        '082bb50e64df5e232673eebb1cd8b0dd752a394105f600cb0262730833f6b7f3',
+        '8247045bd78e467aa010674277048658cdf04a95bd15b25dabc973fce011d6db',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -33,11 +33,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2229096,
+        2236584,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-amd64/tracebox',
     'sha256':
-        'c99120caedb845e1c3fad4428263a683b44c357c76d65848dd8e437250066e38',
+        'adf6f9ebb5686a7d2e056c8f059adf4d95e867ffa39d27b041bef16d3cb7e1c7',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -47,11 +47,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1339796,
+        1344188,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm/tracebox',
     'sha256':
-        '6732165916b74f0b820991d1aaed2086a6b56e91f6c604291efe6636f0bdda71',
+        'e2feeeaf38c3cd9efbf992dbe09f40fca81651d9fd9159e0f9a95868e2c4e07e',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2157312,
+        2164560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm64/tracebox',
     'sha256':
-        '7d09865a6d7118e67d2acd0c56b2a94ce8bd5f614869d29a72fe633515ab1fbd',
+        '5d89a6c16819a74be44bf731f2d07bfb83924a2e560554a373feeb2bb9940ef1',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -77,31 +77,31 @@
     'file_size':
         1247188,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm/tracebox',
     'sha256':
-        '4ecc192172ac2bca49557cbdbb1f7d660718d4fb4a7314fd19b2b2e52be8bc0c'
+        '5837e88a92b8bc00d5575f9dc02dd314f16b2e0a1bb174efb6c092a8f639e7c4'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'tracebox',
     'file_size':
-        1854120,
+        1870504,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm64/tracebox',
     'sha256':
-        '1ca89113279d5c6a9ae273bde03b4d84373efe6923dc637cb840908f13b9639e'
+        '75431d11aec11f59b87a76fa31cf92a1f8e534ad4118357b3654a458cf547081'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'tracebox',
     'file_size':
-        1853356,
+        1869740,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x86/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x86/tracebox',
     'sha256':
-        'cf689a191c1252734ebbfda3106600da324610f761515cfbffbeac2ebdfee715'
+        '388da3a4248f105bc56685db4835ce487633035334efa543b7190ac4a9e26bde'
 }, {
     'arch':
         'android-x64',
@@ -110,7 +110,7 @@
     'file_size':
         2149032,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x64/tracebox',
     'sha256':
-        '99e9ebdb5b5308d95551a4ad060d615d7defb6877c4061d21c783c45a71d372f'
+        '1c3d50a4066f9b3478ad65431532c3503a2ad73ee89d346e6ee12f7fb0c93aaa'
 }]
diff --git a/python/perfetto/prebuilts/manifests/traceconv.py b/python/perfetto/prebuilts/manifests/traceconv.py
index f01ef0a..92ad14a 100755
--- a/python/perfetto/prebuilts/manifests/traceconv.py
+++ b/python/perfetto/prebuilts/manifests/traceconv.py
@@ -1,15 +1,15 @@
-# This file has been generated by: tools/roll-prebuilts v40.0
+# This file has been generated by: tools/roll-prebuilts 3e53e144bee271ec558363df2e561a77d7e0b789
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        9184800,
+        9348712,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-amd64/traceconv',
     'sha256':
-        'b651d0a5b5606c1c3e24723e94d8ecb233a01f0dfccc95a2c6a4e773cb8f52d7',
+        '466110b5d92cfc7951ae3223147156dc3ddfad055f8c7a93fea91b2f1844d013',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -19,11 +19,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7761896,
+        7927048,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-arm64/traceconv',
     'sha256':
-        '3b019f5ddd5293d3181f7c30f91dc7b08f3a2e83ebb3b52b8f3905dc5161747d',
+        '46663d0eaa88bc821ad71872fd9789c340f9100e22a78494006b94225e2cfe9b',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -33,11 +33,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8928296,
+        9091432,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-amd64/traceconv',
     'sha256':
-        '830d20ffec266218d49f6b6c8efed4538bc59b51d8d2f735cbbb6a1435131b50',
+        'bb0eabefb6cb22623368d1b51cd95a6d9485781f102a9293443659fdd826fc8f',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -47,11 +47,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        6770204,
+        6934532,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm/traceconv',
     'sha256':
-        '93a9e5ccb94559b871af8f6da45f858aee01801b31776703892dcf3d7ea769b7',
+        'ce90e026c71a006b01e4d3c247772cfeeeb7581cd2ce1b003de35ddd87e9d349',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8393944,
+        8558824,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm64/traceconv',
     'sha256':
-        '88a92ccbcd8e851673e018b7f599514daf05dde9b7e4de9641fa5629124abf12',
+        'f26446b306b0025c54d39dac5b62ae015e0d1771ff5ec461ccbbcc93f1dcb335',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -75,55 +75,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        6378744,
+        6542848,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm/traceconv',
     'sha256':
-        '6cb7d30d656aa4f172e6724f105a56e249e7043ecf637c65e1e3868885535cff'
+        '901512a8243b4015aeba59f547d40736fcf032edeedcd1ecb59c259bba0122cf'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        7692488,
+        7856856,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm64/traceconv',
     'sha256':
-        '1668808efbdf8d5b116d4716d61d2bd002f71ce465206d3b83af4fcc7a4c19cd'
+        '0eda603b5bc4925b98e0d9656dc18e3e7c4db4685c49f902fd3e1fad1be75efd'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        8557756,
+        8721860,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x86/traceconv',
     'sha256':
-        '653733582cae0021eae0e1b5d8db387c1bae772d77b307f1e2111b78ec4ea67c'
+        'd9bc2acf1b280198402a81019d6d5c9f75849d0254fd929b62671af6095d6f57'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        8708352,
+        8872720,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x64/traceconv',
     'sha256':
-        '7fc564ac581b81d79573f57dae027c47bd7a857ff0f89df984380c3c657d5876'
+        '3c33c2f79a4fb9760cdf2348a09d6fb6fbb8b5098e014e83cd14d3f3dfb8661d'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        8204288,
+        8369664,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/windows-amd64/traceconv.exe',
     'sha256':
-        'e33bad8061f08f9c3cfe6e91ef6f1696b6ac90d0799edcb57052f24888b436e2',
+        '900f931d89af74b84229e1c396898a9c492d49acd15977692040c95a50499936',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/python/tools/check_imports.py b/python/tools/check_imports.py
index 3634419..a114982 100755
--- a/python/tools/check_imports.py
+++ b/python/tools/check_imports.py
@@ -353,10 +353,18 @@
     return result
 
 
+def remove_prefix(s, prefix):
+  return s[len(prefix):] if s.startswith(prefix) else s
+
+
+def remove_suffix(s, suffix):
+  return s[:-len(suffix)] if s.endswith(suffix) else s
+
+
 def find_imports(path):
   src = path
-  src = src.removeprefix(UI_SRC_DIR)
-  src = src.removesuffix('.ts')
+  src = remove_prefix(src, UI_SRC_DIR)
+  src = remove_suffix(src, '.ts')
   directory, _ = os.path.split(src)
   with open(path) as f:
     s = f.read()
diff --git a/src/trace_processor/db/BUILD.gn b/src/trace_processor/db/BUILD.gn
index 6b02d28..5dc4901 100644
--- a/src/trace_processor/db/BUILD.gn
+++ b/src/trace_processor/db/BUILD.gn
@@ -48,6 +48,14 @@
   ]
 }
 
+source_set("compare") {
+  sources = [ "compare.h" ]
+  deps = [
+    "../../../include/perfetto/trace_processor",
+    "../containers",
+  ]
+}
+
 perfetto_tp_tables("view_unittest") {
   sources = [ "view_unittest.py" ]
 }
diff --git a/src/trace_processor/db/query_executor.cc b/src/trace_processor/db/query_executor.cc
index cb9a2cf..a1df9f6 100644
--- a/src/trace_processor/db/query_executor.cc
+++ b/src/trace_processor/db/query_executor.cc
@@ -173,17 +173,6 @@
     use_legacy = use_legacy || (col.overlay().size() != column_size &&
                                 col.overlay().row_map().IsRange());
 
-    // Comparing ints with doubles and doubles with ints.
-    bool int_with_double =
-        col.type() == SqlValue::kLong && c.value.type == SqlValue::kDouble;
-    bool double_with_int =
-        col.type() == SqlValue::kDouble && c.value.type == SqlValue::kLong;
-    bool double_int_enabled_col_type = col.IsId() || col.IsSetId();
-    use_legacy =
-        use_legacy ||
-        (!double_int_enabled_col_type && c.op != FilterOp::kIsNull &&
-         c.op != FilterOp::kIsNotNull && (int_with_double || double_with_int));
-
     // Extrinsically sorted columns.
     use_legacy = use_legacy ||
                  (col.IsSorted() && col.overlay().row_map().IsIndexVector());
diff --git a/src/trace_processor/db/storage/BUILD.gn b/src/trace_processor/db/storage/BUILD.gn
index d13b530..065d787 100644
--- a/src/trace_processor/db/storage/BUILD.gn
+++ b/src/trace_processor/db/storage/BUILD.gn
@@ -81,6 +81,7 @@
   deps = [
     ":fake_storage",
     ":storage",
+    "../:compare",
     "../../../../gn:default_deps",
     "../../../../gn:gtest_and_gmock",
     "../../../../include/perfetto/trace_processor:basic_types",
diff --git a/src/trace_processor/db/storage/dense_null_storage.cc b/src/trace_processor/db/storage/dense_null_storage.cc
index cbe9743..7a59d09 100644
--- a/src/trace_processor/db/storage/dense_null_storage.cc
+++ b/src/trace_processor/db/storage/dense_null_storage.cc
@@ -78,7 +78,6 @@
   } else {
     res = std::move(inner_res).TakeIfBitVector();
   }
-  PERFETTO_DCHECK(res.size() == in.end);
 
   if (op == FilterOp::kIsNull) {
     // For IS NULL, we need to add any rows in |non_null_| which are zeros: we
@@ -93,6 +92,8 @@
     // are removed as they would not match.
     res.And(*non_null_);
   }
+
+  PERFETTO_DCHECK(res.size() == in.end);
   return RangeOrBitVector(std::move(res));
 }
 
@@ -119,10 +120,8 @@
     builder.Append(non_null_->IsSet(indices[i]));
   }
   BitVector non_null = std::move(builder).Build();
-  PERFETTO_DCHECK(non_null.size() == indices_size);
 
   BitVector res = std::move(inner_res).TakeIfBitVector();
-  PERFETTO_DCHECK(res.size() == indices_size);
 
   if (op == FilterOp::kIsNull) {
     BitVector null = std::move(non_null);
@@ -131,6 +130,8 @@
   } else {
     res.And(non_null);
   }
+
+  PERFETTO_DCHECK(res.size() == indices_size);
   return RangeOrBitVector(std::move(res));
 }
 
diff --git a/src/trace_processor/db/storage/null_storage.cc b/src/trace_processor/db/storage/null_storage.cc
index a1ca7d0..0ded1af 100644
--- a/src/trace_processor/db/storage/null_storage.cc
+++ b/src/trace_processor/db/storage/null_storage.cc
@@ -32,10 +32,10 @@
 
 using Range = RowMap::Range;
 
-RangeOrBitVector ReconcileStorageResult(FilterOp op,
-                                        const BitVector& non_null,
-                                        RangeOrBitVector storage_result,
-                                        Range in_range) {
+BitVector ReconcileStorageResult(FilterOp op,
+                                 const BitVector& non_null,
+                                 RangeOrBitVector storage_result,
+                                 Range in_range) {
   PERFETTO_CHECK(in_range.end <= non_null.size());
 
   // Reconcile the results of the Search operation with the non-null indices
@@ -68,7 +68,7 @@
     null.Not();
     res.Or(null);
   }
-  return RangeOrBitVector(std::move(res));
+  return res;
 }
 
 }  // namespace
@@ -101,7 +101,7 @@
         // intersect the |non_null_|.
         BitVector res = non_null_->IntersectRange(in.start, in.end);
         res.Not();
-        res.Resize(non_null_->size(), false);
+        res.Resize(in.end, false);
         return RangeOrBitVector(std::move(res));
       }
       case SearchValidationResult::kAllData:
@@ -115,9 +115,12 @@
   // it.
   uint32_t start = non_null_->CountSetBits(in.start);
   uint32_t end = non_null_->CountSetBits(in.end);
-  return ReconcileStorageResult(
+  BitVector res = ReconcileStorageResult(
       op, *non_null_, storage_->Search(op, sql_val, RowMap::Range(start, end)),
       in);
+
+  PERFETTO_DCHECK(res.size() == in.end);
+  return RangeOrBitVector(std::move(res));
 }
 
 RangeOrBitVector NullStorage::IndexSearch(FilterOp op,
@@ -158,8 +161,12 @@
   RangeOrBitVector range_or_bv =
       storage_->IndexSearch(op, sql_val, storage_iv.data(),
                             static_cast<uint32_t>(storage_iv.size()), sorted);
-  return ReconcileStorageResult(op, std::move(storage_non_null).Build(),
-                                std::move(range_or_bv), Range(0, indices_size));
+  BitVector res =
+      ReconcileStorageResult(op, std::move(storage_non_null).Build(),
+                             std::move(range_or_bv), Range(0, indices_size));
+
+  PERFETTO_DCHECK(res.size() == indices_size);
+  return RangeOrBitVector(std::move(res));
 }
 
 void NullStorage::StableSort(uint32_t*, uint32_t) const {
diff --git a/src/trace_processor/db/storage/numeric_storage.cc b/src/trace_processor/db/storage/numeric_storage.cc
index b090cb8..8bd2fa8 100644
--- a/src/trace_processor/db/storage/numeric_storage.cc
+++ b/src/trace_processor/db/storage/numeric_storage.cc
@@ -19,6 +19,8 @@
 
 #include <cmath>
 #include <cstddef>
+#include <cstdint>
+#include <functional>
 #include <string>
 
 #include "perfetto/base/compiler.h"
@@ -193,6 +195,80 @@
   }
 }
 
+SearchValidationResult IntColumnToDouble(SqlValue* sql_val, FilterOp op) {
+  double double_val = sql_val->AsDouble();
+
+  // Case when |sql_val| can be interpreted as a SqlValue::Double.
+  if (std::equal_to<double>()(
+          static_cast<double>(static_cast<int64_t>(double_val)), double_val)) {
+    *sql_val = SqlValue::Long(static_cast<int64_t>(double_val));
+    return SearchValidationResult::kOk;
+  }
+
+  // Logic for when the value is a real double.
+  switch (op) {
+    case FilterOp::kEq:
+      return SearchValidationResult::kNoData;
+    case FilterOp::kNe:
+      return SearchValidationResult::kAllData;
+
+    case FilterOp::kLe:
+    case FilterOp::kGt:
+      *sql_val = SqlValue::Long(static_cast<int64_t>(std::floor(double_val)));
+      return SearchValidationResult::kOk;
+
+    case FilterOp::kLt:
+    case FilterOp::kGe:
+      *sql_val = SqlValue::Long(static_cast<int64_t>(std::ceil(double_val)));
+      return SearchValidationResult::kOk;
+
+    case FilterOp::kIsNotNull:
+    case FilterOp::kIsNull:
+    case FilterOp::kGlob:
+    case FilterOp::kRegex:
+      PERFETTO_FATAL("Invalid filter operation");
+  }
+  PERFETTO_FATAL("For GCC");
+}
+
+SearchValidationResult DoubleColumnWithInt(SqlValue* sql_val, FilterOp op) {
+  int64_t i = sql_val->AsLong();
+  double i_as_d = static_cast<double>(i);
+
+  // Case when |sql_val| can be interpreted as a SqlValue::Long.
+  if (std::equal_to<int64_t>()(i, static_cast<int64_t>(i_as_d))) {
+    *sql_val = SqlValue::Double(i_as_d);
+    return SearchValidationResult::kOk;
+  }
+
+  // Logic for when the value can't be represented as double.
+  switch (op) {
+    case FilterOp::kEq:
+      return SearchValidationResult::kNoData;
+    case FilterOp::kNe:
+      return SearchValidationResult::kAllData;
+
+    case FilterOp::kLe:
+    case FilterOp::kGt:
+      // The first double value smaller than |i|.
+      *sql_val = SqlValue::Double(std::nextafter(i_as_d, i - 1));
+      return SearchValidationResult::kOk;
+
+    case FilterOp::kLt:
+    case FilterOp::kGe:
+      // The first double value bigger than |i|.
+      *sql_val = SqlValue::Double(std::nextafter(i_as_d, i + 1));
+      return SearchValidationResult::kOk;
+
+    case FilterOp::kIsNotNull:
+    case FilterOp::kIsNull:
+    case FilterOp::kGlob:
+    case FilterOp::kRegex:
+      PERFETTO_FATAL("Invalid filter operation");
+  }
+  PERFETTO_FATAL("For GCC");
+}
+
 }  // namespace
 
 SearchValidationResult NumericStorageBase::ValidateSearchConstraints(
@@ -245,17 +321,15 @@
       return SearchValidationResult::kNoData;
   }
 
-  // TODO(b/307482437): There is currently no support for comparison with double
-  // and it is prevented on QueryExecutor level.
-  if (type_ != ColumnType::kDouble) {
-    PERFETTO_CHECK(val.type != SqlValue::kDouble);
-  }
-
   // Bounds of the value.
   enum ExtremeVal { kTooBig, kTooSmall, kOk };
   ExtremeVal extreme_validator = kOk;
 
-  switch (type_) {
+  double_t num_val = val.type == SqlValue::kLong
+                         ? static_cast<double_t>(val.AsLong())
+                         : val.AsDouble();
+
+  switch (storage_type_) {
     case ColumnType::kDouble:
       // Any value would make a sensible comparison with a double.
     case ColumnType::kInt64:
@@ -263,21 +337,21 @@
       // to verify here, as all values are going to be in the int64_t limits.
       break;
     case ColumnType::kInt32:
-      if (val.AsLong() > std::numeric_limits<int32_t>::max()) {
+      if (num_val > std::numeric_limits<int32_t>::max()) {
         extreme_validator = kTooBig;
         break;
       }
-      if (val.AsLong() < std::numeric_limits<int32_t>::min()) {
+      if (num_val < std::numeric_limits<int32_t>::min()) {
         extreme_validator = kTooSmall;
         break;
       }
       break;
     case ColumnType::kUint32:
-      if (val.AsLong() > std::numeric_limits<uint32_t>::max()) {
+      if (num_val > std::numeric_limits<uint32_t>::max()) {
         extreme_validator = kTooBig;
         break;
       }
-      if (val.AsLong() < std::numeric_limits<uint32_t>::min()) {
+      if (num_val < std::numeric_limits<uint32_t>::min()) {
         extreme_validator = kTooSmall;
         break;
       }
@@ -317,7 +391,33 @@
                                 std::to_string(static_cast<uint32_t>(op)));
                     });
 
-  NumericValue val = GetNumericTypeVariant(type_, sql_val);
+  if (sql_val.type == SqlValue::kDouble &&
+      storage_type_ != ColumnType::kDouble) {
+    auto comp = IntColumnToDouble(&sql_val, op);
+    switch (comp) {
+      case SearchValidationResult::kOk:
+        break;
+      case SearchValidationResult::kAllData:
+        return RangeOrBitVector(search_range);
+      case SearchValidationResult::kNoData:
+        return RangeOrBitVector(Range());
+    }
+  }
+
+  if (sql_val.type != SqlValue::kDouble &&
+      storage_type_ == ColumnType::kDouble) {
+    auto comp = DoubleColumnWithInt(&sql_val, op);
+    switch (comp) {
+      case SearchValidationResult::kOk:
+        break;
+      case SearchValidationResult::kAllData:
+        return RangeOrBitVector(search_range);
+      case SearchValidationResult::kNoData:
+        return RangeOrBitVector(Range());
+    }
+  }
+
+  NumericValue val = GetNumericTypeVariant(storage_type_, sql_val);
 
   if (is_sorted_) {
     if (op != FilterOp::kNe) {
@@ -347,7 +447,34 @@
                                 std::to_string(static_cast<uint32_t>(op)));
                     });
 
-  NumericValue val = GetNumericTypeVariant(type_, sql_val);
+  if (sql_val.type == SqlValue::kDouble &&
+      storage_type_ != ColumnType::kDouble) {
+    auto comp = IntColumnToDouble(&sql_val, op);
+    switch (comp) {
+      case SearchValidationResult::kOk:
+        break;
+      case SearchValidationResult::kAllData:
+        return RangeOrBitVector(Range(0, indices_size));
+      case SearchValidationResult::kNoData:
+        return RangeOrBitVector(Range());
+    }
+  }
+
+  if (sql_val.type != SqlValue::kDouble &&
+      storage_type_ == ColumnType::kDouble) {
+    auto comp = DoubleColumnWithInt(&sql_val, op);
+    switch (comp) {
+      case SearchValidationResult::kOk:
+        break;
+      case SearchValidationResult::kAllData:
+        return RangeOrBitVector(Range(0, indices_size));
+      case SearchValidationResult::kNoData:
+        return RangeOrBitVector(Range());
+    }
+  }
+
+  NumericValue val = GetNumericTypeVariant(storage_type_, sql_val);
+
   if (sorted) {
     return RangeOrBitVector(
         BinarySearchExtrinsic(op, val, indices, indices_size));
@@ -473,7 +600,7 @@
                            return first_val < second_val;
                          });
       },
-      GetNumericTypeVariant(type_, SqlValue::Long(0)));
+      GetNumericTypeVariant(storage_type_, SqlValue::Long(0)));
 }
 
 void NumericStorageBase::Sort(uint32_t*, uint32_t) const {
@@ -484,10 +611,10 @@
 void NumericStorageBase::Serialize(StorageProto* msg) const {
   auto* numeric_storage_msg = msg->set_numeric_storage();
   numeric_storage_msg->set_is_sorted(is_sorted_);
-  numeric_storage_msg->set_column_type(static_cast<uint32_t>(type_));
+  numeric_storage_msg->set_column_type(static_cast<uint32_t>(storage_type_));
 
   uint32_t type_size;
-  switch (type_) {
+  switch (storage_type_) {
     case ColumnType::kInt64:
       type_size = sizeof(int64_t);
       break;
diff --git a/src/trace_processor/db/storage/numeric_storage.h b/src/trace_processor/db/storage/numeric_storage.h
index 741a8e0..6af150a 100644
--- a/src/trace_processor/db/storage/numeric_storage.h
+++ b/src/trace_processor/db/storage/numeric_storage.h
@@ -60,7 +60,7 @@
                      uint32_t size,
                      ColumnType type,
                      bool is_sorted = false)
-      : size_(size), data_(data), type_(type), is_sorted_(is_sorted) {}
+      : size_(size), data_(data), storage_type_(type), is_sorted_(is_sorted) {}
 
  private:
   // All viable numeric values for ColumnTypes.
@@ -86,7 +86,7 @@
 
   const uint32_t size_ = 0;
   const void* data_ = nullptr;
-  const ColumnType type_ = ColumnType::kDummy;
+  const ColumnType storage_type_ = ColumnType::kDummy;
   const bool is_sorted_ = false;
 };
 
diff --git a/src/trace_processor/db/storage/numeric_storage_unittest.cc b/src/trace_processor/db/storage/numeric_storage_unittest.cc
index 5ba32ad..5806f83 100644
--- a/src/trace_processor/db/storage/numeric_storage_unittest.cc
+++ b/src/trace_processor/db/storage/numeric_storage_unittest.cc
@@ -16,6 +16,8 @@
 #include "src/trace_processor/db/storage/numeric_storage.h"
 #include <cstdint>
 
+#include "perfetto/trace_processor/basic_types.h"
+#include "src/trace_processor/db/compare.h"
 #include "src/trace_processor/db/storage/types.h"
 #include "test/gtest_and_gmock.h"
 
@@ -29,8 +31,21 @@
 namespace storage {
 namespace {
 
+using testing::ElementsAre;
+using testing::IsEmpty;
 using Range = RowMap::Range;
 
+std::vector<uint32_t> ToIndexVector(RangeOrBitVector& r_or_bv) {
+  RowMap rm;
+  if (r_or_bv.IsBitVector()) {
+    rm = RowMap(std::move(r_or_bv).TakeIfBitVector());
+  } else {
+    Range range = std::move(r_or_bv).TakeIfRange();
+    rm = RowMap(range.start, range.end);
+  }
+  return rm.GetAllIndices();
+}
+
 TEST(NumericStorageUnittest, InvalidSearchConstraintsGeneralChecks) {
   std::vector<uint32_t> data_vec(128);
   std::iota(data_vec.begin(), data_vec.end(), 0);
@@ -160,7 +175,111 @@
   ASSERT_EQ(out, stable_out);
 }
 
-TEST(NumericStorageUnittest, CompareFast) {
+TEST(NumericStorageUnittest, Search) {
+  std::vector<int32_t> data_vec{-5, 5, -4, 4, -3, 3, 0};
+  NumericStorage<int32_t> storage(&data_vec, ColumnType::kInt32);
+  Range test_range(1, 5);
+  SqlValue val = SqlValue::Long(4);
+
+  auto res = storage.Search(FilterOp::kEq, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(3));
+
+  res = storage.Search(FilterOp::kNe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 2, 4));
+
+  res = storage.Search(FilterOp::kLt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(2, 4));
+
+  res = storage.Search(FilterOp::kLe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(2, 3, 4));
+
+  res = storage.Search(FilterOp::kGt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1));
+
+  res = storage.Search(FilterOp::kGe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 3));
+}
+
+TEST(NumericStorageUnittest, SearchCompareWithNegative) {
+  std::vector<int32_t> data_vec{-5, 5, -4, 4, -3, 3, 0};
+  NumericStorage<int32_t> storage(&data_vec, ColumnType::kInt32);
+  Range test_range(1, 5);
+  SqlValue val = SqlValue::Long(-3);
+
+  auto res = storage.Search(FilterOp::kEq, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(4));
+
+  res = storage.Search(FilterOp::kNe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 2, 3));
+
+  res = storage.Search(FilterOp::kLt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(2));
+
+  res = storage.Search(FilterOp::kLe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(2, 4));
+
+  res = storage.Search(FilterOp::kGt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 3));
+
+  res = storage.Search(FilterOp::kGe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 3, 4));
+}
+
+TEST(NumericStorageUnittest, IndexSearch) {
+  std::vector<int32_t> data_vec{-5, 5, -4, 4, -3, 3, 0};
+  NumericStorage<int32_t> storage(&data_vec, ColumnType::kInt32);
+
+  // -5, -3, -3, 3, 5, 0
+  std::vector<uint32_t> indices{0, 4, 4, 5, 1, 6};
+  SqlValue val = SqlValue::Long(3);
+
+  auto res = storage.IndexSearch(FilterOp::kEq, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(3));
+
+  res = storage.IndexSearch(FilterOp::kNe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2, 4, 5));
+
+  res = storage.IndexSearch(FilterOp::kLt, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2, 5));
+
+  res = storage.IndexSearch(FilterOp::kLe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2, 3, 5));
+
+  res = storage.IndexSearch(FilterOp::kGt, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(4));
+
+  res = storage.IndexSearch(FilterOp::kGe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(3, 4));
+}
+
+TEST(NumericStorageUnittest, IndexSearchCompareWithNegative) {
+  std::vector<int32_t> data_vec{-5, 5, -4, 4, -3, 3, 0};
+  NumericStorage<int32_t> storage(&data_vec, ColumnType::kInt32);
+
+  // -5, -3, -3, 3, 5, 0
+  std::vector<uint32_t> indices{0, 4, 4, 5, 1, 6};
+  SqlValue val = SqlValue::Long(-3);
+
+  auto res = storage.IndexSearch(FilterOp::kEq, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 2));
+
+  res = storage.IndexSearch(FilterOp::kNe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 3, 4, 5));
+
+  res = storage.IndexSearch(FilterOp::kLt, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0));
+
+  res = storage.IndexSearch(FilterOp::kLe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2));
+
+  res = storage.IndexSearch(FilterOp::kGt, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(3, 4, 5));
+
+  res = storage.IndexSearch(FilterOp::kGe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 2, 3, 4, 5));
+}
+
+TEST(NumericStorageUnittest, SearchFast) {
   std::vector<uint32_t> data_vec(128);
   std::iota(data_vec.begin(), data_vec.end(), 0);
   NumericStorage<uint32_t> storage(&data_vec, ColumnType::kUint32);
@@ -172,7 +291,7 @@
   ASSERT_EQ(bv.IndexOfNthSet(0), 100u);
 }
 
-TEST(NumericStorageUnittest, CompareSorted) {
+TEST(NumericStorageUnittest, SearchSorted) {
   std::vector<uint32_t> data_vec(128);
   std::iota(data_vec.begin(), data_vec.end(), 0);
   NumericStorage<uint32_t> storage(&data_vec, ColumnType::kUint32, true);
@@ -184,7 +303,7 @@
   ASSERT_EQ(range.end, 128u);
 }
 
-TEST(NumericStorageUnittest, CompareSortedNe) {
+TEST(NumericStorageUnittest, SearchSortedNe) {
   std::vector<uint32_t> data_vec(128);
   std::iota(data_vec.begin(), data_vec.end(), 0);
   NumericStorage<uint32_t> storage(&data_vec, ColumnType::kUint32, true);
@@ -194,7 +313,7 @@
   ASSERT_EQ(bv.CountSetBits(), 127u);
 }
 
-TEST(NumericStorageUnittest, CompareSortedSubset) {
+TEST(NumericStorageUnittest, SearchSortedSubset) {
   std::vector<uint32_t> data_vec(128);
   std::iota(data_vec.begin(), data_vec.end(), 0);
   NumericStorage<uint32_t> storage(&data_vec, ColumnType::kUint32, true);
@@ -206,7 +325,7 @@
   ASSERT_EQ(range.end, 104u);
 }
 
-TEST(NumericStorageUnittest, CompareSortedIndexesGreaterEqual) {
+TEST(NumericStorageUnittest, IndexSearcgExtrinsicGe) {
   std::vector<uint32_t> data_vec{30, 40, 50, 60, 90, 80, 70, 0, 10, 20};
   std::vector<uint32_t> sorted_order{7, 8, 9, 0, 1, 2, 3, 6, 5, 4};
 
@@ -222,7 +341,7 @@
   ASSERT_EQ(range.end, 10u);
 }
 
-TEST(NumericStorageUnittest, CompareSortedIndexesLess) {
+TEST(NumericStorageUnittest, IndexSearchExtrinsicLt) {
   std::vector<uint32_t> data_vec{30, 40, 50, 60, 90, 80, 70, 0, 10, 20};
   std::vector<uint32_t> sorted_order{7, 8, 9, 0, 1, 2, 3, 6, 5, 4};
 
@@ -238,7 +357,7 @@
   ASSERT_EQ(range.end, 6u);
 }
 
-TEST(NumericStorageUnittest, CompareSortedIndexesEqual) {
+TEST(NumericStorageUnittest, IndexSearchExtrinsicEq) {
   std::vector<uint32_t> data_vec{30, 40, 50, 60, 90, 80, 70, 0, 10, 20};
   std::vector<uint32_t> sorted_order{7, 8, 9, 0, 1, 2, 3, 6, 5, 4};
 
@@ -254,6 +373,284 @@
   ASSERT_EQ(range.end, 7u);
 }
 
+TEST(NumericStorageUnittest, SearchWithIntAsDouble) {
+  std::vector<int32_t> data_vec{-5, 5, -4, 4, -3, 3, 0};
+  NumericStorage<int32_t> storage(&data_vec, ColumnType::kInt32);
+  Range test_range(1, 5);
+  SqlValue val = SqlValue::Double(4);
+
+  auto res = storage.Search(FilterOp::kEq, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(3));
+
+  res = storage.Search(FilterOp::kNe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 2, 4));
+
+  res = storage.Search(FilterOp::kLt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(2, 4));
+
+  res = storage.Search(FilterOp::kLe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(2, 3, 4));
+
+  res = storage.Search(FilterOp::kGt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1));
+
+  res = storage.Search(FilterOp::kGe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 3));
+}
+
+TEST(NumericStorageUnittest, IndexSearchWithIntAsDouble) {
+  std::vector<int32_t> data_vec{-5, 5, -4, 4, -3, 3, 0};
+  NumericStorage<int32_t> storage(&data_vec, ColumnType::kInt32);
+
+  // -5, -3, -3, 3, 5, 0
+  std::vector<uint32_t> indices{0, 4, 4, 5, 1, 6};
+  SqlValue val = SqlValue::Double(3);
+
+  auto res = storage.IndexSearch(FilterOp::kEq, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(3));
+
+  res = storage.IndexSearch(FilterOp::kNe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2, 4, 5));
+
+  res = storage.IndexSearch(FilterOp::kLt, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2, 5));
+
+  res = storage.IndexSearch(FilterOp::kLe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2, 3, 5));
+
+  res = storage.IndexSearch(FilterOp::kGt, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(4));
+
+  res = storage.IndexSearch(FilterOp::kGe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(3, 4));
+}
+
+TEST(NumericStorageUnittest, SearchInt32WithDouble) {
+  std::vector<int32_t> data_vec{-5, 5, -4, 4, -3, 3, 0};
+  NumericStorage<int32_t> storage(&data_vec, ColumnType::kInt32);
+  Range test_range(1, 5);
+  SqlValue val = SqlValue::Double(3.5);
+
+  auto res = storage.Search(FilterOp::kEq, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), IsEmpty());
+
+  res = storage.Search(FilterOp::kNe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 2, 3, 4));
+
+  res = storage.Search(FilterOp::kLt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(2, 4));
+
+  res = storage.Search(FilterOp::kLe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(2, 4));
+
+  res = storage.Search(FilterOp::kGt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 3));
+
+  res = storage.Search(FilterOp::kGe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 3));
+}
+
+TEST(NumericStorageUnittest, SearchInt32WithNegDouble) {
+  std::vector<int32_t> data_vec{-5, 5, -4, 4, -3, 3, 0};
+  NumericStorage<int32_t> storage(&data_vec, ColumnType::kInt32);
+  Range test_range(1, 5);
+  SqlValue val = SqlValue::Double(-3.5);
+
+  auto res = storage.Search(FilterOp::kEq, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), IsEmpty());
+
+  res = storage.Search(FilterOp::kNe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 2, 3, 4));
+
+  res = storage.Search(FilterOp::kLt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(2));
+
+  res = storage.Search(FilterOp::kLe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(2));
+
+  res = storage.Search(FilterOp::kGt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 3, 4));
+
+  res = storage.Search(FilterOp::kGe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 3, 4));
+}
+
+TEST(NumericStorageUnittest, IndexSearchInt32WithDouble) {
+  std::vector<int32_t> data_vec{-5, 5, -4, 4, -3, 3, 0};
+  NumericStorage<int32_t> storage(&data_vec, ColumnType::kInt32);
+
+  // -5, -3, -3, 3, 5, 0
+  std::vector<uint32_t> indices{0, 4, 4, 5, 1, 6};
+  SqlValue val = SqlValue::Double(1.5);
+
+  auto res = storage.IndexSearch(FilterOp::kEq, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), IsEmpty());
+
+  res = storage.IndexSearch(FilterOp::kNe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2, 3, 4, 5));
+
+  res = storage.IndexSearch(FilterOp::kLt, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2, 5));
+
+  res = storage.IndexSearch(FilterOp::kLe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2, 5));
+
+  res = storage.IndexSearch(FilterOp::kGt, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(3, 4));
+
+  res = storage.IndexSearch(FilterOp::kGe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(3, 4));
+}
+
+TEST(NumericStorageUnittest, IndexSearchInt32WithNegDouble) {
+  std::vector<int32_t> data_vec{-5, 5, -4, 4, -3, 3, 0};
+  NumericStorage<int32_t> storage(&data_vec, ColumnType::kInt32);
+
+  // -5, -3, -3, 3, 5, 0
+  std::vector<uint32_t> indices{0, 4, 4, 5, 1, 6};
+  SqlValue val = SqlValue::Double(-2.5);
+
+  auto res = storage.IndexSearch(FilterOp::kEq, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), IsEmpty());
+
+  res = storage.IndexSearch(FilterOp::kNe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2, 3, 4, 5));
+
+  res = storage.IndexSearch(FilterOp::kLt, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2));
+
+  res = storage.IndexSearch(FilterOp::kLe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2));
+
+  res = storage.IndexSearch(FilterOp::kGt, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(3, 4, 5));
+
+  res = storage.IndexSearch(FilterOp::kGe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(3, 4, 5));
+}
+
+TEST(NumericStorageUnittest, SearchUint32WithNegDouble) {
+  std::vector<uint32_t> data_vec{0, 1, 2, 3, 4, 5};
+  NumericStorage<uint32_t> storage(&data_vec, ColumnType::kInt32);
+  Range test_range(1, 5);
+  SqlValue val = SqlValue::Double(-3.5);
+
+  auto res = storage.Search(FilterOp::kEq, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), IsEmpty());
+
+  res = storage.Search(FilterOp::kNe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 2, 3, 4));
+
+  res = storage.Search(FilterOp::kLt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), IsEmpty());
+
+  res = storage.Search(FilterOp::kLe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), IsEmpty());
+
+  res = storage.Search(FilterOp::kGt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 2, 3, 4));
+
+  res = storage.Search(FilterOp::kGe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 2, 3, 4));
+}
+
+TEST(NumericStorageUnittest, IndexSearchUint32WithNegDouble) {
+  std::vector<uint32_t> data_vec{0, 1, 2, 3, 4, 5, 6};
+  NumericStorage<uint32_t> storage(&data_vec, ColumnType::kInt32);
+
+  std::vector<uint32_t> indices{0, 4, 4, 5, 1, 6};
+  SqlValue val = SqlValue::Double(-2.5);
+
+  auto res = storage.IndexSearch(FilterOp::kEq, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), IsEmpty());
+
+  res = storage.IndexSearch(FilterOp::kNe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2, 3, 4, 5));
+
+  res = storage.IndexSearch(FilterOp::kLt, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), IsEmpty());
+
+  res = storage.IndexSearch(FilterOp::kLe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), IsEmpty());
+
+  res = storage.IndexSearch(FilterOp::kGt, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2, 3, 4, 5));
+
+  res = storage.IndexSearch(FilterOp::kGe, val, indices.data(), 6, false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2, 3, 4, 5));
+}
+
+TEST(NumericStorageUnittest, DoubleColumnWithIntThatCantBeRepresentedAsDouble) {
+  // Sanity check that this value can't be represented as double.
+  int64_t not_rep_i = 9007199254740993;
+  EXPECT_FALSE(std::nextafter(static_cast<double>(not_rep_i), 1.0) ==
+               static_cast<double>(not_rep_i));
+  SqlValue val = SqlValue::Long(not_rep_i);
+
+  std::vector<double> data_vec{9007199254740992.0, 9007199254740994.0};
+
+  // Whether LongToDouble has the expected results.
+  ASSERT_TRUE(compare::LongToDouble(not_rep_i, data_vec[0]) > 0);
+  ASSERT_TRUE(compare::LongToDouble(not_rep_i, data_vec[1]) < 0);
+
+  NumericStorage<double> storage(&data_vec, ColumnType::kDouble);
+  Range test_range(0, 2);
+
+  auto res = storage.Search(FilterOp::kEq, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), IsEmpty());
+
+  res = storage.Search(FilterOp::kNe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1));
+
+  res = storage.Search(FilterOp::kLt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0));
+
+  res = storage.Search(FilterOp::kLe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0));
+
+  res = storage.Search(FilterOp::kGt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1));
+
+  res = storage.Search(FilterOp::kGe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1));
+}
+
+TEST(NumericStorageUnittest,
+     DoubleColumnWithNegIntThatCantBeRepresentedAsDouble) {
+  // Sanity check that this value can't be represented as double.
+  int64_t not_rep_i = -9007199254740993;
+  EXPECT_FALSE(std::nextafter(static_cast<double>(not_rep_i), 1.0) ==
+               static_cast<double>(not_rep_i));
+  SqlValue val = SqlValue::Long(not_rep_i);
+
+  std::vector<double> data_vec{-9007199254740992.0, -9007199254740994.0};
+
+  // Whether LongToDouble has the expected results.
+  ASSERT_TRUE(compare::LongToDouble(not_rep_i, data_vec[0]) < 0);
+  ASSERT_TRUE(compare::LongToDouble(not_rep_i, data_vec[1]) > 0);
+
+  NumericStorage<double> storage(&data_vec, ColumnType::kDouble);
+  Range test_range(0, 2);
+
+  auto res = storage.Search(FilterOp::kEq, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), IsEmpty());
+
+  res = storage.Search(FilterOp::kNe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1));
+
+  res = storage.Search(FilterOp::kLt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1));
+
+  res = storage.Search(FilterOp::kLe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1));
+
+  res = storage.Search(FilterOp::kGt, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0));
+
+  res = storage.Search(FilterOp::kGe, val, test_range);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0));
+}
+
 }  // namespace
 }  // namespace storage
 }  // namespace trace_processor
diff --git a/tools/cpu_profile b/tools/cpu_profile
index 9654a2a..c33235f 100755
--- a/tools/cpu_profile
+++ b/tools/cpu_profile
@@ -37,18 +37,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
-# This file has been generated by: tools/roll-prebuilts v40.0
+# This file has been generated by: tools/roll-prebuilts 3e53e144bee271ec558363df2e561a77d7e0b789
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        9184800,
+        9348712,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-amd64/traceconv',
     'sha256':
-        'b651d0a5b5606c1c3e24723e94d8ecb233a01f0dfccc95a2c6a4e773cb8f52d7',
+        '466110b5d92cfc7951ae3223147156dc3ddfad055f8c7a93fea91b2f1844d013',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -58,11 +58,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7761896,
+        7927048,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-arm64/traceconv',
     'sha256':
-        '3b019f5ddd5293d3181f7c30f91dc7b08f3a2e83ebb3b52b8f3905dc5161747d',
+        '46663d0eaa88bc821ad71872fd9789c340f9100e22a78494006b94225e2cfe9b',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -72,11 +72,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8928296,
+        9091432,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-amd64/traceconv',
     'sha256':
-        '830d20ffec266218d49f6b6c8efed4538bc59b51d8d2f735cbbb6a1435131b50',
+        'bb0eabefb6cb22623368d1b51cd95a6d9485781f102a9293443659fdd826fc8f',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -86,11 +86,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        6770204,
+        6934532,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm/traceconv',
     'sha256':
-        '93a9e5ccb94559b871af8f6da45f858aee01801b31776703892dcf3d7ea769b7',
+        'ce90e026c71a006b01e4d3c247772cfeeeb7581cd2ce1b003de35ddd87e9d349',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -100,11 +100,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8393944,
+        8558824,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm64/traceconv',
     'sha256':
-        '88a92ccbcd8e851673e018b7f599514daf05dde9b7e4de9641fa5629124abf12',
+        'f26446b306b0025c54d39dac5b62ae015e0d1771ff5ec461ccbbcc93f1dcb335',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -114,55 +114,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        6378744,
+        6542848,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm/traceconv',
     'sha256':
-        '6cb7d30d656aa4f172e6724f105a56e249e7043ecf637c65e1e3868885535cff'
+        '901512a8243b4015aeba59f547d40736fcf032edeedcd1ecb59c259bba0122cf'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        7692488,
+        7856856,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm64/traceconv',
     'sha256':
-        '1668808efbdf8d5b116d4716d61d2bd002f71ce465206d3b83af4fcc7a4c19cd'
+        '0eda603b5bc4925b98e0d9656dc18e3e7c4db4685c49f902fd3e1fad1be75efd'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        8557756,
+        8721860,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x86/traceconv',
     'sha256':
-        '653733582cae0021eae0e1b5d8db387c1bae772d77b307f1e2111b78ec4ea67c'
+        'd9bc2acf1b280198402a81019d6d5c9f75849d0254fd929b62671af6095d6f57'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        8708352,
+        8872720,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x64/traceconv',
     'sha256':
-        '7fc564ac581b81d79573f57dae027c47bd7a857ff0f89df984380c3c657d5876'
+        '3c33c2f79a4fb9760cdf2348a09d6fb6fbb8b5098e014e83cd14d3f3dfb8661d'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        8204288,
+        8369664,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/windows-amd64/traceconv.exe',
     'sha256':
-        'e33bad8061f08f9c3cfe6e91ef6f1696b6ac90d0799edcb57052f24888b436e2',
+        '900f931d89af74b84229e1c396898a9c492d49acd15977692040c95a50499936',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/heap_profile b/tools/heap_profile
index 77885fc..e002051 100755
--- a/tools/heap_profile
+++ b/tools/heap_profile
@@ -34,18 +34,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
-# This file has been generated by: tools/roll-prebuilts v40.0
+# This file has been generated by: tools/roll-prebuilts 3e53e144bee271ec558363df2e561a77d7e0b789
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        9184800,
+        9348712,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-amd64/traceconv',
     'sha256':
-        'b651d0a5b5606c1c3e24723e94d8ecb233a01f0dfccc95a2c6a4e773cb8f52d7',
+        '466110b5d92cfc7951ae3223147156dc3ddfad055f8c7a93fea91b2f1844d013',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -55,11 +55,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7761896,
+        7927048,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-arm64/traceconv',
     'sha256':
-        '3b019f5ddd5293d3181f7c30f91dc7b08f3a2e83ebb3b52b8f3905dc5161747d',
+        '46663d0eaa88bc821ad71872fd9789c340f9100e22a78494006b94225e2cfe9b',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -69,11 +69,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8928296,
+        9091432,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-amd64/traceconv',
     'sha256':
-        '830d20ffec266218d49f6b6c8efed4538bc59b51d8d2f735cbbb6a1435131b50',
+        'bb0eabefb6cb22623368d1b51cd95a6d9485781f102a9293443659fdd826fc8f',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -83,11 +83,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        6770204,
+        6934532,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm/traceconv',
     'sha256':
-        '93a9e5ccb94559b871af8f6da45f858aee01801b31776703892dcf3d7ea769b7',
+        'ce90e026c71a006b01e4d3c247772cfeeeb7581cd2ce1b003de35ddd87e9d349',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -97,11 +97,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8393944,
+        8558824,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm64/traceconv',
     'sha256':
-        '88a92ccbcd8e851673e018b7f599514daf05dde9b7e4de9641fa5629124abf12',
+        'f26446b306b0025c54d39dac5b62ae015e0d1771ff5ec461ccbbcc93f1dcb335',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -111,55 +111,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        6378744,
+        6542848,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm/traceconv',
     'sha256':
-        '6cb7d30d656aa4f172e6724f105a56e249e7043ecf637c65e1e3868885535cff'
+        '901512a8243b4015aeba59f547d40736fcf032edeedcd1ecb59c259bba0122cf'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        7692488,
+        7856856,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm64/traceconv',
     'sha256':
-        '1668808efbdf8d5b116d4716d61d2bd002f71ce465206d3b83af4fcc7a4c19cd'
+        '0eda603b5bc4925b98e0d9656dc18e3e7c4db4685c49f902fd3e1fad1be75efd'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        8557756,
+        8721860,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x86/traceconv',
     'sha256':
-        '653733582cae0021eae0e1b5d8db387c1bae772d77b307f1e2111b78ec4ea67c'
+        'd9bc2acf1b280198402a81019d6d5c9f75849d0254fd929b62671af6095d6f57'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        8708352,
+        8872720,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x64/traceconv',
     'sha256':
-        '7fc564ac581b81d79573f57dae027c47bd7a857ff0f89df984380c3c657d5876'
+        '3c33c2f79a4fb9760cdf2348a09d6fb6fbb8b5098e014e83cd14d3f3dfb8661d'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        8204288,
+        8369664,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/windows-amd64/traceconv.exe',
     'sha256':
-        'e33bad8061f08f9c3cfe6e91ef6f1696b6ac90d0799edcb57052f24888b436e2',
+        '900f931d89af74b84229e1c396898a9c492d49acd15977692040c95a50499936',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/record_android_trace b/tools/record_android_trace
index d4a740a..59bde1c 100755
--- a/tools/record_android_trace
+++ b/tools/record_android_trace
@@ -33,18 +33,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
-# This file has been generated by: tools/roll-prebuilts v40.0
+# This file has been generated by: tools/roll-prebuilts 3e53e144bee271ec558363df2e561a77d7e0b789
 TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'tracebox',
     'file_size':
-        1498816,
+        1515200,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-amd64/tracebox',
     'sha256':
-        '185014447d35357edbd20e7ce9924842a0d5c6576bd2257abae2ed48b65fd3b8',
+        'b451e873b1f6c8bd2fc3f1e12adc381c77a5b6dce9ec28ad8788e6f7f5efd348',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -56,9 +56,9 @@
     'file_size':
         1392776,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-arm64/tracebox',
     'sha256':
-        '082bb50e64df5e232673eebb1cd8b0dd752a394105f600cb0262730833f6b7f3',
+        '8247045bd78e467aa010674277048658cdf04a95bd15b25dabc973fce011d6db',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -68,11 +68,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2229096,
+        2236584,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-amd64/tracebox',
     'sha256':
-        'c99120caedb845e1c3fad4428263a683b44c357c76d65848dd8e437250066e38',
+        'adf6f9ebb5686a7d2e056c8f059adf4d95e867ffa39d27b041bef16d3cb7e1c7',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -82,11 +82,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1339796,
+        1344188,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm/tracebox',
     'sha256':
-        '6732165916b74f0b820991d1aaed2086a6b56e91f6c604291efe6636f0bdda71',
+        'e2feeeaf38c3cd9efbf992dbe09f40fca81651d9fd9159e0f9a95868e2c4e07e',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -96,11 +96,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2157312,
+        2164560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm64/tracebox',
     'sha256':
-        '7d09865a6d7118e67d2acd0c56b2a94ce8bd5f614869d29a72fe633515ab1fbd',
+        '5d89a6c16819a74be44bf731f2d07bfb83924a2e560554a373feeb2bb9940ef1',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -112,31 +112,31 @@
     'file_size':
         1247188,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm/tracebox',
     'sha256':
-        '4ecc192172ac2bca49557cbdbb1f7d660718d4fb4a7314fd19b2b2e52be8bc0c'
+        '5837e88a92b8bc00d5575f9dc02dd314f16b2e0a1bb174efb6c092a8f639e7c4'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'tracebox',
     'file_size':
-        1854120,
+        1870504,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm64/tracebox',
     'sha256':
-        '1ca89113279d5c6a9ae273bde03b4d84373efe6923dc637cb840908f13b9639e'
+        '75431d11aec11f59b87a76fa31cf92a1f8e534ad4118357b3654a458cf547081'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'tracebox',
     'file_size':
-        1853356,
+        1869740,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x86/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x86/tracebox',
     'sha256':
-        'cf689a191c1252734ebbfda3106600da324610f761515cfbffbeac2ebdfee715'
+        '388da3a4248f105bc56685db4835ce487633035334efa543b7190ac4a9e26bde'
 }, {
     'arch':
         'android-x64',
@@ -145,9 +145,9 @@
     'file_size':
         2149032,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x64/tracebox',
     'sha256':
-        '99e9ebdb5b5308d95551a4ad060d615d7defb6877c4061d21c783c45a71d372f'
+        '1c3d50a4066f9b3478ad65431532c3503a2ad73ee89d346e6ee12f7fb0c93aaa'
 }]
 
 # ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py
diff --git a/tools/trace_processor b/tools/trace_processor
index 60de377..4a839a3 100755
--- a/tools/trace_processor
+++ b/tools/trace_processor
@@ -30,18 +30,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/trace_processor_shell.py
-# This file has been generated by: tools/roll-prebuilts v40.0
+# This file has been generated by: tools/roll-prebuilts 3e53e144bee271ec558363df2e561a77d7e0b789
 TRACE_PROCESSOR_SHELL_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9978200,
+        10142120,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-amd64/trace_processor_shell',
     'sha256':
-        'f3e21eb29fb51cb2ea9b81b69132c5ae93ce3276c57ccd27fcf7c675306b4e41',
+        '44585789d420d0bc38edc3dd6fbade4c4a718dc535fb68ac7a2449bf1a251f30',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -51,11 +51,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8493976,
+        8659128,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-arm64/trace_processor_shell',
     'sha256':
-        '84f35765141374b8d883813ac533e0c004cf72d1c6f05aef0c973364ff541eb9',
+        'bccca60b99fb2c587503a6430e0b15204ebeddb97607006d54f203e64ac923af',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -65,11 +65,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9830856,
+        9987880,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-amd64/trace_processor_shell',
     'sha256':
-        'b3dc0a9c641b84a57fa5d59637921ae2237e4f05b1778341a691df220faf0cd7',
+        '3510cfc89e627a95ede2d06e77d1ddd70d31ef99d0d213d1fbaba4f438030e4c',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -79,11 +79,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        7231096,
+        7389416,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm/trace_processor_shell',
     'sha256':
-        'a21252830fb1bbb7b3fd9665ce6e70920cffa6b1e72c16589c90896c002c3348',
+        '2a87d3587e9a756ea486746ba08e7535dacf8872b1d2081e9381a612668d154d',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9238056,
+        9396856,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm64/trace_processor_shell',
     'sha256':
-        'f77519ec19743ec2c22ed78fe3a20106a482a28d77c4154378af108c5f7bdd4a',
+        '42738a5aa187fe7644d87361fcf9854459f3d46a301026c9ac76ac4bf0ec9d3e',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -107,55 +107,55 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        6870968,
+        7018688,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm/trace_processor_shell',
     'sha256':
-        '2c7055fb44085ec60ad8bb970d495c9c88070fce08902f11fcd44e0ae3369876'
+        '9d8247c09b82835dc5019fed2f41e8844162a8cbf520e4243111852d6398911b'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8414568,
+        8578936,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm64/trace_processor_shell',
     'sha256':
-        'd8ca0dc2bab7ea604a6721f0ac0e2b433b43261f247c6c98c510dc17aafe5a72'
+        '91405004e1a47b1170eb32315f3d0a5b287bc71cb856e7403fad0f02e8b4dfd1'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9328508,
+        9492612,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x86/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x86/trace_processor_shell',
     'sha256':
-        'de6a6ea45769888e59a1678d37b6e355b27b834d34a0b9e4980a942d333b88cc'
+        '01364fc6fb485b20d838326462239d14c8f2daf1e7dde524b2cba4fd5acb8a73'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9577896,
+        9742264,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x64/trace_processor_shell',
     'sha256':
-        'cd4b16c5f78a060934204737ba8b312e824ff7cc28f3732daf7d64e733a727f9'
+        '43b456dcd0238f52f5730c6f5b9f8249f096df718d3fefa443c3246d66df4bd6'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'trace_processor_shell.exe',
     'file_size':
-        9248256,
+        9408000,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/windows-amd64/trace_processor_shell.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/windows-amd64/trace_processor_shell.exe',
     'sha256':
-        '26584b4bbab40f8b0ad991a869e7483f92d7223e1473b879a6ceafa49b76390a',
+        'b4dc6a7968673373265344e8e5249ccc31f7d12bb9df527370065dff71b1e74a',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/tracebox b/tools/tracebox
index a4c278b..402a52d 100755
--- a/tools/tracebox
+++ b/tools/tracebox
@@ -30,18 +30,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
-# This file has been generated by: tools/roll-prebuilts v40.0
+# This file has been generated by: tools/roll-prebuilts 3e53e144bee271ec558363df2e561a77d7e0b789
 TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'tracebox',
     'file_size':
-        1498816,
+        1515200,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-amd64/tracebox',
     'sha256':
-        '185014447d35357edbd20e7ce9924842a0d5c6576bd2257abae2ed48b65fd3b8',
+        'b451e873b1f6c8bd2fc3f1e12adc381c77a5b6dce9ec28ad8788e6f7f5efd348',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -53,9 +53,9 @@
     'file_size':
         1392776,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-arm64/tracebox',
     'sha256':
-        '082bb50e64df5e232673eebb1cd8b0dd752a394105f600cb0262730833f6b7f3',
+        '8247045bd78e467aa010674277048658cdf04a95bd15b25dabc973fce011d6db',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -65,11 +65,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2229096,
+        2236584,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-amd64/tracebox',
     'sha256':
-        'c99120caedb845e1c3fad4428263a683b44c357c76d65848dd8e437250066e38',
+        'adf6f9ebb5686a7d2e056c8f059adf4d95e867ffa39d27b041bef16d3cb7e1c7',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -79,11 +79,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1339796,
+        1344188,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm/tracebox',
     'sha256':
-        '6732165916b74f0b820991d1aaed2086a6b56e91f6c604291efe6636f0bdda71',
+        'e2feeeaf38c3cd9efbf992dbe09f40fca81651d9fd9159e0f9a95868e2c4e07e',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2157312,
+        2164560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm64/tracebox',
     'sha256':
-        '7d09865a6d7118e67d2acd0c56b2a94ce8bd5f614869d29a72fe633515ab1fbd',
+        '5d89a6c16819a74be44bf731f2d07bfb83924a2e560554a373feeb2bb9940ef1',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -109,31 +109,31 @@
     'file_size':
         1247188,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm/tracebox',
     'sha256':
-        '4ecc192172ac2bca49557cbdbb1f7d660718d4fb4a7314fd19b2b2e52be8bc0c'
+        '5837e88a92b8bc00d5575f9dc02dd314f16b2e0a1bb174efb6c092a8f639e7c4'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'tracebox',
     'file_size':
-        1854120,
+        1870504,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm64/tracebox',
     'sha256':
-        '1ca89113279d5c6a9ae273bde03b4d84373efe6923dc637cb840908f13b9639e'
+        '75431d11aec11f59b87a76fa31cf92a1f8e534ad4118357b3654a458cf547081'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'tracebox',
     'file_size':
-        1853356,
+        1869740,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x86/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x86/tracebox',
     'sha256':
-        'cf689a191c1252734ebbfda3106600da324610f761515cfbffbeac2ebdfee715'
+        '388da3a4248f105bc56685db4835ce487633035334efa543b7190ac4a9e26bde'
 }, {
     'arch':
         'android-x64',
@@ -142,9 +142,9 @@
     'file_size':
         2149032,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x64/tracebox',
     'sha256':
-        '99e9ebdb5b5308d95551a4ad060d615d7defb6877c4061d21c783c45a71d372f'
+        '1c3d50a4066f9b3478ad65431532c3503a2ad73ee89d346e6ee12f7fb0c93aaa'
 }]
 
 # ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py
diff --git a/tools/traceconv b/tools/traceconv
index 0136e37..63997fb 100755
--- a/tools/traceconv
+++ b/tools/traceconv
@@ -30,18 +30,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
-# This file has been generated by: tools/roll-prebuilts v40.0
+# This file has been generated by: tools/roll-prebuilts 3e53e144bee271ec558363df2e561a77d7e0b789
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        9184800,
+        9348712,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-amd64/traceconv',
     'sha256':
-        'b651d0a5b5606c1c3e24723e94d8ecb233a01f0dfccc95a2c6a4e773cb8f52d7',
+        '466110b5d92cfc7951ae3223147156dc3ddfad055f8c7a93fea91b2f1844d013',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -51,11 +51,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7761896,
+        7927048,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/mac-arm64/traceconv',
     'sha256':
-        '3b019f5ddd5293d3181f7c30f91dc7b08f3a2e83ebb3b52b8f3905dc5161747d',
+        '46663d0eaa88bc821ad71872fd9789c340f9100e22a78494006b94225e2cfe9b',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -65,11 +65,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8928296,
+        9091432,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-amd64/traceconv',
     'sha256':
-        '830d20ffec266218d49f6b6c8efed4538bc59b51d8d2f735cbbb6a1435131b50',
+        'bb0eabefb6cb22623368d1b51cd95a6d9485781f102a9293443659fdd826fc8f',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -79,11 +79,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        6770204,
+        6934532,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm/traceconv',
     'sha256':
-        '93a9e5ccb94559b871af8f6da45f858aee01801b31776703892dcf3d7ea769b7',
+        'ce90e026c71a006b01e4d3c247772cfeeeb7581cd2ce1b003de35ddd87e9d349',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8393944,
+        8558824,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/linux-arm64/traceconv',
     'sha256':
-        '88a92ccbcd8e851673e018b7f599514daf05dde9b7e4de9641fa5629124abf12',
+        'f26446b306b0025c54d39dac5b62ae015e0d1771ff5ec461ccbbcc93f1dcb335',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -107,55 +107,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        6378744,
+        6542848,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm/traceconv',
     'sha256':
-        '6cb7d30d656aa4f172e6724f105a56e249e7043ecf637c65e1e3868885535cff'
+        '901512a8243b4015aeba59f547d40736fcf032edeedcd1ecb59c259bba0122cf'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        7692488,
+        7856856,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-arm64/traceconv',
     'sha256':
-        '1668808efbdf8d5b116d4716d61d2bd002f71ce465206d3b83af4fcc7a4c19cd'
+        '0eda603b5bc4925b98e0d9656dc18e3e7c4db4685c49f902fd3e1fad1be75efd'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        8557756,
+        8721860,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x86/traceconv',
     'sha256':
-        '653733582cae0021eae0e1b5d8db387c1bae772d77b307f1e2111b78ec4ea67c'
+        'd9bc2acf1b280198402a81019d6d5c9f75849d0254fd929b62671af6095d6f57'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        8708352,
+        8872720,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/android-x64/traceconv',
     'sha256':
-        '7fc564ac581b81d79573f57dae027c47bd7a857ff0f89df984380c3c657d5876'
+        '3c33c2f79a4fb9760cdf2348a09d6fb6fbb8b5098e014e83cd14d3f3dfb8661d'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        8204288,
+        8369664,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v40.0/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/3e53e144bee271ec558363df2e561a77d7e0b789/windows-amd64/traceconv.exe',
     'sha256':
-        'e33bad8061f08f9c3cfe6e91ef6f1696b6ac90d0799edcb57052f24888b436e2',
+        '900f931d89af74b84229e1c396898a9c492d49acd15977692040c95a50499936',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/ui/release/channels.json b/ui/release/channels.json
index cce73f1..a8c38ae 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -2,11 +2,11 @@
   "channels": [
     {
       "name": "stable",
-      "rev": "fa6ee75b6f4a9374ade506697c65a8a73edc54be"
+      "rev": "ce5cae18bd9a71f9948a6c33315a4cbd2df4c44b"
     },
     {
       "name": "canary",
-      "rev": "46dae5655847f65acd49cad6a24ef37c6a85383d"
+      "rev": "4a28823dd05cf2642e17419a4bbbdf5c39dc52bc"
     },
     {
       "name": "autopush",
diff --git a/ui/src/base/logging.ts b/ui/src/base/logging.ts
index 945d96a..8387783 100644
--- a/ui/src/base/logging.ts
+++ b/ui/src/base/logging.ts
@@ -12,11 +12,21 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {SCM_REVISION, VERSION} from '../gen/perfetto_version';
+import {VERSION} from '../gen/perfetto_version';
 
-export type ErrorHandler = (err: string) => void;
+export type ErrorType = 'ERROR'|'PROMISE_REJ'|'OTHER';
+export interface ErrorStackEntry {
+  name: string;      // e.g. renderCanvas
+  location: string;  // e.g. frontend_bundle.js:12:3
+}
+export interface ErrorDetails {
+  errType: ErrorType;
+  message: string;  // Uncaught StoreError: No such subtree: tracks,1374,state
+  stack: ErrorStackEntry[];
+}
 
-let errorHandler: ErrorHandler = (_: string) => {};
+export type ErrorHandler = (err: ErrorDetails) => void;
+const errorHandlers: ErrorHandler[] = [];
 
 export function assertExists<A>(value: A | null | undefined): A {
   if (value === null || value === undefined) {
@@ -35,34 +45,86 @@
   assertTrue(!value, optMsg);
 }
 
-export function setErrorHandler(handler: ErrorHandler) {
-  errorHandler = handler;
+export function addErrorHandler(handler: ErrorHandler) {
+  if (!errorHandlers.includes(handler)) {
+    errorHandlers.push(handler);
+  }
 }
 
 export function reportError(err: ErrorEvent|PromiseRejectionEvent|{}) {
-  let errLog = '';
   let errorObj = undefined;
+  let errMsg = '';
+  let errType: ErrorType;
+  const stack: ErrorStackEntry[] = [];
+  const baseUrl = `${location.protocol}//${location.host}`;
 
   if (err instanceof ErrorEvent) {
-    errLog = err.message;
+    errType = 'ERROR';
+    errMsg = err.message;
     errorObj = err.error;
   } else if (err instanceof PromiseRejectionEvent) {
-    errLog = `${err.reason}`;
+    errType = 'PROMISE_REJ';
+    errMsg = `PromiseRejection: ${err.reason}`;
     errorObj = err.reason;
   } else {
-    errLog = `${err}`;
+    errType = 'OTHER';
+    errMsg = `Err: ${err}`;
   }
   if (errorObj !== undefined && errorObj !== null) {
-    const errStack = (errorObj as {stack?: string}).stack;
-    errLog += '\n';
-    errLog += errStack !== undefined ? errStack : JSON.stringify(errorObj);
-  }
-  errLog += '\n\n';
-  errLog += `${VERSION} ${SCM_REVISION}\n`;
-  errLog += `UA: ${navigator.userAgent}\n`;
+    const maybeStack = (errorObj as {stack?: string}).stack;
+    let errStack = maybeStack !== undefined ? `${maybeStack}` : '';
+    errStack = errStack.replaceAll(/\r/g, '');  // Strip Windows CR.
+    for (let line of errStack.split('\n')) {
+      if (errMsg.includes(line)) continue;
+      // Chrome, Firefox and safari don't agree on the stack format:
+      // Chrome: prefixes entries with a '  at ' and uses the format
+      //         function(https://url:line:col), e.g.
+      //         '    at FooBar (https://.../frontend_bundle.js:2073:15)'
+      //         however, if the function name is not known, it prints just:
+      //         '     at https://.../frontend_bundle.js:2073:15'
+      //         or also:
+      //         '     at <anonymous>:5:11'
+      // Firefox and Safari: don't have any prefix and use @ as a separator:
+      //         redrawCanvas@https://.../frontend_bundle.js:468814:26
+      //         @debugger eval code:1:32
 
-  console.error(errLog, err);
-  errorHandler(errLog);
+      // Here we first normalize Chrome into the Firefox/Safari format by
+      // removing the '   at ' prefix and replacing (xxx)$ into @xxx.
+      line = line.replace(/^\s*at\s*/, '');
+      line = line.replace(/\s*\(([^)]+)\)$/, '@$1');
+
+      // This leaves us still with two possible options here:
+      // 1. FooBar@https://ui.perfetto.dev/v123/frontend_bundle.js:2073:15
+      // 2. https://ui.perfetto.dev/v123/frontend_bundle.js:2073:15
+      const lastAt = line.lastIndexOf('@');
+      let entryName = '';
+      let entryLocation = '';
+      if (lastAt >= 0) {
+        entryLocation = line.substring(lastAt + 1);
+        entryName = line.substring(0, lastAt);
+      } else {
+        entryLocation = line;
+      }
+
+      // Remove redundant https://ui.perfetto.dev/v38.0-d6ed090ee/ as we have
+      // that information already and don't need to repeat it on each line.
+      if (entryLocation.includes(baseUrl)) {
+        entryLocation = entryLocation.replace(baseUrl, '');
+        entryLocation = entryLocation.replace(`/${VERSION}/`, '');
+      }
+      stack.push({name: entryName, location: entryLocation});
+    }
+  }
+  // Invoke all the handlers registered through addErrorHandler.
+  // There are usually two handlers registered, one for the UI (error_dialog.ts)
+  // and one for Analytics (analytics.ts).
+  for (const handler of errorHandlers) {
+    handler({
+      errType,
+      message: errMsg,
+      stack,
+    } as ErrorDetails);
+  }
 }
 
 // This function serves two purposes.
diff --git a/ui/src/common/canvas_utils.ts b/ui/src/common/canvas_utils.ts
index ad78f96..2f56b5f 100644
--- a/ui/src/common/canvas_utils.ts
+++ b/ui/src/common/canvas_utils.ts
@@ -151,7 +151,7 @@
   y -= 10;
 
   // Ensure the box is on screen:
-  const endPx = globals.frontendLocalState.visibleTimeScale.pxSpan.end;
+  const endPx = globals.timeline.visibleTimeScale.pxSpan.end;
   if (x + width > endPx) {
     x -= x + width - endPx;
   }
diff --git a/ui/src/common/recordingV2/recording_config_utils.ts b/ui/src/common/recordingV2/recording_config_utils.ts
index aadb254..94e4993 100644
--- a/ui/src/common/recordingV2/recording_config_utils.ts
+++ b/ui/src/common/recordingV2/recording_config_utils.ts
@@ -154,12 +154,14 @@
     protoCfg.dataSources.push(ds);
   }
 
+  let ftrace = false;
+  let symbolizeKsyms = false;
   if (uiCfg.cpuSched) {
     procThreadAssociationPolling = true;
     procThreadAssociationFtrace = true;
-    uiCfg.ftrace = true;
+    ftrace = true;
     if (enableSchedBlockedReason(androidApiLevel)) {
-      uiCfg.symbolizeKsyms = true;
+      symbolizeKsyms = true;
     }
     ftraceEvents.add('sched/sched_switch');
     ftraceEvents.add('power/suspend_resume');
@@ -612,7 +614,7 @@
     protoCfg.dataSources.push(ds);
   }
 
-  if (uiCfg.ftrace || uiCfg.atrace || ftraceEvents.size > 0 ||
+  if (uiCfg.ftrace || ftrace || uiCfg.atrace || ftraceEvents.size > 0 ||
       atraceCats.size > 0 || atraceApps.size > 0) {
     const ds = new TraceConfig.DataSource();
     ds.config = new DataSourceConfig();
@@ -620,14 +622,14 @@
     ds.config.ftraceConfig = new FtraceConfig();
     // Override the advanced ftrace parameters only if the user has ticked the
     // "Advanced ftrace config" tab.
-    if (uiCfg.ftrace) {
+    if (uiCfg.ftrace || ftrace) {
       if (uiCfg.ftraceBufferSizeKb) {
         ds.config.ftraceConfig.bufferSizeKb = uiCfg.ftraceBufferSizeKb;
       }
       if (uiCfg.ftraceDrainPeriodMs) {
         ds.config.ftraceConfig.drainPeriodMs = uiCfg.ftraceDrainPeriodMs;
       }
-      if (uiCfg.symbolizeKsyms) {
+      if (uiCfg.symbolizeKsyms || symbolizeKsyms) {
         ds.config.ftraceConfig.symbolizeKsyms = true;
         ftraceEvents.add('sched/sched_blocked_reason');
       }
diff --git a/ui/src/common/recordingV2/recording_config_utils_unittest.ts b/ui/src/common/recordingV2/recording_config_utils_unittest.ts
new file mode 100644
index 0000000..aca575e
--- /dev/null
+++ b/ui/src/common/recordingV2/recording_config_utils_unittest.ts
@@ -0,0 +1,90 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {createEmptyRecordConfig} from '../../controller/record_config_types';
+
+import {genTraceConfig} from './recording_config_utils';
+import {AndroidTargetInfo} from './recording_interfaces_v2';
+
+test('genTraceConfig() can run without manipulating the input config', () => {
+  const config = createEmptyRecordConfig();
+  config.cpuSched = true;  // Exercise ftrace
+
+  const targetInfo: AndroidTargetInfo = {
+    name: 'test',
+    targetType: 'ANDROID',
+    androidApiLevel: 31,  // >= 32 to exercise symbolizeKsyms
+    dataSources: [],
+  };
+
+  Object.freeze(config);
+  const actual = genTraceConfig(config, targetInfo);
+
+  const expected = {
+    'buffers': [
+      {
+        'sizeKb': 63488,
+        'fillPolicy': 'DISCARD',
+      },
+      {
+        'sizeKb': 2048,
+        'fillPolicy': 'DISCARD',
+      },
+    ],
+    'dataSources': [
+      {
+        'config': {
+          'name': 'android.packages_list',
+          'targetBuffer': 1,
+        },
+      },
+      {
+        'config': {
+          'name': 'linux.process_stats',
+          'targetBuffer': 1,
+          'processStatsConfig': {
+            'scanAllProcessesOnStart': true,
+          },
+        },
+      },
+      {
+        'config': {
+          'name': 'linux.ftrace',
+          'ftraceConfig': {
+            'ftraceEvents': [
+              'sched/sched_switch',
+              'power/suspend_resume',
+              'sched/sched_wakeup',
+              'sched/sched_wakeup_new',
+              'sched/sched_waking',
+              'sched/sched_process_exit',
+              'sched/sched_process_free',
+              'task/task_newtask',
+              'task/task_rename',
+              'sched/sched_blocked_reason',
+            ],
+            'compactSched': {
+              'enabled': true,
+            },
+            'symbolizeKsyms': true,
+          },
+        },
+      },
+    ],
+    'durationMs': 10000,
+  };
+
+  // Compare stringified versions to void issues with JS objects.
+  expect(JSON.stringify(actual)).toEqual(JSON.stringify(expected));
+});
diff --git a/ui/src/common/track_helper.ts b/ui/src/common/track_helper.ts
index 8990a44..3a11596 100644
--- a/ui/src/common/track_helper.ts
+++ b/ui/src/common/track_helper.ts
@@ -14,14 +14,13 @@
 
 import m from 'mithril';
 
-import {duration, Time, time} from '../base/time';
+import {Disposable} from '../base/disposable';
+import {duration, Time, time, TimeSpan} from '../base/time';
 import {raf} from '../core/raf_scheduler';
 import {globals} from '../frontend/globals';
 import {PanelSize} from '../frontend/panel';
 import {SliceRect, Track, TrackContext} from '../public';
 
-import {TrackData} from './track_data';
-
 export {Store} from '../frontend/store';
 export {EngineProxy} from '../trace_processor/engine';
 export {
@@ -33,6 +32,97 @@
   STR_NULL,
 } from '../trace_processor/query_result';
 
+type FetchTimeline<Data> = (start: time, end: time, resolution: duration) =>
+    Promise<Data>;
+
+// This helper provides the logic to call |doFetch()| only when more
+// data is needed as the visible window is panned and zoomed about, and
+// includes an FSM to ensure doFetch is not re-entered.
+class TimelineFetcher<Data> implements Disposable {
+  private requestingData = false;
+  private queuedRequest = false;
+  private doFetch: FetchTimeline<Data>;
+
+  private data_?: Data;
+
+  // Timespan and resolution of the latest *request*. data_ may cover
+  // a different time window.
+  private latestTimespan: TimeSpan;
+  private latestResolution: duration;
+
+  constructor(doFetch: FetchTimeline<Data>) {
+    this.doFetch = doFetch;
+    this.latestTimespan = TimeSpan.ZERO;
+    this.latestResolution = 0n;
+  }
+
+  requestDataForCurrentTime(): void {
+    const currentTimeSpan = globals.timeline.visibleTimeSpan;
+    const currentResolution = globals.getCurResolution();
+    this.requestData(currentTimeSpan, currentResolution);
+  }
+
+  requestData(timespan: TimeSpan, resolution: duration): void {
+    if (this.shouldLoadNewData(timespan, resolution)) {
+      // Over request data, one page worth to the left and right.
+      const start = Time.sub(timespan.start, timespan.duration);
+      const end = Time.add(timespan.end, timespan.duration);
+      this.latestTimespan = new TimeSpan(start, end);
+      this.latestResolution = resolution;
+      this.loadData();
+    }
+  }
+
+  get data(): Data|undefined {
+    return this.data_;
+  }
+
+  dispose() {
+    this.queuedRequest = false;
+    this.data_ = undefined;
+  }
+
+  private shouldLoadNewData(timespan: TimeSpan, resolution: duration): boolean {
+    if (this.data_ === undefined) {
+      return true;
+    }
+
+    if (timespan.start < this.latestTimespan.start) {
+      return true;
+    }
+
+    if (timespan.end > this.latestTimespan.end) {
+      return true;
+    }
+
+    if (resolution !== this.latestResolution) {
+      return true;
+    }
+
+    return false;
+  }
+
+  private loadData(): void {
+    if (this.requestingData) {
+      this.queuedRequest = true;
+      return;
+    }
+    const {start, end} = this.latestTimespan;
+    const resolution = this.latestResolution;
+    this.doFetch(start, end, resolution).then((data) => {
+      this.requestingData = false;
+      this.data_ = data;
+      if (this.queuedRequest) {
+        this.queuedRequest = false;
+        this.loadData();
+      } else {
+        raf.scheduleRedraw();
+      }
+    });
+    this.requestingData = true;
+  }
+}
+
 // A helper class which provides a base track implementation for tracks which
 // load their content asynchronously from the trace.
 //
@@ -49,17 +139,21 @@
 // Note: This class is deprecated and should not be used for new tracks. Use
 // |BaseSliceTrack| instead.
 export abstract class TrackHelperLEGACY<Data> implements Track {
-  private requestingData = false;
-  private queuedRequest = false;
-  private currentState?: TrackData;
-  protected data?: Data;
+  private timelineFetcher: TimelineFetcher<Data>;
+
+  constructor() {
+    this.timelineFetcher =
+        new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
+  }
 
   onCreate(_ctx: TrackContext): void {}
 
   onDestroy(): void {
-    this.queuedRequest = false;
-    this.currentState = undefined;
-    this.data = undefined;
+    this.timelineFetcher.dispose();
+  }
+
+  get data(): Data|undefined {
+    return this.timelineFetcher.data;
   }
 
   // Returns a place where a given slice should be drawn. Should be implemented
@@ -93,65 +187,7 @@
   abstract renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize): void;
 
   render(ctx: CanvasRenderingContext2D, size: PanelSize): void {
-    if (this.shouldLoadNewData()) {
-      this.loadData();
-    }
-
+    this.timelineFetcher.requestDataForCurrentTime();
     this.renderCanvas(ctx, size);
   }
-
-  private loadData(): void {
-    if (this.requestingData) {
-      this.queuedRequest = true;
-      return;
-    }
-
-    const ts = globals.frontendLocalState.visibleTimeSpan;
-    const resolution = globals.getCurResolution();
-
-    const start = Time.sub(ts.start, ts.duration);
-    const end = Time.add(ts.end, ts.duration);
-
-    this.currentState = {
-      start,
-      end,
-      resolution,
-      length: 0,
-    };
-
-    this.onBoundsChange(start, end, resolution).then((data) => {
-      this.requestingData = false;
-      this.data = data;
-
-      if (this.queuedRequest) {
-        this.queuedRequest = false;
-        this.loadData();
-      } else {
-        raf.scheduleRedraw();
-      }
-    });
-
-    this.requestingData = true;
-  }
-
-  private shouldLoadNewData(): boolean {
-    if (!this.currentState) {
-      return true;
-    }
-
-    const ts = globals.frontendLocalState.visibleTimeSpan;
-    if (ts.start < this.currentState.start) {
-      return true;
-    }
-
-    if (ts.end > this.currentState.end) {
-      return true;
-    }
-
-    if (globals.getCurResolution() !== this.currentState.resolution) {
-      return true;
-    }
-
-    return false;
-  }
 }
diff --git a/ui/src/controller/ftrace_controller.ts b/ui/src/controller/ftrace_controller.ts
index e596ff4..e4f13cc 100644
--- a/ui/src/controller/ftrace_controller.ts
+++ b/ui/src/controller/ftrace_controller.ts
@@ -49,7 +49,7 @@
 
   run() {
     if (this.shouldUpdate()) {
-      this.oldSpan = globals.frontendLocalState.visibleWindowTime;
+      this.oldSpan = globals.timeline.visibleWindowTime;
       this.oldFtraceFilter = globals.state.ftraceFilter;
       this.oldPagination = globals.state.ftracePagination;
       if (globals.state.ftracePagination.count > 0) {
@@ -69,7 +69,7 @@
 
   private shouldUpdate(): boolean {
     // Has the visible window moved?
-    const visibleWindow = globals.frontendLocalState.visibleWindowTime;
+    const visibleWindow = globals.timeline.visibleWindowTime;
     if (!this.oldSpan.equals(visibleWindow)) {
       return true;
     }
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index 1df6bee..afeb64f 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -1658,7 +1658,7 @@
 
   async decideTracks(): Promise<DeferredAction[]> {
     {
-      const result = screenshotDecideTracks();
+      const result = screenshotDecideTracks(this.engine);
       if (result !== null) {
         const {tracksToAdd} = await result;
         this.tracksToAdd.push(...tracksToAdd);
diff --git a/ui/src/frontend/analytics.ts b/ui/src/frontend/analytics.ts
index 74c42fc..25f8e96 100644
--- a/ui/src/frontend/analytics.ts
+++ b/ui/src/frontend/analytics.ts
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {ErrorDetails} from '../base/logging';
 import {getCurrentChannel} from '../common/channels';
 import {VERSION} from '../gen/perfetto_version';
 
@@ -58,16 +59,16 @@
 export interface Analytics {
   initialize(): void;
   updatePath(_: string): void;
-  logEvent(_x: TraceCategories|null, _y: string): void;
-  logError(_x: string, _y?: boolean): void;
+  logEvent(category: TraceCategories|null, event: string): void;
+  logError(err: ErrorDetails): void;
   isEnabled(): boolean;
 }
 
 export class NullAnalytics implements Analytics {
   initialize() {}
   updatePath(_: string) {}
-  logEvent(_x: TraceCategories|null, _y: string) {}
-  logError(_x: string) {}
+  logEvent(_category: TraceCategories|null, _event: string) {}
+  logError(_err: ErrorDetails) {}
   isEnabled(): boolean {
     return false;
   }
@@ -135,8 +136,24 @@
     gtagGlobals.gtag('event', event, {event_category: category});
   }
 
-  logError(description: string, fatal = true) {
-    gtagGlobals.gtag('event', 'exception', {description, fatal});
+  logError(err: ErrorDetails) {
+    let stack = '';
+    for (const entry of err.stack) {
+      const shortLocation = entry.location.replace('frontend_bundle.js', '$');
+      stack += `${entry.name}(${shortLocation}),`;
+    }
+    // Strip trailing ',' (works also for empty strings without extra checks).
+    stack = stack.substring(0, stack.length - 1);
+
+    gtagGlobals.gtag('event', 'exception', {
+      description: err.message,
+      error_type: err.errType,
+
+      // As per GA4 all field are restrictred to 100 chars.
+      // page_title is the only one restricted to 1000 chars and we use that for
+      // the full crash report.
+      page_location: `http://crash?/${encodeURI(stack)}`,
+    });
   }
 
   isEnabled(): boolean {
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index 5081cb7..079fb1a 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -180,7 +180,7 @@
     const {
       visibleTimeScale: timeScale,
       visibleWindowTime: vizTime,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
 
     {
       const windowSizePx = Math.max(1, timeScale.pxSpan.delta);
@@ -383,7 +383,7 @@
     const data = this.counters;
     if (data === undefined) return;
     this.mousePos = pos;
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const time = visibleTimeScale.pxToHpTime(pos.x);
 
     let values = data.lastValues;
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 95855cf..30778c2 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -332,7 +332,7 @@
     const {
       visibleTimeScale: timeScale,
       visibleWindowTime: vizTime,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
 
     {
       const windowSizePx = Math.max(1, timeScale.pxSpan.delta);
@@ -816,7 +816,7 @@
     }
 
     for (const slice of this.incomplete) {
-      const visibleTimeScale = globals.frontendLocalState.visibleTimeScale;
+      const visibleTimeScale = globals.timeline.visibleTimeScale;
       const startPx = CROP_INCOMPLETE_SLICE_FLAG.get() ?
           visibleTimeScale.timeToPx(slice.startNsQ) :
           slice.x;
@@ -973,7 +973,7 @@
       windowSpan,
       visibleTimeScale,
       visibleTimeSpan,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
 
     const pxEnd = windowSpan.end;
     const left = Math.max(visibleTimeScale.timeToPx(tStart), 0);
diff --git a/ui/src/frontend/drag/drag_strategy.ts b/ui/src/frontend/drag/drag_strategy.ts
index 7c6a1ca..869921e 100644
--- a/ui/src/frontend/drag/drag_strategy.ts
+++ b/ui/src/frontend/drag/drag_strategy.ts
@@ -28,7 +28,7 @@
 
   protected updateGlobals(tStart: HighPrecisionTime, tEnd: HighPrecisionTime) {
     const vizTime = new HighPrecisionTimeSpan(tStart, tEnd);
-    globals.frontendLocalState.updateVisibleTime(vizTime);
+    globals.timeline.updateVisibleTime(vizTime);
     raf.scheduleRedraw();
   }
 }
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index e94bfd7..008f1bc 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -14,30 +14,26 @@
 
 import m from 'mithril';
 
-import {assertExists} from '../base/logging';
+import {assertExists, ErrorDetails} from '../base/logging';
 import {EXTENSION_URL} from '../common/recordingV2/recording_utils';
 import {TraceUrlSource} from '../common/state';
 import {saveTrace} from '../common/upload_utils';
 import {RECORDING_V2_FLAG} from '../core/feature_flags';
+import {VERSION} from '../gen/perfetto_version';
 
 import {globals} from './globals';
 import {showModal} from './modal';
 import {isShareable} from './trace_attrs';
 
-// Never show more than one dialog per minute.
-const MIN_REPORT_PERIOD_MS = 60000;
+// Never show more than one dialog per 10s.
+const MIN_REPORT_PERIOD_MS = 10000;
 let timeLastReport = 0;
 
-// Keeps the last ERR_QUEUE_MAX_LEN errors while the dialog is throttled.
-const queuedErrors = new Array<string>();
-const ERR_QUEUE_MAX_LEN = 10;
-
-export function maybeShowErrorDialog(errLog: string) {
-  globals.logging.logError(errLog);
+export function maybeShowErrorDialog(err: ErrorDetails) {
   const now = performance.now();
 
   // Here we rely on the exception message from onCannotGrowMemory function
-  if (errLog.includes('Cannot enlarge memory')) {
+  if (err.message.includes('Cannot enlarge memory')) {
     showOutOfMemoryDialog();
     // Refresh timeLastReport to prevent a different error showing a dialog
     timeLastReport = now;
@@ -45,46 +41,37 @@
   }
 
   if (!RECORDING_V2_FLAG.get()) {
-    if (errLog.includes('Unable to claim interface')) {
+    if (err.message.includes('Unable to claim interface')) {
       showWebUSBError();
       timeLastReport = now;
       return;
     }
 
-    if (errLog.includes('A transfer error has occurred') ||
-        errLog.includes('The device was disconnected') ||
-        errLog.includes('The transfer was cancelled')) {
+    if (err.message.includes('A transfer error has occurred') ||
+        err.message.includes('The device was disconnected') ||
+        err.message.includes('The transfer was cancelled')) {
       showConnectionLostError();
       timeLastReport = now;
       return;
     }
   }
 
-  if (errLog.includes('(ERR:fmt)')) {
+  if (err.message.includes('(ERR:fmt)')) {
     showUnknownFileError();
     return;
   }
 
-  if (errLog.includes('(ERR:rpc_seq)')) {
+  if (err.message.includes('(ERR:rpc_seq)')) {
     showRpcSequencingError();
     return;
   }
 
   if (timeLastReport > 0 && now - timeLastReport <= MIN_REPORT_PERIOD_MS) {
-    queuedErrors.unshift(errLog);
-    if (queuedErrors.length > ERR_QUEUE_MAX_LEN) queuedErrors.pop();
     console.log('Suppressing crash dialog, last error notified too soon.');
     return;
   }
   timeLastReport = now;
-
-  // Append queued errors.
-  while (queuedErrors.length > 0) {
-    const queuedErr = queuedErrors.shift();
-    errLog += `\n\n---------------------------------------\n${queuedErr}`;
-  }
-
-  const errTitle = errLog.split('\n', 1)[0].substr(0, 80);
+  const errTitle = err.message.split('\n', 1)[0].substring(0, 80);
   const userDescription = '';
   let checked = false;
   const engine = globals.getCurrentEngine();
@@ -98,13 +85,13 @@
             checked = (ev.target as HTMLInputElement).checked;
             if (checked && engine && engine.source.type === 'FILE') {
               saveTrace(engine.source.file).then((url) => {
-                const errMessage = createErrorMessage(errLog, checked, url);
+                const errMessage = createErrorMessage(err, checked, url);
                 renderModal(
                     errTitle, errMessage, userDescription, shareTraceSection);
                 return;
               });
             }
-            const errMessage = createErrorMessage(errLog, checked);
+            const errMessage = createErrorMessage(err, checked);
             renderModal(
                 errTitle, errMessage, userDescription, shareTraceSection);
           },
@@ -118,7 +105,7 @@
   }
   renderModal(
       errTitle,
-      createErrorMessage(errLog, checked),
+      createErrorMessage(err, checked),
       userDescription,
       shareTraceSection);
 }
@@ -171,20 +158,28 @@
       engine.source.url !== undefined;
 }
 
-function createErrorMessage(errLog: string, checked: boolean, url?: string) {
-  let errMessage = '';
+function createErrorMessage(err: ErrorDetails, checked: boolean, url?: string) {
+  let msg = `UI: ${location.protocol}//${location.host}/${VERSION}\n\n`;
+
+  // Append the trace stack.
+  msg += `${err.message}\n`;
+  for (const entry of err.stack) {
+    msg += ` - ${entry.name} (${entry.location})\n`;
+  }
+  msg += '\n';
+
+  // Append the trace URL.
   const engine = globals.getCurrentEngine();
   if (checked && url !== undefined) {
-    errMessage += `Trace: ${url}`;
+    msg += `Trace: ${url}\n`;
   } else if (urlExists()) {
-    errMessage +=
-        `Trace: ${(assertExists(engine).source as TraceUrlSource).url}`;
+    msg += `Trace: ${(assertExists(engine).source as TraceUrlSource).url}\n`;
   } else {
-    errMessage += 'To assist with debugging please attach or link to the ' +
-        'trace you were viewing.';
+    msg += 'Trace: not available, please provide repro steps.\n';
   }
-  return errMessage + '\n\n' +
-      'Viewed on: ' + self.location.origin + '\n\n' + errLog;
+  msg += `UA: ${navigator.userAgent}\n`;
+  msg += `Referrer: ${document.referrer}\n`;
+  return msg;
 }
 
 function createLink(
diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
index 12ce93c..8d2da34 100644
--- a/ui/src/frontend/flow_events_renderer.ts
+++ b/ui/src/frontend/flow_events_renderer.ts
@@ -135,7 +135,7 @@
   }
 
   private getXCoordinate(ts: time): number {
-    return globals.frontendLocalState.visibleTimeScale.timeToPx(ts);
+    return globals.timeline.visibleTimeScale.timeToPx(ts);
   }
 
   private getSliceRect(args: FlowEventsRendererArgs, point: FlowPoint):
diff --git a/ui/src/frontend/frontend_local_state.ts b/ui/src/frontend/frontend_local_state.ts
index a649966..6394672 100644
--- a/ui/src/frontend/frontend_local_state.ts
+++ b/ui/src/frontend/frontend_local_state.ts
@@ -136,7 +136,7 @@
  * State that is shared between several frontend components, but not the
  * controller. This state is updated at 60fps.
  */
-export class FrontendLocalState {
+export class Timeline {
   private visibleWindow = new TimeWindow();
   private _timeScale = this.visibleWindow.createTimeScale(0, 0);
   private _windowSpan = PxSpan.ZERO;
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 435bf99..845c0d5 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -51,7 +51,7 @@
 
 import {Analytics, initAnalytics} from './analytics';
 import {BottomTabList} from './bottom_tab';
-import {FrontendLocalState} from './frontend_local_state';
+import {Timeline} from './frontend_local_state';
 import {Router} from './router';
 import {horizontalScrollToTs} from './scroll_helper';
 import {ServiceWorkerController} from './service_worker_controller';
@@ -248,7 +248,7 @@
   private _testing = false;
   private _dispatch?: Dispatch = undefined;
   private _store?: Store<State>;
-  private _frontendLocalState?: FrontendLocalState = undefined;
+  private _timeline?: Timeline = undefined;
   private _serviceWorkerController?: ServiceWorkerController = undefined;
   private _logging?: Analytics = undefined;
   private _isInternalUser: boolean|undefined = undefined;
@@ -315,7 +315,7 @@
     this._router = router;
     this._store = createStore(initialState);
     this._cmdManager = cmdManager;
-    this._frontendLocalState = new FrontendLocalState();
+    this._timeline = new Timeline();
 
     setPerfHooks(
         () => this.state.perfDebug,
@@ -379,8 +379,8 @@
     }
   }
 
-  get frontendLocalState() {
-    return assertExists(this._frontendLocalState);
+  get timeline() {
+    return assertExists(this._timeline);
   }
 
   get logging() {
@@ -599,7 +599,7 @@
     // levels. Logic: each zoom level represents a delta of 0.1 * (visible
     // window span). Therefore, zooming out by six levels is 1.1^6 ~= 2.
     // Similarily, zooming in six levels is 0.9^6 ~= 0.5.
-    const timeScale = this.frontendLocalState.visibleTimeScale;
+    const timeScale = this.timeline.visibleTimeScale;
     // TODO(b/186265930): Remove once fixed:
     if (timeScale.pxSpan.delta === 0) {
       console.error(`b/186265930: Bad pxToSec suppressed`);
@@ -656,7 +656,7 @@
   resetForTesting() {
     this._dispatch = undefined;
     this._store = undefined;
-    this._frontendLocalState = undefined;
+    this._timeline = undefined;
     this._serviceWorkerController = undefined;
 
     // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
diff --git a/ui/src/frontend/gridline_helper.ts b/ui/src/frontend/gridline_helper.ts
index 04c24b3..60d959f 100644
--- a/ui/src/frontend/gridline_helper.ts
+++ b/ui/src/frontend/gridline_helper.ts
@@ -178,7 +178,7 @@
 // Gets the timescale associated with the current visible window.
 export function timeScaleForVisibleWindow(
     startPx: number, endPx: number): TimeScale {
-  return globals.frontendLocalState.getTimeScale(startPx, endPx);
+  return globals.timeline.getTimeScale(startPx, endPx);
 }
 
 export function drawGridLines(
@@ -188,7 +188,7 @@
   ctx.strokeStyle = TRACK_BORDER_COLOR;
   ctx.lineWidth = 1;
 
-  const span = globals.frontendLocalState.visibleTimeSpan;
+  const span = globals.timeline.visibleTimeSpan;
   if (width > TRACK_SHELL_WIDTH && span.duration > 0n) {
     const maxMajorTicks = getMaxMajorTicks(width - TRACK_SHELL_WIDTH);
     const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, width);
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 52f0c1a..866e567 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -20,7 +20,7 @@
 import m from 'mithril';
 
 import {defer} from '../base/deferred';
-import {reportError, setErrorHandler} from '../base/logging';
+import {addErrorHandler, reportError} from '../base/logging';
 import {Actions, DeferredAction, StateActions} from '../common/actions';
 import {CommandManager} from '../common/commands';
 import {createEmptyState} from '../common/empty_state';
@@ -72,7 +72,7 @@
     // recently than the visible time handled by the frontend @ 60fps,
     // update it. This typically happens when restoring the state from a
     // permalink.
-    globals.frontendLocalState.mergeState(state.frontendLocalState);
+    globals.timeline.mergeState(state.frontendLocalState);
 
     // Only redraw if something other than the frontendLocalState changed.
     let key: keyof State;
@@ -206,8 +206,11 @@
 
   document.head.append(script, css);
 
+  // Route errors to both the UI bugreport dialog and Analytics (if enabled).
+  addErrorHandler(maybeShowErrorDialog);
+  addErrorHandler((e) => globals.logging.logError(e));
+
   // Add Error handlers for JS error and for uncaught exceptions in promises.
-  setErrorHandler((err: string) => maybeShowErrorDialog(err));
   window.addEventListener('error', (e) => reportError(e));
   window.addEventListener('unhandledrejection', (e) => reportError(e));
 
diff --git a/ui/src/frontend/keyboard_event_handler.ts b/ui/src/frontend/keyboard_event_handler.ts
index e27f83c..386167f 100644
--- a/ui/src/frontend/keyboard_event_handler.ts
+++ b/ui/src/frontend/keyboard_event_handler.ts
@@ -80,7 +80,7 @@
     return true;
   }
   if (down && 'escape' === key) {
-    globals.frontendLocalState.deselectArea();
+    globals.timeline.deselectArea();
     globals.makeSelection(Actions.deselect({}));
     globals.dispatch(Actions.removeNote({id: '0'}));
     return true;
diff --git a/ui/src/frontend/logs_panel.ts b/ui/src/frontend/logs_panel.ts
index c05b881..cd17638 100644
--- a/ui/src/frontend/logs_panel.ts
+++ b/ui/src/frontend/logs_panel.ts
@@ -93,7 +93,7 @@
       firstVisibleLogTs,
       lastVisibleLogTs,
     } = this.bounds;
-    const vis = globals.frontendLocalState.visibleWindowTime;
+    const vis = globals.timeline.visibleWindowTime;
 
     const visibleLogSpan =
         new HighPrecisionTimeSpan(firstVisibleLogTs, lastVisibleLogTs);
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index 1d7589c..b3a3d13 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -124,8 +124,8 @@
     ctx.rect(TRACK_SHELL_WIDTH, 0, size.width - TRACK_SHELL_WIDTH, size.height);
     ctx.clip();
 
-    const span = globals.frontendLocalState.visibleTimeSpan;
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const span = globals.timeline.visibleTimeSpan;
+    const {visibleTimeScale} = globals.timeline;
     if (size.width > TRACK_SHELL_WIDTH && span.duration > 0n) {
       const maxMajorTicks = getMaxMajorTicks(size.width - TRACK_SHELL_WIDTH);
       const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
@@ -267,7 +267,7 @@
 
   private onClick(x: number, _: number) {
     if (x < 0) return;
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const timestamp = visibleTimeScale.pxToHpTime(x).toTime();
     for (const note of Object.values(globals.state.notes)) {
       if (this.hoveredX && this.mouseOverNote(this.hoveredX, note)) {
@@ -285,7 +285,7 @@
   }
 
   private mouseOverNote(x: number, note: AreaNote|Note): boolean {
-    const timeScale = globals.frontendLocalState.visibleTimeScale;
+    const timeScale = globals.timeline.visibleTimeScale;
     const noteX = timeScale.timeToPx(getStartTimestamp(note));
     if (note.noteType === 'AREA') {
       const noteArea = globals.state.areas[note.areaId];
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index edda1d3..12ffc28 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -226,7 +226,7 @@
   }
 
   private static extractBounds(timeScale: TimeScale): [number, number] {
-    const vizTime = globals.frontendLocalState.visibleWindowTime;
+    const vizTime = globals.timeline.visibleWindowTime;
     return [
       Math.floor(timeScale.hpTimeToPx(vizTime.start)),
       Math.ceil(timeScale.hpTimeToPx(vizTime.end)),
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 5497d8e..4365896 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -121,10 +121,9 @@
   // This finds the tracks covered by the in-progress area selection. When
   // editing areaY is not set, so this will not be used.
   handleAreaSelection() {
-    const area = globals.frontendLocalState.selectedArea;
-    if (area === undefined ||
-        globals.frontendLocalState.areaY.start === undefined ||
-        globals.frontendLocalState.areaY.end === undefined ||
+    const area = globals.timeline.selectedArea;
+    if (area === undefined || globals.timeline.areaY.start === undefined ||
+        globals.timeline.areaY.end === undefined ||
         this.panelInfos.length === 0) {
       return;
     }
@@ -133,22 +132,20 @@
     const panelContainerTop = this.panelInfos[0].y;
     const panelContainerBottom = this.panelInfos[this.panelInfos.length - 1].y +
         this.panelInfos[this.panelInfos.length - 1].height;
-    if (globals.frontendLocalState.areaY.start + TOPBAR_HEIGHT <
-            panelContainerTop ||
-        globals.frontendLocalState.areaY.start + TOPBAR_HEIGHT >
-            panelContainerBottom) {
+    if (globals.timeline.areaY.start + TOPBAR_HEIGHT < panelContainerTop ||
+        globals.timeline.areaY.start + TOPBAR_HEIGHT > panelContainerBottom) {
       return;
     }
 
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
 
     // The Y value is given from the top of the pan and zoom region, we want it
     // from the top of the panel container. The parent offset corrects that.
     const panels = this.getPanelsInRegion(
         visibleTimeScale.timeToPx(area.start),
         visibleTimeScale.timeToPx(area.end),
-        globals.frontendLocalState.areaY.start + TOPBAR_HEIGHT,
-        globals.frontendLocalState.areaY.end + TOPBAR_HEIGHT);
+        globals.timeline.areaY.start + TOPBAR_HEIGHT,
+        globals.timeline.areaY.end + TOPBAR_HEIGHT);
     // Get the track ids from the panels.
     const tracks = [];
     for (const panel of panels) {
@@ -167,7 +164,7 @@
         }
       }
     }
-    globals.frontendLocalState.selectArea(area.start, area.end, tracks);
+    globals.timeline.selectArea(area.start, area.end, tracks);
   }
 
   constructor(vnode: m.CVnode<Attrs>) {
@@ -288,7 +285,7 @@
       this.updateCanvasDimensions();
       this.repositionCanvas();
       if (this.attrs.kind === 'TRACKS') {
-        globals.frontendLocalState.updateLocalLimits(
+        globals.timeline.updateLocalLimits(
             0, this.parentWidth - TRACK_SHELL_WIDTH);
       }
       this.redrawCanvas();
@@ -430,10 +427,9 @@
   // the whole canvas rather than per panel.
   private drawTopLayerOnCanvas() {
     if (!this.ctx) return;
-    const area = globals.frontendLocalState.selectedArea;
-    if (area === undefined ||
-        globals.frontendLocalState.areaY.start === undefined ||
-        globals.frontendLocalState.areaY.end === undefined) {
+    const area = globals.timeline.selectedArea;
+    if (area === undefined || globals.timeline.areaY.start === undefined ||
+        globals.timeline.areaY.end === undefined) {
       return;
     }
     if (this.panelInfos.length === 0 || area.tracks.length === 0) return;
@@ -458,7 +454,7 @@
       return;
     }
 
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const startX = visibleTimeScale.timeToPx(area.start);
     const endX = visibleTimeScale.timeToPx(area.end);
     // To align with where to draw on the canvas subtract the first panel Y.
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
index d23be62..052cdea 100644
--- a/ui/src/frontend/publish.ts
+++ b/ui/src/frontend/publish.ts
@@ -153,7 +153,6 @@
 
 export function publishMetricError(error: string) {
   globals.setMetricError(error);
-  globals.logging.logError(error, false);
   globals.publishRedraw();
 }
 
diff --git a/ui/src/frontend/scroll_helper.ts b/ui/src/frontend/scroll_helper.ts
index b514b61..4df9b14 100644
--- a/ui/src/frontend/scroll_helper.ts
+++ b/ui/src/frontend/scroll_helper.ts
@@ -27,14 +27,14 @@
 // center |ts|, keeping the same zoom level.
 export function horizontalScrollToTs(ts: time) {
   const time = HighPrecisionTime.fromTime(ts);
-  const visibleWindow = globals.frontendLocalState.visibleWindowTime;
+  const visibleWindow = globals.timeline.visibleWindowTime;
   if (!visibleWindow.contains(time)) {
     // TODO(hjd): This is an ugly jump, we should do a smooth pan instead.
     const halfDuration = visibleWindow.duration.divide(2);
     const newStart = time.sub(halfDuration);
     const newWindow = new HighPrecisionTimeSpan(
         newStart, newStart.add(visibleWindow.duration));
-    globals.frontendLocalState.updateVisibleTime(newWindow);
+    globals.timeline.updateVisibleTime(newWindow);
   }
 }
 
@@ -52,7 +52,7 @@
 // - Otherwise, preserve the zoom range.
 export function focusHorizontalRange(
     start: time, end: time, viewPercentage?: number) {
-  const visible = globals.frontendLocalState.visibleWindowTime;
+  const visible = globals.timeline.visibleWindowTime;
   const trace = globals.stateTraceTime();
   const select = HighPrecisionTimeSpan.fromTime(start, end);
 
@@ -68,13 +68,13 @@
     const paddingPercentage = 1.0 - viewPercentage;
     const paddingTime = select.duration.multiply(paddingPercentage);
     const halfPaddingTime = paddingTime.divide(2);
-    globals.frontendLocalState.updateVisibleTime(select.pad(halfPaddingTime));
+    globals.timeline.updateVisibleTime(select.pad(halfPaddingTime));
     return;
   }
   // If the range is too large to fit on the current zoom level, resize.
   if (select.duration.gt(visible.duration.multiply(0.5))) {
     const paddedRange = select.pad(select.duration.multiply(2));
-    globals.frontendLocalState.updateVisibleTime(paddedRange);
+    globals.timeline.updateVisibleTime(paddedRange);
     return;
   }
   // Calculate the new visible window preserving the zoom level.
@@ -99,9 +99,9 @@
   // level.
   if (view.start.eq(visible.start) && view.end.eq(visible.end)) {
     const padded = select.pad(select.duration.multiply(2));
-    globals.frontendLocalState.updateVisibleTime(padded);
+    globals.timeline.updateVisibleTime(padded);
   } else {
-    globals.frontendLocalState.updateVisibleTime(view);
+    globals.timeline.updateVisibleTime(view);
   }
 }
 
diff --git a/ui/src/frontend/slice_layout.ts b/ui/src/frontend/slice_layout.ts
index ca655bd..9a2fa15 100644
--- a/ui/src/frontend/slice_layout.ts
+++ b/ui/src/frontend/slice_layout.ts
@@ -13,15 +13,15 @@
 // limitations under the License.
 
 export interface SliceLayoutBase {
-  padding: number;     // top/bottom pixel padding between slices and track.
-  rowSpacing: number;  // Spacing between rows.
-  minDepth: number;    // Minimum depth a slice can be (normally zero)
+  readonly padding: number;  // vertical pixel padding between slices and track.
+  readonly rowSpacing: number;  // Spacing between rows.
+  readonly minDepth: number;    // Minimum depth a slice can be (normally zero)
   // Maximum depth a slice can be plus 1 (a half open range with minDepth).
   // We have a optimization for when maxDepth - minDepth == 1 so it is useful
   // to set this correctly:
-  maxDepth: number;
-  titleSizePx?: number;
-  subtitleSizePx?: number;
+  readonly maxDepth: number;
+  readonly titleSizePx?: number;
+  readonly subtitleSizePx?: number;
 }
 
 export const SLICE_LAYOUT_BASE_DEFAULTS: SliceLayoutBase = Object.freeze({
@@ -34,8 +34,8 @@
 });
 
 export interface SliceLayoutFixed extends SliceLayoutBase {
-  heightMode: 'FIXED';
-  fixedHeight: number;  // Outer height of the track.
+  readonly heightMode: 'FIXED';
+  readonly fixedHeight: number;  // Outer height of the track.
 }
 
 export const SLICE_LAYOUT_FIXED_DEFAULTS: SliceLayoutFixed = Object.freeze({
@@ -45,8 +45,8 @@
 });
 
 export interface SliceLayoutFitContent extends SliceLayoutBase {
-  heightMode: 'FIT_CONTENT';
-  sliceHeight: number;  // Only when heightMode = 'FIT_CONTENT'.
+  readonly heightMode: 'FIT_CONTENT';
+  readonly sliceHeight: number;  // Only when heightMode = 'FIT_CONTENT'.
 }
 
 export const SLICE_LAYOUT_FIT_CONTENT_DEFAULTS: SliceLayoutFitContent =
@@ -57,10 +57,10 @@
     });
 
 export interface SliceLayoutFlat extends SliceLayoutBase {
-  heightMode: 'FIXED';
-  fixedHeight: number;  // Outer height of the track.
-  minDepth: 0;
-  maxDepth: 1;
+  readonly heightMode: 'FIXED';
+  readonly fixedHeight: number;  // Outer height of the track.
+  readonly minDepth: 0;
+  readonly maxDepth: 1;
 }
 
 export const SLICE_LAYOUT_FLAT_DEFAULTS: SliceLayoutFlat = Object.freeze({
diff --git a/ui/src/frontend/slice_track.ts b/ui/src/frontend/slice_track.ts
index 685ff8c..21885d0 100644
--- a/ui/src/frontend/slice_track.ts
+++ b/ui/src/frontend/slice_track.ts
@@ -82,7 +82,7 @@
     const data = this.data;
     if (data === undefined) return;  // Can't possibly draw anything.
 
-    const {visibleTimeSpan, visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeSpan, visibleTimeScale} = globals.timeline;
 
     // If the cached trace slices don't fully cover the visible time range,
     // show a gray rectangle with a "Loading..." label.
@@ -262,7 +262,7 @@
     if (data === undefined) return;
     const {
       visibleTimeScale: timeScale,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
     if (y < TRACK_PADDING) return;
     const instantWidthTime = timeScale.pxDeltaToDuration(HALF_CHEVRON_WIDTH_PX);
     const t = timeScale.pxToHpTime(x);
@@ -293,7 +293,7 @@
   }
 
   getEndTimeIfInComplete(start: time): time {
-    const {visibleTimeScale, visibleWindowTime} = globals.frontendLocalState;
+    const {visibleTimeScale, visibleWindowTime} = globals.timeline;
 
     let end = visibleWindowTime.end.toTime('ceil');
     if (CROP_INCOMPLETE_SLICE_FLAG.get()) {
@@ -349,7 +349,7 @@
       windowSpan,
       visibleTimeScale,
       visibleTimeSpan,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
 
     const pxEnd = windowSpan.end;
     const left = Math.max(visibleTimeScale.timeToPx(tStart), 0);
diff --git a/ui/src/frontend/tickmark_panel.ts b/ui/src/frontend/tickmark_panel.ts
index 9390c7f..5a363cc 100644
--- a/ui/src/frontend/tickmark_panel.ts
+++ b/ui/src/frontend/tickmark_panel.ts
@@ -33,7 +33,7 @@
   }
 
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
 
     ctx.fillStyle = '#999';
     ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
@@ -43,7 +43,7 @@
     ctx.rect(TRACK_SHELL_WIDTH, 0, size.width - TRACK_SHELL_WIDTH, size.height);
     ctx.clip();
 
-    const visibleSpan = globals.frontendLocalState.visibleTimeSpan;
+    const visibleSpan = globals.timeline.visibleTimeSpan;
     if (size.width > TRACK_SHELL_WIDTH && visibleSpan.duration > 0n) {
       const maxMajorTicks = getMaxMajorTicks(size.width - TRACK_SHELL_WIDTH);
       const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
diff --git a/ui/src/frontend/time_axis_panel.ts b/ui/src/frontend/time_axis_panel.ts
index f4372af..6238c8d 100644
--- a/ui/src/frontend/time_axis_panel.ts
+++ b/ui/src/frontend/time_axis_panel.ts
@@ -66,7 +66,7 @@
     ctx.clip();
 
     // Draw time axis.
-    const span = globals.frontendLocalState.visibleTimeSpan;
+    const span = globals.timeline.visibleTimeSpan;
     if (size.width > TRACK_SHELL_WIDTH && span.duration > 0n) {
       const maxMajorTicks = getMaxMajorTicks(size.width - TRACK_SHELL_WIDTH);
       const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
diff --git a/ui/src/frontend/time_selection_panel.ts b/ui/src/frontend/time_selection_panel.ts
index 8137c6f..0b61594 100644
--- a/ui/src/frontend/time_selection_panel.ts
+++ b/ui/src/frontend/time_selection_panel.ts
@@ -144,7 +144,7 @@
     ctx.rect(TRACK_SHELL_WIDTH, 0, size.width - TRACK_SHELL_WIDTH, size.height);
     ctx.clip();
 
-    const span = globals.frontendLocalState.visibleTimeSpan;
+    const span = globals.timeline.visibleTimeSpan;
     if (size.width > TRACK_SHELL_WIDTH && span.duration > 0n) {
       const maxMajorTicks = getMaxMajorTicks(size.width - TRACK_SHELL_WIDTH);
       const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
@@ -159,7 +159,7 @@
       }
     }
 
-    const localArea = globals.frontendLocalState.selectedArea;
+    const localArea = globals.timeline.selectedArea;
     const selection = globals.state.currentSelection;
     if (localArea !== undefined) {
       const start = Time.min(localArea.start, localArea.end);
@@ -190,7 +190,7 @@
   }
 
   renderHover(ctx: CanvasRenderingContext2D, size: PanelSize, ts: time) {
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const xPos = TRACK_SHELL_WIDTH + Math.floor(visibleTimeScale.timeToPx(ts));
     const domainTime = globals.toDomainTime(ts);
     const label = stringifyTimestamp(domainTime);
@@ -200,7 +200,7 @@
   renderSpan(
       ctx: CanvasRenderingContext2D, size: PanelSize,
       span: Span<time, duration>) {
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const xLeft = visibleTimeScale.timeToPx(span.start);
     const xRight = visibleTimeScale.timeToPx(span.end);
     const label = renderDuration(span.duration);
diff --git a/ui/src/frontend/trace_converter.ts b/ui/src/frontend/trace_converter.ts
index a08f622..fb0bc12 100644
--- a/ui/src/frontend/trace_converter.ts
+++ b/ui/src/frontend/trace_converter.ts
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import {download} from '../base/clipboard';
+import {ErrorDetails} from '../base/logging';
 import {time} from '../base/time';
 import {Actions} from '../common/actions';
 import {
@@ -51,7 +52,7 @@
 
 interface ErrorArgs {
   kind: 'error';
-  error: string;
+  error: ErrorDetails;
 }
 
 
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index 6b69607..04ae41e 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -204,7 +204,7 @@
   }
 
   highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const selection = globals.state.currentSelection;
     if (!selection || selection.kind !== 'AREA') return;
     const selectedArea = globals.state.areas[selection.areaId];
@@ -244,7 +244,7 @@
 
     this.highlightIfTrackSelected(ctx, size);
 
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     // Draw vertical line when hovering on the notes panel.
     if (globals.state.hoveredNoteTimestamp !== -1n) {
       drawVerticalLineAtTime(
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index afd58c6..b4fcda4 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -416,7 +416,7 @@
   }
 
   highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const selection = globals.state.currentSelection;
     const trackState = this.trackState;
     if (!selection || selection.kind !== 'AREA' || trackState === undefined) {
@@ -453,7 +453,7 @@
 
     this.highlightIfTrackSelected(ctx, size);
 
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     // Draw vertical line when hovering on the notes panel.
     if (globals.state.hoveredNoteTimestamp !== -1n) {
       drawVerticalLineAtTime(
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 57aa0a3..071c968 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -51,10 +51,10 @@
   if (selection !== null && selection.kind === 'AREA') {
     // If frontend selectedArea exists then we are in the process of editing the
     // time range and need to use that value instead.
-    const area = globals.frontendLocalState.selectedArea ?
-        globals.frontendLocalState.selectedArea :
+    const area = globals.timeline.selectedArea ?
+        globals.timeline.selectedArea :
         globals.state.areas[selection.areaId];
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const start = visibleTimeScale.timeToPx(area.start);
     const end = visibleTimeScale.timeToPx(area.end);
     const startDrag = mousePos - TRACK_SHELL_WIDTH;
@@ -94,10 +94,10 @@
   private keepCurrentSelection = false;
 
   oncreate(vnode: m.CVnodeDOM) {
-    const frontendLocalState = globals.frontendLocalState;
+    const timeline = globals.timeline;
     const updateDimensions = () => {
       const rect = vnode.dom.getBoundingClientRect();
-      frontendLocalState.updateLocalLimits(
+      timeline.updateLocalLimits(
           0, rect.width - TRACK_SHELL_WIDTH - getScrollbarWidth());
     };
 
@@ -120,11 +120,11 @@
       onPanned: (pannedPx: number) => {
         const {
           visibleTimeScale,
-        } = globals.frontendLocalState;
+        } = globals.timeline;
 
         this.keepCurrentSelection = true;
         const tDelta = visibleTimeScale.pxDeltaToDuration(pannedPx);
-        frontendLocalState.panVisibleWindow(tDelta);
+        timeline.panVisibleWindow(tDelta);
 
         // If the user has panned they no longer need the hint.
         localStorage.setItem(DISMISSED_PANNING_HINT_KEY, 'true');
@@ -136,7 +136,7 @@
         const zoomPx = zoomedPositionPx - TRACK_SHELL_WIDTH;
         const rect = vnode.dom.getBoundingClientRect();
         const centerPoint = zoomPx / (rect.width - TRACK_SHELL_WIDTH);
-        frontendLocalState.zoomVisibleWindow(1 - zoomRatio, centerPoint);
+        timeline.zoomVisibleWindow(1 - zoomRatio, centerPoint);
         raf.scheduleRedraw();
       },
       editSelection: (currentPx: number) => {
@@ -150,13 +150,13 @@
           currentY: number,
           editing: boolean) => {
         const traceTime = globals.state.traceTime;
-        const {visibleTimeScale} = frontendLocalState;
+        const {visibleTimeScale} = timeline;
         this.keepCurrentSelection = true;
         if (editing) {
           const selection = globals.state.currentSelection;
           if (selection !== null && selection.kind === 'AREA') {
-            const area = globals.frontendLocalState.selectedArea ?
-                globals.frontendLocalState.selectedArea :
+            const area = globals.timeline.selectedArea ?
+                globals.timeline.selectedArea :
                 globals.state.areas[selection.areaId];
             let newTime =
                 visibleTimeScale.pxToHpTime(currentX - TRACK_SHELL_WIDTH)
@@ -175,7 +175,7 @@
             }
             // When editing the time range we always use the saved tracks,
             // since these will not change.
-            frontendLocalState.selectArea(
+            timeline.selectArea(
                 Time.max(Time.min(keepTime, newTime), traceTime.start),
                 Time.min(Time.max(keepTime, newTime), traceTime.end),
                 globals.state.areas[selection.areaId].tracks);
@@ -187,20 +187,20 @@
           const {pxSpan} = visibleTimeScale;
           startPx = clamp(startPx, pxSpan.start, pxSpan.end);
           endPx = clamp(endPx, pxSpan.start, pxSpan.end);
-          frontendLocalState.selectArea(
+          timeline.selectArea(
               visibleTimeScale.pxToHpTime(startPx).toTime('floor'),
               visibleTimeScale.pxToHpTime(endPx).toTime('ceil'),
           );
-          frontendLocalState.areaY.start = dragStartY;
-          frontendLocalState.areaY.end = currentY;
+          timeline.areaY.start = dragStartY;
+          timeline.areaY.end = currentY;
           publishShowPanningHint();
         }
         raf.scheduleRedraw();
       },
       endSelection: (edit: boolean) => {
-        globals.frontendLocalState.areaY.start = undefined;
-        globals.frontendLocalState.areaY.end = undefined;
-        const area = globals.frontendLocalState.selectedArea;
+        globals.timeline.areaY.start = undefined;
+        globals.timeline.areaY.end = undefined;
+        const area = globals.timeline.selectedArea;
         // If we are editing we need to pass the current id through to ensure
         // the marked area with that id is also updated.
         if (edit) {
@@ -214,8 +214,8 @@
         }
         // Now the selection has ended we stored the final selected area in the
         // global state and can remove the in progress selection from the
-        // frontendLocalState.
-        globals.frontendLocalState.deselectArea();
+        // timeline.
+        globals.timeline.deselectArea();
         // Full redraw to color track shell.
         raf.scheduleFullRedraw();
       },
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
index 46fbb46..3f7d272 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
@@ -21,7 +21,6 @@
 import {addDebugSliceTrack} from '../../tracks/debug/slice_track';
 
 class AndroidPerfTraceCounters implements Plugin {
-
   onActivate(_: PluginContext): void {}
 
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
@@ -33,7 +32,7 @@
           tid = prompt('Enter a thread tid', '');
           if (tid === null) return;
         }
-        const sql_prefix = `
+        const sqlPrefix = `
 WITH
   sched_switch_ipc AS (
     SELECT
@@ -49,7 +48,8 @@
     WHERE name = 'sched_switch_with_ctrs' AND tid = ${tid}
   ),
   target_thread_sched_slice AS (
-    SELECT s.*, t.tid, t.name FROM sched s LEFT JOIN thread t USING (utid) WHERE t.tid = ${tid}
+    SELECT s.*, t.tid, t.name FROM sched s LEFT JOIN thread t USING (utid) WHERE t.tid = ${
+            tid}
   ),
   target_thread_ipc_slice AS (
     SELECT
@@ -76,19 +76,20 @@
       ssi.l3_cache_miss
     FROM sched_switch_ipc ssi
   )
-`
+`;
 
         await addDebugSliceTrack(
-          ctx.engine,
-          {
-            sqlSource: sql_prefix + `
+            ctx.engine,
+            {
+              sqlSource: sqlPrefix + `
 SELECT * FROM target_thread_ipc_slice WHERE ts IS NOT NULL`,
-          },
-          'Rutime IPC:' + tid,
-          {ts: 'ts', dur: 'dur', name: 'ipc'},
-          ['instruction', 'cycle', 'stall_backend_mem', 'l3_cache_miss' ],
+            },
+            'Rutime IPC:' + tid,
+            {ts: 'ts', dur: 'dur', name: 'ipc'},
+            ['instruction', 'cycle', 'stall_backend_mem', 'l3_cache_miss'],
         );
-        ctx.tabs.openQuery(sql_prefix + `
+        ctx.tabs.openQuery(
+            sqlPrefix + `
 SELECT
   (sum(instruction) * 1.0 / sum(cycle)*1.0) AS avg_ipc,
   sum(dur)/1e6 as total_runtime_ms,
@@ -97,7 +98,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');
       },
     });
   }
diff --git a/ui/src/trace_processor/engine.ts b/ui/src/trace_processor/engine.ts
index f59f33f..c6f9ea2 100644
--- a/ui/src/trace_processor/engine.ts
+++ b/ui/src/trace_processor/engine.ts
@@ -469,12 +469,16 @@
 export class EngineProxy implements Disposable {
   private engine: Engine;
   private tag: string;
-  private isAlive: boolean;
+  private _isAlive: boolean;
+
+  get isAlive(): boolean {
+    return this._isAlive;
+  }
 
   constructor(engine: Engine, tag: string) {
     this.engine = engine;
     this.tag = tag;
-    this.isAlive = true;
+    this._isAlive = true;
   }
 
   query(query: string, tag?: string): Promise<QueryResult>&QueryResult {
@@ -511,6 +515,6 @@
   }
 
   dispose() {
-    this.isAlive = false;
+    this._isAlive = false;
   }
 }
diff --git a/ui/src/traceconv/index.ts b/ui/src/traceconv/index.ts
index 627df91..d9c6aed 100644
--- a/ui/src/traceconv/index.ts
+++ b/ui/src/traceconv/index.ts
@@ -13,7 +13,12 @@
 // limitations under the License.
 
 import {defer} from '../base/deferred';
-import {assertExists, reportError, setErrorHandler} from '../base/logging';
+import {
+  addErrorHandler,
+  assertExists,
+  ErrorDetails,
+  reportError,
+} from '../base/logging';
 import {time} from '../base/time';
 import {
   ConversionJobName,
@@ -60,7 +65,7 @@
   });
 }
 
-function forwardError(error: string) {
+function forwardError(error: ErrorDetails) {
   selfWorker.postMessage({
     kind: 'error',
     error,
@@ -221,7 +226,7 @@
 selfWorker.onmessage = (msg: MessageEvent) => {
   self.addEventListener('error', (e) => reportError(e));
   self.addEventListener('unhandledrejection', (e) => reportError(e));
-  setErrorHandler((err: string) => forwardError(err));
+  addErrorHandler((error: ErrorDetails) => forwardError(error));
   const args = msg.data as Args;
   if (isConvertTraceAndDownload(args)) {
     ConvertTraceAndDownload(args.trace, args.format, args.truncate);
diff --git a/ui/src/tracks/android_log/index.ts b/ui/src/tracks/android_log/index.ts
index 04237b6..b1a31e2 100644
--- a/ui/src/tracks/android_log/index.ts
+++ b/ui/src/tracks/android_log/index.ts
@@ -109,7 +109,7 @@
   }
 
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize): void {
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
 
     const data = this.data();
 
diff --git a/ui/src/tracks/async_slices/async_slice_track_v2.ts b/ui/src/tracks/async_slices/async_slice_track_v2.ts
index 4e87c9b..cd24132 100644
--- a/ui/src/tracks/async_slices/async_slice_track_v2.ts
+++ b/ui/src/tracks/async_slices/async_slice_track_v2.ts
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import {NamedSliceTrack} from '../../frontend/named_slice_track';
+import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../frontend/slice_layout';
 import {NewTrackArgs} from '../../frontend/track';
 import {Slice} from '../../public';
 
@@ -20,7 +21,10 @@
   constructor(
       args: NewTrackArgs, maxDepth: number, private trackIds: number[]) {
     super(args);
-    this.sliceLayout.maxDepth = maxDepth + 1;
+    this.sliceLayout = {
+      ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
+      minDepth: maxDepth + 1,
+    };
   }
 
   getSqlSource(): string {
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_jank_cause_link_utils.ts b/ui/src/tracks/chrome_scroll_jank/scroll_jank_cause_link_utils.ts
index 7209356..df919ce 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_jank_cause_link_utils.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_jank_cause_link_utils.ts
@@ -201,7 +201,7 @@
             verticalScrollToTrack(trackKeys[0], true);
             if (exists(ts) && exists(dur)) {
               focusHorizontalRange(ts, Time.fromRaw(ts + dur), 0.3);
-              globals.frontendLocalState.selectArea(
+              globals.timeline.selectArea(
                   ts, Time.fromRaw(ts + dur), trackKeys);
 
               globals.dispatch(Actions.selectArea({
diff --git a/ui/src/tracks/chrome_slices/index.ts b/ui/src/tracks/chrome_slices/index.ts
index 8d36341..3d9e638 100644
--- a/ui/src/tracks/chrome_slices/index.ts
+++ b/ui/src/tracks/chrome_slices/index.ts
@@ -20,6 +20,7 @@
   NamedSliceTrack,
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
+import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../frontend/slice_layout';
 import {
   SliceData,
   SliceTrackLEGACY,
@@ -165,8 +166,12 @@
 }
 
 export class ChromeSliceTrackV2 extends NamedSliceTrack<ChromeSliceTrackTypes> {
-  constructor(args: NewTrackArgs, private trackId: number) {
+  constructor(args: NewTrackArgs, private trackId: number, maxDepth: number) {
     super(args);
+    this.sliceLayout = {
+      ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
+      minDepth: maxDepth + 1,
+    };
   }
 
   // This is used by the base class to call iter().
@@ -283,7 +288,7 @@
             engine: ctx.engine,
             trackKey,
           };
-          return new ChromeSliceTrackV2(newTrackArgs, trackId);
+          return new ChromeSliceTrackV2(newTrackArgs, trackId, maxDepth);
         },
       });
     }
diff --git a/ui/src/tracks/counter/index.ts b/ui/src/tracks/counter/index.ts
index f9b61a5..5178321 100644
--- a/ui/src/tracks/counter/index.ts
+++ b/ui/src/tracks/counter/index.ts
@@ -353,7 +353,7 @@
     // TODO: fonts and colors should come from the CSS and not hardcoded here.
     const {
       visibleTimeScale: timeScale,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
     const data = this.data;
 
     // Can't possibly draw anything.
@@ -560,7 +560,7 @@
     const data = this.data;
     if (data === undefined) return;
     this.mousePos = pos;
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const time = visibleTimeScale.pxToHpTime(pos.x);
 
     let values = data.lastValues;
@@ -587,7 +587,7 @@
   onMouseClick({x}: {x: number}): boolean {
     const data = this.data;
     if (data === undefined) return false;
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const time = visibleTimeScale.pxToHpTime(x);
     const [left, right] = searchSegment(data.timestamps, time.toTime());
     if (left === -1) {
@@ -606,8 +606,10 @@
   }
 
   async onDestroy(): Promise<void> {
-    await this.engine.query(
-        `DROP VIEW IF EXISTS ${this.tableName('counter_view')}`);
+    if (this.engine.isAlive) {
+      await this.engine.query(
+          `DROP VIEW IF EXISTS ${this.tableName('counter_view')}`);
+    }
   }
 }
 
diff --git a/ui/src/tracks/cpu_freq/index.ts b/ui/src/tracks/cpu_freq/index.ts
index 06f3087..7d1377b 100644
--- a/ui/src/tracks/cpu_freq/index.ts
+++ b/ui/src/tracks/cpu_freq/index.ts
@@ -294,7 +294,7 @@
     const {
       visibleTimeScale,
       visibleWindowTime,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
     const data = this.data();
 
     if (data === undefined || data.timestamps.length === 0) {
@@ -466,7 +466,7 @@
     const data = this.data();
     if (data === undefined) return;
     this.mousePos = pos;
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const time = visibleTimeScale.pxToHpTime(pos.x);
 
     const [left, right] = searchSegment(data.timestamps, time.toTime());
diff --git a/ui/src/tracks/cpu_profile/index.ts b/ui/src/tracks/cpu_profile/index.ts
index df0916b..bc4c6bd 100644
--- a/ui/src/tracks/cpu_profile/index.ts
+++ b/ui/src/tracks/cpu_profile/index.ts
@@ -106,7 +106,7 @@
   renderCanvas(ctx: CanvasRenderingContext2D, _size: PanelSize): void {
     const {
       visibleTimeScale: timeScale,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
     const data = this.data();
 
     if (data === undefined) return;
@@ -181,7 +181,7 @@
     if (data === undefined) return;
     const {
       visibleTimeScale: timeScale,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
     const time = timeScale.pxToHpTime(x);
     const [left, right] = searchSegment(data.tsStarts, time.toTime());
     const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
@@ -198,7 +198,7 @@
     if (data === undefined) return false;
     const {
       visibleTimeScale: timeScale,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
 
     const time = timeScale.pxToHpTime(x);
     const [left, right] = searchSegment(data.tsStarts, time.toTime());
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index f4b2830..90926b7 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -198,7 +198,10 @@
   }
 
   async onDestroy() {
-    await this.query(`drop table if exists ${this.tableName('sched_cached')}`);
+    if (this.engine.isAlive) {
+      await this.engine.query(
+          `drop table if exists ${this.tableName('sched_cached')}`);
+    }
   }
 }
 
@@ -220,7 +223,7 @@
 
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize): void {
     // TODO: fonts and colors should come from the CSS and not hardcoded here.
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const data = this.data();
 
     if (data === undefined) return;  // Can't possibly draw anything.
@@ -243,7 +246,7 @@
       visibleTimeScale,
       visibleTimeSpan,
       visibleWindowTime,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
     assertTrue(data.starts.length === data.ends.length);
     assertTrue(data.starts.length === data.utids.length);
 
@@ -422,7 +425,7 @@
     const data = this.data();
     this.mousePos = pos;
     if (data === undefined) return;
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     if (pos.y < MARGIN_TOP || pos.y > MARGIN_TOP + RECT_HEIGHT) {
       this.utidHoveredInThisTrack = -1;
       globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
@@ -456,7 +459,7 @@
   onMouseClick({x}: {x: number}) {
     const data = this.data();
     if (data === undefined) return false;
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const time = visibleTimeScale.pxToHpTime(x);
     const index = search(data.starts, time.toTime());
     const id = index === -1 ? undefined : data.ids[index];
diff --git a/ui/src/tracks/custom_sql_table_slices/index.ts b/ui/src/tracks/custom_sql_table_slices/index.ts
index 40c980a..ec7c3ea 100644
--- a/ui/src/tracks/custom_sql_table_slices/index.ts
+++ b/ui/src/tracks/custom_sql_table_slices/index.ts
@@ -92,7 +92,9 @@
         });
     await this.engine.query(sql);
     return DisposableCallback.from(() => {
-      this.engine.query(`DROP VIEW ${this.tableName}`);
+      if (this.engine.isAlive) {
+        this.engine.query(`DROP VIEW ${this.tableName}`);
+      }
     });
   }
 
diff --git a/ui/src/tracks/frames/actual_frames_track_v2.ts b/ui/src/tracks/frames/actual_frames_track_v2.ts
index c30e673..766041f 100644
--- a/ui/src/tracks/frames/actual_frames_track_v2.ts
+++ b/ui/src/tracks/frames/actual_frames_track_v2.ts
@@ -19,6 +19,7 @@
   NamedSliceTrack,
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
+import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../frontend/slice_layout';
 import {EngineProxy, Slice, STR_NULL} from '../../public';
 
 const BLUE = makeColorScheme(new HSLColor('#03A9F4'));    // Blue 500
@@ -47,7 +48,10 @@
       engine: EngineProxy, maxDepth: number, trackKey: string,
       private trackIds: number[]) {
     super({engine, trackKey});
-    this.sliceLayout.maxDepth = maxDepth + 1;
+    this.sliceLayout = {
+      ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
+      minDepth: maxDepth + 1,
+    };
   }
 
   // This is used by the base class to call iter().
diff --git a/ui/src/tracks/frames/expected_frames_track_v2.ts b/ui/src/tracks/frames/expected_frames_track_v2.ts
index 268b4b6..a3caf5a 100644
--- a/ui/src/tracks/frames/expected_frames_track_v2.ts
+++ b/ui/src/tracks/frames/expected_frames_track_v2.ts
@@ -15,6 +15,7 @@
 import {HSLColor} from '../../common/color';
 import {makeColorScheme} from '../../common/colorizer';
 import {NamedRow, NamedSliceTrack} from '../../frontend/named_slice_track';
+import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../frontend/slice_layout';
 import {EngineProxy, Slice} from '../../public';
 
 const GREEN = makeColorScheme(new HSLColor('#4CAF50'));  // Green 500
@@ -24,7 +25,10 @@
       engine: EngineProxy, maxDepth: number, trackKey: string,
       private trackIds: number[]) {
     super({engine, trackKey});
-    this.sliceLayout.maxDepth = maxDepth + 1;
+    this.sliceLayout = {
+      ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
+      minDepth: maxDepth + 1,
+    };
   }
 
   getSqlSource(): string {
diff --git a/ui/src/tracks/ftrace/index.ts b/ui/src/tracks/ftrace/index.ts
index 6ae8474..531bad8 100644
--- a/ui/src/tracks/ftrace/index.ts
+++ b/ui/src/tracks/ftrace/index.ts
@@ -93,7 +93,7 @@
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize): void {
     const {
       visibleTimeScale,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
 
     const data = this.data;
 
diff --git a/ui/src/tracks/perf_samples_profile/index.ts b/ui/src/tracks/perf_samples_profile/index.ts
index a78e36d..17734f1 100644
--- a/ui/src/tracks/perf_samples_profile/index.ts
+++ b/ui/src/tracks/perf_samples_profile/index.ts
@@ -103,7 +103,7 @@
   renderCanvas(ctx: CanvasRenderingContext2D, _size: PanelSize): void {
     const {
       visibleTimeScale,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
     const data = this.data();
 
     if (data === undefined) return;
@@ -147,7 +147,7 @@
   onMouseMove({x, y}: {x: number, y: number}) {
     const data = this.data();
     if (data === undefined) return;
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const time = visibleTimeScale.pxToHpTime(x);
     const [left, right] = searchSegment(data.tsStarts, time.toTime());
     const index =
@@ -163,7 +163,7 @@
   onMouseClick({x, y}: {x: number, y: number}) {
     const data = this.data();
     if (data === undefined) return false;
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
 
     const time = visibleTimeScale.pxToHpTime(x);
     const [left, right] = searchSegment(data.tsStarts, time.toTime());
diff --git a/ui/src/tracks/process_summary/process_scheduling_track.ts b/ui/src/tracks/process_summary/process_scheduling_track.ts
index 455cac5..b818d31 100644
--- a/ui/src/tracks/process_summary/process_scheduling_track.ts
+++ b/ui/src/tracks/process_summary/process_scheduling_track.ts
@@ -206,7 +206,7 @@
     const {
       visibleTimeScale,
       visibleTimeSpan,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
     const data = this.data();
 
     if (data === undefined) return;  // Can't possibly draw anything.
@@ -288,7 +288,7 @@
 
     const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.maxCpu);
     const cpu = Math.floor((pos.y - MARGIN_TOP) / (cpuTrackHeight + 1));
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const t = visibleTimeScale.pxToHpTime(pos.x).toTime('floor');
 
     const [i, j] = searchRange(data.starts, t, searchEq(data.cpus, cpu));
diff --git a/ui/src/tracks/process_summary/process_summary_track.ts b/ui/src/tracks/process_summary/process_summary_track.ts
index 383dcb3..f47e6e9 100644
--- a/ui/src/tracks/process_summary/process_summary_track.ts
+++ b/ui/src/tracks/process_summary/process_summary_track.ts
@@ -131,9 +131,11 @@
   }
 
   async onDestroy(): Promise<void> {
-    await this.query(`drop table if exists ${
-        this.tableName(
-            'window')}; drop table if exists ${this.tableName('span')}`);
+    if (this.engine.isAlive) {
+      await this.engine.query(`drop table if exists ${
+          this.tableName(
+              'window')}; drop table if exists ${this.tableName('span')}`);
+    }
   }
 }
 
@@ -154,7 +156,7 @@
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize): void {
     const {
       visibleTimeScale,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
     const data = this.data();
     if (data === undefined) return;  // Can't possibly draw anything.
 
@@ -171,7 +173,7 @@
 
   // TODO(dproy): Dedup with CPU slices.
   renderSummary(ctx: CanvasRenderingContext2D, data: Data): void {
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const startPx = 0;
     const bottomY = TRACK_HEIGHT;
 
diff --git a/ui/src/tracks/screenshots/index.ts b/ui/src/tracks/screenshots/index.ts
index 4af3c94..d5860b8 100644
--- a/ui/src/tracks/screenshots/index.ts
+++ b/ui/src/tracks/screenshots/index.ts
@@ -17,12 +17,14 @@
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
 import {
+  NUM,
   Plugin,
   PluginContext,
   PluginContextTrace,
   PluginDescriptor,
   PrimaryTrackSortKey,
 } from '../../public';
+import {Engine} from '../../trace_processor/engine';
 import {
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
@@ -59,16 +61,24 @@
 };
 
 // TODO(stevegolton): Use suggestTrack().
-export async function decideTracks(): Promise<DecideTracksResult> {
+export async function decideTracks(engine: Engine):
+    Promise<DecideTracksResult> {
   const result: DecideTracksResult = {
     tracksToAdd: [],
   };
 
-  result.tracksToAdd.push({
-    uri: 'perfetto.Screenshots',
-    name: 'Screenshots',
-    trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
-  });
+  const res =
+      await engine.query('select count() as count from android_screenshots');
+  const {count} = res.firstRow({count: NUM});
+
+  if (count > 0) {
+    result.tracksToAdd.push({
+      uri: 'perfetto.Screenshots',
+      name: 'Screenshots',
+      trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
+    });
+  }
+
   return result;
 }
 
@@ -78,17 +88,25 @@
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     await ctx.engine.query(`INCLUDE PERFETTO MODULE android.screenshots`);
 
-    ctx.registerStaticTrack({
-      uri: 'perfetto.Screenshots',
-      displayName: 'Screenshots',
-      kind: ScreenshotsTrack.kind,
-      track: ({trackKey}) => {
-        return new ScreenshotsTrack({
-          engine: ctx.engine,
-          trackKey,
-        });
-      },
-    });
+    const res = await ctx.engine.query(
+        'select count() as count from android_screenshots');
+    const {count} = res.firstRow({count: NUM});
+
+    if (count > 0) {
+      const displayName = 'Screenshots';
+      const uri = 'perfetto.Screenshots';
+      ctx.registerStaticTrack({
+        uri,
+        displayName,
+        kind: ScreenshotsTrack.kind,
+        track: ({trackKey}) => {
+          return new ScreenshotsTrack({
+            engine: ctx.engine,
+            trackKey,
+          });
+        },
+      });
+    }
   }
 }
 
diff --git a/ui/src/tracks/thread_state/index.ts b/ui/src/tracks/thread_state/index.ts
index 074025e..c72d61a 100644
--- a/ui/src/tracks/thread_state/index.ts
+++ b/ui/src/tracks/thread_state/index.ts
@@ -171,7 +171,10 @@
   }
 
   async onDestroy() {
-    await this.query(`drop view if exists ${this.tableName('thread_state')}`);
+    if (this.engine.isAlive) {
+      await this.engine.query(
+          `drop view if exists ${this.tableName('thread_state')}`);
+    }
   }
 }
 
@@ -192,7 +195,7 @@
     const {
       visibleTimeScale: timeScale,
       visibleTimeSpan,
-    } = globals.frontendLocalState;
+    } = globals.timeline;
     const data = this.data();
     const charWidth = ctx.measureText('dbpqaouk').width / 8;
 
@@ -276,7 +279,7 @@
   onMouseClick({x}: {x: number}) {
     const data = this.data();
     if (data === undefined) return false;
-    const {visibleTimeScale} = globals.frontendLocalState;
+    const {visibleTimeScale} = globals.timeline;
     const time = visibleTimeScale.pxToHpTime(x);
     const index = search(data.starts, time.toTime());
     if (index === -1) return false;
diff --git a/ui/src/tracks/visualised_args/index.ts b/ui/src/tracks/visualised_args/index.ts
index 0111078..bd0c746 100644
--- a/ui/src/tracks/visualised_args/index.ts
+++ b/ui/src/tracks/visualised_args/index.ts
@@ -80,7 +80,9 @@
   }
 
   async onDestroy(): Promise<void> {
-    this.engine.query(`drop view ${this.helperViewName}`);
+    if (this.engine.isAlive) {
+      this.engine.query(`drop view ${this.helperViewName}`);
+    }
   }
 
   getFont() {