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