Merge "record_android_trace: allow temporary ADB failures"
diff --git a/include/perfetto/ext/base/string_view.h b/include/perfetto/ext/base/string_view.h
index dce2fdc..4f16f90 100644
--- a/include/perfetto/ext/base/string_view.h
+++ b/include/perfetto/ext/base/string_view.h
@@ -119,7 +119,7 @@
   }
 
   std::string ToStdString() const {
-    return data_ == nullptr ? "" : std::string(data_, size_);
+    return size_ == 0 ? "" : std::string(data_, size_);
   }
 
   uint64_t Hash() const {
diff --git a/src/base/string_view_unittest.cc b/src/base/string_view_unittest.cc
index bb00035..5bda159 100644
--- a/src/base/string_view_unittest.cc
+++ b/src/base/string_view_unittest.cc
@@ -34,6 +34,8 @@
   EXPECT_EQ(StringView("ax", 1), StringView("ay", 1));
   EXPECT_EQ(StringView("ax", 1), StringView("a"));
   EXPECT_EQ(StringView("ax", 1), "a");
+  EXPECT_EQ(StringView(reinterpret_cast<const char*>(0x100), 0).ToStdString(),
+            std::string(""));
   EXPECT_EQ(StringView("foo|", 3).ToStdString(), std::string("foo"));
   EXPECT_TRUE(StringView("x") != StringView(""));
   EXPECT_TRUE(StringView("") != StringView("y"));
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer.cc b/src/trace_processor/importers/json/json_trace_tokenizer.cc
index 695e692..aa2b259 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer.cc
+++ b/src/trace_processor/importers/json/json_trace_tokenizer.cc
@@ -33,8 +33,6 @@
 
 namespace {
 
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
-
 util::Status AppendUnescapedCharacter(char c,
                                       bool is_escaping,
                                       std::string* key) {
@@ -103,11 +101,8 @@
   return ReadStringRes::kNeedsMoreData;
 }
 
-#endif  // PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
-
 }  // namespace
 
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 ReadDictRes ReadOneJsonDict(const char* start,
                             const char* end,
                             base::StringView* value,
@@ -365,7 +360,54 @@
   }
   return ReadSystemLineRes::kNeedsMoreData;
 }
-#endif  // PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
+
+base::Optional<json::TimeUnit> MaybeParseDisplayTimeUnit(
+    base::StringView buffer) {
+  size_t idx = buffer.find("\"displayTimeUnit\"");
+  if (idx == base::StringView::npos)
+    return base::nullopt;
+
+  base::StringView time_unit = buffer.substr(idx);
+  std::string key;
+  const char* value_start = nullptr;
+  ReadKeyRes res =
+      ReadOneJsonKey(time_unit.data(), time_unit.end(), &key, &value_start);
+
+  // If we didn't read the key successfully, it could have been that
+  // displayTimeUnit is used somewhere else in the dict (maybe as a value?) so
+  // just ignore it.
+  if (res == ReadKeyRes::kEndOfDictionary || res == ReadKeyRes::kFatalError ||
+      res == ReadKeyRes::kNeedsMoreData) {
+    return base::nullopt;
+  }
+
+  // Double check that we did actually get the displayTimeUnit key.
+  if (key != "displayTimeUnit")
+    return base::nullopt;
+
+  // If the value isn't a string, we can do little except bail.
+  if (value_start[0] != '"')
+    return base::nullopt;
+
+  std::string value;
+  const char* unused;
+  ReadStringRes value_res =
+      ReadOneJsonString(value_start + 1, time_unit.end(), &value, &unused);
+
+  // If we didn't read the value successfully, we could be in another key
+  // in a nested dictionary. Just ignore the error.
+  if (value_res == ReadStringRes::kFatalError ||
+      value_res == ReadStringRes::kNeedsMoreData) {
+    return base::nullopt;
+  }
+
+  if (value == "ns") {
+    return json::TimeUnit::kNs;
+  } else if (value == "ms") {
+    return json::TimeUnit::kMs;
+  }
+  return base::nullopt;
+}
 
 JsonTraceTokenizer::JsonTraceTokenizer(TraceProcessorContext* ctx)
     : context_(ctx) {}
@@ -375,28 +417,23 @@
                                        size_t size) {
   PERFETTO_DCHECK(json::IsJsonSupported());
 
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
   buffer_.insert(buffer_.end(), data.get(), data.get() + size);
   const char* buf = buffer_.data();
   const char* next = buf;
   const char* end = buf + buffer_.size();
 
-  JsonTracker* json_tracker = JsonTracker::GetOrCreate(context_);
-
-  // It's possible the displayTimeUnit key is at the end of the json
-  // file so to be correct we ought to parse the whole file looking
-  // for this key before parsing any events however this would require
-  // two passes on the file so for now we only handle displayTimeUnit
-  // correctly if it is at the beginning of the file.
-  const base::StringView view(buf, size);
-  if (view.find("\"displayTimeUnit\":\"ns\"") != base::StringView::npos) {
-    json_tracker->SetTimeUnit(json::TimeUnit::kNs);
-  } else if (view.find("\"displayTimeUnit\":\"ms\"") !=
-             base::StringView::npos) {
-    json_tracker->SetTimeUnit(json::TimeUnit::kMs);
-  }
-
   if (offset_ == 0) {
+    // It's possible the displayTimeUnit key is at the end of the json
+    // file so to be correct we ought to parse the whole file looking
+    // for this key before parsing any events however this would require
+    // two passes on the file so for now we only handle displayTimeUnit
+    // correctly if it is at the beginning of the file.
+    base::Optional<json::TimeUnit> timeunit =
+        MaybeParseDisplayTimeUnit(base::StringView(buf, size));
+    if (timeunit) {
+      JsonTracker::GetOrCreate(context_)->SetTimeUnit(*timeunit);
+    }
+
     // Strip leading whitespace.
     while (next != end && isspace(*next)) {
       next++;
@@ -436,18 +473,8 @@
   offset_ += static_cast<uint64_t>(next - buf);
   buffer_.erase(buffer_.begin(), buffer_.begin() + (next - buf));
   return util::OkStatus();
-#else
-  perfetto::base::ignore_result(data);
-  perfetto::base::ignore_result(size);
-  perfetto::base::ignore_result(context_);
-  perfetto::base::ignore_result(format_);
-  perfetto::base::ignore_result(position_);
-  perfetto::base::ignore_result(offset_);
-  return util::ErrStatus("Cannot parse JSON trace due to missing JSON support");
-#endif  // PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 }
 
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 util::Status JsonTraceTokenizer::ParseInternal(const char* start,
                                                const char* end,
                                                const char** out) {
@@ -491,6 +518,12 @@
           return util::ErrStatus("displayTimeUnit too large");
         if (time_unit != "ms" && time_unit != "ns")
           return util::ErrStatus("displayTimeUnit unknown");
+        // If the displayTimeUnit came after the first chunk, the increment the
+        // too late stat.
+        if (offset_ != 0) {
+          context_->storage->IncrementStats(
+              stats::json_display_time_unit_too_late);
+        }
         return ParseInternal(next, end, out);
       } else {
         // If we don't recognize the key, just ignore the rest of the trace and
@@ -595,7 +628,6 @@
   *out = next;
   return util::OkStatus();
 }
-#endif  // PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 
 void JsonTraceTokenizer::NotifyEndOfFile() {}
 
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer.h b/src/trace_processor/importers/json/json_trace_tokenizer.h
index 86f5731..4daf661 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer.h
+++ b/src/trace_processor/importers/json/json_trace_tokenizer.h
@@ -20,6 +20,7 @@
 #include <stdint.h>
 
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/importers/json/json_utils.h"
 #include "src/trace_processor/importers/systrace/systrace_line_tokenizer.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
@@ -32,7 +33,6 @@
 
 class TraceProcessorContext;
 
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 // Visible for testing.
 enum class ReadDictRes {
   kFoundDict,
@@ -93,7 +93,11 @@
                                          const char* end,
                                          std::string* line,
                                          const char** next);
-#endif
+
+// Parses the "displayTimeUnit" key from the given trace buffer
+// and returns the associated time unit if one exists.
+base::Optional<json::TimeUnit> MaybeParseDisplayTimeUnit(
+    base::StringView buffer);
 
 // Reads a JSON trace in chunks and extracts top level json objects.
 class JsonTraceTokenizer : public ChunkedTraceReader {
@@ -139,11 +143,9 @@
     kEof,
   };
 
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
   util::Status ParseInternal(const char* start,
                              const char* end,
                              const char** next);
-#endif
 
   TraceProcessorContext* const context_;
 
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer_unittest.cc b/src/trace_processor/importers/json/json_trace_tokenizer_unittest.cc
index 051f52a..6a11a44 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer_unittest.cc
+++ b/src/trace_processor/importers/json/json_trace_tokenizer_unittest.cc
@@ -272,6 +272,58 @@
   ASSERT_EQ(*line, R"({"ts": 149029, "foo": "bar"})");
 }
 
+TEST(JsonTraceTokenizerTest, DisplayTimeUnit) {
+  ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
+    {
+    }
+  )"),
+            base::nullopt);
+  ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
+    {
+      "displayTimeUnit": 0
+    }
+  )"),
+            base::nullopt);
+  ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
+    {
+      "str": "displayTimeUnit"
+    }
+  )"),
+            base::nullopt);
+
+  ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
+    {
+      "traceEvents": [
+        {
+          "pid": 1, "tid": 1, "name": "test",
+          "ts": 1, "dur": 1000, "ph": "X", "cat": "fee"
+        }
+      ],
+      "displayTimeUnit": "ms"
+    }
+  )"),
+            json::TimeUnit::kMs);
+  ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
+    {
+      "traceEvents": [
+        {
+          "pid": 1, "tid": 1, "name": "test",
+          "ts": 1, "dur": 1000, "ph": "X", "cat": "fee"
+        }
+      ],
+      "displayTimeUnit": "ns"
+    }
+  )"),
+            json::TimeUnit::kNs);
+
+  ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
+    {
+      "displayTimeUnit":"ms"
+    }
+  )"),
+            json::TimeUnit::kMs);
+}
+
 }  // namespace
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/json/json_tracker.h b/src/trace_processor/importers/json/json_tracker.h
index d786bf1..167881e 100644
--- a/src/trace_processor/importers/json/json_tracker.h
+++ b/src/trace_processor/importers/json/json_tracker.h
@@ -47,6 +47,9 @@
   base::Optional<int64_t> CoerceToTs(const Json::Value& value) {
     return json::CoerceToTs(time_unit_, value);
   }
+  base::Optional<int64_t> CoerceToTs(const std::string& value) {
+    return json::CoerceToTs(time_unit_, value);
+  }
 
  private:
   json::TimeUnit time_unit_ = json::TimeUnit::kUs;
diff --git a/src/trace_processor/importers/json/json_utils.cc b/src/trace_processor/importers/json/json_utils.cc
index 5d0b2d1..c44efa1 100644
--- a/src/trace_processor/importers/json/json_utils.cc
+++ b/src/trace_processor/importers/json/json_utils.cc
@@ -57,22 +57,8 @@
     case Json::uintValue:
     case Json::intValue:
       return value.asInt64() * TimeUnitToNs(unit);
-    case Json::stringValue: {
-      std::string s = value.asString();
-      size_t lhs_end = std::min<size_t>(s.find('.'), s.size());
-      size_t rhs_start = std::min<size_t>(lhs_end + 1, s.size());
-      base::Optional<int64_t> lhs = base::StringToInt64(s.substr(0, lhs_end));
-      base::Optional<double> rhs =
-          base::StringToDouble("0." + s.substr(rhs_start, std::string::npos));
-      if ((!lhs.has_value() && lhs_end > 0) ||
-          (!rhs.has_value() && rhs_start < s.size())) {
-        return base::nullopt;
-      }
-      int64_t factor = TimeUnitToNs(unit);
-      return lhs.value_or(0) * factor +
-             static_cast<int64_t>(rhs.value_or(0) *
-                                  static_cast<double>(factor));
-    }
+    case Json::stringValue:
+      return CoerceToTs(unit, value.asString());
     default:
       return base::nullopt;
   }
@@ -83,6 +69,29 @@
 #endif
 }
 
+base::Optional<int64_t> CoerceToTs(TimeUnit unit, const std::string& s) {
+  PERFETTO_DCHECK(IsJsonSupported());
+
+#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
+  size_t lhs_end = std::min<size_t>(s.find('.'), s.size());
+  size_t rhs_start = std::min<size_t>(lhs_end + 1, s.size());
+  base::Optional<int64_t> lhs = base::StringToInt64(s.substr(0, lhs_end));
+  base::Optional<double> rhs =
+      base::StringToDouble("0." + s.substr(rhs_start, std::string::npos));
+  if ((!lhs.has_value() && lhs_end > 0) ||
+      (!rhs.has_value() && rhs_start < s.size())) {
+    return base::nullopt;
+  }
+  int64_t factor = TimeUnitToNs(unit);
+  return lhs.value_or(0) * factor +
+         static_cast<int64_t>(rhs.value_or(0) * static_cast<double>(factor));
+#else
+  perfetto::base::ignore_result(unit);
+  perfetto::base::ignore_result(s);
+  return base::nullopt;
+#endif
+}
+
 base::Optional<int64_t> CoerceToInt64(const Json::Value& value) {
   PERFETTO_DCHECK(IsJsonSupported());
 
diff --git a/src/trace_processor/importers/json/json_utils.h b/src/trace_processor/importers/json/json_utils.h
index f4a70b3..74a2c3a 100644
--- a/src/trace_processor/importers/json/json_utils.h
+++ b/src/trace_processor/importers/json/json_utils.h
@@ -42,6 +42,7 @@
 
 enum class TimeUnit { kNs = 1, kUs = 1000, kMs = 1000000 };
 base::Optional<int64_t> CoerceToTs(TimeUnit unit, const Json::Value& value);
+base::Optional<int64_t> CoerceToTs(TimeUnit unit, const std::string& value);
 base::Optional<int64_t> CoerceToInt64(const Json::Value& value);
 base::Optional<uint32_t> CoerceToUint32(const Json::Value& value);
 
diff --git a/src/trace_processor/importers/systrace/systrace_parser.cc b/src/trace_processor/importers/systrace/systrace_parser.cc
index 607b2ba..b9655d4 100644
--- a/src/trace_processor/importers/systrace/systrace_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_parser.cc
@@ -59,7 +59,7 @@
                                     int64_t value) {
   systrace_utils::SystraceTracePoint point{};
   point.name = name;
-  point.value = static_cast<double>(value);
+  point.value = value;
 
   // Hardcode the tgid to 0 (i.e. no tgid available) because zero events can
   // come from kernel threads and as we group kernel threads into the kthreadd
@@ -108,7 +108,7 @@
   // the UI.
   point.tgid = 0;
 
-  point.value = static_cast<double>(value);
+  point.value = value;
   // Some versions of this trace point fill trace_type with one of (B/E/C),
   // others use the trace_begin boolean and only support begin/end events:
   if (trace_type == 0) {
@@ -164,7 +164,7 @@
     case 'S':
     case 'F': {
       StringId name_id = context_->storage->InternString(point.name);
-      int64_t cookie = static_cast<int64_t>(point.value);
+      int64_t cookie = point.value;
       UniquePid upid =
           context_->process_tracker->GetOrCreateProcess(point.tgid);
 
@@ -223,7 +223,8 @@
         // Promote ScreenState to its own top level counter.
         TrackId track =
             context_->track_tracker->InternGlobalCounterTrack(screen_state_id_);
-        context_->event_tracker->PushCounter(ts, point.value, track);
+        context_->event_tracker->PushCounter(
+            ts, static_cast<double>(point.value), track);
         return;
       }
 
@@ -243,7 +244,8 @@
         track_id =
             context_->track_tracker->InternProcessCounterTrack(name_id, upid);
       }
-      context_->event_tracker->PushCounter(ts, point.value, track_id);
+      context_->event_tracker->PushCounter(ts, static_cast<double>(point.value),
+                                           track_id);
     }
   }
 }
diff --git a/src/trace_processor/importers/systrace/systrace_parser.h b/src/trace_processor/importers/systrace/systrace_parser.h
index 1c0b9e2..a369af9 100644
--- a/src/trace_processor/importers/systrace/systrace_parser.h
+++ b/src/trace_processor/importers/systrace/systrace_parser.h
@@ -48,30 +48,24 @@
 
   static SystraceTracePoint C(uint32_t tgid,
                               base::StringView name,
-                              double value,
-                              base::StringView category_group = "") {
-    return SystraceTracePoint('C', tgid, std::move(name), value,
-                              category_group);
+                              int64_t value) {
+    return SystraceTracePoint('C', tgid, std::move(name), value);
   }
 
   static SystraceTracePoint S(uint32_t tgid,
                               base::StringView name,
-                              double value) {
-    return SystraceTracePoint('S', tgid, std::move(name), value);
+                              int64_t cookie) {
+    return SystraceTracePoint('S', tgid, std::move(name), cookie);
   }
 
   static SystraceTracePoint F(uint32_t tgid,
                               base::StringView name,
-                              double value) {
-    return SystraceTracePoint('F', tgid, std::move(name), value);
+                              int64_t cookie) {
+    return SystraceTracePoint('F', tgid, std::move(name), cookie);
   }
 
-  SystraceTracePoint(char p,
-                     uint32_t tg,
-                     base::StringView n,
-                     double v,
-                     base::StringView c = "")
-      : phase(p), tgid(tg), name(std::move(n)), value(v), category_group(c) {}
+  SystraceTracePoint(char p, uint32_t tg, base::StringView n, int64_t v)
+      : phase(p), tgid(tg), name(std::move(n)), value(v) {}
 
   // Phase can be one of B, E, C, S, F.
   char phase = '\0';
@@ -81,11 +75,8 @@
   // For phase = 'B' and phase = 'C' only.
   base::StringView name;
 
-  // For phase = 'C' only.
-  double value = 0;
-
-  // For phase = 'C' only (from Chrome).
-  base::StringView category_group;
+  // For phase = 'C' (counter value) and 'B', 'F' (async cookie).
+  int64_t value = 0;
 
   // Visible for unittesting.
   friend std::ostream& operator<<(std::ostream& os,
@@ -103,112 +94,94 @@
 // 4. C|1636|wq:monitor|0
 // 5. S|1636|frame_capture|123
 // 6. F|1636|frame_capture|456
+// Counters emitted by chromium can have a further "category group" appended
+// ("Blob" in the example below). We ignore the category group.
 // 7. C|3209|TransfersBytesPendingOnDisk-value|0|Blob
-// Visible for unittesting.
-inline SystraceParseResult ParseSystraceTracePoint(base::StringView str,
-                                                   SystraceTracePoint* out) {
-  const char* s = str.data();
-  size_t len = str.size();
+inline SystraceParseResult ParseSystraceTracePoint(
+    base::StringView str_untrimmed,
+    SystraceTracePoint* out) {
   *out = {};
 
-  // We don't support empty events.
-  if (len == 0)
-    return SystraceParseResult::kFailure;
-
-  constexpr const char* kClockSyncPrefix = "trace_event_clock_sync:";
-  if (len >= strlen(kClockSyncPrefix) &&
-      strncmp(kClockSyncPrefix, s, strlen(kClockSyncPrefix)) == 0)
-    return SystraceParseResult::kUnsupported;
-
-  char ph = s[0];
-  if (ph != 'B' && ph != 'E' && ph != 'C' && ph != 'S' && ph != 'F')
-    return SystraceParseResult::kFailure;
-
-  out->phase = ph;
-
-  // We only support E events with no arguments.
-  if (len == 1 && ph != 'E')
-    return SystraceParseResult::kFailure;
-
-  // If str matches '[BEC]\|[0-9]+[\|\n]?' set tgid_length to the length of
-  // the number. Otherwise return kFailure.
-  if (len >= 2 && s[1] != '|' && s[1] != '\n')
-    return SystraceParseResult::kFailure;
-
-  size_t tgid_length = 0;
-  for (size_t i = 2; i < len; i++) {
-    if (s[i] == '|' || s[i] == '\n') {
+  // Strip trailing \n and \0. StringViews are not null-terminated, but the
+  // writer could have appended a stray \0 depending on where the trace comes
+  // from.
+  size_t len = str_untrimmed.size();
+  for (; len > 0; --len) {
+    char last_char = str_untrimmed.at(len - 1);
+    if (last_char != '\n' && last_char != '\0')
       break;
-    }
-    if (s[i] < '0' || s[i] > '9')
-      return SystraceParseResult::kFailure;
-    tgid_length++;
   }
+  base::StringView str = str_untrimmed.substr(0, len);
 
-  // If len == 1, tgid_length will be 0 which will ensure we don't do
-  // an out of bounds read.
-  std::string tgid_str(s + 2, tgid_length);
-  out->tgid = base::StringToUInt32(tgid_str).value_or(0);
+  size_t off = 0;
 
-  switch (ph) {
-    case 'B': {
-      size_t name_index = 2 + tgid_length + 1;
-      if (name_index > len || s[name_index - 1] != '|')
+  // This function reads the next field up to the next '|', '\0' or end(). It
+  // advances |off| as it goes through fields.
+  auto read_next_field = [&off, &str, len]() {
+    for (size_t field_start = off;; ++off) {
+      char c = off >= len ? '\0' : str.at(off);
+      if (c == '|' || c == '\0') {
+        auto res = str.substr(field_start, off - field_start);
+        ++off;  // Eat the separator.
+        return res;
+      }
+    }
+  };
+
+  auto f0_phase = read_next_field();
+  if (PERFETTO_UNLIKELY(f0_phase.empty()))
+    return SystraceParseResult::kFailure;
+  out->phase = f0_phase.at(0);
+
+  auto f1_tgid = read_next_field();
+  auto opt_tgid = base::StringToUInt32(f1_tgid.ToStdString());
+  out->tgid = opt_tgid.value_or(0);
+  const bool has_tgid = opt_tgid.has_value();
+
+  switch (out->phase) {
+    case 'B': {  // Begin thread-scoped synchronous slice.
+      if (!has_tgid)
         return SystraceParseResult::kFailure;
-      out->name = base::StringView(
-          s + name_index, len - name_index - (s[len - 1] == '\n' ? 1 : 0));
-      if (out->name.empty()) {
-        static const char kEmptySliceName[] = "[empty slice name]";
-        out->name = base::StringView(kEmptySliceName);
+      auto f2_name = str.substr(off);  // It's fine even if |off| >= end().
+      if (f2_name.empty()) {
+        out->name = base::StringView("[empty slice name]");
+      } else {
+        out->name = f2_name;
       }
       return SystraceParseResult::kSuccess;
     }
-    case 'E': {
+    case 'E':  // End thread-scoped synchronous slice.
+      // Some non-Android traces (Flutter) use just "E" (aosp/1244409). Allow
+      // empty TGID on end slices. By design they are thread-scoped anyways.
+      return SystraceParseResult::kSuccess;
+    case 'S':    // Begin of async slice.
+    case 'F': {  // End of async slice.
+      auto f2_name = read_next_field();
+      auto f3_cookie = read_next_field();
+      auto maybe_cookie = base::StringToInt64(f3_cookie.ToStdString());
+      if (PERFETTO_UNLIKELY(!has_tgid || f2_name.empty() || f3_cookie.empty() ||
+                            !maybe_cookie)) {
+        return SystraceParseResult::kFailure;
+      }
+      out->name = f2_name;
+      out->value = *maybe_cookie;
       return SystraceParseResult::kSuccess;
     }
-    case 'S':
-    case 'F':
-    case 'C': {
-      size_t name_index = 2 + tgid_length + 1;
-      base::Optional<size_t> name_length;
-      for (size_t i = name_index; i < len; i++) {
-        if (s[i] == '|') {
-          name_length = i - name_index;
-          break;
-        }
-      }
-      if (!name_length.has_value())
-        return SystraceParseResult::kFailure;
-      out->name = base::StringView(s + name_index, name_length.value());
-
-      size_t value_index = name_index + name_length.value() + 1;
-      size_t value_pipe = str.find('|', value_index);
-      size_t value_len = value_pipe == base::StringView::npos
-                             ? len - value_index
-                             : value_pipe - value_index;
-      if (value_len == 0)
-        return SystraceParseResult::kFailure;
-      if (s[value_index + value_len - 1] == '\n')
-        value_len--;
-      std::string value_str(s + value_index, value_len);
-      base::Optional<double> maybe_value = base::StringToDouble(value_str);
-      if (!maybe_value.has_value()) {
+    case 'C': {  // Counter.
+      auto f2_name = read_next_field();
+      auto f3_value = read_next_field();
+      auto maybe_value = base::StringToInt64(f3_value.ToStdString());
+      if (PERFETTO_UNLIKELY(!has_tgid || f2_name.empty() || f3_value.empty() ||
+                            !maybe_value)) {
         return SystraceParseResult::kFailure;
       }
-      out->value = maybe_value.value();
-
-      if (value_pipe != base::StringView::npos) {
-        size_t group_len = len - value_pipe - 1;
-        if (group_len == 0)
-          return SystraceParseResult::kFailure;
-        if (s[len - 1] == '\n')
-          group_len--;
-        out->category_group = base::StringView(s + value_pipe + 1, group_len);
-      }
-
+      out->name = f2_name;
+      out->value = *maybe_value;
       return SystraceParseResult::kSuccess;
     }
     default:
+      if (str.find("trace_event_clock_sync:") == 0)
+        return SystraceParseResult::kUnsupported;
       return SystraceParseResult::kFailure;
   }
 }
diff --git a/src/trace_processor/importers/systrace/systrace_parser_unittest.cc b/src/trace_processor/importers/systrace/systrace_parser_unittest.cc
index 563df7f..d0c89cf 100644
--- a/src/trace_processor/importers/systrace/systrace_parser_unittest.cc
+++ b/src/trace_processor/importers/systrace/systrace_parser_unittest.cc
@@ -39,6 +39,9 @@
   ASSERT_EQ(ParseSystraceTracePoint("||\n", &result), Result::kFailure);
   ASSERT_EQ(ParseSystraceTracePoint("B", &result), Result::kFailure);
   ASSERT_EQ(ParseSystraceTracePoint("B\n", &result), Result::kFailure);
+  ASSERT_EQ(ParseSystraceTracePoint("C\n", &result), Result::kFailure);
+  ASSERT_EQ(ParseSystraceTracePoint("S\n", &result), Result::kFailure);
+  ASSERT_EQ(ParseSystraceTracePoint("F\n", &result), Result::kFailure);
   ASSERT_EQ(ParseSystraceTracePoint("C", &result), Result::kFailure);
   ASSERT_EQ(ParseSystraceTracePoint("S", &result), Result::kFailure);
   ASSERT_EQ(ParseSystraceTracePoint("F", &result), Result::kFailure);
@@ -64,14 +67,13 @@
   ASSERT_EQ(ParseSystraceTracePoint("E|42", &result), Result::kSuccess);
   EXPECT_EQ(result, SystraceTracePoint::E(42));
 
-  ASSERT_EQ(ParseSystraceTracePoint("C|543|foo|", &result), Result::kFailure);
   ASSERT_EQ(ParseSystraceTracePoint("C|543|foo|8", &result), Result::kSuccess);
   EXPECT_EQ(result, SystraceTracePoint::C(543, "foo", 8));
 
-  ASSERT_EQ(ParseSystraceTracePoint("C|543|foo|8|", &result), Result::kFailure);
-  ASSERT_EQ(ParseSystraceTracePoint("C|543|foo|8|group", &result),
-            Result::kSuccess);
-  EXPECT_EQ(result, SystraceTracePoint::C(543, "foo", 8, "group"));
+  ASSERT_EQ(
+      ParseSystraceTracePoint("C|543|foo|8|chromium_group_ignored", &result),
+      Result::kSuccess);
+  EXPECT_EQ(result, SystraceTracePoint::C(543, "foo", 8));
 
   ASSERT_EQ(ParseSystraceTracePoint("S|", &result), Result::kFailure);
 
diff --git a/src/trace_processor/storage/stats.h b/src/trace_processor/storage/stats.h
index bc44fef..4a05a74 100644
--- a/src/trace_processor/storage/stats.h
+++ b/src/trace_processor/storage/stats.h
@@ -124,6 +124,10 @@
   F(process_tracker_errors,             kSingle,  kError,    kAnalysis, ""),   \
   F(json_tokenizer_failure,             kSingle,  kError,    kTrace,    ""),   \
   F(json_parser_failure,                kSingle,  kError,    kTrace,    ""),   \
+  F(json_display_time_unit_too_late,    kSingle,  kError,    kTrace,           \
+      "The displayTimeUnit key came too late in the JSON trace so was "        \
+      "ignored. Trace processor only supports displayTimeUnit appearing "      \
+      "at the start of JSON traces"),                                          \
   F(heap_graph_invalid_string_id,       kIndexed, kError,    kTrace,    ""),   \
   F(heap_graph_non_finalized_graph,     kSingle,  kError,    kTrace,    ""),   \
   F(heap_graph_malformed_packet,        kIndexed, kError,    kTrace,    ""),   \
diff --git a/ui/src/common/proto_ring_buffer.ts b/ui/src/common/proto_ring_buffer.ts
new file mode 100644
index 0000000..b03c1c9
--- /dev/null
+++ b/ui/src/common/proto_ring_buffer.ts
@@ -0,0 +1,156 @@
+// Copyright (C) 2021 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 {assertTrue} from '../base/logging';
+
+// This class is the TypeScript equivalent of the identically-named C++ class in
+// //protozero/proto_ring_buffer.h. See comments in that header for a detailed
+// description. The architecture is identical.
+
+const kGrowBytes = 128 * 1024;
+const kMaxMsgSize = 64 * 1024 * 1024;
+
+export class ProtoRingBuffer {
+  private buf = new Uint8Array(kGrowBytes);
+  private fastpath?: Uint8Array;
+  private rd = 0;
+  private wr = 0;
+
+  // The caller must call ReadMessage() after each append() call.
+  // The |data| might be either copied in the internal ring buffer or returned
+  // (% subarray()) to the next ReadMessage() call.
+  append(data: Uint8Array) {
+    assertTrue(this.wr <= this.buf.length);
+    assertTrue(this.rd <= this.wr);
+
+    // If the last call to ReadMessage() consumed all the data in the buffer and
+    // there are no incomplete messages pending, restart from the beginning
+    // rather than keep ringing. This is the most common case.
+    if (this.rd === this.wr) {
+      this.rd = this.wr = 0;
+    }
+
+    // The caller is expected to issue a ReadMessage() after each append().
+    const dataLen = data.length;
+    if (dataLen === 0) return;
+    assertTrue(this.fastpath === undefined);
+    if (this.rd === this.wr) {
+      const msg = ProtoRingBuffer.tryReadMessage(data, 0, dataLen);
+      if (msg !== undefined &&
+          ((msg.byteOffset + msg.length) === (data.byteOffset + dataLen))) {
+        // Fastpath: in many cases, the underlying stream will effectively
+        // preserve the atomicity of messages for most small messages.
+        // In this case we can avoid the extra buffer roundtrip and return the
+        // original array (actually a subarray that skips the proto header).
+        // The next call to ReadMessage() will return this.
+        this.fastpath = msg;
+        return;
+      }
+    }
+
+    let avail = this.buf.length - this.wr;
+    if (dataLen > avail) {
+      // This whole section should be hit extremely rarely.
+
+      // Try first just recompacting the buffer by moving everything to the
+      // left. This can happen if we received "a message and a bit" on each
+      // append() call.
+      this.buf.copyWithin(0, this.rd, this.wr);
+      avail += this.rd;
+      this.wr -= this.rd;
+      this.rd = 0;
+      if (dataLen > avail) {
+        // Still not enough, expand the buffer.
+        let newSize = this.buf.length;
+        while (dataLen > newSize - this.wr) {
+          newSize += kGrowBytes;
+        }
+        assertTrue(newSize <= kMaxMsgSize * 2);
+        const newBuf = new Uint8Array(newSize);
+        newBuf.set(this.buf);
+        this.buf = newBuf;
+        // No need to touch rd / wr.
+      }
+    }
+
+    // Append the received data at the end of the ring buffer.
+    this.buf.set(data, this.wr);
+    this.wr += dataLen;
+  }
+
+  // Tries to extract a message from the ring buffer. If there is no message,
+  // or if the current message is still incomplete, returns undefined.
+  // The caller is expected to call this in a loop until it returns undefined.
+  // Note that a single write to Append() can yield more than one message
+  // (see ProtoRingBufferTest.CoalescingStream in the unittest).
+  readMessage(): Uint8Array|undefined {
+    if (this.fastpath !== undefined) {
+      assertTrue(this.rd === this.wr);
+      const msg = this.fastpath;
+      this.fastpath = undefined;
+      return msg;
+    }
+    assertTrue(this.rd <= this.wr);
+    if (this.rd >= this.wr) {
+      return undefined;  // Completely empty.
+    }
+    const msg = ProtoRingBuffer.tryReadMessage(this.buf, this.rd, this.wr);
+    if (msg === undefined) return undefined;
+    assertTrue(msg.buffer === this.buf.buffer);
+    assertTrue(this.buf.byteOffset === 0);
+    this.rd = msg.byteOffset + msg.length;
+
+    // Deliberately returning a copy of the data with slice(). In various cases
+    // (streaming query response) the caller will hold onto the returned buffer.
+    // If we get to this point, |msg| is a view of the circular buffer that we
+    // will overwrite on the next calls to append().
+    return msg.slice();
+  }
+
+  private static tryReadMessage(
+      data: Uint8Array, dataStart: number, dataEnd: number): Uint8Array
+      |undefined {
+    assertTrue(dataEnd <= data.length);
+    let pos = dataStart;
+    if (pos >= dataEnd) return undefined;
+    const tag = data[pos++];  // Assume one-byte tag.
+    if (tag >= 0x80 || (tag & 0x07) !== 2 /* len delimited */) {
+      throw new Error(
+          `RPC framing error, unexpected tag ${tag} @ offset ${pos - 1}`);
+    }
+
+    let len = 0;
+    for (let shift = 0;; shift += 7) {
+      if (pos >= dataEnd) {
+        return undefined;  // Not enough data to read varint.
+      }
+      const val = data[pos++];
+      len |= ((val & 0x7f) << shift) >>> 0;
+      if (val < 0x80) break;
+    }
+
+    if (len >= kMaxMsgSize) {
+      throw new Error(
+          `RPC framing error, message too large (${len} > ${kMaxMsgSize}`);
+    }
+    const end = pos + len;
+    if (end > dataEnd) return undefined;
+
+    // This is a subarray() and not a slice() because in the |fastpath| case
+    // we want to just return the original buffer pushed by append().
+    // In the slow-path (ring-buffer) case, the readMessage() above will create
+    // a copy via slice() before returning it.
+    return data.subarray(pos, end);
+  }
+}
diff --git a/ui/src/common/proto_ring_buffer_unittest.ts b/ui/src/common/proto_ring_buffer_unittest.ts
new file mode 100644
index 0000000..6c06ae0
--- /dev/null
+++ b/ui/src/common/proto_ring_buffer_unittest.ts
@@ -0,0 +1,140 @@
+// Copyright (C) 2021 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 * as protobuf from 'protobufjs/minimal';
+import {assertTrue} from '../base/logging';
+
+import {ProtoRingBuffer} from './proto_ring_buffer';
+
+let seed = 1;
+
+// For reproducibility.
+function Rnd(max: number) {
+  seed = seed * 16807 % 2147483647;
+  return seed % max;
+}
+
+function MakeProtoMessage(fieldId: number, len: number) {
+  const writer = protobuf.Writer.create();
+  const tag = (fieldId << 3) | 2 /*length-delimited*/;
+  assertTrue(tag < 0x80 && (tag & 7) === 2);
+  writer.uint32(tag);
+  const data = new Uint8Array(len);
+  for (let i = 0; i < len; i++) {
+    data[i] = 48 + ((fieldId + i) % 73);
+  }
+  writer.bytes(data);
+  const res = writer.finish();
+  // For whatever reason the object returned by protobufjs' Writer cannot be
+  // directly .toEqual()-ed with Uint8Arrays.
+  const buf = new Uint8Array(res.length);
+  buf.set(res);
+  return buf;
+}
+
+test('ProtoRingBufferTest.Fastpath', () => {
+  const buf = new ProtoRingBuffer();
+
+  for (let rep = 0; rep < 3; rep++) {
+    let inputBuf = MakeProtoMessage(1, 32);
+    buf.append(inputBuf);
+    let msg = buf.readMessage();
+    expect(msg).toBeDefined();
+    expect(msg).toBeInstanceOf(Uint8Array);
+    expect(msg!.length).toBe(32);
+
+    // subarray(2) is to strip the proto preamble. The returned buffer starts at
+    // the start of the payload.
+    expect(msg).toEqual(inputBuf.subarray(2));
+
+    // When we hit the fastpath, the returned message should be a subarray of
+    // the same ArrayBuffer passed to append.
+    expect(msg!.buffer).toBe(inputBuf.buffer);
+
+    inputBuf = MakeProtoMessage(2, 32);
+    buf.append(inputBuf.subarray(0, 13));
+    expect(buf.readMessage()).toBeUndefined();
+    buf.append(inputBuf.subarray(13));
+    msg = buf.readMessage();
+    expect(msg).toBeDefined();
+    expect(msg).toBeInstanceOf(Uint8Array);
+    expect(msg).toEqual(inputBuf.subarray(2));
+    expect(msg!.buffer !== inputBuf.buffer).toBeTruthy();
+  }
+});
+
+test('ProtoRingBufferTest.CoalescingStream', () => {
+  const buf = new ProtoRingBuffer();
+
+  const mergedBuf = new Uint8Array(612);
+  const expected = new Array<Uint8Array>();
+  for (let i = 1, pos = 0; i <= 6; i++) {
+    const msg = MakeProtoMessage(i, 100);
+    expected.push(msg);
+    mergedBuf.set(msg, pos);
+    pos += msg.length;
+  }
+
+  const fragLens = [120, 20, 471, 1];
+  let fragSum = 0;
+  fragLens.map(fragLen => {
+    buf.append(mergedBuf.subarray(fragSum, fragSum + fragLen));
+    fragSum += fragLen;
+    for (;;) {
+      const msg = buf.readMessage();
+      if (msg === undefined) break;
+      const exp = expected.shift();
+      expect(exp).toBeDefined();
+      expect(msg).toEqual(exp!.subarray(-1 * msg.length));
+    }
+  });
+  expect(expected.length).toEqual(0);
+});
+
+
+test('ProtoRingBufferTest.RandomSizes', () => {
+  const buf = new ProtoRingBuffer();
+  const kNumMsg = 100;
+  const mergedBuf = new Uint8Array(1024 * 1024 * 32);
+  const expectedLengths = [];
+  let mergedLen = 0;
+  for (let i = 0; i < kNumMsg; i++) {
+    const fieldId = 1 + Rnd(15);  // We support only one byte tag.
+    const rndVal = Rnd(1024);
+    let len = 1 + rndVal;
+    if ((rndVal % 100) < 5) {
+      len *= 1000;
+    }
+    const msg = MakeProtoMessage(fieldId, len);
+    assertTrue(mergedBuf.length >= mergedLen + msg.length);
+    expectedLengths.push(len);
+    mergedBuf.set(msg, mergedLen);
+    mergedLen += msg.length;
+  }
+
+  for (let fragSum = 0; fragSum < mergedLen;) {
+      let fragLen = 1 + Rnd(1024 * 32);
+      fragLen = Math.min(fragLen, mergedLen - fragSum);
+      buf.append(mergedBuf.subarray(fragSum, fragSum + fragLen));
+      fragSum += fragLen;
+      for (;;) {
+        const msg = buf.readMessage();
+        if (msg === undefined) break;
+        const expLen = expectedLengths.shift();
+        expect(expLen).toBeDefined();
+        expect(msg.length).toEqual(expLen);
+      }
+    }
+  expect(expectedLengths.length).toEqual(0);
+});