Merge "tp: introduce bump allocator for object allocation in trace sorter"
diff --git a/docs/contributing/getting-started.md b/docs/contributing/getting-started.md
index a101f29..bbac4c2 100644
--- a/docs/contributing/getting-started.md
+++ b/docs/contributing/getting-started.md
@@ -109,6 +109,9 @@
 exceptions are UI-only, docs-only or GN-only changes, for which the Android CI
 can be bypassed, as those are not built as part of the Android tree.
 
+You can also
+[test a pending Perfetto CL against Chrome's TryBots](testing.md#chromium).
+
 ## Community
 
 You can reach us on our [Discord channel](https://discord.gg/35ShE3A).
diff --git a/docs/contributing/testing.md b/docs/contributing/testing.md
index 49c10a9..e09514b 100644
--- a/docs/contributing/testing.md
+++ b/docs/contributing/testing.md
@@ -177,7 +177,7 @@
 adb shell /data/local/tmp/CtsPerfettoTestCases64
 ```
 
-Chromium waterfall
+{#chromium} Chromium waterfall
 ------------------
 Perfetto is constantly rolled into chromium's //third_party/perfetto via
 [this autoroller](https://autoroll.skia.org/r/perfetto-chromium-autoroll).
@@ -185,4 +185,66 @@
 The [Chromium CI](https://build.chromium.org) runs the `perfetto_unittests`
 target, as defined in the [buildbot config][chromium_buildbot].
 
+You can also test a pending Perfetto CL against Chromium's CI / TryBots
+before submitting it. This can be useful when making trickier API changes or to
+test on platforms that the Perfetto CI doesn't cover (e.g. Windows, MacOS),
+allowing you to verify the patch before you submit it (and it then eventually
+auto-rolls into Chromium).
+
+To do this, first make sure you have uploaded your Perfetto patch to the
+Android Gerrit. Next, create a new Chromium CL that modifies Chromium's
+`//src/DEPS` file.
+
+If you recently uploaded your change, it may be enough to modify the git commit
+hash in the `DEPS` entry for `src/third_party/perfetto`:
+
+```
+  'src/third_party/perfetto':
+    Var('android_git') + '/platform/external/perfetto.git' + '@' + '8fe19f55468ee227e99c1a682bd8c0e8f7e5bcdb',
+```
+
+Replace the git hash with the commit hash of your most recent patch set, which
+you can find in gerrit next to the active patch set number.
+
+Alternatively, you can add `hooks` to patch in the pending CL on top of
+Chromium's current third_party/perfetto revision. For this, add the following
+entries to the `hooks` array in Chromium's `//src/DEPS` file, modifying the
+`refs/changes/XX/YYYYYYY/ZZ` to the appropriate values for your gerrit change.
+You can see these values when pressing the "Download" button in gerrit. You can
+also use this method to patch in multiple Perfetto changes at once by
+adding additional `hooks` entries. [Here][chromium_cl]'s an example CL.
+
+```
+  {
+    'name': 'fetch_custom_patch',
+    'pattern': '.',
+    'action': [ 'git', '-C', 'src/third_party/perfetto/',
+                'fetch', 'https://android.googlesource.com/platform/external/perfetto',
+                'refs/changes/XX/YYYYYYY/ZZ',
+    ],
+  },
+  {
+    'name': 'apply_custom_patch',
+    'pattern': '.',
+    'action': ['git', '-C', 'src/third_party/perfetto/',
+               '-c', 'user.name=Custom Patch', '-c', 'user.email=custompatch@example.com',
+               'cherry-pick', 'FETCH_HEAD',
+    ],
+  },
+```
+
+If you'd like to test your change against the SDK build of Chrome, you
+can add `Cq-Include-Trybots:` lines for perfetto SDK trybots to the change
+description in gerrit (this won't be needed once Chrome's migration to the
+SDK is complete, see [tracking bug][sdk_migration_bug]):
+
+```
+Cq-Include-Trybots: luci.chromium.try:linux-perfetto-rel
+Cq-Include-Trybots: luci.chromium.try:android-perfetto-rel
+Cq-Include-Trybots: luci.chromium.try:mac-perfetto-rel
+Cq-Include-Trybots: luci.chromium.try:win-perfetto-rel
+```
+
 [chromium_buildbot]: https://cs.chromium.org/search/?q=perfetto_.*tests+f:%5Esrc/testing.*json$&sq=package:chromium&type=cs
+[chromium_cl]: https://chromium-review.googlesource.com/c/chromium/src/+/2030528
+[sdk_migration_bug]: https://crbug.com/1006541
diff --git a/include/perfetto/ext/base/string_view.h b/include/perfetto/ext/base/string_view.h
index 7c48ea0..1fe3696 100644
--- a/include/perfetto/ext/base/string_view.h
+++ b/include/perfetto/ext/base/string_view.h
@@ -127,11 +127,7 @@
       return false;
     if (other.size() > size())
       return false;
-    for (uint32_t i = 0; i < other.size(); ++i) {
-      if (at(i) != other.at(i))
-        return false;
-    }
-    return true;
+    return memcmp(data(), other.data(), other.size()) == 0;
   }
 
   bool EndsWith(const StringView& other) const {
@@ -141,12 +137,8 @@
       return false;
     if (other.size() > size())
       return false;
-    const size_t off = size() - other.size();
-    for (size_t i = 0; i < other.size(); ++i) {
-      if (at(off + i) != other.at(i))
-        return false;
-    }
-    return true;
+    size_t off = size() - other.size();
+    return memcmp(data() + off, other.data(), other.size()) == 0;
   }
 
   std::string ToStdString() const {
diff --git a/include/perfetto/tracing/track_event.h b/include/perfetto/tracing/track_event.h
index 2651927..7366b85 100644
--- a/include/perfetto/tracing/track_event.h
+++ b/include/perfetto/tracing/track_event.h
@@ -17,7 +17,6 @@
 #ifndef INCLUDE_PERFETTO_TRACING_TRACK_EVENT_H_
 #define INCLUDE_PERFETTO_TRACING_TRACK_EVENT_H_
 
-#include "perfetto/base/time.h"
 #include "perfetto/tracing/internal/track_event_data_source.h"
 #include "perfetto/tracing/internal/track_event_internal.h"
 #include "perfetto/tracing/internal/track_event_macros.h"
diff --git a/protos/perfetto/trace/chrome/chrome_metadata.proto b/protos/perfetto/trace/chrome/chrome_metadata.proto
index 8882593..d2c46e4 100644
--- a/protos/perfetto/trace/chrome/chrome_metadata.proto
+++ b/protos/perfetto/trace/chrome/chrome_metadata.proto
@@ -85,4 +85,7 @@
 
   // List of all active triggers in current session, when trace was triggered.
   repeated TriggerRule active_rules = 2;
+
+  // Hash of the scenario name.
+  optional fixed32 scenario_name_hash = 3;
 }
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 5f3473a..a215592 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -3823,6 +3823,9 @@
 
   // List of all active triggers in current session, when trace was triggered.
   repeated TriggerRule active_rules = 2;
+
+  // Hash of the scenario name.
+  optional fixed32 scenario_name_hash = 3;
 }
 
 // End of protos/perfetto/trace/chrome/chrome_metadata.proto
diff --git a/src/trace_processor/dynamic/experimental_annotated_stack_generator.cc b/src/trace_processor/dynamic/experimental_annotated_stack_generator.cc
index b26cb84..1527295 100644
--- a/src/trace_processor/dynamic/experimental_annotated_stack_generator.cc
+++ b/src/trace_processor/dynamic/experimental_annotated_stack_generator.cc
@@ -56,41 +56,56 @@
 //   /system/lib64/libc.so
 //   /system/framework/framework.jar
 //   /memfd:jit-cache (deleted)
+//   /data/dalvik-cache/arm64/<snip>.apk@classes.dex
+//   /data/app/<snip>/base.apk!libmonochrome_64.so
 //   [vdso]
 // TODO(rsavitski): consider moving this to a hidden column on
-// stack_profile_mapping, once this logic is sufficiently stable.
+// stack_profile_mapping.
 MapType ClassifyMap(NullTermStringView map) {
   if (map.empty())
     return MapType::kOther;
 
   // Primary mapping where modern ART puts jitted code.
-  // TODO(rsavitski): look into /memfd:jit-zygote-cache.
-  if (!strncmp(map.c_str(), "/memfd:jit-cache", 16))
+  // The Zygote's JIT region is inherited by all descendant apps, so it can
+  // still appear in their callstacks.
+  if (map.StartsWith("/memfd:jit-cache") ||
+      map.StartsWith("/memfd:jit-zygote-cache")) {
     return MapType::kArtJit;
+  }
 
   size_t last_slash_pos = map.rfind('/');
   if (last_slash_pos != NullTermStringView::npos) {
-    if (!strncmp(map.c_str() + last_slash_pos, "/libart.so", 10))
-      return MapType::kNativeLibart;
-    if (!strncmp(map.c_str() + last_slash_pos, "/libartd.so", 11))
+    base::StringView suffix = map.substr(last_slash_pos);
+    if (suffix.StartsWith("/libart.so") || suffix.StartsWith("/libartd.so"))
       return MapType::kNativeLibart;
   }
-
   size_t extension_pos = map.rfind('.');
   if (extension_pos != NullTermStringView::npos) {
-    if (!strncmp(map.c_str() + extension_pos, ".so", 3))
+    base::StringView suffix = map.substr(extension_pos);
+    if (suffix.StartsWith(".so"))
       return MapType::kNativeOther;
+    // unqualified dex
+    if (suffix.StartsWith(".dex"))
+      return MapType::kArtInterp;
     // dex with verification speedup info, produced by dex2oat
-    if (!strncmp(map.c_str() + extension_pos, ".vdex", 5))
+    if (suffix.StartsWith(".vdex"))
       return MapType::kArtInterp;
     // possibly uncompressed dex in a jar archive
-    if (!strncmp(map.c_str() + extension_pos, ".jar", 4))
+    if (suffix.StartsWith(".jar"))
+      return MapType::kArtInterp;
+    // android package (zip file), this can contain uncompressed dexes or
+    // native libraries that are mmap'd directly into the process. We rely on
+    // libunwindstack's MapInfo::GetFullName, which suffixes the mapping with
+    // "!lib.so" if it knows that the referenced piece of the archive is an
+    // uncompressed ELF file. So an unadorned ".apk" is assumed to be a dex
+    // file.
+    if (suffix.StartsWith(".apk"))
       return MapType::kArtInterp;
     // ahead of time compiled ELFs
-    if (!strncmp(map.c_str() + extension_pos, ".oat", 4))
+    if (suffix.StartsWith(".oat"))
       return MapType::kArtAot;
     // older/alternative name for .oat
-    if (!strncmp(map.c_str() + extension_pos, ".odex", 5))
+    if (suffix.StartsWith(".odex"))
       return MapType::kArtAot;
   }
   return MapType::kOther;
@@ -167,7 +182,8 @@
 
   // Walk the callsites root-to-leaf, annotating:
   // * managed frames with their execution state (interpreted/jit/aot)
-  // * common ART frames, which are usually not relevant
+  // * common ART frames, which are usually not relevant to
+  //   visualisation/inspection
   //
   // This is not a per-frame decision, because we do not want to filter out ART
   // frames immediately after a JNI transition (such frames are often relevant).
@@ -175,13 +191,12 @@
   // As a consequence of the logic being based on a root-to-leaf walk, a given
   // callsite will always have the same annotation, as the parent path is always
   // the same, and children callsites do not affect their parents' annotations.
-  //
-  // This could also be implemented as a hidden column on the callsite table
-  // (populated at import time), but we want to be more flexible for now.
   StringId art_jni_trampoline =
       context_->storage->InternString("art_jni_trampoline");
 
   StringId common_frame = context_->storage->InternString("common-frame");
+  StringId common_frame_interp =
+      context_->storage->InternString("common-frame-interp");
   StringId art_interp = context_->storage->InternString("interp");
   StringId art_jit = context_->storage->InternString("jit");
   StringId art_aot = context_->storage->InternString("aot");
@@ -249,12 +264,41 @@
       continue;
     }
 
+    // Mixed callstack, tag libart frames as uninteresting (common-frame).
+    // Special case a subset of interpreter implementation frames as
+    // "common-frame-interp" using frame name prefixes. Those functions are
+    // actually executed, whereas the managed "interp" frames are synthesised as
+    // their caller by the unwinding library (based on the dex_pc virtual
+    // register restored using the libart's DWARF info). The heuristic covers
+    // the "nterp" and "switch" interpreter implementations.
+    //
+    // Example:
+    //  <towards root>
+    //  android.view.WindowLayout.computeFrames [interp]
+    //  nterp_op_iget_object_slow_path [common-frame-interp]
+    //
+    // This annotation is helpful when trying to answer "what mode was the
+    // process in?" based on the leaf frame of the callstack. As we want to
+    // classify such cases as interpreted, even though the leaf frame is
+    // libart.so.
+    //
+    // For "switch" interpreter, we match any frame starting with
+    // "art::interpreter::" according to itanium mangling.
     if (annotation_state == State::kEraseLibart &&
         map_type == MapType::kNativeLibart) {
+      NullTermStringView fname = context_->storage->GetString(fname_id);
+      if (fname.StartsWith("nterp_") || fname.StartsWith("Nterp") ||
+          fname.StartsWith("ExecuteNterp") ||
+          fname.StartsWith("ExecuteSwitchImpl") ||
+          fname.StartsWith("_ZN3art11interpreter")) {
+        annotations_reversed.push_back(common_frame_interp);
+        continue;
+      }
       annotations_reversed.push_back(common_frame);
       continue;
     }
 
+    // default - no special annotation
     annotations_reversed.push_back(kNullStringId);
   }
 
diff --git a/src/trace_processor/metrics/sql/android/android_startup.sql b/src/trace_processor/metrics/sql/android/android_startup.sql
index 634e908..bd80e8a 100644
--- a/src/trace_processor/metrics/sql/android/android_startup.sql
+++ b/src/trace_processor/metrics/sql/android/android_startup.sql
@@ -151,7 +151,7 @@
           MAIN_THREAD_TIME_FOR_LAUNCH_AND_STATE(launches.startup_id, 'Running'), 0
         ),
         'runnable_dur_ns', IFNULL(
-          MAIN_THREAD_TIME_FOR_LAUNCH_AND_STATE(launches.startup_id, 'R'), 0
+          MAIN_THREAD_TIME_FOR_LAUNCH_AND_STATE(launches.startup_id, 'R*'), 0
         ),
         'uninterruptible_sleep_dur_ns', IFNULL(
           MAIN_THREAD_TIME_FOR_LAUNCH_AND_STATE(launches.startup_id, 'D*'), 0
@@ -320,7 +320,7 @@
         UNION ALL
         SELECT 'Main Thread - Time spent in Runnable state'
           AS slow_cause
-        WHERE MAIN_THREAD_TIME_FOR_LAUNCH_AND_STATE(launches.startup_id, 'R') > 1e8
+        WHERE MAIN_THREAD_TIME_FOR_LAUNCH_AND_STATE(launches.startup_id, 'R*') > 100e6
 
         UNION ALL
         SELECT 'Main Thread - Time spent in interruptible sleep state'
@@ -358,6 +358,13 @@
         WHERE ANDROID_SUM_DUR_FOR_STARTUP_AND_SLICE(launches.startup_id, 'VerifyClass*') > 10e6
 
         UNION ALL
+        SELECT 'Potential CPU contention with '
+          || MOST_ACTIVE_PROCESS_FOR_LAUNCH(launches.startup_id)
+          AS slow_cause
+        WHERE MAIN_THREAD_TIME_FOR_LAUNCH_AND_STATE(launches.startup_id, 'R*') > 100e6
+          AND MOST_ACTIVE_PROCESS_FOR_LAUNCH(launches.startup_id) IS NOT NULL
+
+        UNION ALL
         SELECT 'JIT Activity'
           AS slow_cause
         WHERE THREAD_TIME_FOR_LAUNCH_STATE_AND_THREAD(
diff --git a/src/trace_processor/metrics/sql/android/startup/mcycles_per_launch.sql b/src/trace_processor/metrics/sql/android/startup/mcycles_per_launch.sql
index 993d61e..b47381e 100644
--- a/src/trace_processor/metrics/sql/android/startup/mcycles_per_launch.sql
+++ b/src/trace_processor/metrics/sql/android/startup/mcycles_per_launch.sql
@@ -111,3 +111,16 @@
     );
   '
 );
+
+-- Given a launch id, returns the most active process name.
+SELECT CREATE_FUNCTION(
+  'MOST_ACTIVE_PROCESS_FOR_LAUNCH(startup_id INT)',
+  'STRING',
+  '
+    SELECT process.name AS process_name
+    FROM top_mcyles_process_excluding_started_per_launch
+    JOIN process USING (upid)
+    WHERE startup_id = $startup_id
+    ORDER BY mcycles DESC LIMIT 1;
+  '
+);
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_long_tasks.sql b/src/trace_processor/metrics/sql/chrome/chrome_long_tasks.sql
index ec05765..3bd052b 100644
--- a/src/trace_processor/metrics/sql/chrome/chrome_long_tasks.sql
+++ b/src/trace_processor/metrics/sql/chrome/chrome_long_tasks.sql
@@ -24,6 +24,12 @@
   'function_prefix', ''
 );
 
+SELECT CREATE_FUNCTION(
+  'IS_LONG_CHOREOGRAPHER_TASK(dur LONG)',
+  'BOOL',
+  'SELECT $dur >= 4 * 1e6'
+);
+
 -- Note that not all slices will be mojo slices; filter on interface_name IS
 -- NOT NULL for mojo slices specifically.
 DROP TABLE IF EXISTS long_tasks_extracted_slices;
@@ -72,7 +78,7 @@
     SELECT *
     FROM SELECT_BEGIN_MAIN_FRAME_JAVA_SLICES('LongTaskTracker')
     UNION ALL
-    SELECT * FROM chrome_choreographer_tasks
+    SELECT * FROM chrome_choreographer_tasks WHERE IS_LONG_CHOREOGRAPHER_TASK(dur)
   ),
   -- Intermediate step to allow us to sort java view names.
   root_slice_and_java_view_not_grouped AS (
@@ -101,6 +107,7 @@
       GET_JAVA_VIEWS_TASK_TYPE(kind) AS task_type,
       id
     FROM long_task_slices_with_java_views
+    WHERE kind = "SingleThreadProxy::BeginMainFrame"
   ),
   scheduler_tasks_with_mojo AS (
     SELECT
@@ -129,7 +136,16 @@
 FROM long_tasks_internal_tbl s1
 LEFT JOIN scheduler_tasks_with_mojo s2 ON s2.id = s1.id
 LEFT JOIN java_views_tasks s3 ON s3.id = s1.id
-LEFT JOIN navigation_tasks s4 ON s4.id = s1.id;
+LEFT JOIN navigation_tasks s4 ON s4.id = s1.id
+UNION ALL
+-- Choreographer slices won't necessarily be associated with an overlying
+-- LongTaskTracker slice, so join them separately.
+SELECT
+  printf('%s(java_views=%s)', kind, java_views) as full_name,
+  GET_JAVA_VIEWS_TASK_TYPE(kind) AS task_type,
+  id
+FROM long_task_slices_with_java_views
+WHERE kind = "Choreographer";
 
 DROP VIEW IF EXISTS chrome_long_tasks;
 CREATE VIEW chrome_long_tasks AS
diff --git a/src/trace_processor/util/annotated_callsites.cc b/src/trace_processor/util/annotated_callsites.cc
index 3428330..5e23130 100644
--- a/src/trace_processor/util/annotated_callsites.cc
+++ b/src/trace_processor/util/annotated_callsites.cc
@@ -101,8 +101,34 @@
       return {state, CallsiteAnnotation::kArtAot};
   }
 
+  // Mixed callstack, tag libart frames as uninteresting (common-frame).
+  // Special case a subset of interpreter implementation frames as
+  // "common-frame-interp" using frame name prefixes. Those functions are
+  // actually executed, whereas the managed "interp" frames are synthesised as
+  // their caller by the unwinding library (based on the dex_pc virtual
+  // register restored using the libart's DWARF info). The heuristic covers
+  // the "nterp" and "switch" interpreter implementations.
+  //
+  // Example:
+  //  <towards root>
+  //  android.view.WindowLayout.computeFrames [interp]
+  //  nterp_op_iget_object_slow_path [common-frame-interp]
+  //
+  // This annotation is helpful when trying to answer "what mode was the
+  // process in?" based on the leaf frame of the callstack. As we want to
+  // classify such cases as interpreted, even though the leaf frame is
+  // libart.so.
+  //
+  // For "switch" interpreter, we match any frame starting with
+  // "art::interpreter::" according to itanium mangling.
   if (state == State::kEraseLibart && map_type == MapType::kNativeLibart) {
-    states_.emplace(callsite.id(), state);
+    NullTermStringView fname = context_.storage->GetString(frame.name());
+    if (fname.StartsWith("nterp_") || fname.StartsWith("Nterp") ||
+        fname.StartsWith("ExecuteNterp") ||
+        fname.StartsWith("ExecuteSwitchImpl") ||
+        fname.StartsWith("_ZN3art11interpreter")) {
+      return {state, CallsiteAnnotation::kCommonFrameInterp};
+    }
     return {state, CallsiteAnnotation::kCommonFrame};
   }
 
@@ -129,33 +155,47 @@
     return MapType::kOther;
 
   // Primary mapping where modern ART puts jitted code.
-  // TODO(rsavitski): look into /memfd:jit-zygote-cache.
-  if (!strncmp(map.c_str(), "/memfd:jit-cache", 16))
+  // The Zygote's JIT region is inherited by all descendant apps, so it can
+  // still appear in their callstacks.
+  if (map.StartsWith("/memfd:jit-cache") ||
+      map.StartsWith("/memfd:jit-zygote-cache")) {
     return MapType::kArtJit;
+  }
 
   size_t last_slash_pos = map.rfind('/');
   if (last_slash_pos != NullTermStringView::npos) {
-    if (!strncmp(map.c_str() + last_slash_pos, "/libart.so", 10))
-      return MapType::kNativeLibart;
-    if (!strncmp(map.c_str() + last_slash_pos, "/libartd.so", 11))
+    base::StringView suffix = map.substr(last_slash_pos);
+    if (suffix.StartsWith("/libart.so") || suffix.StartsWith("/libartd.so"))
       return MapType::kNativeLibart;
   }
 
   size_t extension_pos = map.rfind('.');
   if (extension_pos != NullTermStringView::npos) {
-    if (!strncmp(map.c_str() + extension_pos, ".so", 3))
+    base::StringView suffix = map.substr(extension_pos);
+    if (suffix.StartsWith(".so"))
       return MapType::kNativeOther;
+    // unqualified dex
+    if (suffix.StartsWith(".dex"))
+      return MapType::kArtInterp;
     // dex with verification speedup info, produced by dex2oat
-    if (!strncmp(map.c_str() + extension_pos, ".vdex", 5))
+    if (suffix.StartsWith(".vdex"))
       return MapType::kArtInterp;
     // possibly uncompressed dex in a jar archive
-    if (!strncmp(map.c_str() + extension_pos, ".jar", 4))
+    if (suffix.StartsWith(".jar"))
+      return MapType::kArtInterp;
+    // android package (zip file), this can contain uncompressed dexes or
+    // native libraries that are mmap'd directly into the process. We rely on
+    // libunwindstack's MapInfo::GetFullName, which suffixes the mapping with
+    // "!lib.so" if it knows that the referenced piece of the archive is an
+    // uncompressed ELF file. So an unadorned ".apk" is assumed to be a dex
+    // file.
+    if (suffix.StartsWith(".apk"))
       return MapType::kArtInterp;
     // ahead of time compiled ELFs
-    if (!strncmp(map.c_str() + extension_pos, ".oat", 4))
+    if (suffix.StartsWith(".oat"))
       return MapType::kArtAot;
     // older/alternative name for .oat
-    if (!strncmp(map.c_str() + extension_pos, ".odex", 5))
+    if (suffix.StartsWith(".odex"))
       return MapType::kArtAot;
   }
   return MapType::kOther;
diff --git a/src/trace_processor/util/annotated_callsites.h b/src/trace_processor/util/annotated_callsites.h
index 4e617f6..0fbb555 100644
--- a/src/trace_processor/util/annotated_callsites.h
+++ b/src/trace_processor/util/annotated_callsites.h
@@ -30,6 +30,7 @@
 enum class CallsiteAnnotation {
   kNone,
   kCommonFrame,
+  kCommonFrameInterp,
   kArtInterpreted,
   kArtJit,
   kArtAot,
@@ -38,7 +39,7 @@
 // Helper class to augment callsite with (currently Android-specific)
 // annotations. A given callsite will always have the same annotation. This
 // class will internally cache already computed annotations. An annotation
-// depends only of the current callsite and the annotation sof its parent
+// depends only of the current callsite and the annotations of its parent
 // callsites (going to the root).
 class AnnotatedCallsites {
  public:
diff --git a/src/trace_processor/util/profile_builder.cc b/src/trace_processor/util/profile_builder.cc
index e1e53ac..f859d53 100644
--- a/src/trace_processor/util/profile_builder.cc
+++ b/src/trace_processor/util/profile_builder.cc
@@ -45,6 +45,8 @@
       return "jit";
     case CallsiteAnnotation::kCommonFrame:
       return "common-frame";
+    case CallsiteAnnotation::kCommonFrameInterp:
+      return "common-frame-interp";
   }
   PERFETTO_FATAL("For GCC");
 }
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.cc b/src/traced/probes/ftrace/ftrace_config_muxer.cc
index 5b0ad79..b0d75e5 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.cc
@@ -597,28 +597,41 @@
 FtraceConfigId FtraceConfigMuxer::SetupConfig(const FtraceConfig& request,
                                               FtraceSetupErrors* errors) {
   EventFilter filter;
-  bool is_ftrace_enabled = ftrace_->IsTracingEnabled();
   if (ds_configs_.empty()) {
     PERFETTO_DCHECK(active_configs_.empty());
 
-    // If someone outside of perfetto is using ftrace give up now.
-    if (!request.preserve_ftrace_buffer() && is_ftrace_enabled &&
-        !IsOldAtrace()) {
-      PERFETTO_ELOG("ftrace in use by non-Perfetto.");
+    // If someone outside of perfetto is using a non-nop tracer, yield. We can't
+    // realistically figure out all notions of "in use" even if we look at
+    // set_event or events/enable, so this is all we check for.
+    if (!request.preserve_ftrace_buffer() && !ftrace_->IsTracingAvailable()) {
+      PERFETTO_ELOG(
+          "ftrace in use by non-Perfetto. Check that %s current_tracer is nop.",
+          ftrace_->GetRootPath().c_str());
       return 0;
     }
 
-    // Setup ftrace, without starting it. Setting buffers can be quite slow
-    // (up to hundreds of ms).
-    if (!request.preserve_ftrace_buffer())
-      SetupClock(request);
-    SetupBufferSize(request);
-  } else {
-    // Did someone turn ftrace off behind our back? If so give up.
-    if (!active_configs_.empty() && !is_ftrace_enabled && !IsOldAtrace()) {
-      PERFETTO_ELOG("ftrace disabled by non-Perfetto.");
-      return 0;
+    // Clear tracefs state, remembering which value of "tracing_on" to restore
+    // to after we're done, though we won't restore the rest of the tracefs
+    // state.
+    current_state_.saved_tracing_on = ftrace_->GetTracingOn();
+    if (!request.preserve_ftrace_buffer()) {
+      ftrace_->SetTracingOn(false);
+      // This will fail on release ("user") builds due to ACLs, but that's
+      // acceptable since the per-event enabling/disabling should still be
+      // balanced.
+      ftrace_->DisableAllEvents();
+      ftrace_->ClearTrace();
     }
+
+    // Set up the rest of the tracefs state, without starting it.
+    // Notes:
+    // * resizing buffers can be quite slow (up to hundreds of ms).
+    // * resizing buffers doesn't clear their existing contents, which matters
+    // to the preserve_ftrace_buffer option.
+    if (!request.preserve_ftrace_buffer()) {
+      SetupClock(request);
+    }
+    SetupBufferSize(request);
   }
 
   std::set<GroupAndName> events = GetFtraceEvents(request, table_);
@@ -754,14 +767,10 @@
     return false;
   }
 
+  // Enable tracing_on to activate ftrace ring buffer before activate the first
+  // config.
   if (active_configs_.empty()) {
-    if (!ds_configs_.at(id).preserve_ftrace_buffer &&
-        ftrace_->IsTracingEnabled() && !IsOldAtrace()) {
-      // If someone outside of perfetto is using ftrace give up now.
-      PERFETTO_ELOG("ftrace in use by non-Perfetto.");
-      return false;
-    }
-    if (!ftrace_->EnableTracing()) {
+    if (!ftrace_->SetTracingOn(true)) {
       PERFETTO_ELOG("Failed to enable ftrace.");
       return false;
     }
@@ -816,14 +825,14 @@
       current_state_.ftrace_events.DisableEvent(event->ftrace_event_id);
   }
 
-  // If there aren't any more active configs, disable ftrace.
   auto active_it = active_configs_.find(config_id);
   if (active_it != active_configs_.end()) {
     active_configs_.erase(active_it);
     if (active_configs_.empty()) {
-      // This was the last active config, disable ftrace.
-      if (!ftrace_->DisableTracing())
-        PERFETTO_ELOG("Failed to disable ftrace.");
+      // This was the last active config for now, but potentially more dormant
+      // configs need to be activated. We are not interested in reading while no
+      // active configs so diasble tracing_on here.
+      ftrace_->SetTracingOn(false);
     }
   }
 
@@ -835,6 +844,7 @@
       current_state_.cpu_buffer_size_pages = 1;
     ftrace_->DisableAllEvents();
     ftrace_->ClearTrace();
+    ftrace_->SetTracingOn(current_state_.saved_tracing_on);
   }
 
   if (current_state_.atrace_on) {
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.h b/src/traced/probes/ftrace/ftrace_config_muxer.h
index 4247af1..fb5850a 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.h
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.h
@@ -185,6 +185,7 @@
     bool atrace_on = false;
     std::vector<std::string> atrace_apps;
     std::vector<std::string> atrace_categories;
+    bool saved_tracing_on;  // Backup for the original tracing_on.
   };
 
   FtraceConfigMuxer(const FtraceConfigMuxer&) = delete;
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
index 96c8da6..5be1f1f 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
@@ -237,6 +237,14 @@
 
   FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {});
 
+  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
   EXPECT_CALL(ftrace, WriteToFile(_, _)).WillRepeatedly(Return(true));
   EXPECT_CALL(ftrace, WriteToFile("/root/events/raw_syscalls/sys_enter/filter",
                                   "id == 0 || id == 1"));
@@ -259,6 +267,15 @@
   config.add_syscall_events("sys_open");
   config.add_syscall_events("sys_not_a_call");
 
+  ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+
   // Unknown syscall is ignored.
   ASSERT_TRUE(model.SetupConfig(config));
   ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre(0));
@@ -280,6 +297,9 @@
   FtraceConfig syscall_read_config = syscall_config;
   syscall_read_config.add_syscall_events("sys_read");
 
+  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+
   // Expect no filter for non-syscall config.
   model.SetupConfig(empty_config);
   ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre());
@@ -314,17 +334,20 @@
 
   FtraceConfigMuxer model(&ftrace, mock_table.get(), GetSyscallTable(), {});
 
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
   ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
       .WillByDefault(Return("[local] global boot"));
   EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
       .Times(AnyNumber());
-
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .Times(2)
-      .WillRepeatedly(Return('0'));
   EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
   EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
   EXPECT_CALL(ftrace,
               WriteToFile("/root/events/power/cpu_frequency/enable", "1"));
   EXPECT_CALL(*mock_table, GetEvent(GroupAndName("power", "cpu_frequency")))
@@ -341,6 +364,8 @@
               GetOrCreateEvent(GroupAndName("power", "cpu_frequency")));
 
   FtraceConfigId id = model.SetupConfig(config);
+
+  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
   ASSERT_TRUE(model.ActivateConfig(id));
 
   const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
@@ -379,6 +404,11 @@
       .WillByDefault(Return(&event2));
   EXPECT_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_two", "foo")));
 
+  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+
   FtraceConfigId id = model.SetupConfig(config);
   ASSERT_TRUE(model.ActivateConfig(id));
 
@@ -397,17 +427,20 @@
 
   FtraceConfig config = CreateFtraceConfig({"sched/*"});
 
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
   ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
       .WillByDefault(Return("[local] global boot"));
   EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
       .Times(AnyNumber());
-
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .Times(2)
-      .WillRepeatedly(Return('0'));
   EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
   EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
   EXPECT_CALL(ftrace,
               WriteToFile("/root/events/sched/sched_switch/enable", "1"));
   EXPECT_CALL(ftrace,
@@ -443,6 +476,8 @@
 
   FtraceConfigId id = model.SetupConfig(config);
   ASSERT_TRUE(id);
+
+  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
   ASSERT_TRUE(model.ActivateConfig(id));
 
   const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
@@ -492,6 +527,11 @@
       .WillByDefault(Return(&event2));
   EXPECT_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_two", "foo")));
 
+  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+
   FtraceConfigId id = model.SetupConfig(config);
   ASSERT_TRUE(model.ActivateConfig(id));
 
@@ -512,22 +552,27 @@
 
   FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
   ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
       .WillByDefault(Return("[local] global boot"));
   EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
       .Times(AnyNumber());
-
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .Times(2)
-      .WillRepeatedly(Return('0'));
   EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
   EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
   EXPECT_CALL(ftrace,
               WriteToFile("/root/events/sched/sched_switch/enable", "1"));
 
   FtraceConfigId id = model.SetupConfig(config);
   ASSERT_TRUE(id);
+
+  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
   ASSERT_TRUE(model.ActivateConfig(id));
 
   const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
@@ -542,13 +587,14 @@
   ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace));
   EXPECT_CALL(ftrace, NumberOfCpus()).Times(AnyNumber());
 
+  EXPECT_CALL(ftrace,
+              WriteToFile("/root/events/sched/sched_switch/enable", "0"));
   EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
   EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", "4"));
   EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
-  EXPECT_CALL(ftrace,
-              WriteToFile("/root/events/sched/sched_switch/enable", "0"));
   EXPECT_CALL(ftrace, ClearFile("/root/trace"));
   EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
 
   ASSERT_TRUE(model.RemoveConfig(id));
 }
@@ -561,8 +607,8 @@
   FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   // If someone is using ftrace already don't stomp on what they are doing.
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("function"));
   FtraceConfigId id = model.SetupConfig(config);
   ASSERT_FALSE(id);
 }
@@ -576,8 +622,10 @@
 
   FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillOnce(Return('0'));
+  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
   EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
                                                   "--only_userspace", "sched"}),
                                 _))
@@ -618,8 +666,10 @@
 
   FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillOnce(Return('0'));
+  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
   EXPECT_CALL(
       atrace,
       RunAtrace(
@@ -663,6 +713,10 @@
 
   FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
+  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
   EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
                                                   "--only_userspace", "cat_a",
                                                   "-a", "app_a"}),
@@ -734,6 +788,10 @@
 
   FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
+  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
   EXPECT_CALL(
       atrace,
       RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
@@ -795,6 +853,10 @@
 
   FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
+  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
   EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
                                                   "--only_userspace", "cat_1",
                                                   "-a", "app_1"}),
@@ -832,6 +894,10 @@
 
   FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
+  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
   FtraceConfigId id_a = model.SetupConfig(config_a);
   ASSERT_TRUE(id_a);
 
@@ -879,8 +945,10 @@
   *config.add_atrace_categories() = "cat_1";
   *config.add_atrace_categories() = "cat_2";
 
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .WillRepeatedly(Return('0'));
+  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
 
   FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
@@ -990,17 +1058,20 @@
       CreateFtraceConfig({"sched/sched_switch", "cgroup/cgroup_mkdir"});
   FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
+  EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
   ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
       .WillByDefault(Return("[local] global boot"));
   EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
       .Times(AnyNumber());
-
-  EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
-      .Times(2)
-      .WillRepeatedly(Return('0'));
   EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
   EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
   EXPECT_CALL(ftrace,
               WriteToFile("/root/events/sched/sched_switch/enable", "1"));
   EXPECT_CALL(ftrace,
@@ -1010,6 +1081,8 @@
       .WillOnce(Return(true));
   FtraceConfigId id = model.SetupConfig(config);
   ASSERT_TRUE(id);
+
+  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
   ASSERT_TRUE(model.ActivateConfig(id));
 
   const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
@@ -1025,9 +1098,6 @@
   EXPECT_THAT(central_filter->GetEnabledEvents(),
               Contains(kCgroupMkdirEventId));
 
-  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", "4"));
-  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
   EXPECT_CALL(ftrace,
               WriteToFile("/root/events/sched/sched_switch/enable", "0"));
   EXPECT_CALL(ftrace,
@@ -1035,8 +1105,12 @@
       .WillOnce(Return(false));
   EXPECT_CALL(ftrace, AppendToFile("/root/set_event", "!cgroup:cgroup_mkdir"))
       .WillOnce(Return(true));
+  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", "4"));
+  EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
   EXPECT_CALL(ftrace, ClearFile("/root/trace"));
   EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
   ASSERT_TRUE(model.RemoveConfig(id));
 }
 
@@ -1058,6 +1132,11 @@
   // Second data source - no compact encoding (default).
   FtraceConfig config_disabled = CreateFtraceConfig({"sched/sched_switch"});
 
+  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+
   {
     FtraceConfigId id = model.SetupConfig(config_enabled);
     ASSERT_TRUE(id);
@@ -1086,6 +1165,11 @@
   FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
   config.mutable_compact_sched()->set_enabled(true);
 
+  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+
   FtraceConfigId id = model.SetupConfig(config);
   ASSERT_TRUE(id);
 
@@ -1124,6 +1208,11 @@
       CreateFtraceConfig({"sched/sched_switch", "sched/generic"});
   config_with_disable.set_disable_generic_events(true);
 
+  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+
   {
     FtraceConfigId id = model.SetupConfig(config_default);
     ASSERT_TRUE(id);
@@ -1158,8 +1247,14 @@
   *config.add_function_graph_roots() = "sched*";
   *config.add_function_graph_roots() = "*mm_fault";
 
+  ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+
   EXPECT_CALL(ftrace, WriteToFile(_, _)).WillRepeatedly(Return(true));
 
+  EXPECT_CALL(ftrace, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+
   // Set up config, assert that the tracefs writes happened:
   EXPECT_CALL(ftrace, ClearFile("/root/set_ftrace_filter"));
   EXPECT_CALL(ftrace, ClearFile("/root/set_graph_function"));
diff --git a/src/traced/probes/ftrace/ftrace_controller.cc b/src/traced/probes/ftrace/ftrace_controller.cc
index d7eb5e6..d82d115 100644
--- a/src/traced/probes/ftrace/ftrace_controller.cc
+++ b/src/traced/probes/ftrace/ftrace_controller.cc
@@ -365,8 +365,8 @@
   ftrace_procfs_->ClearTrace();
 }
 
-void FtraceController::DisableAllEvents() {
-  ftrace_procfs_->DisableAllEvents();
+bool FtraceController::IsTracingAvailable() {
+  return ftrace_procfs_->IsTracingAvailable();
 }
 
 void FtraceController::Flush(FlushRequestID flush_id) {
diff --git a/src/traced/probes/ftrace/ftrace_controller.h b/src/traced/probes/ftrace/ftrace_controller.h
index 9a8ec2b..fd8cdee 100644
--- a/src/traced/probes/ftrace/ftrace_controller.h
+++ b/src/traced/probes/ftrace/ftrace_controller.h
@@ -77,8 +77,8 @@
                                                   bool preserve_ftrace_buffer);
   virtual ~FtraceController();
 
-  void DisableAllEvents();
   void ClearTrace();
+  bool IsTracingAvailable();
 
   bool AddDataSource(FtraceDataSource*) PERFETTO_WARN_UNUSED_RESULT;
   bool StartDataSource(FtraceDataSource*);
diff --git a/src/traced/probes/ftrace/ftrace_controller_unittest.cc b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
index 01e075f..ba86ca7 100644
--- a/src/traced/probes/ftrace/ftrace_controller_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
@@ -132,6 +132,13 @@
         .WillByDefault(Invoke(this, &MockFtraceProcfs::ReadTracingOn));
     EXPECT_CALL(*this, ReadOneCharFromFile("/root/tracing_on"))
         .Times(AnyNumber());
+
+    ON_CALL(*this, WriteToFile("/root/current_tracer", _))
+        .WillByDefault(Invoke(this, &MockFtraceProcfs::WriteCurrentTracer));
+    ON_CALL(*this, ReadFileIntoString("/root/current_tracer"))
+        .WillByDefault(Invoke(this, &MockFtraceProcfs::ReadCurrentTracer));
+    EXPECT_CALL(*this, ReadFileIntoString("/root/current_tracer"))
+        .Times(AnyNumber());
   }
 
   bool WriteTracingOn(const std::string& /*path*/, const std::string& value) {
@@ -144,6 +151,16 @@
     return tracing_on_ ? '1' : '0';
   }
 
+  bool WriteCurrentTracer(const std::string& /*path*/,
+                          const std::string& value) {
+    current_tracer_ = value;
+    return true;
+  }
+
+  std::string ReadCurrentTracer(const std::string& /*path*/) {
+    return current_tracer_;
+  }
+
   base::ScopedFile OpenPipeForCpu(size_t /*cpu*/) override {
     return base::ScopedFile(base::OpenFile("/dev/null", O_RDONLY));
   }
@@ -159,7 +176,8 @@
   bool is_tracing_on() { return tracing_on_; }
 
  private:
-  bool tracing_on_ = false;
+  bool tracing_on_ = true;
+  std::string current_tracer_ = "nop";
 };
 
 }  // namespace
@@ -261,8 +279,16 @@
 
   FtraceConfig config = CreateFtraceConfig({"group/foo"});
 
-  EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "1"));
+  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(*controller->procfs(), ClearFile("/root/trace"))
+      .WillOnce(Return(true));
+  EXPECT_CALL(*controller->procfs(),
+              ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")))
+      .WillRepeatedly(Return(true));
   EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", _));
+  EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "1"));
+
   auto data_source = controller->AddFakeDataSource(config);
   ASSERT_TRUE(data_source);
 
@@ -279,19 +305,19 @@
   Mock::VerifyAndClearExpectations(controller->runner());
 
   // State clearing on tracing teardown.
+  EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "0"));
+  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
   EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", "4"));
+  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
   EXPECT_CALL(*controller->procfs(), ClearFile("/root/trace"))
       .WillOnce(Return(true));
   EXPECT_CALL(*controller->procfs(),
               ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")))
       .WillRepeatedly(Return(true));
-  EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "0"));
-  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
-  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
-  EXPECT_TRUE(controller->procfs()->is_tracing_on());
+  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "1"));
 
   data_source.reset();
-  EXPECT_FALSE(controller->procfs()->is_tracing_on());
+  EXPECT_TRUE(controller->procfs()->is_tracing_on());
 }
 
 TEST(FtraceControllerTest, MultipleSinks) {
@@ -303,6 +329,13 @@
   // No read tasks posted as part of adding the data sources.
   EXPECT_CALL(*controller->runner(), PostDelayedTask(_, _)).Times(0);
 
+  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(*controller->procfs(), ClearFile("/root/trace"))
+      .WillOnce(Return(true));
+  EXPECT_CALL(*controller->procfs(),
+              ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")))
+      .WillRepeatedly(Return(true));
   EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", _));
   EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "1"));
   auto data_sourceA = controller->AddFakeDataSource(configA);
@@ -323,17 +356,22 @@
   Mock::VerifyAndClearExpectations(controller->runner());
 
   data_sourceA.reset();
+  EXPECT_TRUE(controller->procfs()->is_tracing_on());
 
   // State clearing on tracing teardown.
   EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "0"));
   EXPECT_CALL(*controller->procfs(), WriteToFile(kBarEnablePath, "0"));
-  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", "4"));
   EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", "4"));
   EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
-  EXPECT_CALL(*controller->procfs(), ClearFile("/root/trace"));
+  EXPECT_CALL(*controller->procfs(), ClearFile("/root/trace"))
+      .WillOnce(Return(true));
   EXPECT_CALL(*controller->procfs(),
-              ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+              ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")))
+      .WillRepeatedly(Return(true));
+  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "1"));
   data_sourceB.reset();
+  EXPECT_TRUE(controller->procfs()->is_tracing_on());
 }
 
 TEST(FtraceControllerTest, ControllerMayDieFirst) {
@@ -341,6 +379,13 @@
 
   FtraceConfig config = CreateFtraceConfig({"group/foo"});
 
+  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(*controller->procfs(), ClearFile("/root/trace"))
+      .WillOnce(Return(true));
+  EXPECT_CALL(*controller->procfs(),
+              ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")))
+      .WillRepeatedly(Return(true));
   EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", _));
   EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "1"));
   auto data_source = controller->AddFakeDataSource(config);
@@ -350,14 +395,15 @@
 
   // State clearing on tracing teardown.
   EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "0"));
+  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", "4"));
+  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
   EXPECT_CALL(*controller->procfs(), ClearFile("/root/trace"))
       .WillOnce(Return(true));
   EXPECT_CALL(*controller->procfs(),
               ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")))
       .WillRepeatedly(Return(true));
-  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
-  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", "4"));
-  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "1"));
   controller.reset();
   data_source.reset();
 }
diff --git a/src/traced/probes/ftrace/ftrace_procfs.cc b/src/traced/probes/ftrace/ftrace_procfs.cc
index 9865512..bd1c60c 100644
--- a/src/traced/probes/ftrace/ftrace_procfs.cc
+++ b/src/traced/probes/ftrace/ftrace_procfs.cc
@@ -180,6 +180,12 @@
   return ReadFileIntoString(path);
 }
 
+std::string FtraceProcfs::GetCurrentTracer() {
+  std::string path = root_ + "current_tracer";
+  std::string current_tracer = ReadFileIntoString(path);
+  return base::StripSuffix(current_tracer, "\n");
+}
+
 bool FtraceProcfs::SetCurrentTracer(const std::string& tracer) {
   std::string path = root_ + "current_tracer";
   return WriteToFile(path, tracer);
@@ -407,25 +413,7 @@
   return WriteNumberToFile(path, pages * (base::kPageSize / 1024ul));
 }
 
-bool FtraceProcfs::EnableTracing() {
-  KernelLogWrite("perfetto: enabled ftrace\n");
-  PERFETTO_LOG("enabled ftrace in %s", root_.c_str());
-  std::string path = root_ + "tracing_on";
-  return WriteToFile(path, "1");
-}
-
-bool FtraceProcfs::DisableTracing() {
-  KernelLogWrite("perfetto: disabled ftrace\n");
-  PERFETTO_LOG("disabled ftrace in %s", root_.c_str());
-  std::string path = root_ + "tracing_on";
-  return WriteToFile(path, "0");
-}
-
-bool FtraceProcfs::SetTracingOn(bool enable) {
-  return enable ? EnableTracing() : DisableTracing();
-}
-
-bool FtraceProcfs::IsTracingEnabled() {
+bool FtraceProcfs::GetTracingOn() {
   std::string path = root_ + "tracing_on";
   char tracing_on = ReadOneCharFromFile(path);
   if (tracing_on == '\0')
@@ -433,6 +421,37 @@
   return tracing_on == '1';
 }
 
+bool FtraceProcfs::SetTracingOn(bool on) {
+  std::string path = root_ + "tracing_on";
+  if (!WriteToFile(path, on ? "1" : "0")) {
+    PERFETTO_PLOG("Failed to write %s", path.c_str());
+    return false;
+  }
+  if (on) {
+    KernelLogWrite("perfetto: enabled ftrace\n");
+    PERFETTO_LOG("enabled ftrace in %s", root_.c_str());
+  } else {
+    KernelLogWrite("perfetto: disabled ftrace\n");
+    PERFETTO_LOG("disabled ftrace in %s", root_.c_str());
+  }
+
+  return true;
+}
+
+bool FtraceProcfs::IsTracingAvailable() {
+  std::string current_tracer = GetCurrentTracer();
+
+  // Ftrace tracing is available if current_tracer == "nop".
+  // events/enable could be 0, 1, X or 0*. 0* means events would be
+  // dynamically enabled so we need to treat as event tracing is in use.
+  // However based on the discussion in asop/2328817, on Android events/enable
+  // is "X" after boot up. To avoid causing more problem, the decision is just
+  // look at current_tracer.
+  // As the discussion in asop/2328817, if GetCurrentTracer failed to
+  // read file and return "", we treat it as tracing is available.
+  return current_tracer == "nop" || current_tracer == "";
+}
+
 bool FtraceProcfs::SetClock(const std::string& clock_name) {
   std::string path = root_ + "trace_clock";
   return WriteToFile(path, clock_name);
diff --git a/src/traced/probes/ftrace/ftrace_procfs.h b/src/traced/probes/ftrace/ftrace_procfs.h
index 115d300..40a7da9 100644
--- a/src/traced/probes/ftrace/ftrace_procfs.h
+++ b/src/traced/probes/ftrace/ftrace_procfs.h
@@ -68,6 +68,7 @@
 
   virtual std::string ReadPageHeaderFormat() const;
 
+  std::string GetCurrentTracer();
   // Sets the "current_tracer". Might fail with EBUSY if tracing pipes have
   // already been opened for reading.
   bool SetCurrentTracer(const std::string& tracer);
@@ -134,19 +135,17 @@
   // Writes the string |str| as an event into the trace buffer.
   bool WriteTraceMarker(const std::string& str);
 
-  // Enable tracing.
-  bool EnableTracing();
+  // Read tracing_on and return true if tracing_on is 1, otherwise return false.
+  bool GetTracingOn();
 
-  // Disables tracing, does not clear the buffer.
-  bool DisableTracing();
+  // Write 1 to tracing_on if |on| is true, otherwise write 0.
+  bool SetTracingOn(bool on);
 
-  // Enables/disables tracing, does not clear the buffer.
-  bool SetTracingOn(bool enable);
-
-  // Returns true iff tracing is enabled.
-  // Necessarily racy: another program could enable/disable tracing at any
-  // point.
-  bool IsTracingEnabled();
+  // Returns true if ftrace tracing is available.
+  // Ftrace tracing is available iff "/current_tracer" is "nop", indicates
+  // function tracing is not in use. Necessarily
+  // racy: another program could enable/disable tracing at any point.
+  bool IsTracingAvailable();
 
   // Set the clock. |clock_name| should be one of the names returned by
   // AvailableClocks. Setting the clock clears the buffer.
diff --git a/src/traced/probes/ftrace/ftrace_procfs_integrationtest.cc b/src/traced/probes/ftrace/ftrace_procfs_integrationtest.cc
index 25e1aa7..24ad32a 100644
--- a/src/traced/probes/ftrace/ftrace_procfs_integrationtest.cc
+++ b/src/traced/probes/ftrace/ftrace_procfs_integrationtest.cc
@@ -81,19 +81,18 @@
 void FtraceProcfsIntegrationTest::SetUp() {
   ftrace_ = FtraceProcfs::Create(GetFtracePath());
   ASSERT_TRUE(ftrace_);
-  if (ftrace_->IsTracingEnabled()) {
+  if (!ftrace_->IsTracingAvailable()) {
     GTEST_SKIP() << "Something else is using ftrace, skipping";
   }
 
-  ftrace_->DisableAllEvents();
   ftrace_->ClearTrace();
-  ftrace_->EnableTracing();
+  ftrace_->SetTracingOn(true);
 }
 
 void FtraceProcfsIntegrationTest::TearDown() {
   ftrace_->DisableAllEvents();
   ftrace_->ClearTrace();
-  ftrace_->DisableTracing();
+  ftrace_->SetTracingOn(false);
 }
 
 TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(CreateWithBadPath)) {
@@ -123,20 +122,30 @@
   EXPECT_THAT(GetTraceOutput(), Not(HasSubstr("sched_switch")));
 }
 
-TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(EnableDisableTracing)) {
-  EXPECT_TRUE(ftrace_->IsTracingEnabled());
+TEST_F(FtraceProcfsIntegrationTest,
+       ANDROID_ONLY_TEST(EnableDisableTraceBuffer)) {
   ftrace_->WriteTraceMarker("Before");
-  ftrace_->DisableTracing();
-  EXPECT_FALSE(ftrace_->IsTracingEnabled());
+  ftrace_->SetTracingOn(false);
   ftrace_->WriteTraceMarker("During");
-  ftrace_->EnableTracing();
-  EXPECT_TRUE(ftrace_->IsTracingEnabled());
+  ftrace_->SetTracingOn(true);
   ftrace_->WriteTraceMarker("After");
   EXPECT_THAT(GetTraceOutput(), HasSubstr("Before"));
   EXPECT_THAT(GetTraceOutput(), Not(HasSubstr("During")));
   EXPECT_THAT(GetTraceOutput(), HasSubstr("After"));
 }
 
+TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(IsTracingAvailable)) {
+  EXPECT_TRUE(ftrace_->IsTracingAvailable());
+  ftrace_->SetCurrentTracer("function");
+  EXPECT_FALSE(ftrace_->IsTracingAvailable());
+  ftrace_->SetCurrentTracer("nop");
+  EXPECT_TRUE(ftrace_->IsTracingAvailable());
+  ASSERT_TRUE(ftrace_->EnableEvent("sched", "sched_switch"));
+  EXPECT_FALSE(ftrace_->IsTracingAvailable());
+  ftrace_->DisableAllEvents();
+  EXPECT_TRUE(ftrace_->IsTracingAvailable());
+}
+
 TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(ReadFormatFile)) {
   std::string format = ftrace_->ReadEventFormat("ftrace", "print");
   EXPECT_THAT(format, HasSubstr("name: print"));
@@ -168,7 +177,6 @@
 TEST_F(FtraceProcfsIntegrationTest,
        ANDROID_ONLY_TEST(FtraceControllerHardReset)) {
   ftrace_->SetCpuBufferSizeInPages(4ul);
-  ftrace_->EnableTracing();
   ftrace_->EnableEvent("sched", "sched_switch");
   ftrace_->WriteTraceMarker("Hello, World!");
 
diff --git a/src/traced/probes/kmem_activity_trigger.cc b/src/traced/probes/kmem_activity_trigger.cc
index b30108c..7e29a96 100644
--- a/src/traced/probes/kmem_activity_trigger.cc
+++ b/src/traced/probes/kmem_activity_trigger.cc
@@ -52,7 +52,7 @@
 KmemActivityTrigger::WorkerData::~WorkerData() {
   PERFETTO_DCHECK_THREAD(thread_checker_);
   if (ftrace_procfs_) {
-    ftrace_procfs_->DisableTracing();
+    ftrace_procfs_->SetTracingOn(false);
     ftrace_procfs_->ClearTrace();
   }
   DisarmFtraceFDWatches();
@@ -79,7 +79,7 @@
   ftrace_procfs_->DisableAllEvents();
   ftrace_procfs_->EnableEvent("vmscan", "mm_vmscan_direct_reclaim_begin");
   ftrace_procfs_->EnableEvent("compaction", "mm_compaction_begin");
-  ftrace_procfs_->EnableTracing();
+  ftrace_procfs_->SetTracingOn(true);
 
   num_cpus_ = ftrace_procfs_->NumberOfCpus();
   for (size_t cpu = 0; cpu < num_cpus_; cpu++) {
diff --git a/src/traced/probes/probes_producer.cc b/src/traced/probes/probes_producer.cc
index 04e5248..0d2573a 100644
--- a/src/traced/probes/probes_producer.cc
+++ b/src/traced/probes/probes_producer.cc
@@ -137,11 +137,6 @@
       ftrace_creation_failed_ = true;
       return nullptr;
     }
-
-    if (!ftrace_config.preserve_ftrace_buffer()) {
-      ftrace_->DisableAllEvents();
-      ftrace_->ClearTrace();
-    }
   }
 
   PERFETTO_LOG("Ftrace setup (target_buf=%" PRIu32 ")", config.target_buffer());
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index 0e131dd..f0f2f5d 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -45,6 +45,7 @@
 #include "src/tracing/test/api_test_support.h"
 #include "src/tracing/test/tracing_module.h"
 
+#include "perfetto/base/time.h"
 #include "perfetto/protozero/scattered_heap_buffer.h"
 #include "perfetto/tracing/core/data_source_descriptor.h"
 #include "perfetto/tracing/core/trace_config.h"
diff --git a/test/cts/heapprofd_java_test_cts.cc b/test/cts/heapprofd_java_test_cts.cc
index 10bc985..c2f3f77 100644
--- a/test/cts/heapprofd_java_test_cts.cc
+++ b/test/cts/heapprofd_java_test_cts.cc
@@ -89,7 +89,8 @@
   return helper.trace();
 }
 
-std::vector<protos::gen::TracePacket> TriggerOomHeapDump(std::string app_name) {
+std::vector<protos::gen::TracePacket> TriggerOomHeapDump(std::string app_name,
+                                                         std::string heap_dump_target) {
   base::TestTaskRunner task_runner;
 
   // (re)start the target app's main activity
@@ -119,6 +120,10 @@
   ds_config->set_name("android.java_hprof.oom");
   ds_config->set_target_buffer(0);
 
+  protos::gen::JavaHprofConfig java_hprof_config;
+  java_hprof_config.add_process_cmdline(heap_dump_target.c_str());
+  ds_config->set_java_hprof_config_raw(java_hprof_config.SerializeAsString());
+
   // start tracing
   helper.StartTracing(trace_config);
   StartAppActivity(app_name, "JavaOomActivity", "target.app.running", &task_runner,
@@ -233,9 +238,17 @@
   if (IsUserBuild()) return;
 
   std::string app_name = "android.perfetto.cts.app.debuggable";
-  const auto& packets = TriggerOomHeapDump(app_name);
+  const auto& packets = TriggerOomHeapDump(app_name, "*");
   AssertGraphPresent(packets);
 }
 
+TEST(HeapprofdJavaCtsTest, DebuggableAppOomNotSelected) {
+  if (IsUserBuild()) return;
+
+  std::string app_name = "android.perfetto.cts.app.debuggable";
+  const auto& packets = TriggerOomHeapDump(app_name, "not.this.app");
+  AssertNoProfileContents(packets);
+}
+
 }  // namespace
 }  // namespace perfetto
diff --git a/test/data/perf_sample_annotations.pftrace.sha256 b/test/data/perf_sample_annotations.pftrace.sha256
new file mode 100644
index 0000000..4eb6deb
--- /dev/null
+++ b/test/data/perf_sample_annotations.pftrace.sha256
@@ -0,0 +1 @@
+067957102d4f2c4dbb05e6d1948c76ab426b06d0ba07d0e7237e1442218b700e
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
index f0f270b..f48f5b8 100644
--- a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
+++ b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
@@ -1 +1 @@
-9afd1d1f82d762dc2b2e294bdbd68f834f10ca7c7eab4a9787eaf3e75e8f1bbb
\ No newline at end of file
+2642993d0ccbdcc083d863a15d3a60e4e4458c278190bd057491a68727601ac5
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
index de62f0b..8049eda 100644
--- a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
+++ b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
@@ -1 +1 @@
-a7da2d0e1f2f20dffad44e554dc5a6998bf70d0128ca900907157b4a2303c63a
\ No newline at end of file
+604b0837fc54444c0f347a26821892ce502796859daea4634bbd371b0248b4fd
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256 b/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256
index 8528d4c..f5893a1 100644
--- a/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256
@@ -1 +1 @@
-d10161589d43cb6a0bc1df341581db5489f3ac6efcdf254894a5851178165a13
\ No newline at end of file
+9ce961410f075c01643dbee74ad0f57155f799c2af1c0164e9cbebc9681c65a1
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
index e223d72..9950bf8 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
@@ -1 +1 @@
-0f3c280c0b44ed4e9d4a0849512ca3f21914cd86581cde1bb5e6f1dce4ea7843
\ No newline at end of file
+b94937e792f3103cbb504357fb3cf4c7e0e6e0bad788c8eef05fb7954f52abf9
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
index b4eb4f7..bd9d924 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
@@ -1 +1 @@
-77ad6a3f90b414957bd1fd78d39ceb380b28f384b4d4a0a3ed1e11e394f635be
\ No newline at end of file
+fd6a8a39085b3b2d4fd9f719f08c3f529eac99c06f30dc457278ebc4231dc6fe
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
index 98195eb..70a462d 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
@@ -1 +1 @@
-b4e711f0c25cea261c84ec594ac0939887d81687a70a47e291075c8bb24ff1f1
\ No newline at end of file
+72b31cf41144b44831443984cf80abe87fb213a8d73df767268d7c9ba0a63398
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
index fd28c9c..d9fed2d 100644
--- a/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
@@ -1 +1 @@
-21d088add5542b38703452ac16600f878f865bc220baad0d912bc56d982ab207
\ No newline at end of file
+a9bf192426e9a29380ae8dd47d3974e8607bc7fd01ab0498e9b2cb1cdca75acb
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256
index 32cf9b1..4ee2372 100644
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256
@@ -1 +1 @@
-6bb4ec59ccea148ed9ddd2878dad220dcdeb0264a3a61334c9ee66d57cd8094d
\ No newline at end of file
+c4773556fad9c5b83fc6d8cd2f271faef9e088da862b32a0b1ef14f9d56fa162
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
index fd28c9c..d9fed2d 100644
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
@@ -1 +1 @@
-21d088add5542b38703452ac16600f878f865bc220baad0d912bc56d982ab207
\ No newline at end of file
+a9bf192426e9a29380ae8dd47d3974e8607bc7fd01ab0498e9b2cb1cdca75acb
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256
index 32cf9b1..4ee2372 100644
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256
@@ -1 +1 @@
-6bb4ec59ccea148ed9ddd2878dad220dcdeb0264a3a61334c9ee66d57cd8094d
\ No newline at end of file
+c4773556fad9c5b83fc6d8cd2f271faef9e088da862b32a0b1ef14f9d56fa162
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256
index 651681e..a00bc34 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256
@@ -1 +1 @@
-d8c9876668f6f0673d99adf2ad7ee30dad0ffafe60bd0faa9fb709e49addba20
\ No newline at end of file
+c3461778784561480bd02f6202651ffa3736ac19abfefc8ddd3bbed1e5a2fe08
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
index fd28c9c..d9fed2d 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
@@ -1 +1 @@
-21d088add5542b38703452ac16600f878f865bc220baad0d912bc56d982ab207
\ No newline at end of file
+a9bf192426e9a29380ae8dd47d3974e8607bc7fd01ab0498e9b2cb1cdca75acb
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256
index 32cf9b1..4ee2372 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256
@@ -1 +1 @@
-6bb4ec59ccea148ed9ddd2878dad220dcdeb0264a3a61334c9ee66d57cd8094d
\ No newline at end of file
+c4773556fad9c5b83fc6d8cd2f271faef9e088da862b32a0b1ef14f9d56fa162
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256
index 32cf9b1..4ee2372 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256
@@ -1 +1 @@
-6bb4ec59ccea148ed9ddd2878dad220dcdeb0264a3a61334c9ee66d57cd8094d
\ No newline at end of file
+c4773556fad9c5b83fc6d8cd2f271faef9e088da862b32a0b1ef14f9d56fa162
\ No newline at end of file
diff --git a/test/ftrace_integrationtest.cc b/test/ftrace_integrationtest.cc
index 42d251f..068ebf7 100644
--- a/test/ftrace_integrationtest.cc
+++ b/test/ftrace_integrationtest.cc
@@ -173,7 +173,7 @@
   helper.StartTracing(trace_config);
 
   // Wait for traced_probes to start.
-  helper.WaitFor([&] { return ftrace_procfs_->IsTracingEnabled(); }, "ftrace");
+  helper.WaitFor([&] { return ftrace_procfs_->GetTracingOn(); }, "ftrace");
 
   // Do a first flush just to synchronize with the producer. The problem here
   // is that, on a Linux workstation, the producer can take several seconds just
diff --git a/test/stress_test/stress_test.cc b/test/stress_test/stress_test.cc
index 513b4bb..213017e 100644
--- a/test/stress_test/stress_test.cc
+++ b/test/stress_test/stress_test.cc
@@ -27,6 +27,7 @@
 
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/compiler.h"
+#include "perfetto/base/time.h"
 #include "perfetto/ext/base/ctrl_c_handler.h"
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/scoped_file.h"
diff --git a/test/trace_processor/profiling/perf_sample_switch_interp.textproto b/test/trace_processor/profiling/perf_sample_switch_interp.textproto
new file mode 100644
index 0000000..a8ddcbf
--- /dev/null
+++ b/test/trace_processor/profiling/perf_sample_switch_interp.textproto
@@ -0,0 +1,127 @@
+packet {
+  interned_data {
+    build_ids {
+      iid: 0
+      str: ""
+    }
+    mapping_paths {
+      iid: 0
+      str: ""
+    }
+    function_names {
+      iid: 0
+      str: ""
+    }
+  }
+  sequence_flags: 1
+  trusted_uid: 9999
+  trusted_packet_sequence_id: 2
+  trusted_pid: 24388
+}
+packet {
+  timestamp: 214681394021835
+  interned_data {
+    mappings {
+      iid: 1
+      path_string_ids: 1
+      path_string_ids: 2
+      path_string_ids: 3
+      path_string_ids: 4
+      build_id: 1
+    }
+    build_ids {
+      iid: 1
+      str: ""
+    }
+    mapping_paths {
+      iid: 1
+      str: "apex"
+    }
+    mapping_paths {
+      iid: 2
+      str: "com.android.art"
+    }
+    mapping_paths {
+      iid: 3
+      str: "lib64"
+    }
+    mapping_paths {
+      iid: 4
+      str: "libart.so"
+    }
+    mappings {
+      iid: 2
+      path_string_ids: 6
+      build_id: 1
+    }
+    mapping_paths {
+      iid: 6
+      str: "example.vdex"
+    }
+    frames {
+      iid: 1
+      function_name_id: 1
+      mapping_id: 2
+    }
+    function_names {
+      iid: 1
+      str: "com.example.managed.frame"
+    }
+    frames {
+      iid: 2
+      function_name_id: 2
+      mapping_id: 1
+    }
+    function_names {
+      iid: 2
+      str: "ExecuteSwitchImplAsm"
+    }
+    frames {
+      iid: 3
+      function_name_id: 3
+      mapping_id: 1
+    }
+    function_names {
+      iid: 3
+      str: "_ZN3art11interpreter20ExecuteSwitchImplCppILb0EEEvPNS0_17SwitchImplContextE"
+    }
+    frames {
+      iid: 4
+      function_name_id: 4
+      mapping_id: 1
+    }
+    function_names {
+      iid: 4
+      str: "_ZN3art11interpreter6DoCallILb1EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtbPNS_6JValueE"
+    }
+    frames {
+      iid: 5
+      function_name_id: 5
+      mapping_id: 1
+    }
+    function_names {
+      iid: 5
+      str: "_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc"
+    }
+    callstacks {
+      iid: 1
+      frame_ids: 1
+      frame_ids: 2
+      frame_ids: 3
+      frame_ids: 4
+      frame_ids: 5
+    }
+  }
+  perf_sample {
+    cpu: 0
+    pid: 1000
+    tid: 1000
+    cpu_mode: MODE_USER
+    timebase_count: 42
+    callstack_iid: 1
+  }
+  trusted_uid: 9999
+  trusted_packet_sequence_id: 2
+  trusted_pid: 24388
+}
+
diff --git a/test/trace_processor/profiling/tests.py b/test/trace_processor/profiling/tests.py
index 6bcb65d..33ed4fc 100644
--- a/test/trace_processor/profiling/tests.py
+++ b/test/trace_processor/profiling/tests.py
@@ -285,3 +285,74 @@
         ORDER BY ps.ts ASC, eac.depth ASC;
         """,
         out=Path('perf_sample_sc.out'))
+
+  def test_annotations(self):
+    return DiffTestBlueprint(
+        trace=Path('../../data/perf_sample_annotations.pftrace'),
+        query="""
+        select
+          eac.depth, eac.annotation, spm.name as map_name,
+          ifnull(demangle(spf.name), spf.name) as frame_name
+        from experimental_annotated_callstack eac
+          join stack_profile_frame spf on (eac.frame_id = spf.id)
+          join stack_profile_mapping spm on (spf.mapping = spm.id)
+        where eac.start_id = (
+          select spc.id as callsite_id
+          from stack_profile_callsite spc
+          join stack_profile_frame spf on (spc.frame_id = spf.id)
+          where spf.name = "_ZN3art28ResolveFieldWithAccessChecksEPNS_6ThreadEPNS_11ClassLinkerEtPNS_9ArtMethodEbbm")
+        order by depth asc;
+        """,
+        out=Csv("""
+        "depth","annotation","map_name","frame_name"
+        0,"[NULL]","/apex/com.android.runtime/lib64/bionic/libc.so","__libc_init"
+        1,"[NULL]","/system/bin/app_process64","main"
+        2,"[NULL]","/system/lib64/libandroid_runtime.so","android::AndroidRuntime::start(char const*, android::Vector<android::String8> const&, bool)"
+        3,"[NULL]","/system/lib64/libandroid_runtime.so","_JNIEnv::CallStaticVoidMethod(_jclass*, _jmethodID*, ...)"
+        4,"[NULL]","/apex/com.android.art/lib64/libart.so","art::JNI<true>::CallStaticVoidMethodV(_JNIEnv*, _jclass*, _jmethodID*, std::__va_list)"
+        5,"[NULL]","/apex/com.android.art/lib64/libart.so","art::JValue art::InvokeWithVarArgs<_jmethodID*>(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jmethodID*, std::__va_list)"
+        6,"[NULL]","/apex/com.android.art/lib64/libart.so","art_quick_invoke_static_stub"
+        7,"aot","/system/framework/arm64/boot-framework.oat","com.android.internal.os.ZygoteInit.main"
+        8,"aot","/system/framework/arm64/boot-framework.oat","com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run"
+        9,"common-frame","/system/framework/arm64/boot.oat","art_jni_trampoline"
+        10,"[NULL]","/apex/com.android.art/lib64/libart.so","art::Method_invoke(_JNIEnv*, _jobject*, _jobject*, _jobjectArray*) (.__uniq.165753521025965369065708152063621506277)"
+        11,"common-frame","/apex/com.android.art/lib64/libart.so","_jobject* art::InvokeMethod<(art::PointerSize)8>(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jobject*, _jobject*, unsigned long)"
+        12,"common-frame","/apex/com.android.art/lib64/libart.so","art_quick_invoke_static_stub"
+        13,"aot","/system/framework/arm64/boot-framework.oat","android.app.ActivityThread.main"
+        14,"aot","/system/framework/arm64/boot-framework.oat","android.os.Looper.loop"
+        15,"aot","/system/framework/arm64/boot-framework.oat","android.os.Looper.loopOnce"
+        16,"aot","/system/framework/arm64/boot-framework.oat","android.os.Handler.dispatchMessage"
+        17,"aot","/system/framework/arm64/boot-framework.oat","android.view.Choreographer$FrameDisplayEventReceiver.run"
+        18,"aot","/system/framework/arm64/boot-framework.oat","android.view.Choreographer.doFrame"
+        19,"aot","/system/framework/arm64/boot-framework.oat","android.view.Choreographer.doCallbacks"
+        20,"aot","/system/framework/arm64/boot-framework.oat","android.view.ViewRootImpl$TraversalRunnable.run"
+        21,"aot","/system/framework/arm64/boot-framework.oat","android.view.ViewRootImpl.doTraversal"
+        22,"aot","/system/framework/arm64/boot-framework.oat","android.view.ViewRootImpl.performTraversals"
+        23,"interp","/system/framework/framework.jar","android.view.ViewRootImpl.notifyDrawStarted"
+        24,"common-frame-interp","/apex/com.android.art/lib64/libart.so","nterp_op_iget_object_slow_path"
+        25,"common-frame-interp","/apex/com.android.art/lib64/libart.so","nterp_get_instance_field_offset"
+        26,"common-frame-interp","/apex/com.android.art/lib64/libart.so","NterpGetInstanceFieldOffset"
+        27,"common-frame","/apex/com.android.art/lib64/libart.so","art::ResolveFieldWithAccessChecks(art::Thread*, art::ClassLinker*, unsigned short, art::ArtMethod*, bool, bool, unsigned long)"
+        """))
+
+  def test_annotations_switch_interpreter(self):
+    return DiffTestBlueprint(
+        trace=Path('perf_sample_switch_interp.textproto'),
+        query="""
+        select
+          eac.depth, eac.annotation, spm.name as map_name,
+          ifnull(demangle(spf.name), spf.name) as frame_name
+        from experimental_annotated_callstack eac
+          join stack_profile_frame spf on (eac.frame_id = spf.id)
+          join stack_profile_mapping spm on (spf.mapping = spm.id)
+        where eac.start_id = (select callsite_id from perf_sample)
+        order by depth asc;
+        """,
+        out=Csv("""
+        "depth","annotation","map_name","frame_name"
+        0,"interp","/example.vdex","com.example.managed.frame"
+        1,"common-frame-interp","/apex/com.android.art/lib64/libart.so","ExecuteSwitchImplAsm"
+        2,"common-frame-interp","/apex/com.android.art/lib64/libart.so","void art::interpreter::ExecuteSwitchImplCpp<false>(art::interpreter::SwitchImplContext*)"
+        3,"common-frame-interp","/apex/com.android.art/lib64/libart.so","bool art::interpreter::DoCall<true>(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, bool, art::JValue*)"
+        4,"common-frame","/apex/com.android.art/lib64/libart.so","art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)"
+        """))
diff --git a/test/trace_processor/startup/android_startup.out b/test/trace_processor/startup/android_startup.out
index f804a34..752db7b 100644
--- a/test/trace_processor/startup/android_startup.out
+++ b/test/trace_processor/startup/android_startup.out
@@ -8,7 +8,7 @@
       dur_ns: 108
       main_thread_by_task_state {
         running_dur_ns: 10
-        runnable_dur_ns: 80
+        runnable_dur_ns: 90
         uninterruptible_sleep_dur_ns: 0
         interruptible_sleep_dur_ns: 10
         uninterruptible_io_sleep_dur_ns: 0
diff --git a/test/trace_processor/startup/android_startup_breakdown.out b/test/trace_processor/startup/android_startup_breakdown.out
index d58a886..8b35bb5 100644
--- a/test/trace_processor/startup/android_startup_breakdown.out
+++ b/test/trace_processor/startup/android_startup_breakdown.out
@@ -8,7 +8,7 @@
       dur_ns: 108000000000
       main_thread_by_task_state {
         running_dur_ns: 25000000000
-        runnable_dur_ns: 5000000000
+        runnable_dur_ns: 30000000000
         uninterruptible_sleep_dur_ns: 0
         interruptible_sleep_dur_ns: 0
         uninterruptible_io_sleep_dur_ns: 0
@@ -107,6 +107,7 @@
     slow_start_reason: "Time spent in bindApplication"
     slow_start_reason: "Time spent in view inflation"
     slow_start_reason: "Time spent in ResourcesManager#getResources"
+    slow_start_reason: "Potential CPU contention with init"
     slow_start_reason: "No baseline or cloud profiles"
     slow_start_reason: "Optimized artifacts missing, run from apk"
     startup_type: "cold"
diff --git a/test/trace_processor/startup/android_startup_breakdown_slow.out b/test/trace_processor/startup/android_startup_breakdown_slow.out
index fec1c2e..3ed8ce3 100644
--- a/test/trace_processor/startup/android_startup_breakdown_slow.out
+++ b/test/trace_processor/startup/android_startup_breakdown_slow.out
@@ -8,7 +8,7 @@
       dur_ns: 108000000000
       main_thread_by_task_state {
         running_dur_ns: 25000000000
-        runnable_dur_ns: 5000000000
+        runnable_dur_ns: 30000000000
         uninterruptible_sleep_dur_ns: 0
         interruptible_sleep_dur_ns: 0
         uninterruptible_io_sleep_dur_ns: 0
@@ -107,6 +107,7 @@
     slow_start_reason: "Time spent in bindApplication"
     slow_start_reason: "Time spent in view inflation"
     slow_start_reason: "Time spent in ResourcesManager#getResources"
+    slow_start_reason: "Potential CPU contention with init"
     slow_start_reason: "Optimized artifacts missing, run from apk"
     startup_type: "cold"
   }
diff --git a/test/trace_processor/startup/android_startup_slow.out b/test/trace_processor/startup/android_startup_slow.out
index 9ae0024..333883a 100644
--- a/test/trace_processor/startup/android_startup_slow.out
+++ b/test/trace_processor/startup/android_startup_slow.out
@@ -8,7 +8,7 @@
       dur_ns: 108000000000
       main_thread_by_task_state {
         running_dur_ns: 10000000000
-        runnable_dur_ns: 80000000000
+        runnable_dur_ns: 90000000000
         uninterruptible_sleep_dur_ns: 5000000000
         interruptible_sleep_dur_ns: 5000000000
         uninterruptible_io_sleep_dur_ns: 5000000000
@@ -67,5 +67,6 @@
     slow_start_reason: "Main Thread - Time spent in Runnable state"
     slow_start_reason: "Main Thread - Time spent in interruptible sleep state"
     slow_start_reason: "Main Thread - Time spent in Blocking I/O"
+    slow_start_reason: "Potential CPU contention with init"
   }
 }
diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js
index 16553be..b890b89 100644
--- a/ui/.eslintrc.js
+++ b/ui/.eslintrc.js
@@ -47,7 +47,7 @@
     // https://github.com/typescript-eslint/typescript-eslint/issues/2621
     'no-unused-vars': 'off',
     '@typescript-eslint/no-unused-vars':
-        ['error', {'argsIgnorePattern': '^_.*'}],
+        ['error', {'argsIgnorePattern': '^_.*', 'varsIgnorePattern': '^_.*'}],
 
     // new Array() is banned (use [] instead) but new Array<Foo>() is
     // allowed since it can be clearer to put the type by the
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 570fa44..5470643 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -11,6 +11,9 @@
 // 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 "fonts";
+
 :root {
   --sidebar-width: 256px;
   --topbar-height: 48px;
@@ -199,28 +202,64 @@
   width: 100%;
 }
 
+@mixin table-font-size {
+  font-size: 14px;
+  line-height: 18px;
+}
+
+$table-hover-color: hsl(214, 22%, 90%);
+
+$table-border-color: rgba(60, 76, 92, 0.4);
+
+.pivot-table {
+  @include bottom-panel-font;
+  @include table-font-size;
+
+  width: 100%;
+  border-collapse: collapse;
+
+  thead,
+  i {
+    cursor: pointer;
+  }
+  thead {
+    font-weight: normal;
+  }
+  td {
+    padding: 2px 1px;
+  }
+  td.first {
+    border-left: 1px solid $table-border-color;
+    padding-left: 6px;
+  }
+  tr.header {
+    border-bottom: 1px solid $table-border-color;
+    text-align: center;
+  }
+  thead td.reorderable-cell {
+    cursor: grab;
+  }
+  tr:hover td {
+    background-color: $table-hover-color;
+  }
+  .disabled {
+    cursor: default;
+  }
+  .indent {
+    display: inline-block;
+    // 16px is the width of expand_more/expand_less icon to pad out cells
+    // without the button
+    width: 16px;
+  }
+  strong {
+    font-weight: 400;
+  }
+}
+
 .query-table {
   width: 100%;
   font-size: 14px;
   border: 0;
-  &.pivot-table {
-    thead,
-    i {
-      cursor: pointer;
-    }
-    thead td.reorderable-cell {
-      cursor: grab;
-    }
-    .disabled {
-      cursor: default;
-    }
-    .indent {
-      display: inline-block;
-      // 16px is the width of expand_more/expand_less icon to pad out cells
-      // without the button
-      width: 16px;
-    }
-  }
   thead td {
     position: sticky;
     top: 0;
@@ -755,6 +794,7 @@
 
 .pivot-table-redux {
   user-select: text;
+  padding: 10px;
 
   button.mode-button {
     border-radius: 10px;
@@ -763,11 +803,6 @@
     background-color: #c7d0db;
   }
 
-  &.edit {
-    padding: 10px;
-    display: flex;
-  }
-
   &.query-error {
     color: red;
   }
@@ -817,11 +852,11 @@
   }
 
   &.highlight-left {
-    border-left-color: red;
+    background: linear-gradient(90deg, $table-border-color, transparent 20%);
   }
 
   &.highlight-right {
-    border-right-color: red;
+    background: linear-gradient(270deg, $table-border-color, transparent 20%);
   }
 }
 
diff --git a/ui/src/assets/details.scss b/ui/src/assets/details.scss
index 11186c2..3faaadd 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -97,9 +97,7 @@
 }
 
 .details-panel {
-  font-family: "Roboto Condensed", sans-serif;
-  font-weight: 300;
-  color: #3c4b5d;
+  @include bottom-panel-font;
 
   .material-icons {
     @include transition(0.3s);
@@ -234,8 +232,7 @@
 
   table {
     @include transition(0.1s);
-    font-size: 14px;
-    line-height: 18px;
+    @include table-font-size;
     width: 100%;
     // Aggregation panel uses multiple table elements that need to be aligned,
     // which is done by using fixed table layout.
@@ -245,7 +242,7 @@
     tr:hover {
       td,
       th {
-        background-color: hsl(214, 22%, 90%);
+        background-color: $table-hover-color;
 
         &.no-highlight {
           background-color: white;
@@ -392,7 +389,7 @@
   font-weight: bolder;
   font-size: 12px;
   .sum-data {
-    border-bottom: 1px solid rgba(60, 76, 92, 0.4);
+    border-bottom: 1px solid $table-border-color;
   }
 }
 
@@ -543,7 +540,7 @@
         background-color: hsl(214, 22%, 95%);
       }
       &:hover {
-        background-color: hsl(214, 22%, 90%);
+        background-color: $table-hover-color;
       }
       .cell {
         font-size: 11px;
diff --git a/ui/src/assets/fonts.scss b/ui/src/assets/fonts.scss
new file mode 100644
index 0000000..6f52d19
--- /dev/null
+++ b/ui/src/assets/fonts.scss
@@ -0,0 +1,20 @@
+// 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.
+
+@mixin bottom-panel-font {
+  font-family: "Roboto Condensed", sans-serif;
+  font-weight: 300;
+  color: #3c4b5d;
+}
+
diff --git a/ui/src/base/math_utils.ts b/ui/src/base/math_utils.ts
new file mode 100644
index 0000000..f1c1816
--- /dev/null
+++ b/ui/src/base/math_utils.ts
@@ -0,0 +1,23 @@
+// 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.
+
+// Round a number up to the nearest stepsize.
+export function roundUpNearest(val: number, stepsize: number): number {
+  return stepsize * Math.ceil(val / stepsize);
+}
+
+// Round a number down to the nearest stepsize.
+export function roundDownNearest(val: number, stepsize: number): number {
+  return stepsize * Math.floor(val / stepsize);
+}
diff --git a/ui/src/base/math_utils_unittest.ts b/ui/src/base/math_utils_unittest.ts
new file mode 100644
index 0000000..169b793
--- /dev/null
+++ b/ui/src/base/math_utils_unittest.ts
@@ -0,0 +1,29 @@
+// 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 {roundDownNearest, roundUpNearest} from './math_utils';
+
+describe('roundUpNearest()', () => {
+  it('rounds decimal values up to the right step size', () => {
+    expect(roundUpNearest(0.1, 0.5)).toBeCloseTo(0.5);
+    expect(roundUpNearest(17.2, 0.5)).toBeCloseTo(17.5);
+  });
+});
+
+describe('roundDownNearest()', () => {
+  it('rounds decimal values down to the right step size', () => {
+    expect(roundDownNearest(0.4, 0.5)).toBeCloseTo(0.0);
+    expect(roundDownNearest(17.4, 0.5)).toBeCloseTo(17.0);
+  });
+});
diff --git a/ui/src/common/query_result.ts b/ui/src/common/query_result.ts
index bae847d..9d44c9e 100644
--- a/ui/src/common/query_result.ts
+++ b/ui/src/common/query_result.ts
@@ -57,8 +57,10 @@
 export const STR = 'str';
 export const NUM_NULL: number|null = 1;
 export const STR_NULL: string|null = 'str_null';
+export const BLOB: Uint8Array = new Uint8Array();
+export const BLOB_NULL: Uint8Array|null = new Uint8Array();
 
-export type ColumnType = string|number|null;
+export type ColumnType = string|number|null|Uint8Array;
 
 // Info that could help debug a query error. For example the query
 // in question, the stack where the query was issued, the active
@@ -119,11 +121,32 @@
       return 'STR';
     case STR_NULL:
       return 'STR_NULL';
+    case BLOB:
+      return 'BLOB';
+    case BLOB_NULL:
+      return 'BLOB_NULL';
     default:
       return `INVALID(${t})`;
   }
 }
 
+function isCompatible(actual: CellType, expected: ColumnType): boolean {
+  switch (actual) {
+    case CellType.CELL_NULL:
+      return expected === NUM_NULL || expected === STR_NULL ||
+          expected === BLOB_NULL;
+    case CellType.CELL_VARINT:
+    case CellType.CELL_FLOAT64:
+      return expected === NUM || expected === NUM_NULL;
+    case CellType.CELL_STRING:
+      return expected === STR || expected === STR_NULL;
+    case CellType.CELL_BLOB:
+      return expected === BLOB || expected === BLOB_NULL;
+    default:
+      throw new Error(`Unknown CellType ${actual}`);
+  }
+}
+
 // Disable Long.js support in protobuf. This seems to be enabled only in tests
 // but not in production code. In any case, for now we want casting to number
 // accepting the 2**53 limitation. This is consistent with passing
@@ -652,8 +675,7 @@
 
         case CellType.CELL_BLOB:
           const blob = this.blobCells[this.nextBlobCell++];
-          throw new Error(`TODO implement BLOB support (${blob})`);
-          // outRow[colName] = blob;
+          rowData[colName] = blob;
           break;
 
         default:
@@ -721,20 +743,16 @@
       if (expType === undefined) continue;
 
       let err = '';
-      if (actualType === CellType.CELL_NULL &&
-          (expType !== STR_NULL && expType !== NUM_NULL)) {
-        err = 'SQL value is NULL but that was not expected' +
-            ` (expected type: ${columnTypeToString(expType)}). ` +
-            'Did you intend to use NUM_NULL or STR_NULL?';
-      } else if (
-          ((actualType === CellType.CELL_VARINT ||
-            actualType === CellType.CELL_FLOAT64) &&
-           (expType !== NUM && expType !== NUM_NULL)) ||
-          ((actualType === CellType.CELL_STRING) &&
-           (expType !== STR && expType !== STR_NULL))) {
-        err = `Incompatible cell type. Expected: ${
-            columnTypeToString(
-                expType)} actual: ${CELL_TYPE_NAMES[actualType]}`;
+      if (!isCompatible(actualType, expType)) {
+        if (actualType === CellType.CELL_NULL) {
+          err = 'SQL value is NULL but that was not expected' +
+              ` (expected type: ${columnTypeToString(expType)}). ` +
+              'Did you intend to use NUM_NULL, STR_NULL or BLOB_NULL?';
+        } else {
+          err = `Incompatible cell type. Expected: ${
+              columnTypeToString(
+                  expType)} actual: ${CELL_TYPE_NAMES[actualType]}`;
+        }
       }
       if (err.length > 0) {
         throw new Error(
diff --git a/ui/src/controller/aggregation/aggregation_controller.ts b/ui/src/controller/aggregation/aggregation_controller.ts
index 0510ca0..4fa1ecc 100644
--- a/ui/src/controller/aggregation/aggregation_controller.ts
+++ b/ui/src/controller/aggregation/aggregation_controller.ts
@@ -146,6 +146,8 @@
           column.data[i] = isStringColumn(column) ? internString('NULL') : 0;
         } else if (typeof item === 'string') {
           column.data[i] = internString(item);
+        } else if (item instanceof Uint8Array) {
+          column.data[i] = internString('<Binary blob>');
         } else {
           column.data[i] = item;
         }
diff --git a/ui/src/frontend/gridline_helper.ts b/ui/src/frontend/gridline_helper.ts
index 6c0b74d..1c9dbfe 100644
--- a/ui/src/frontend/gridline_helper.ts
+++ b/ui/src/frontend/gridline_helper.ts
@@ -12,94 +12,172 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {TimeSpan} from '../common/time';
-
+import {assertTrue} from '../base/logging';
+import {roundDownNearest} from '../base/math_utils';
 import {TRACK_BORDER_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
+import {globals} from './globals';
 import {TimeScale} from './time_scale';
 
-export const DESIRED_PX_PER_STEP = 80;
-
-// Returns the step size of a grid line in seconds.
-// The returned step size has two properties:
-// (1) It is 1, 2, or 5, multiplied by some integer power of 10.
-// (2) The number steps in |range| produced by |stepSize| is as close as
-//     possible to |desiredSteps|.
-export function getGridStepSize(range: number, desiredSteps: number): number {
+// Returns the optimal step size (in seconds) and tick pattern of ticks within
+// the step. The returned step size has two properties: (1) It is 1, 2, or 5,
+// multiplied by some integer power of 10. (2) It is maximised given the
+// constraint: |range| / stepSize <= |maxNumberOfSteps|.
+export function getStepSize(
+    range: number, maxNumberOfSteps: number): [number, string] {
   // First, get the largest possible power of 10 that is smaller than the
-  // desired step size, and set it to the current step size.
+  // desired step size, and use it as our initial step size.
   // For example, if the range is 2345ms and the desired steps is 10, then the
-  // desired step size is 234.5 and the step size will be set to 100.
-  const desiredStepSize = range / desiredSteps;
-  const zeros = Math.floor(Math.log10(desiredStepSize));
+  // minimum step size is 234.5ms so the step size will initialise to 100.
+  const minStepSize = range / maxNumberOfSteps;
+  const zeros = Math.floor(Math.log10(minStepSize));
   const initialStepSize = Math.pow(10, zeros);
 
-  // This function first calculates how many steps within the range a certain
-  // stepSize will produce, and returns the difference between that and
-  // desiredSteps.
-  const distToDesired = (evaluatedStepSize: number) =>
-      Math.abs(range / evaluatedStepSize - desiredSteps);
-
   // We know that |initialStepSize| is a power of 10, and
   // initialStepSize <= desiredStepSize <= 10 * initialStepSize. There are four
   // possible candidates for final step size: 1, 2, 5 or 10 * initialStepSize.
-  // We pick the candidate that minimizes distToDesired(stepSize).
-  const stepSizeMultipliers = [2, 5, 10];
+  // For our example above, this would result in a step size of 500ms, as both
+  // 100ms and 200ms are smaller than the minimum step size of 234.5ms.
+  // We pick the candidate that minimizes the step size without letting the
+  // number of steps exceed |maxNumberOfSteps|. The factor we pick to also
+  // determines the pattern of ticks. This pattern is represented using a string
+  // where:
+  //  | = Major tick
+  //  : = Medium tick
+  //  . = Minor tick
+  const stepSizeMultipliers: [number, string][] =
+      [[1, '|....:....'], [2, '|.:.'], [5, '|....'], [10, '|....:....']];
 
-  let minimalDistance = distToDesired(initialStepSize);
-  let minimizingStepSize = initialStepSize;
-
-  for (const multiplier of stepSizeMultipliers) {
+  for (const [multiplier, pattern] of stepSizeMultipliers) {
     const newStepSize = multiplier * initialStepSize;
-    const newDistance = distToDesired(newStepSize);
-    if (newDistance < minimalDistance) {
-      minimalDistance = newDistance;
-      minimizingStepSize = newStepSize;
+    const numberOfNewSteps = range / newStepSize;
+    if (numberOfNewSteps <= maxNumberOfSteps) {
+      return [newStepSize, pattern];
     }
   }
-  return minimizingStepSize;
+
+  throw new Error('Something has gone horribly wrong with maths');
 }
 
-// Generator that returns that (given a width im px, span, and scale) returns
-// pairs of [xInPx, timestampInS] pairs describing where gridlines should be
-// drawn.
-export function gridlines(width: number, span: TimeSpan, timescale: TimeScale):
-    Array<[number, number]> {
-  const desiredSteps = width / DESIRED_PX_PER_STEP;
-  const step = getGridStepSize(span.duration, desiredSteps);
-  const actualSteps = Math.floor(span.duration / step);
-  const start = Math.round(span.start / step) * step;
-  const lines: Array<[number, number]> = [];
-  let previousTimestamp = Number.NEGATIVE_INFINITY;
-  // Iterating over the number of steps instead of
-  // for (let s = start; s < span.end; s += step) because if start is very large
-  // number and step very small, s will never reach end.
-  for (let i = 0; i <= actualSteps; i++) {
-    let xPos = TRACK_SHELL_WIDTH;
-    const timestamp = start + i * step;
-    xPos += Math.floor(timescale.timeToPx(timestamp));
-    if (xPos < TRACK_SHELL_WIDTH) continue;
-    if (xPos > width) break;
-    if (Math.abs(timestamp - previousTimestamp) > Number.EPSILON) {
-      previousTimestamp = timestamp;
-      lines.push([xPos, timestamp]);
+function tickPatternToArray(pattern: string): TickType[] {
+  const array = Array.from(pattern);
+  return array.map((char) => {
+    switch (char) {
+      case '|':
+        return TickType.MAJOR;
+      case ':':
+        return TickType.MEDIUM;
+      case '.':
+        return TickType.MINOR;
+      default:
+        // This is almost certainly a developer/fat-finger error
+        throw Error(`Invalid char "${char}" in pattern "${pattern}"`);
+    }
+  });
+}
+
+// Assuming a number only has one non-zero decimal digit, find the number of
+// decimal places required to accurately print that number. I.e. the parameter
+// we should pass to number.toFixed(x). To account for floating point
+// innaccuracies when representing numbers in base-10, we only take the first
+// nonzero fractional digit into account. E.g.
+//  1.0 -> 0
+//  0.5 -> 1
+//  0.009 -> 3
+//  0.00007 -> 5
+//  30000 -> 0
+//  0.30000000000000004 -> 1
+export function guessDecimalPlaces(val: number): number {
+  const neglog10 = -Math.floor(Math.log10(val));
+  const clamped = Math.max(0, neglog10);
+  return clamped;
+}
+
+export enum TickType {
+  MAJOR,
+  MEDIUM,
+  MINOR
+}
+
+export interface Tick {
+  type: TickType;
+  time: number;
+  position: number;
+}
+
+const MIN_PX_PER_STEP = 80;
+
+// An iterable which generates a series of ticks for a given timescale.
+export class TickGenerator implements Iterable<Tick> {
+  private _tickPattern: TickType[];
+  private _patternSize: number;
+
+  constructor(private scale: TimeScale, {minLabelPx = MIN_PX_PER_STEP} = {}) {
+    assertTrue(minLabelPx > 0, 'minLabelPx cannot be lte 0');
+    assertTrue(scale.widthPx > 0, 'widthPx cannot be lte 0');
+    assertTrue(
+        scale.timeSpan.duration > 0, 'timeSpan.duration cannot be lte 0');
+
+    const desiredSteps = scale.widthPx / minLabelPx;
+    const [size, pattern] = getStepSize(scale.timeSpan.duration, desiredSteps);
+    this._patternSize = size;
+    this._tickPattern = tickPatternToArray(pattern);
+  }
+
+  // Returns an iterable, so this object can be iterated over directly using the
+  // `for x of y` notation. The use of a generator here is just to make things
+  // more elegant than creating an array of ticks and building an iterator for
+  // it.
+  * [Symbol.iterator](): Generator<Tick> {
+    const span = this.scale.timeSpan;
+    const stepSize = this._patternSize / this._tickPattern.length;
+    const start = roundDownNearest(span.start, this._patternSize);
+    const timeAtStep = (i: number) => start + (i * stepSize);
+
+    // Iterating using steps instead of
+    // for (let s = start; s < span.end; s += stepSize) because if start is much
+    // larger than stepSize we can enter an infinite loop due to floating
+    // point precision errors.
+    for (let i = 0; timeAtStep(i) < span.end; i++) {
+      const time = timeAtStep(i);
+      if (time >= span.start) {
+        const position = Math.floor(this.scale.timeToPx(time));
+        const type = this._tickPattern[i % this._tickPattern.length];
+        yield {type, time, position};
+      }
     }
   }
-  return lines;
+
+  // The number of decimal places labels should be printed with, assuming labels
+  // are only printed on major ticks.
+  get digits(): number {
+    return guessDecimalPlaces(this._patternSize);
+  }
+}
+
+// Gets the timescale associated with the current visible window.
+export function timeScaleForVisibleWindow(
+    startPx: number, endPx: number): TimeScale {
+  const span = globals.frontendLocalState.visibleWindowTime;
+  const spanRelative = span.add(-globals.state.traceTime.startSec);
+  return new TimeScale(spanRelative, [startPx, endPx]);
 }
 
 export function drawGridLines(
     ctx: CanvasRenderingContext2D,
-    x: TimeScale,
-    timeSpan: TimeSpan,
     width: number,
     height: number): void {
   ctx.strokeStyle = TRACK_BORDER_COLOR;
   ctx.lineWidth = 1;
 
-  for (const xAndTime of gridlines(width, timeSpan, x)) {
-    ctx.beginPath();
-    ctx.moveTo(xAndTime[0] + 0.5, 0);
-    ctx.lineTo(xAndTime[0] + 0.5, height);
-    ctx.stroke();
+  const timeScale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, width);
+  if (timeScale.timeSpan.duration > 0 && timeScale.widthPx > 0) {
+    for (const {type, position} of new TickGenerator(timeScale)) {
+      if (type === TickType.MAJOR) {
+        ctx.beginPath();
+        ctx.moveTo(position + 0.5, 0);
+        ctx.lineTo(position + 0.5, height);
+        ctx.stroke();
+      }
+    }
   }
 }
diff --git a/ui/src/frontend/gridline_helper_unittest.ts b/ui/src/frontend/gridline_helper_unittest.ts
index 98724d5..3b6dcac 100644
--- a/ui/src/frontend/gridline_helper_unittest.ts
+++ b/ui/src/frontend/gridline_helper_unittest.ts
@@ -12,36 +12,303 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {getGridStepSize} from './gridline_helper';
+import {TimeSpan} from '../common/time';
+
+import {getStepSize, Tick, TickGenerator, TickType} from './gridline_helper';
+import {TimeScale} from './time_scale';
+
+const pattern1 = '|....:....';
+const pattern2 = '|.:.';
+const pattern5 = '|....';
+const timeScale = new TimeScale(new TimeSpan(0, 1), [1, 2]);
 
 test('gridline helper to have sensible step sizes', () => {
-  expect(getGridStepSize(10, 14)).toEqual(1);
-  expect(getGridStepSize(30, 14)).toEqual(2);
-  expect(getGridStepSize(60, 14)).toEqual(5);
-  expect(getGridStepSize(100, 14)).toEqual(10);
+  expect(getStepSize(10, 14)).toEqual([1, pattern1]);
+  expect(getStepSize(30, 14)).toEqual([5, pattern5]);
+  expect(getStepSize(60, 14)).toEqual([5, pattern5]);
+  expect(getStepSize(100, 14)).toEqual([10, pattern1]);
 
-  expect(getGridStepSize(10, 21)).toEqual(0.5);
-  expect(getGridStepSize(30, 21)).toEqual(2);
-  expect(getGridStepSize(60, 21)).toEqual(2);
-  expect(getGridStepSize(100, 21)).toEqual(5);
+  expect(getStepSize(10, 21)).toEqual([0.5, pattern5]);
+  expect(getStepSize(30, 21)).toEqual([2, pattern2]);
+  expect(getStepSize(60, 21)).toEqual([5, pattern5]);
+  expect(getStepSize(100, 21)).toEqual([5, pattern5]);
 
-  expect(getGridStepSize(10, 3)).toEqual(5);
-  expect(getGridStepSize(30, 3)).toEqual(10);
-  expect(getGridStepSize(60, 3)).toEqual(20);
-  expect(getGridStepSize(100, 3)).toEqual(50);
+  expect(getStepSize(10, 3)).toEqual([5, pattern5]);
+  expect(getStepSize(30, 3)).toEqual([10, pattern1]);
+  expect(getStepSize(60, 3)).toEqual([20, pattern2]);
+  expect(getStepSize(100, 3)).toEqual([50, pattern5]);
 
-  expect(getGridStepSize(800, 4)).toEqual(200);
+  expect(getStepSize(800, 4)).toEqual([200, pattern2]);
 });
 
 test('gridline helper to scale to very small and very large values', () => {
-  expect(getGridStepSize(.01, 14)).toEqual(.001);
-  expect(getGridStepSize(10000, 14)).toEqual(1000);
+  expect(getStepSize(.01, 14)).toEqual([.001, pattern1]);
+  expect(getStepSize(10000, 14)).toEqual([1000, pattern1]);
 });
 
 test('gridline helper to always return a reasonable number of steps', () => {
   for (let i = 1; i <= 1000; i++) {
-    const stepSize = getGridStepSize(i, 14);
-    expect(Math.round(i / stepSize)).toBeGreaterThanOrEqual(7);
-    expect(Math.round(i / stepSize)).toBeLessThanOrEqual(21);
+    const [stepSize, _] = getStepSize(i, 14);
+    expect(Math.round(i / stepSize)).toBeGreaterThanOrEqual(6);
+    expect(Math.round(i / stepSize)).toBeLessThanOrEqual(14);
   }
 });
+
+describe('TickGenerator with range 0.0-1.0 and room for 2 labels', () => {
+  let tickGen: TickGenerator|undefined = undefined;
+  beforeAll(() => {
+    const timeSpan = new TimeSpan(0.0, 1.0);
+    const timeScale = new TimeScale(timeSpan, [0, 200]);
+    tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
+  });
+  it('should produce major ticks at 0.5s and minor ticks at 0.1s starting at 0',
+     () => {
+       const expected = [
+         {type: TickType.MAJOR, time: 0.0},
+         {type: TickType.MINOR, time: 0.1},
+         {type: TickType.MINOR, time: 0.2},
+         {type: TickType.MINOR, time: 0.3},
+         {type: TickType.MINOR, time: 0.4},
+         {type: TickType.MAJOR, time: 0.5},
+         {type: TickType.MINOR, time: 0.6},
+         {type: TickType.MINOR, time: 0.7},
+         {type: TickType.MINOR, time: 0.8},
+         {type: TickType.MINOR, time: 0.9},
+       ];
+       const actual = Array.from(tickGen!);
+       expectTicksEqual(actual, expected);
+     });
+  it('should tell us to use 1 decimal place for labels', () => {
+    expect(tickGen!.digits).toEqual(1);
+  });
+});
+
+describe('TickGenerator with range 0.3-1.3 and room for 2 labels', () => {
+  let tickGen: TickGenerator|undefined = undefined;
+  beforeAll(() => {
+    const timeSpan = new TimeSpan(0.3, 1.3);
+    const timeScale = new TimeScale(timeSpan, [0, 200]);
+    tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
+  });
+  it('should produce major ticks at 0.5s and minor ticks at 0.1s starting at 0',
+     () => {
+       const expected = [
+         {type: TickType.MINOR, time: 0.3},
+         {type: TickType.MINOR, time: 0.4},
+         {type: TickType.MAJOR, time: 0.5},
+         {type: TickType.MINOR, time: 0.6},
+         {type: TickType.MINOR, time: 0.7},
+         {type: TickType.MINOR, time: 0.8},
+         {type: TickType.MINOR, time: 0.9},
+         {type: TickType.MAJOR, time: 1.0},
+         {type: TickType.MINOR, time: 1.1},
+         {type: TickType.MINOR, time: 1.2},
+       ];
+       const actual = Array.from(tickGen!);
+       expectTicksEqual(actual, expected);
+     });
+  it('should tell us to use 1 decimal place for labels', () => {
+    expect(tickGen!.digits).toEqual(1);
+  });
+});
+
+describe('TickGenerator with range 0.0-0.2 and room for 1 label', () => {
+  let tickGen: TickGenerator|undefined = undefined;
+  beforeAll(() => {
+    const timeSpan = new TimeSpan(0.0, 0.2);
+    const timeScale = new TimeScale(timeSpan, [0, 100]);
+    tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
+  });
+  it('should produce major ticks at 0.2s and minor ticks at 0.1s starting at 0',
+     () => {
+       const expected = [
+         {type: TickType.MAJOR, time: 0.0},
+         {type: TickType.MINOR, time: 0.05},
+         {type: TickType.MEDIUM, time: 0.1},
+         {type: TickType.MINOR, time: 0.15},
+       ];
+       const actual = Array.from(tickGen!);
+       expectTicksEqual(actual, expected);
+     });
+  it('should tell us to use 1 decimal place for labels', () => {
+    expect(tickGen!.digits).toEqual(1);
+  });
+});
+
+describe('TickGenerator with range 0.0-0.1 and room for 1 label', () => {
+  let tickGen: TickGenerator|undefined = undefined;
+  beforeAll(() => {
+    const timeSpan = new TimeSpan(0.0, 0.1);
+    const timeScale = new TimeScale(timeSpan, [0, 100]);
+    tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
+  });
+  it('should produce major ticks at 0.1s & minor ticks at 0.02s starting at 0',
+     () => {
+       const expected = [
+         {type: TickType.MAJOR, time: 0.0},
+         {type: TickType.MINOR, time: 0.01},
+         {type: TickType.MINOR, time: 0.02},
+         {type: TickType.MINOR, time: 0.03},
+         {type: TickType.MINOR, time: 0.04},
+         {type: TickType.MEDIUM, time: 0.05},
+         {type: TickType.MINOR, time: 0.06},
+         {type: TickType.MINOR, time: 0.07},
+         {type: TickType.MINOR, time: 0.08},
+         {type: TickType.MINOR, time: 0.09},
+       ];
+       const actual = Array.from(tickGen!);
+       expect(tickGen!.digits).toEqual(1);
+       expectTicksEqual(actual, expected);
+     });
+  it('should tell us to use 1 decimal place for labels', () => {
+    expect(tickGen!.digits).toEqual(1);
+  });
+});
+
+describe('TickGenerator with a very small timespan', () => {
+  let tickGen: TickGenerator|undefined = undefined;
+  beforeAll(() => {
+    const timeSpan = new TimeSpan(0.0, 1e-9);
+    const timeScale = new TimeScale(timeSpan, [0, 100]);
+    tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
+  });
+  it('should generate minor ticks at 2e-10s and one major tick at the start',
+     () => {
+       const expected = [
+         {type: TickType.MAJOR, time: 0.0},
+         {type: TickType.MINOR, time: 1e-10},
+         {type: TickType.MINOR, time: 2e-10},
+         {type: TickType.MINOR, time: 3e-10},
+         {type: TickType.MINOR, time: 4e-10},
+         {type: TickType.MEDIUM, time: 5e-10},
+         {type: TickType.MINOR, time: 6e-10},
+         {type: TickType.MINOR, time: 7e-10},
+         {type: TickType.MINOR, time: 8e-10},
+         {type: TickType.MINOR, time: 9e-10},
+       ];
+       const actual = Array.from(tickGen!);
+       expectTicksEqual(actual, expected);
+     });
+  it('should tell us to use 9 decimal places for labels', () => {
+    expect(tickGen!.digits).toEqual(9);
+  });
+});
+
+describe('TickGenerator with a very large timespan', () => {
+  let tickGen: TickGenerator|undefined = undefined;
+  beforeAll(() => {
+    const timeSpan = new TimeSpan(0.0, 1e9);
+    const timeScale = new TimeScale(timeSpan, [0, 100]);
+    tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
+  });
+  it('should generate minor ticks at 2e8 and one major tick at the start',
+     () => {
+       const expected = [
+         {type: TickType.MAJOR, time: 0.0},
+         {type: TickType.MINOR, time: 1e8},
+         {type: TickType.MINOR, time: 2e8},
+         {type: TickType.MINOR, time: 3e8},
+         {type: TickType.MINOR, time: 4e8},
+         {type: TickType.MEDIUM, time: 5e8},
+         {type: TickType.MINOR, time: 6e8},
+         {type: TickType.MINOR, time: 7e8},
+         {type: TickType.MINOR, time: 8e8},
+         {type: TickType.MINOR, time: 9e8},
+       ];
+       const actual = Array.from(tickGen!);
+       expectTicksEqual(actual, expected);
+     });
+  it('should tell us to use 0 decimal places for labels', () => {
+    expect(tickGen!.digits).toEqual(0);
+  });
+});
+
+describe('TickGenerator where the timespan has a dynamic range of 1e12', () => {
+  // This is the equivalent of zooming in to the nanosecond level, 1000 seconds
+  // into a trace Note: this is about the limit of what this generator can
+  // handle.
+  let tickGen: TickGenerator|undefined = undefined;
+  beforeAll(() => {
+    const timeSpan = new TimeSpan(1000, 1000.000000001);
+    const timeScale = new TimeScale(timeSpan, [0, 100]);
+    tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
+  });
+  it('should generate minor ticks at 1e-10s and one major tick at the start',
+     () => {
+       const expected = [
+         {type: TickType.MAJOR, time: 1000.0000000000},
+         {type: TickType.MINOR, time: 1000.0000000001},
+         {type: TickType.MINOR, time: 1000.0000000002},
+         {type: TickType.MINOR, time: 1000.0000000003},
+         {type: TickType.MINOR, time: 1000.0000000004},
+         {type: TickType.MEDIUM, time: 1000.0000000005},
+         {type: TickType.MINOR, time: 1000.0000000006},
+         {type: TickType.MINOR, time: 1000.0000000007},
+         {type: TickType.MINOR, time: 1000.0000000008},
+         {type: TickType.MINOR, time: 1000.0000000009},
+       ];
+       const actual = Array.from(tickGen!);
+       expectTicksEqual(actual, expected);
+     });
+  it('should tell us to use 9 decimal places for labels', () => {
+    expect(tickGen!.digits).toEqual(9);
+  });
+});
+
+describe(
+    'TickGenerator where the timespan has a ridiculously huge dynamic range',
+    () => {
+      // We don't expect this to work, just wanna make sure it doesn't crash or
+      // get stuck
+      it('should not crash or get stuck in an infinite loop', () => {
+        const timeSpan = new TimeSpan(1000, 1000.000000000001);
+        const timeScale = new TimeScale(timeSpan, [0, 100]);
+        new TickGenerator(timeScale);
+      });
+    });
+
+describe(
+    'TickGenerator where the timespan has a ridiculously huge dynamic range',
+    () => {
+      // We don't expect this to work, just wanna make sure it doesn't crash or
+      // get stuck
+      it('should not crash or get stuck in an infinite loop', () => {
+        const timeSpan = new TimeSpan(1000, 1000.000000000001);
+        const timeScale = new TimeScale(timeSpan, [0, 100]);
+        new TickGenerator(timeScale);
+      });
+    });
+
+test('TickGenerator constructed with a 0 width throws an error', () => {
+  expect(() => {
+    const timeScale = new TimeScale(new TimeSpan(0.0, 1.0), [0, 0]);
+    new TickGenerator(timeScale);
+  }).toThrow(Error);
+});
+
+test(
+    'TickGenerator constructed with desiredPxPerStep of 0 throws an error',
+    () => {
+      expect(() => {
+        new TickGenerator(timeScale, {minLabelPx: 0});
+      }).toThrow(Error);
+    });
+
+test('TickGenerator constructed with a 0 duration throws an error', () => {
+  expect(() => {
+    const timeScale = new TimeScale(new TimeSpan(0.0, 0.0), [0, 1]);
+    new TickGenerator(timeScale);
+  }).toThrow(Error);
+});
+
+function expectTicksEqual(actual: Tick[], expected: any[]) {
+  // TODO(stevegolton) We could write a custom matcher for this; this approach
+  // produces cryptic error messages.
+  expect(actual.length).toEqual(expected.length);
+  for (let i = 0; i < actual.length; ++i) {
+    const ex = expected[i];
+    const ac = actual[i];
+    expect(ac.type).toEqual(ex.type);
+    expect(ac.time).toBeCloseTo(ex.time, 9);
+  }
+}
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index 1a91678..9896611 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -27,7 +27,11 @@
 import {TRACK_SHELL_WIDTH} from './css_constants';
 import {PerfettoMouseEvent} from './events';
 import {globals} from './globals';
-import {gridlines} from './gridline_helper';
+import {
+  TickGenerator,
+  TickType,
+  timeScaleForVisibleWindow,
+} from './gridline_helper';
 import {Panel, PanelSize} from './panel';
 import {isTraceLoaded} from './sidebar';
 
@@ -95,13 +99,15 @@
 
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
     const timeScale = globals.frontendLocalState.timeScale;
-    const range = globals.frontendLocalState.visibleWindowTime;
     let aNoteIsHovered = false;
 
     ctx.fillStyle = '#999';
     ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
-    for (const xAndTime of gridlines(size.width, range, timeScale)) {
-      ctx.fillRect(xAndTime[0], 0, 1, size.height);
+    const relScale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
+    if (relScale.timeSpan.duration > 0 && relScale.widthPx > 0) {
+      for (const {type, position} of new TickGenerator(relScale)) {
+        if (type === TickType.MAJOR) ctx.fillRect(position, 0, 1, size.height);
+      }
     }
 
     ctx.textBaseline = 'bottom';
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index 12b5582..578fa7f 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -16,7 +16,7 @@
 
 import {assertExists} from '../base/logging';
 import {hueForCpu} from '../common/colorizer';
-import {TimeSpan, timeToString} from '../common/time';
+import {TimeSpan} from '../common/time';
 
 import {
   OVERVIEW_TIMELINE_NON_VISIBLE_COLOR,
@@ -29,6 +29,7 @@
 import {OuterDragStrategy} from './drag/outer_drag_strategy';
 import {DragGestureHandler} from './drag_gesture_handler';
 import {globals} from './globals';
+import {TickGenerator, TickType} from './gridline_helper';
 import {Panel, PanelSize} from './panel';
 import {TimeScale} from './time_scale';
 
@@ -80,21 +81,28 @@
     if (this.timeScale === undefined) return;
     const headerHeight = 25;
     const tracksHeight = size.height - headerHeight;
+    const timeSpan = new TimeSpan(0, this.totTime.duration);
 
-    // Draw time labels on the top header.
-    ctx.font = '10px Roboto Condensed';
-    ctx.fillStyle = '#999';
-    for (let i = 0; i < 100; i++) {
-      const xPos =
-          (i * (this.width - TRACK_SHELL_WIDTH) / 100) + TRACK_SHELL_WIDTH;
-      const t = this.timeScale.pxToTime(xPos);
-      if (xPos <= 0) continue;
-      if (xPos > this.width) break;
-      if (i % 10 === 0) {
-        ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
-        ctx.fillText(timeToString(t - this.totTime.start), xPos + 5, 18);
-      } else {
-        ctx.fillRect(xPos - 1, 0, 1, 5);
+    const timeScale = new TimeScale(timeSpan, [TRACK_SHELL_WIDTH, this.width]);
+
+    if (timeScale.widthPx > 0) {
+      const tickGen = new TickGenerator(timeScale);
+
+      // Draw time labels on the top header.
+      ctx.font = '10px Roboto Condensed';
+      ctx.fillStyle = '#999';
+      for (const {type, time, position} of tickGen) {
+        const xPos = Math.round(position);
+        if (xPos <= 0) continue;
+        if (xPos > this.width) break;
+        if (type === TickType.MAJOR) {
+          ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
+          ctx.fillText(time.toFixed(tickGen.digits) + ' s', xPos + 5, 18);
+        } else if (type == TickType.MEDIUM) {
+          ctx.fillRect(xPos - 1, 0, 1, 8);
+        } else if (type == TickType.MINOR) {
+          ctx.fillRect(xPos - 1, 0, 1, 5);
+        }
       }
     }
 
diff --git a/ui/src/frontend/pivot_table_redux.ts b/ui/src/frontend/pivot_table_redux.ts
index e1c880c..f17bb24 100644
--- a/ui/src/frontend/pivot_table_redux.ts
+++ b/ui/src/frontend/pivot_table_redux.ts
@@ -52,7 +52,7 @@
   TableColumn,
 } from './pivot_table_redux_types';
 import {PopupMenuButton, PopupMenuItem} from './popup_menu';
-import {ReorderableCellGroup} from './reorderable_cells';
+import {ReorderableCell, ReorderableCellGroup} from './reorderable_cells';
 
 
 interface PathItem {
@@ -85,6 +85,8 @@
     return `${column} IS NULL`;
   } else if (typeof filter.value === 'number') {
     return `${column} = ${filter.value}`;
+  } else if (filter.value instanceof Uint8Array) {
+    throw new Error(`BLOB as DrillFilter not implemented`);
   }
   return `${column} = ${sqliteString(filter.value)}`;
 }
@@ -98,6 +100,13 @@
   }
 }
 
+export function markFirst(index: number) {
+  if (index === 0) {
+    return '.first';
+  }
+  return '';
+}
+
 export class PivotTableRedux extends Panel<PivotTableReduxAttrs> {
   get pivotState() {
     return globals.state.nonSerializableState.pivotTableRedux;
@@ -165,7 +174,7 @@
     for (let i = 0; i < tree.aggregates.length; i++) {
       const renderedValue = this.renderCell(
           result.metadata.aggregationColumns[i].column, tree.aggregates[i]);
-      renderedCells.push(m('td', renderedValue));
+      renderedCells.push(m('td' + markFirst(i), renderedValue));
     }
 
     const drillFilters: DrillFilter[] = [];
@@ -234,7 +243,7 @@
         const value = row[aggregationIndex(treeDepth, j)];
         const renderedValue = this.renderCell(
             result.metadata.aggregationColumns[j].column, value);
-        renderedCells.push(m('td', renderedValue));
+        renderedCells.push(m('td.aggregation' + markFirst(j), renderedValue));
       }
 
       renderedCells.push(this.renderDrillDownCell(area, drillFilters));
@@ -249,7 +258,7 @@
            m('strong', 'Total values:'))];
     for (let i = 0; i < queryResult.tree.aggregates.length; i++) {
       overallValuesRow.push(
-          m('td',
+          m('td' + markFirst(i),
             this.renderCell(
                 queryResult.metadata.aggregationColumns[i].column,
                 queryResult.tree.aggregates[i])));
@@ -317,7 +326,7 @@
 
   renderAggregationHeaderCell(
       aggregation: Aggregation, index: number,
-      removeItem: boolean): m.Children {
+      removeItem: boolean): ReorderableCell {
     const popupItems: PopupMenuItem[] = [];
     const state = globals.state.nonSerializableState.pivotTableRedux;
     let icon = 'more_horiz';
@@ -382,13 +391,16 @@
       popupItems.push(sliceAggregationsItem);
     }
 
-    return [
-      this.readableAggregationName(aggregation),
-      m(PopupMenuButton, {
-        icon,
-        items: popupItems,
-      }),
-    ];
+    return {
+      extraClass: '.aggregation' + markFirst(index),
+      content: [
+        this.readableAggregationName(aggregation),
+        m(PopupMenuButton, {
+          icon,
+          items: popupItems,
+        }),
+      ],
+    };
   }
 
   showModal = false;
@@ -422,7 +434,7 @@
 
   renderPivotColumnHeader(
       queryResult: PivotTableReduxResult, pivot: TableColumn,
-      selectedPivots: Set<string>): m.Children {
+      selectedPivots: Set<string>): ReorderableCell {
     const items: PopupMenuItem[] = [{
       itemType: 'regular',
       text: 'Add argument pivot',
@@ -476,10 +488,12 @@
       });
     }
 
-    return [
-      readableColumnName(pivot),
-      m(PopupMenuButton, {icon: 'more_horiz', items}),
-    ];
+    return {
+      content: [
+        readableColumnName(pivot),
+        m(PopupMenuButton, {icon: 'more_horiz', items}),
+      ],
+    };
   }
 
   renderResultsTable(attrs: PivotTableReduxAttrs) {
@@ -517,13 +531,13 @@
                 aggregation, index, removeItem));
 
     return m(
-        'table.query-table.pivot-table',
+        'table.pivot-table',
         m('thead',
           // First row of the table, containing names of pivot and aggregation
           // columns, as well as popup menus to modify the columns. Last cell
           // is empty because of an extra column with "drill down" button for
           // each pivot table row.
-          m('tr',
+          m('tr.header',
             m(ReorderableCellGroup, {
               cells: pivotTableHeaders,
               onReorder: (
diff --git a/ui/src/frontend/query_table.ts b/ui/src/frontend/query_table.ts
index ff240d9..2469b24 100644
--- a/ui/src/frontend/query_table.ts
+++ b/ui/src/frontend/query_table.ts
@@ -79,7 +79,12 @@
     const cells = [];
     const {row, columns} = vnode.attrs;
     for (const col of columns) {
-      cells.push(m('td', row[col]));
+      const value = row[col];
+      if (value instanceof Uint8Array) {
+        cells.push(m('td', `<BLOB sz=${value.length}>`));
+      } else {
+        cells.push(m('td', value));
+      }
     }
     const containsSliceLocation =
         QueryTableRow.columnsContainsSliceLocation(columns);
diff --git a/ui/src/frontend/reorderable_cells.ts b/ui/src/frontend/reorderable_cells.ts
index dd5992b..e3977a2 100644
--- a/ui/src/frontend/reorderable_cells.ts
+++ b/ui/src/frontend/reorderable_cells.ts
@@ -20,8 +20,13 @@
 
 import {globals} from './globals';
 
+export interface ReorderableCell {
+  content: m.Children;
+  extraClass?: string;
+}
+
 export interface ReorderableCellGroupAttrs {
-  cells: m.Children[];
+  cells: ReorderableCell[];
   onReorder: (from: number, to: number, side: DropDirection) => void;
 }
 
@@ -62,7 +67,7 @@
   view(vnode: m.Vnode<ReorderableCellGroupAttrs>): m.Children {
     return vnode.attrs.cells.map(
         (cell, index) => m(
-            'td.reorderable-cell',
+            `td.reorderable-cell${cell.extraClass ?? ''}`,
             {
               draggable: 'draggable',
               class: this.getClassForIndex(index),
@@ -138,7 +143,7 @@
                 globals.rafScheduler.scheduleFullRedraw();
               },
             },
-            cell));
+            cell.content));
   }
 
   oncreate(vnode: m.VnodeDOM<ReorderableCellGroupAttrs, this>) {
diff --git a/ui/src/frontend/tickmark_panel.ts b/ui/src/frontend/tickmark_panel.ts
index 6cc8037..aea1085 100644
--- a/ui/src/frontend/tickmark_panel.ts
+++ b/ui/src/frontend/tickmark_panel.ts
@@ -13,11 +13,16 @@
 // limitations under the License.
 
 import * as m from 'mithril';
+
 import {fromNs} from '../common/time';
 
 import {TRACK_SHELL_WIDTH} from './css_constants';
 import {globals} from './globals';
-import {gridlines} from './gridline_helper';
+import {
+  TickGenerator,
+  TickType,
+  timeScaleForVisibleWindow,
+} from './gridline_helper';
 import {Panel, PanelSize} from './panel';
 
 // This is used to display the summary of search results.
@@ -31,9 +36,11 @@
 
     ctx.fillStyle = '#999';
     ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
-    for (const xAndTime of gridlines(
-             size.width, visibleWindowTime, timeScale)) {
-      ctx.fillRect(xAndTime[0], 0, 1, size.height);
+    const relScale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
+    if (relScale.timeSpan.duration > 0 && relScale.widthPx > 0) {
+      for (const {type, position} of new TickGenerator(relScale)) {
+        if (type === TickType.MAJOR) ctx.fillRect(position, 0, 1, size.height);
+      }
     }
 
     const data = globals.searchSummary;
diff --git a/ui/src/frontend/time_axis_panel.ts b/ui/src/frontend/time_axis_panel.ts
index 2f39881..aeb396b 100644
--- a/ui/src/frontend/time_axis_panel.ts
+++ b/ui/src/frontend/time_axis_panel.ts
@@ -18,7 +18,11 @@
 
 import {TRACK_SHELL_WIDTH} from './css_constants';
 import {globals} from './globals';
-import {gridlines} from './gridline_helper';
+import {
+  TickGenerator,
+  TickType,
+  timeScaleForVisibleWindow,
+} from './gridline_helper';
 import {Panel, PanelSize} from './panel';
 
 export class TimeAxisPanel extends Panel {
@@ -27,27 +31,23 @@
   }
 
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
-    const timeScale = globals.frontendLocalState.timeScale;
-    const range = globals.frontendLocalState.visibleWindowTime;
     ctx.fillStyle = '#999';
-
-    // Write trace offset time + line.
-    ctx.font = '12px Roboto Condensed';
-
-    ctx.textAlign = 'right';
-    const offsetTime =
-        timeToString(range.start - globals.state.traceTime.startSec);
-    ctx.fillText(offsetTime, TRACK_SHELL_WIDTH - 6, 11);
-
+    ctx.font = '10px Roboto Condensed';
     ctx.textAlign = 'left';
+
     const startTime = timeToString(globals.state.traceTime.startSec);
     ctx.fillText(startTime + ' +', 6, 11);
 
     // Draw time axis.
-    ctx.font = '10px Roboto Condensed';
-    for (const [x, time] of gridlines(size.width, range, timeScale)) {
-      ctx.fillRect(x, 0, 1, size.height);
-      ctx.fillText('+' + timeToString(time - range.start), x + 5, 10);
+    const timeScale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
+    if (timeScale.timeSpan.duration > 0 && timeScale.widthPx > 0) {
+      const tickGen = new TickGenerator(timeScale);
+      for (const {type, time, position} of tickGen) {
+        if (type === TickType.MAJOR) {
+          ctx.fillRect(position, 0, 1, size.height);
+          ctx.fillText(time.toFixed(tickGen.digits) + ' s', position + 5, 10);
+        }
+      }
     }
 
     ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
diff --git a/ui/src/frontend/time_scale.ts b/ui/src/frontend/time_scale.ts
index cdf3ca2..6f0307a 100644
--- a/ui/src/frontend/time_scale.ts
+++ b/ui/src/frontend/time_scale.ts
@@ -79,6 +79,14 @@
   get endPx(): number {
     return this._endPx;
   }
+
+  get widthPx(): number {
+    return this._endPx - this._startPx;
+  }
+
+  get timeSpan(): TimeSpan {
+    return this.timeBounds;
+  }
 }
 
 export function computeZoom(
diff --git a/ui/src/frontend/time_selection_panel.ts b/ui/src/frontend/time_selection_panel.ts
index f1391da..5fc8a9f 100644
--- a/ui/src/frontend/time_selection_panel.ts
+++ b/ui/src/frontend/time_selection_panel.ts
@@ -19,7 +19,11 @@
 
 import {TRACK_SHELL_WIDTH} from './css_constants';
 import {globals} from './globals';
-import {gridlines} from './gridline_helper';
+import {
+  TickGenerator,
+  TickType,
+  timeScaleForVisibleWindow,
+} from './gridline_helper';
 import {Panel, PanelSize} from './panel';
 
 export interface BBox {
@@ -120,13 +124,15 @@
   }
 
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
-    const range = globals.frontendLocalState.visibleWindowTime;
-    const timeScale = globals.frontendLocalState.timeScale;
-
     ctx.fillStyle = '#999';
     ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
-    for (const xAndTime of gridlines(size.width, range, timeScale)) {
-      ctx.fillRect(xAndTime[0], 0, 1, size.height);
+    const scale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
+    if (scale.timeSpan.duration > 0 && scale.widthPx > 0) {
+      for (const {position, type} of new TickGenerator(scale)) {
+        if (type === TickType.MAJOR) {
+          ctx.fillRect(position, 0, 1, size.height);
+        }
+      }
     }
 
     const localArea = globals.frontendLocalState.selectedArea;
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index 34a7ec4..0c1df65 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -215,8 +215,6 @@
 
     drawGridLines(
         ctx,
-        globals.frontendLocalState.timeScale,
-        globals.frontendLocalState.visibleWindowTime,
         size.width,
         size.height);
 
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 588900f..5ee1a55 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -364,8 +364,6 @@
 
     drawGridLines(
         ctx,
-        globals.frontendLocalState.timeScale,
-        globals.frontendLocalState.visibleWindowTime,
         size.width,
         size.height);