Trace Processor for FrameTimeline events

This change adds the necessary tp implementation to parse Frame Timeline
events from SurfaceFlinger and store them into two tables namely,
ExpectedFrameTimelineSlice and ActualFrameTimelineSlice.

Bug: 173426914
Test: diff_test_trace_processory.py --trace-filter='frame_timeline*'

Change-Id: I77d6b7168828fc90eb633da88dc45232fac2d603
diff --git a/Android.bp b/Android.bp
index 8a69c11..0f6a95d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -7601,6 +7601,7 @@
     "src/trace_processor/importers/proto/android_probes_module.cc",
     "src/trace_processor/importers/proto/android_probes_parser.cc",
     "src/trace_processor/importers/proto/android_probes_tracker.cc",
+    "src/trace_processor/importers/proto/frame_timeline_event_parser.cc",
     "src/trace_processor/importers/proto/gpu_event_parser.cc",
     "src/trace_processor/importers/proto/graphics_event_module.cc",
     "src/trace_processor/importers/proto/graphics_frame_event_parser.cc",
diff --git a/BUILD b/BUILD
index 20db6b0..6189e95 100644
--- a/BUILD
+++ b/BUILD
@@ -1163,6 +1163,8 @@
         "src/trace_processor/importers/proto/android_probes_parser.h",
         "src/trace_processor/importers/proto/android_probes_tracker.cc",
         "src/trace_processor/importers/proto/android_probes_tracker.h",
+        "src/trace_processor/importers/proto/frame_timeline_event_parser.cc",
+        "src/trace_processor/importers/proto/frame_timeline_event_parser.h",
         "src/trace_processor/importers/proto/gpu_event_parser.cc",
         "src/trace_processor/importers/proto/gpu_event_parser.h",
         "src/trace_processor/importers/proto/graphics_event_module.cc",
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index e0c00c4..ea089c1 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -222,6 +222,8 @@
     "importers/proto/gpu_event_parser.h",
     "importers/proto/graphics_event_module.cc",
     "importers/proto/graphics_event_module.h",
+    "importers/proto/frame_timeline_event_parser.cc",
+    "importers/proto/frame_timeline_event_parser.h",
     "importers/proto/graphics_frame_event_parser.cc",
     "importers/proto/graphics_frame_event_parser.h",
     "importers/proto/heap_graph_module.cc",
diff --git a/src/trace_processor/importers/common/slice_tracker.cc b/src/trace_processor/importers/common/slice_tracker.cc
index 0a5ef04..c88e103 100644
--- a/src/trace_processor/importers/common/slice_tracker.cc
+++ b/src/trace_processor/importers/common/slice_tracker.cc
@@ -98,6 +98,34 @@
   });
 }
 
+void SliceTracker::BeginFrameTimeline(
+    tables::ExpectedFrameTimelineSliceTable::Row row,
+    SetArgsCallback args_callback) {
+  // Ensure that the duration is pending for this row.
+  // TODO(lalitm): change this to eventually use null instead of -1.
+  row.dur = kPendingDuration;
+
+  StartSlice(row.ts, row.track_id, args_callback, [this, &row]() {
+    return context_->storage->mutable_expected_frame_timeline_slice_table()
+        ->Insert(row)
+        .id;
+  });
+}
+
+void SliceTracker::BeginFrameTimeline(
+    tables::ActualFrameTimelineSliceTable::Row row,
+    SetArgsCallback args_callback) {
+  // Ensure that the duration is pending for this row.
+  // TODO(lalitm): change this to eventually use null instead of -1.
+  row.dur = kPendingDuration;
+
+  StartSlice(row.ts, row.track_id, args_callback, [this, &row]() {
+    return context_->storage->mutable_actual_frame_timeline_slice_table()
+        ->Insert(row)
+        .id;
+  });
+}
+
 base::Optional<uint32_t> SliceTracker::Scoped(int64_t timestamp,
                                               TrackId track_id,
                                               StringId category,
@@ -192,6 +220,15 @@
   });
 }
 
+base::Optional<SliceId> SliceTracker::EndFrameTimeline(
+    int64_t ts,
+    TrackId t_id,
+    SetArgsCallback args_callback) {
+  return CompleteSlice(ts, t_id, args_callback, [](const SlicesStack& stack) {
+    return static_cast<uint32_t>(stack.size() - 1);
+  });
+}
+
 base::Optional<uint32_t> SliceTracker::StartSlice(
     int64_t timestamp,
     TrackId track_id,
diff --git a/src/trace_processor/importers/common/slice_tracker.h b/src/trace_processor/importers/common/slice_tracker.h
index 197204a..fa0059c 100644
--- a/src/trace_processor/importers/common/slice_tracker.h
+++ b/src/trace_processor/importers/common/slice_tracker.h
@@ -58,6 +58,10 @@
 
   void BeginFrameEvent(tables::GraphicsFrameSliceTable::Row row,
                        SetArgsCallback args_callback = SetArgsCallback());
+  void BeginFrameTimeline(tables::ExpectedFrameTimelineSliceTable::Row row,
+                          SetArgsCallback args_callback = SetArgsCallback());
+  void BeginFrameTimeline(tables::ActualFrameTimelineSliceTable::Row row,
+                          SetArgsCallback args_callback = SetArgsCallback());
 
   // virtual for testing
   virtual base::Optional<uint32_t> Scoped(
@@ -102,6 +106,11 @@
       TrackId track_id,
       SetArgsCallback args_callback = SetArgsCallback());
 
+  base::Optional<SliceId> EndFrameTimeline(
+      int64_t ts,
+      TrackId track_id,
+      SetArgsCallback args_callback = SetArgsCallback());
+
   void FlushPendingSlices();
 
   void SetOnSliceBeginCallback(OnSliceBeginCallback callback);
diff --git a/src/trace_processor/importers/common/track_tracker.cc b/src/trace_processor/importers/common/track_tracker.cc
index 6a4eb03..a43bcac 100644
--- a/src/trace_processor/importers/common/track_tracker.cc
+++ b/src/trace_processor/importers/common/track_tracker.cc
@@ -142,6 +142,13 @@
   return id;
 }
 
+TrackId TrackTracker::CreateFrameTimelineAsyncTrack(StringId name,
+                                                    UniquePid upid) {
+  tables::ProcessTrackTable::Row row(name);
+  row.upid = upid;
+  return context_->storage->mutable_process_track_table()->Insert(row).id;
+}
+
 TrackId TrackTracker::InternLegacyChromeProcessInstantTrack(UniquePid upid) {
   auto it = chrome_process_instant_tracks_.find(upid);
   if (it != chrome_process_instant_tracks_.end())
diff --git a/src/trace_processor/importers/common/track_tracker.h b/src/trace_processor/importers/common/track_tracker.h
index f1462f9..502389a 100644
--- a/src/trace_processor/importers/common/track_tracker.h
+++ b/src/trace_processor/importers/common/track_tracker.h
@@ -55,6 +55,9 @@
   // Creates and inserts a Android async track into the storage.
   TrackId CreateAndroidAsyncTrack(StringId name, UniquePid upid);
 
+  // Creates and inserts a FrameTimeline async track into the storage.
+  TrackId CreateFrameTimelineAsyncTrack(StringId name, UniquePid upid);
+
   // Interns a track for legacy Chrome process-scoped instant events into the
   // storage.
   TrackId InternLegacyChromeProcessInstantTrack(UniquePid upid);
diff --git a/src/trace_processor/importers/proto/async_track_set_tracker.cc b/src/trace_processor/importers/proto/async_track_set_tracker.cc
index 5847d2d..9ad7a7c 100644
--- a/src/trace_processor/importers/proto/async_track_set_tracker.cc
+++ b/src/trace_processor/importers/proto/async_track_set_tracker.cc
@@ -46,6 +46,27 @@
   return id;
 }
 
+AsyncTrackSetTracker::TrackSetId AsyncTrackSetTracker::InternFrameTimelineSet(
+    UniquePid upid,
+    StringId name) {
+  FrameTimelineTuple tuple{upid, name};
+
+  auto it = frame_timeline_track_set_ids_.find(tuple);
+  if (it != frame_timeline_track_set_ids_.end())
+    return it->second;
+
+  uint32_t id = static_cast<uint32_t>(track_sets_.size());
+
+  TrackSet set;
+  set.frame_timeline_tuple = tuple;
+  set.type = TrackSetType::kFrameTimeline;
+  set.nesting_behaviour = NestingBehaviour::kUnnestable;
+  track_sets_.emplace_back(set);
+
+  frame_timeline_track_set_ids_[tuple] = id;
+  return id;
+}
+
 TrackId AsyncTrackSetTracker::Begin(TrackSetId id, int64_t cookie) {
   PERFETTO_DCHECK(id < track_sets_.size());
 
@@ -142,6 +163,9 @@
     case TrackSetType::kAndroid:
       return context_->track_tracker->CreateAndroidAsyncTrack(
           set.android_tuple.name, set.android_tuple.upid);
+    case TrackSetType::kFrameTimeline:
+      return context_->track_tracker->CreateFrameTimelineAsyncTrack(
+          set.frame_timeline_tuple.name, set.frame_timeline_tuple.upid);
   }
   PERFETTO_FATAL("For GCC");
 }
diff --git a/src/trace_processor/importers/proto/async_track_set_tracker.h b/src/trace_processor/importers/proto/async_track_set_tracker.h
index 469217d..526eb9a 100644
--- a/src/trace_processor/importers/proto/async_track_set_tracker.h
+++ b/src/trace_processor/importers/proto/async_track_set_tracker.h
@@ -68,6 +68,10 @@
   // Starts a new slice on the given async track set which has the given cookie.
   TrackId Begin(TrackSetId id, int64_t cookie);
 
+  // Interns the expected and actual timeline tracks coming from FrameTimeline
+  // producer for the associated upid.
+  TrackSetId InternFrameTimelineSet(UniquePid, StringId name);
+
   // Ends a new slice on the given async track set which has the given cookie.
   TrackId End(TrackSetId id, int64_t cookie);
 
@@ -90,6 +94,16 @@
     }
   };
 
+  struct FrameTimelineTuple {
+    UniquePid upid;
+    StringId name;
+
+    friend bool operator<(const FrameTimelineTuple& l,
+                          const FrameTimelineTuple& r) {
+      return std::tie(l.upid, l.name) < std::tie(r.upid, r.name);
+    }
+  };
+
   // Indicates the nesting behaviour of slices associated to a single slice
   // stack.
   enum class NestingBehaviour {
@@ -110,6 +124,7 @@
 
   enum class TrackSetType {
     kAndroid,
+    kFrameTimeline,
   };
 
   struct TrackState {
@@ -135,6 +150,8 @@
     union {
       // Only set when |type| == |TrackSetType::kAndroid|.
       AndroidTuple android_tuple;
+      // Only set when |type| == |TrackSetType::kFrameTimeline|.
+      FrameTimelineTuple frame_timeline_tuple;
     };
     NestingBehaviour nesting_behaviour;
     std::vector<TrackState> tracks;
@@ -160,6 +177,7 @@
   TrackId CreateTrackForSet(const TrackSet& set);
 
   std::map<AndroidTuple, TrackSetId> android_track_set_ids_;
+  std::map<FrameTimelineTuple, TrackSetId> frame_timeline_track_set_ids_;
   std::vector<TrackSet> track_sets_;
 
   TraceProcessorContext* const context_;
diff --git a/src/trace_processor/importers/proto/frame_timeline_event_parser.cc b/src/trace_processor/importers/proto/frame_timeline_event_parser.cc
new file mode 100644
index 0000000..e1c9082
--- /dev/null
+++ b/src/trace_processor/importers/proto/frame_timeline_event_parser.cc
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#include "src/trace_processor/importers/proto/frame_timeline_event_parser.h"
+
+#include <inttypes.h>
+
+#include "perfetto/ext/base/utils.h"
+#include "perfetto/protozero/field.h"
+#include "src/trace_processor/importers/common/args_tracker.h"
+#include "src/trace_processor/importers/common/event_tracker.h"
+#include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/slice_tracker.h"
+#include "src/trace_processor/importers/common/track_tracker.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+#include "protos/perfetto/trace/interned_data/interned_data.pbzero.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+using ExpectedDisplayFrameStartDecoder =
+    protos::pbzero::FrameTimelineEvent_ExpectedDisplayFrameStart_Decoder;
+using ActualDisplayFrameStartDecoder =
+    protos::pbzero::FrameTimelineEvent_ActualDisplayFrameStart_Decoder;
+
+using ExpectedSurfaceFrameStartDecoder =
+    protos::pbzero::FrameTimelineEvent_ExpectedSurfaceFrameStart_Decoder;
+using ActualSurfaceFrameStartDecoder =
+    protos::pbzero::FrameTimelineEvent_ActualSurfaceFrameStart_Decoder;
+
+using FrameEndDecoder = protos::pbzero::FrameTimelineEvent_FrameEnd_Decoder;
+
+FrameTimelineEventParser::FrameTimelineEventParser(
+    TraceProcessorContext* context)
+    : context_(context),
+      jank_type_ids_{
+          {context->storage->InternString(
+               "Unspecified Jank") /* JANK_UNSPECIFIED */,
+           context->storage->InternString("No Jank") /* JANK_NONE */,
+           context->storage->InternString(
+               "SurfaceFlinger Scheduling") /* JANK_SF_SCHEDULING */,
+           context->storage->InternString(
+               "Prediction Error") /* JANK_PREDICTION_ERROR */,
+           context->storage->InternString("Display HAL") /* JANK_DISPLAY_HAL */,
+           context->storage->InternString(
+               "SurfaceFlinger Deadline Missed") /*JANK_SF_DEADLINE_MISSED */,
+           context->storage->InternString(
+               "App Deadline Missed") /* JANK_APP_DEADLINE_MISSED */,
+           context->storage->InternString(
+               "Buffer Stuffing") /* JANK_BUFFER_STUFFING */,
+           context->storage->InternString("Unknown Jank") /* JANK_UNKNOWN */}},
+      present_type_ids_{
+          {context->storage->InternString(
+               "Unspecified Present") /* PRESENT_UNSPECIFIED */,
+           context->storage->InternString(
+               "On-time Present") /* PRESENT_ON_TIME */,
+           context->storage->InternString("Late Present") /* PRESENT_LATE */,
+           context->storage->InternString("Early Present") /* PRESENT_EARLY */,
+           context->storage->InternString(
+               "Dropped Frame") /* PRESENT_DROPPED */}},
+      expected_timeline_track_name_(
+          context->storage->InternString("Expected Timeline")),
+      actual_timeline_track_name_(
+          context->storage->InternString("Actual Timeline")) {}
+
+void FrameTimelineEventParser::ParseExpectedDisplayFrameStart(
+    int64_t timestamp,
+    ConstBytes bufferBlob) {
+  ExpectedDisplayFrameStartDecoder event(bufferBlob.data, bufferBlob.size);
+  if (!event.has_cookie()) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
+  if (!event.has_token()) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
+  if (!event.has_pid()) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
+  int64_t cookie = event.cookie();
+  int64_t token = event.token();
+  StringId name_id =
+      context_->storage->InternString(base::StringView(std::to_string(token)));
+
+  UniquePid upid = context_->process_tracker->GetOrCreateProcess(
+      static_cast<uint32_t>(event.pid()));
+  auto expected_track_set_id =
+      context_->async_track_set_tracker->InternFrameTimelineSet(
+          upid, expected_timeline_track_name_);
+  cookie_track_set_id_map_[cookie] = expected_track_set_id;
+
+  tables::ExpectedFrameTimelineSliceTable::Row expected_row;
+  expected_row.ts = timestamp;
+  expected_row.track_id =
+      context_->async_track_set_tracker->Begin(expected_track_set_id, cookie);
+  expected_row.name = name_id;
+
+  expected_row.display_frame_token = token;
+  expected_row.upid = upid;
+  context_->slice_tracker->BeginFrameTimeline(expected_row);
+}
+
+void FrameTimelineEventParser::ParseActualDisplayFrameStart(
+    int64_t timestamp,
+    ConstBytes bufferBlob) {
+  ActualDisplayFrameStartDecoder event(bufferBlob.data, bufferBlob.size);
+  if (!event.has_cookie()) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
+  if (!event.has_token()) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
+  if (!event.has_pid()) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
+  int64_t cookie = event.cookie();
+  int64_t token = event.token();
+  StringId name_id =
+      context_->storage->InternString(base::StringView(std::to_string(token)));
+
+  UniquePid upid = context_->process_tracker->GetOrCreateProcess(
+      static_cast<uint32_t>(event.pid()));
+  auto actual_track_set_id =
+      context_->async_track_set_tracker->InternFrameTimelineSet(
+          upid, actual_timeline_track_name_);
+  cookie_track_set_id_map_[cookie] = actual_track_set_id;
+
+  tables::ActualFrameTimelineSliceTable::Row actual_row;
+  actual_row.ts = timestamp;
+  actual_row.track_id =
+      context_->async_track_set_tracker->Begin(actual_track_set_id, cookie);
+  actual_row.name = name_id;
+  actual_row.display_frame_token = token;
+  actual_row.upid = upid;
+  actual_row.present_type =
+      present_type_ids_[static_cast<size_t>(event.present_type())];
+  actual_row.on_time_finish = event.on_time_finish();
+  actual_row.gpu_composition = event.gpu_composition();
+  actual_row.jank_type = jank_type_ids_[static_cast<size_t>(event.jank_type())];
+  context_->slice_tracker->BeginFrameTimeline(actual_row);
+}
+
+void FrameTimelineEventParser::ParseExpectedSurfaceFrameStart(
+    int64_t timestamp,
+    ConstBytes bufferBlob) {
+  ExpectedSurfaceFrameStartDecoder event(bufferBlob.data, bufferBlob.size);
+
+  if (!event.has_cookie()) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
+  if (!event.has_token()) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
+  if (!event.has_display_frame_token()) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
+  if (!event.has_pid()) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
+  int64_t cookie = event.cookie();
+  int64_t token = event.token();
+  int64_t display_frame_token = event.display_frame_token();
+  UniquePid upid = context_->process_tracker->GetOrCreateProcess(
+      static_cast<uint32_t>(event.pid()));
+  StringId layer_name_id = event.has_layer_name()
+                               ? context_->storage->InternString(
+                                     base::StringView(event.layer_name()))
+                               : kNullStringId;
+  StringId name_id =
+      context_->storage->InternString(base::StringView(std::to_string(token)));
+
+  auto expected_track_set_id =
+      context_->async_track_set_tracker->InternFrameTimelineSet(
+          upid, expected_timeline_track_name_);
+  cookie_track_set_id_map_[cookie] = expected_track_set_id;
+
+  tables::ExpectedFrameTimelineSliceTable::Row expected_row;
+  expected_row.ts = timestamp;
+  expected_row.track_id =
+      context_->async_track_set_tracker->Begin(expected_track_set_id, cookie);
+  expected_row.name = name_id;
+
+  expected_row.surface_frame_token = token;
+  expected_row.display_frame_token = display_frame_token;
+  expected_row.upid = upid;
+  expected_row.layer_name = layer_name_id;
+  context_->slice_tracker->BeginFrameTimeline(expected_row);
+}
+
+void FrameTimelineEventParser::ParseActualSurfaceFrameStart(
+    int64_t timestamp,
+    ConstBytes bufferBlob) {
+  ActualSurfaceFrameStartDecoder event(bufferBlob.data, bufferBlob.size);
+
+  if (!event.has_cookie()) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
+  if (!event.has_token()) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
+  if (!event.has_display_frame_token()) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
+  if (!event.has_pid()) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
+  int64_t cookie = event.cookie();
+  int64_t token = event.token();
+  int64_t display_frame_token = event.display_frame_token();
+  UniquePid upid = context_->process_tracker->GetOrCreateProcess(
+      static_cast<uint32_t>(event.pid()));
+  StringId layer_name_id;
+  if (event.has_layer_name())
+    layer_name_id =
+        context_->storage->InternString(base::StringView(event.layer_name()));
+  StringId name_id =
+      context_->storage->InternString(base::StringView(std::to_string(token)));
+
+  auto actual_track_set_id =
+      context_->async_track_set_tracker->InternFrameTimelineSet(
+          upid, actual_timeline_track_name_);
+  cookie_track_set_id_map_[cookie] = actual_track_set_id;
+
+  tables::ActualFrameTimelineSliceTable::Row actual_row;
+  actual_row.ts = timestamp;
+  actual_row.track_id =
+      context_->async_track_set_tracker->Begin(actual_track_set_id, cookie);
+  actual_row.name = name_id;
+  actual_row.surface_frame_token = token;
+  actual_row.display_frame_token = display_frame_token;
+  actual_row.upid = upid;
+  actual_row.layer_name = layer_name_id;
+  actual_row.present_type =
+      present_type_ids_[static_cast<size_t>(event.present_type())];
+  actual_row.on_time_finish = event.on_time_finish();
+  actual_row.gpu_composition = event.gpu_composition();
+  actual_row.jank_type = jank_type_ids_[static_cast<size_t>(event.jank_type())];
+  context_->slice_tracker->BeginFrameTimeline(actual_row);
+}
+
+void FrameTimelineEventParser::ParseFrameEnd(int64_t timestamp,
+                                             ConstBytes bufferBlob) {
+  FrameEndDecoder event(bufferBlob.data, bufferBlob.size);
+
+  if (!event.has_cookie()) {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+    return;
+  }
+
+  int64_t cookie = event.cookie();
+  auto it = cookie_track_set_id_map_.find(cookie);
+  if (it == cookie_track_set_id_map_.end())
+    return;
+  auto track_set_id = it->second;
+  auto track_id = context_->async_track_set_tracker->End(track_set_id, cookie);
+  context_->slice_tracker->EndFrameTimeline(timestamp, track_id);
+  cookie_track_set_id_map_.erase(it);
+}
+
+void FrameTimelineEventParser::ParseFrameTimelineEvent(int64_t timestamp,
+                                                       ConstBytes blob) {
+  protos::pbzero::FrameTimelineEvent_Decoder frame_event(blob.data, blob.size);
+  context_->storage->InternString(base::StringView(std::to_string(timestamp)));
+  if (frame_event.has_expected_display_frame_start()) {
+    ParseExpectedDisplayFrameStart(timestamp,
+                                   frame_event.expected_display_frame_start());
+  } else if (frame_event.has_actual_display_frame_start()) {
+    ParseActualDisplayFrameStart(timestamp,
+                                 frame_event.actual_display_frame_start());
+  } else if (frame_event.has_expected_surface_frame_start()) {
+    ParseExpectedSurfaceFrameStart(timestamp,
+                                   frame_event.expected_surface_frame_start());
+  } else if (frame_event.has_actual_surface_frame_start()) {
+    ParseActualSurfaceFrameStart(timestamp,
+                                 frame_event.actual_surface_frame_start());
+  } else if (frame_event.has_frame_end()) {
+    ParseFrameEnd(timestamp, frame_event.frame_end());
+  } else {
+    context_->storage->IncrementStats(
+        stats::frame_timeline_event_parser_errors);
+  }
+}
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/importers/proto/frame_timeline_event_parser.h b/src/trace_processor/importers/proto/frame_timeline_event_parser.h
new file mode 100644
index 0000000..73affb8
--- /dev/null
+++ b/src/trace_processor/importers/proto/frame_timeline_event_parser.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_FRAME_TIMELINE_EVENT_PARSER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_FRAME_TIMELINE_EVENT_PARSER_H_
+
+#include "perfetto/protozero/field.h"
+#include "src/trace_processor/importers/common/args_tracker.h"
+#include "src/trace_processor/importers/proto/async_track_set_tracker.h"
+#include "src/trace_processor/importers/proto/proto_incremental_state.h"
+#include "src/trace_processor/storage/trace_storage.h"
+
+#include "protos/perfetto/trace/android/frame_timeline_event.pbzero.h"
+
+namespace perfetto {
+
+namespace trace_processor {
+
+class TraceProcessorContext;
+
+// Class for parsing graphics frame related events.
+class FrameTimelineEventParser {
+ public:
+  using ConstBytes = protozero::ConstBytes;
+  using TrackSetId = AsyncTrackSetTracker::TrackSetId;
+  explicit FrameTimelineEventParser(TraceProcessorContext*);
+
+  void ParseFrameTimelineEvent(int64_t timestamp, ConstBytes);
+
+ private:
+  using FrameTimelineEvent = protos::pbzero::FrameTimelineEvent;
+  using FrameTimelineEventDecoder = protos::pbzero::FrameTimelineEvent_Decoder;
+  void ParseExpectedDisplayFrameStart(int64_t timestamp, ConstBytes);
+  void ParseActualDisplayFrameStart(int64_t timestamp, ConstBytes);
+
+  void ParseExpectedSurfaceFrameStart(int64_t timestamp, ConstBytes);
+  void ParseActualSurfaceFrameStart(int64_t timestamp, ConstBytes);
+
+  void ParseFrameEnd(int64_t timestamp, ConstBytes);
+
+  TraceProcessorContext* const context_;
+  // Cookie -> TrackSetId map. Since cookies are globally unique per slice, this
+  // helps in allowing the producer to send only the cookie as the End marker
+  // without the need for any other fields. The TrackSets could be interned
+  // based on any number of fields in the Start marker but the global uniqueness
+  // of the cookie makes it so that we can end a slice with just the cookie and
+  // the TrackSetId.
+  std::map<int64_t, TrackSetId> cookie_track_set_id_map_;
+  std::array<StringId, 9> jank_type_ids_;
+  std::array<StringId, 5> present_type_ids_;
+  StringId expected_timeline_track_name_;
+  StringId actual_timeline_track_name_;
+};
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_FRAME_TIMELINE_EVENT_PARSER_H_
diff --git a/src/trace_processor/importers/proto/graphics_event_module.cc b/src/trace_processor/importers/proto/graphics_event_module.cc
index 88b9381..46d8838 100644
--- a/src/trace_processor/importers/proto/graphics_event_module.cc
+++ b/src/trace_processor/importers/proto/graphics_event_module.cc
@@ -22,7 +22,10 @@
 using perfetto::protos::pbzero::TracePacket;
 
 GraphicsEventModule::GraphicsEventModule(TraceProcessorContext* context)
-    : parser_(context), frame_parser_(context) {
+    : parser_(context),
+      frame_parser_(context),
+      frame_timeline_parser_(context) {
+  RegisterForField(TracePacket::kFrameTimelineEventFieldNumber, context);
   RegisterForField(TracePacket::kGpuCounterEventFieldNumber, context);
   RegisterForField(TracePacket::kGpuRenderStageEventFieldNumber, context);
   RegisterForField(TracePacket::kGpuLogFieldNumber, context);
@@ -38,6 +41,10 @@
                                       const TimestampedTracePiece& ttp,
                                       uint32_t field_id) {
   switch (field_id) {
+    case TracePacket::kFrameTimelineEventFieldNumber:
+      frame_timeline_parser_.ParseFrameTimelineEvent(
+          ttp.timestamp, decoder.frame_timeline_event());
+      return;
     case TracePacket::kGpuCounterEventFieldNumber:
       parser_.ParseGpuCounterEvent(ttp.timestamp, decoder.gpu_counter_event());
       return;
diff --git a/src/trace_processor/importers/proto/graphics_event_module.h b/src/trace_processor/importers/proto/graphics_event_module.h
index f84b2c5..b550382 100644
--- a/src/trace_processor/importers/proto/graphics_event_module.h
+++ b/src/trace_processor/importers/proto/graphics_event_module.h
@@ -18,6 +18,7 @@
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_GRAPHICS_EVENT_MODULE_H_
 
 #include "perfetto/base/build_config.h"
+#include "src/trace_processor/importers/proto/frame_timeline_event_parser.h"
 #include "src/trace_processor/importers/proto/gpu_event_parser.h"
 #include "src/trace_processor/importers/proto/graphics_frame_event_parser.h"
 #include "src/trace_processor/importers/proto/proto_importer_module.h"
@@ -41,6 +42,7 @@
  private:
   GpuEventParser parser_;
   GraphicsFrameEventParser frame_parser_;
+  FrameTimelineEventParser frame_timeline_parser_;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/storage/stats.h b/src/trace_processor/storage/stats.h
index 845a541..9b58fea 100644
--- a/src/trace_processor/storage/stats.h
+++ b/src/trace_processor/storage/stats.h
@@ -30,8 +30,9 @@
   F(android_log_num_skipped,            kSingle,  kInfo,     kTrace,    ""),   \
   F(android_log_num_total,              kSingle,  kInfo,     kTrace,    ""),   \
   F(counter_events_out_of_order,        kSingle,  kError,    kAnalysis, ""),   \
-  F(ftrace_bundle_tokenizer_errors,     kSingle,  kError,    kAnalysis, ""),   \
   F(deobfuscate_location_parse_error,   kSingle,  kError,    kTrace,    ""),   \
+  F(frame_timeline_event_parser_errors, kSingle,  kInfo,     kAnalysis, ""),   \
+  F(ftrace_bundle_tokenizer_errors,     kSingle,  kError,    kAnalysis, ""),   \
   F(ftrace_cpu_bytes_read_begin,        kIndexed, kInfo,     kTrace,    ""),   \
   F(ftrace_cpu_bytes_read_end,          kIndexed, kInfo,     kTrace,    ""),   \
   F(ftrace_cpu_commit_overrun_begin,    kIndexed, kError,    kTrace,    ""),   \
diff --git a/src/trace_processor/storage/trace_storage.h b/src/trace_processor/storage/trace_storage.h
index 6c42178..026ea5c 100644
--- a/src/trace_processor/storage/trace_storage.h
+++ b/src/trace_processor/storage/trace_storage.h
@@ -652,6 +652,26 @@
     return &memory_snapshot_edge_table_;
   }
 
+  const tables::ExpectedFrameTimelineSliceTable&
+  expected_frame_timeline_slice_table() const {
+    return expected_frame_timeline_slice_table_;
+  }
+
+  tables::ExpectedFrameTimelineSliceTable*
+  mutable_expected_frame_timeline_slice_table() {
+    return &expected_frame_timeline_slice_table_;
+  }
+
+  const tables::ActualFrameTimelineSliceTable&
+  actual_frame_timeline_slice_table() const {
+    return actual_frame_timeline_slice_table_;
+  }
+
+  tables::ActualFrameTimelineSliceTable*
+  mutable_actual_frame_timeline_slice_table() {
+    return &actual_frame_timeline_slice_table_;
+  }
+
   const StringPool& string_pool() const { return string_pool_; }
   StringPool* mutable_string_pool() { return &string_pool_; }
 
@@ -862,6 +882,12 @@
   tables::MemorySnapshotEdgeTable memory_snapshot_edge_table_{&string_pool_,
                                                               nullptr};
 
+  // FrameTimeline tables
+  tables::ExpectedFrameTimelineSliceTable expected_frame_timeline_slice_table_{
+      &string_pool_, &slice_table_};
+  tables::ActualFrameTimelineSliceTable actual_frame_timeline_slice_table_{
+      &string_pool_, &slice_table_};
+
   // The below array allow us to map between enums and their string
   // representations.
   std::array<StringId, Variadic::kMaxType + 1> variadic_type_ids_;
diff --git a/src/trace_processor/tables/slice_tables.h b/src/trace_processor/tables/slice_tables.h
index a4bb330..f101fc6 100644
--- a/src/trace_processor/tables/slice_tables.h
+++ b/src/trace_processor/tables/slice_tables.h
@@ -123,6 +123,30 @@
 
 PERFETTO_TP_TABLE(PERFETTO_TP_DESCRIBE_SLICE_TABLE);
 
+#define PERFETTO_TP_EXPECTED_FRAME_TIMELINE_SLICES_DEF(NAME, PARENT, C)  \
+  NAME(ExpectedFrameTimelineSliceTable, "expected_frame_timeline_slice") \
+  PARENT(PERFETTO_TP_SLICE_TABLE_DEF, C)                                 \
+  C(int64_t, display_frame_token)                                        \
+  C(int64_t, surface_frame_token)                                        \
+  C(uint32_t, upid)                                                      \
+  C(StringPool::Id, layer_name)
+
+PERFETTO_TP_TABLE(PERFETTO_TP_EXPECTED_FRAME_TIMELINE_SLICES_DEF);
+
+#define PERFETTO_TP_ACTUAL_FRAME_TIMELINE_SLICES_DEF(NAME, PARENT, C) \
+  NAME(ActualFrameTimelineSliceTable, "actual_frame_timeline_slice")  \
+  PARENT(PERFETTO_TP_SLICE_TABLE_DEF, C)                              \
+  C(int64_t, display_frame_token)                                     \
+  C(int64_t, surface_frame_token)                                     \
+  C(uint32_t, upid)                                                   \
+  C(StringPool::Id, layer_name)                                       \
+  C(StringPool::Id, present_type)                                     \
+  C(int32_t, on_time_finish)                                          \
+  C(int32_t, gpu_composition)                                         \
+  C(StringPool::Id, jank_type)
+
+PERFETTO_TP_TABLE(PERFETTO_TP_ACTUAL_FRAME_TIMELINE_SLICES_DEF);
+
 }  // namespace tables
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/tables/table_destructors.cc b/src/trace_processor/tables/table_destructors.cc
index 1bc54b1..ac10a6e 100644
--- a/src/trace_processor/tables/table_destructors.cc
+++ b/src/trace_processor/tables/table_destructors.cc
@@ -73,6 +73,8 @@
 GraphicsFrameSliceTable::~GraphicsFrameSliceTable() = default;
 DescribeSliceTable::~DescribeSliceTable() = default;
 ThreadStateTable::~ThreadStateTable() = default;
+ExpectedFrameTimelineSliceTable::~ExpectedFrameTimelineSliceTable() = default;
+ActualFrameTimelineSliceTable::~ActualFrameTimelineSliceTable() = default;
 
 // track_tables.h
 TrackTable::~TrackTable() = default;
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index b757fba..51e548d 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -809,6 +809,9 @@
 
   RegisterDbTable(storage->graphics_frame_slice_table());
 
+  RegisterDbTable(storage->expected_frame_timeline_slice_table());
+  RegisterDbTable(storage->actual_frame_timeline_slice_table());
+
   RegisterDbTable(storage->metadata_table());
   RegisterDbTable(storage->cpu_table());
   RegisterDbTable(storage->cpu_freq_table());
diff --git a/test/synth_common.py b/test/synth_common.py
index 2ed9c51..222a338 100644
--- a/test/synth_common.py
+++ b/test/synth_common.py
@@ -618,6 +618,59 @@
 
     return metadata
 
+  def add_expected_display_frame_start_event(self, ts, cookie, token, pid):
+    packet = self.add_packet()
+    packet.timestamp = ts
+    event = packet.frame_timeline_event.expected_display_frame_start
+    if token != -1:
+      event.cookie = cookie
+      event.token = token
+      event.pid = pid
+
+  def add_actual_display_frame_start_event(self, ts, cookie, token, pid, present_type, on_time_finish, gpu_composition, jank_type):
+    packet = self.add_packet()
+    packet.timestamp = ts
+    event = packet.frame_timeline_event.actual_display_frame_start
+    if token != -1:
+      event.cookie = cookie
+      event.token = token
+      event.pid = pid
+      event.present_type = present_type
+      event.on_time_finish = on_time_finish
+      event.gpu_composition = gpu_composition
+      event.jank_type = jank_type
+
+  def add_expected_surface_frame_start_event(self, ts, cookie, token, display_frame_token, pid, layer_name):
+    packet = self.add_packet()
+    packet.timestamp = ts
+    event = packet.frame_timeline_event.expected_surface_frame_start
+    if token != -1 and display_frame_token != -1:
+      event.cookie = cookie
+      event.token = token
+      event.display_frame_token = display_frame_token
+      event.pid = pid
+      event.layer_name = layer_name
+
+  def add_actual_surface_frame_start_event(self, ts, cookie, token, display_frame_token, pid, layer_name, present_type, on_time_finish, gpu_composition, jank_type):
+    packet = self.add_packet()
+    packet.timestamp = ts
+    event = packet.frame_timeline_event.actual_surface_frame_start
+    if token != -1 and display_frame_token != -1:
+      event.cookie = cookie
+      event.token = token
+      event.display_frame_token = display_frame_token
+      event.pid = pid
+      event.layer_name = layer_name
+      event.present_type = present_type
+      event.on_time_finish = on_time_finish
+      event.gpu_composition = gpu_composition
+      event.jank_type = jank_type
+
+  def add_frame_end_event(self, ts, cookie):
+    packet = self.add_packet()
+    packet.timestamp = ts
+    event = packet.frame_timeline_event.frame_end
+    event.cookie = cookie
 
 def create_trace():
   parser = argparse.ArgumentParser()
diff --git a/test/trace_processor/graphics/actual_frame_timeline_events.out b/test/trace_processor/graphics/actual_frame_timeline_events.out
new file mode 100644
index 0000000..0776aa8
--- /dev/null
+++ b/test/trace_processor/graphics/actual_frame_timeline_events.out
@@ -0,0 +1,8 @@
+"ts","dur","pid","display_frame_token","surface_frame_token","layer_name","present_type","on_time_finish","gpu_composition","jank_type"
+20,6,666,2,0,"[NULL]","On-time Present",1,0,"No Jank"
+21,16,1000,4,1,"Layer1","On-time Present",1,0,"No Jank"
+41,33,1000,6,5,"Layer1","Late Present",0,0,"App Deadline Missed"
+42,5,666,4,0,"[NULL]","On-time Present",1,0,"No Jank"
+81,7,666,6,0,"[NULL]","On-time Present",1,0,"No Jank"
+90,16,1000,8,7,"Layer1","Early Present",1,0,"SurfaceFlinger Scheduling"
+108,4,666,8,0,"[NULL]","Early Present",1,0,"SurfaceFlinger Scheduling"
diff --git a/test/trace_processor/graphics/actual_frame_timeline_events.sql b/test/trace_processor/graphics/actual_frame_timeline_events.sql
new file mode 100644
index 0000000..dfdd719
--- /dev/null
+++ b/test/trace_processor/graphics/actual_frame_timeline_events.sql
@@ -0,0 +1,22 @@
+--
+-- Copyright 2020 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
+--
+--     https://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.
+
+select ts, dur, process.pid as pid, display_frame_token, surface_frame_token, layer_name,
+    present_type, on_time_finish, gpu_composition, jank_type
+from actual_frame_timeline_slice
+join process_track on process_track.upid = actual_frame_timeline_slice.upid
+join process on process_track.upid = process.upid
+where process_track.name = 'Actual Timeline'
+order by ts
diff --git a/test/trace_processor/graphics/expected_frame_timeline_events.out b/test/trace_processor/graphics/expected_frame_timeline_events.out
new file mode 100644
index 0000000..d695295
--- /dev/null
+++ b/test/trace_processor/graphics/expected_frame_timeline_events.out
@@ -0,0 +1,8 @@
+"ts","dur","pid","display_frame_token","surface_frame_token","layer_name"
+20,6,666,2,0,"[NULL]"
+21,15,1000,4,1,"Layer1"
+40,6,666,4,0,"[NULL]"
+41,15,1000,6,5,"Layer1"
+80,6,666,6,0,"[NULL]"
+90,16,1000,8,7,"Layer1"
+120,6,666,8,0,"[NULL]"
diff --git a/test/trace_processor/graphics/expected_frame_timeline_events.sql b/test/trace_processor/graphics/expected_frame_timeline_events.sql
new file mode 100644
index 0000000..937254d
--- /dev/null
+++ b/test/trace_processor/graphics/expected_frame_timeline_events.sql
@@ -0,0 +1,21 @@
+--
+-- Copyright 2020 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
+--
+--     https://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.
+
+select ts, dur, process.pid as pid, display_frame_token, surface_frame_token, layer_name
+from expected_frame_timeline_slice
+join process_track on process_track.upid = expected_frame_timeline_slice.upid
+join process on process_track.upid = process.upid
+where process_track.name = 'Expected Timeline'
+order by ts
diff --git a/test/trace_processor/graphics/frame_timeline_events.py b/test/trace_processor/graphics/frame_timeline_events.py
new file mode 100644
index 0000000..78496b9
--- /dev/null
+++ b/test/trace_processor/graphics/frame_timeline_events.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 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.
+
+from os import sys, path
+
+import synth_common
+
+class JankType:
+  JANK_UNSPECIFIED = 0;
+  JANK_NONE = 1;
+  JANK_SF_SCHEDULING = 2;
+  JANK_PREDICTION_ERROR = 3;
+  JANK_DISPLAY_HAL = 4;
+  JANK_SF_DEADLINE_MISSED = 5;
+  JANK_APP_DEADLINE_MISSED = 6;
+  JANK_BUFFER_STUFFING = 7;
+  JANK_UNKNOWN = 8;
+
+class PresentType:
+  PRESENT_UNSPECIFIED = 0;
+  PRESENT_ON_TIME = 1;
+  PRESENT_LATE = 2;
+  PRESENT_EARLY = 3;
+  PRESENT_DROPPED = 4;
+
+trace = synth_common.create_trace()
+
+# DisplayFrame without a SurfaceFrame
+trace.add_expected_display_frame_start_event(ts=20, cookie=1, token=2, pid=666)
+trace.add_frame_end_event(ts=26, cookie=1)
+trace.add_actual_display_frame_start_event(ts=20, cookie=2, token=2, pid=666, present_type=PresentType.PRESENT_ON_TIME, on_time_finish=1, gpu_composition=0, jank_type=JankType.JANK_NONE)
+trace.add_frame_end_event(ts=26, cookie=2)
+
+# DisplayFrame with a SurfaceFrame
+trace.add_expected_display_frame_start_event(ts=40, cookie=3, token=4, pid=666)
+trace.add_frame_end_event(ts=46, cookie=3)
+trace.add_actual_display_frame_start_event(ts=42, cookie=4, token=4, pid=666, present_type=PresentType.PRESENT_ON_TIME, on_time_finish=1, gpu_composition=0, jank_type=JankType.JANK_NONE)
+trace.add_frame_end_event(ts=47, cookie=4)
+trace.add_expected_surface_frame_start_event(ts=21, cookie=5, token=1, display_frame_token=4, pid=1000, layer_name="Layer1")
+trace.add_frame_end_event(ts=36, cookie=5)
+trace.add_actual_surface_frame_start_event(ts=21, cookie=6, token=1, display_frame_token=4, pid=1000, layer_name="Layer1", present_type=PresentType.PRESENT_ON_TIME, on_time_finish=1, gpu_composition=0, jank_type=JankType.JANK_NONE)
+trace.add_frame_end_event(ts=37, cookie=6)
+
+
+# DisplayFrame with a janky SurfaceFrame
+trace.add_expected_display_frame_start_event(ts=80, cookie=7, token=6, pid=666)
+trace.add_frame_end_event(ts=86, cookie=7)
+trace.add_actual_display_frame_start_event(ts=81, cookie=8, token=6, pid=666, present_type=PresentType.PRESENT_ON_TIME, on_time_finish=1, gpu_composition=0, jank_type=JankType.JANK_NONE)
+trace.add_frame_end_event(ts=88, cookie=8)
+trace.add_expected_surface_frame_start_event(ts=41, cookie=9, token=5, display_frame_token=6, pid=1000, layer_name="Layer1")
+trace.add_frame_end_event(ts=56, cookie=9)
+trace.add_actual_surface_frame_start_event(ts=41, cookie=10, token=5, display_frame_token=6, pid=1000, layer_name="Layer1", present_type=PresentType.PRESENT_LATE, on_time_finish=0, gpu_composition=0, jank_type=JankType.JANK_APP_DEADLINE_MISSED)
+trace.add_frame_end_event(ts=74, cookie=10)
+
+
+# Janky DisplayFrame with a SurfaceFrame
+trace.add_expected_display_frame_start_event(ts=120, cookie=11, token=8, pid=666)
+trace.add_frame_end_event(ts=126, cookie=11)
+trace.add_actual_display_frame_start_event(ts=108, cookie=12, token=8, pid=666, present_type=PresentType.PRESENT_EARLY, on_time_finish=1, gpu_composition=0, jank_type=JankType.JANK_SF_SCHEDULING)
+trace.add_frame_end_event(ts=112, cookie=12)
+trace.add_expected_surface_frame_start_event(ts=90, cookie=13, token=7, display_frame_token=8, pid=1000, layer_name="Layer1")
+trace.add_frame_end_event(ts=106, cookie=13)
+trace.add_actual_surface_frame_start_event(ts=90, cookie=14, token=7, display_frame_token=8, pid=1000, layer_name="Layer1", present_type=PresentType.PRESENT_EARLY, on_time_finish=1, gpu_composition=0, jank_type=JankType.JANK_SF_SCHEDULING)
+trace.add_frame_end_event(ts=106, cookie=14)
+
+sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/test/trace_processor/graphics/index b/test/trace_processor/graphics/index
index f830cd9..c6e21c0 100644
--- a/test/trace_processor/graphics/index
+++ b/test/trace_processor/graphics/index
@@ -25,3 +25,7 @@
 
 # Android SysUI CUJs metrics
 android_sysui_cuj.py android_sysui_cuj android_sysui_cuj.out
+
+# Frame Timeline event trace tests
+frame_timeline_events.py expected_frame_timeline_events.sql expected_frame_timeline_events.out
+frame_timeline_events.py actual_frame_timeline_events.sql actual_frame_timeline_events.out