Merge "traced_probes: Use libstatspull to collect statsd atoms"
diff --git a/Android.bp b/Android.bp
index 336e0e8..549ea74 100644
--- a/Android.bp
+++ b/Android.bp
@@ -9923,6 +9923,7 @@
"src/trace_processor/prelude/functions/create_function_internal.cc",
"src/trace_processor/prelude/functions/create_view_function.cc",
"src/trace_processor/prelude/functions/import.cc",
+ "src/trace_processor/prelude/functions/layout_functions.cc",
"src/trace_processor/prelude/functions/pprof_functions.cc",
"src/trace_processor/prelude/functions/register_function.cc",
"src/trace_processor/prelude/functions/sqlite3_str_split.cc",
diff --git a/BUILD b/BUILD
index 692c44d..c9bdd52 100644
--- a/BUILD
+++ b/BUILD
@@ -1767,6 +1767,8 @@
"src/trace_processor/prelude/functions/create_view_function.h",
"src/trace_processor/prelude/functions/import.cc",
"src/trace_processor/prelude/functions/import.h",
+ "src/trace_processor/prelude/functions/layout_functions.cc",
+ "src/trace_processor/prelude/functions/layout_functions.h",
"src/trace_processor/prelude/functions/pprof_functions.cc",
"src/trace_processor/prelude/functions/pprof_functions.h",
"src/trace_processor/prelude/functions/register_function.cc",
diff --git a/CHANGELOG b/CHANGELOG
index 8194fa8..7662692 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -7,7 +7,15 @@
UI:
*
SDK:
- *
+ * Changed the type of the static constexpr metadata on protozero
+ generated bindings from a function returning the metadata to
+ metadata itself. For a field 'foo' the variable kFoo previously
+ defined as:
+ `static constexpr FieldMetadata_Foo kFoo() = { return {}; };`
+ it is now defined as:
+ `static constexpr FieldMetadata_Foo kFoo;`
+ This is a breaking change for users who directly access field
+ metadata.
v33.0 - 2023-03-02:
All:
diff --git a/docs/visualization/perfetto-ui.md b/docs/visualization/perfetto-ui.md
index 90d5899..5f094b6 100644
--- a/docs/visualization/perfetto-ui.md
+++ b/docs/visualization/perfetto-ui.md
@@ -6,72 +6,6 @@
## UI Tips and Tricks
-### Debug Slices
-
-Sometimes you may want to insert some fake slices into the timeline to help
-with your understanding of the data. You can do so by inserting rows into a
-magic `debug_slices` table.
-
-`debug_slices` table has five columns:
-
-* `id` (integer) [optional] If present, Perfetto UI will use it as slice id to
- open the details panel when you click on the slices.
-* `name` (string) [optional] The displayed slice title.
-* `ts` (integer) [required] Start of the slice, in nanoseconds.
-* `dur` (integer) [required] Duration of the slice, in nanoseconds. Determines
- slice width.
-* `depth` (integer) [optional] The row at which the slice is drawn. Depth 0 is
- the first row.
-
-You can open the debug track by going to the "Sample queries" menu on the
-left, and clicking "Show Debug Track". A debug slice track will become pinned to
-the top and will initially be empty. After you insert slices in the
-`debug_slices` table, you can click the reload button on the track to refresh
-the information shown in that track.
-
-Here is a simple example with random slices to illustrate the use:
-
-```sql
-CREATE VIEW rand_slices AS SELECT * FROM SLICE
- ORDER BY RANDOM() LIMIT 2000;
-
-INSERT INTO debug_slices(id, name, ts, dur, depth)
- SELECT id, name, ts, dur, depth FROM RAND_SLICES;
-```
-
-After you click the reload button, you should see the slices in the debug track.
-
-![Example of debug slices](/docs/images/debug-slices-random.png)
-
-Once you're done, you can click the X button to hide the track, and you can
-clear the `debug_slices` table (`DELETE FROM debug_slices`) to clear the track.
-
-A more interesting example is seeing RAIL modes in chrome traces:
-
-```sql
-SELECT RUN_METRIC('chrome/rail_modes.sql');
-
--- Depth 0 is the unified RAIL Mode
-INSERT INTO debug_slices
- SELECT NULL, rail_mode, ts, dur, 0 FROM combined_overall_rail_slices;
-
--- Depth 2+ are for each Renderer process with depth 1 left blank
-INSERT INTO debug_slices
- SELECT NULL, short_name, ts, dur, depth + 1 FROM rail_mode_slices,
- (SELECT track_id, row_number() OVER () AS depth FROM
- (SELECT DISTINCT track_id FROM rail_mode_slices)) depth_map,
- rail_modes
- WHERE depth_map.track_id = rail_mode_slices.track_id
- AND rail_mode=rail_modes.mode;
-```
-
-This produces a visualization like this:
-
-![RAIL modes in Debug Track](/docs/images/rail-mode-debug-slices.png)
-
-Note: There is no equivalent debug counters feature yet, but the feature request
-is tracked on [b/168886909](http://b/168886909)).
-
### Pivot Tables
To use pivot tables in the Perfetto UI, you will need to enable the
diff --git a/include/perfetto/ext/base/subprocess.h b/include/perfetto/ext/base/subprocess.h
index cca6c29..6102832 100644
--- a/include/perfetto/ext/base/subprocess.h
+++ b/include/perfetto/ext/base/subprocess.h
@@ -269,7 +269,6 @@
void TryPushStdin();
void TryReadStdoutAndErr();
void TryReadExitStatus();
- void KillAtMostOnce();
bool PollInternal(int poll_timeout_ms);
#endif
diff --git a/include/perfetto/ext/ipc/host.h b/include/perfetto/ext/ipc/host.h
index 0311089..e0f8af4 100644
--- a/include/perfetto/ext/ipc/host.h
+++ b/include/perfetto/ext/ipc/host.h
@@ -72,6 +72,9 @@
virtual void AdoptConnectedSocket_Fuchsia(
base::ScopedSocketHandle,
std::function<bool(int)> send_fd_cb) = 0;
+
+ // Overrides the default send timeout for the per-connection sockets.
+ virtual void SetSocketSendTimeoutMs(uint32_t timeout_ms) = 0;
};
} // namespace ipc
diff --git a/include/perfetto/protozero/proto_utils.h b/include/perfetto/protozero/proto_utils.h
index a0cc7bf..cea6ab3 100644
--- a/include/perfetto/protozero/proto_utils.h
+++ b/include/perfetto/protozero/proto_utils.h
@@ -289,21 +289,6 @@
using message_type = MessageType;
};
-namespace internal {
-
-// Ideally we would create variables of FieldMetadata<...> type directly,
-// but before C++17's support for constexpr inline variables arrive, we have to
-// actually use pointers to inline functions instead to avoid having to define
-// symbols in *.pbzero.cc files.
-//
-// Note: protozero bindings will generate Message::kFieldName variable and which
-// can then be passed to TRACE_EVENT macro for inline writing of typed messages.
-// The fact that the former can be passed to the latter is a part of the stable
-// API, while the particular type is not and users should not rely on it.
-template <typename T>
-using FieldMetadataHelper = T (*)(void);
-
-} // namespace internal
} // namespace proto_utils
} // namespace protozero
diff --git a/include/perfetto/tracing/internal/track_event_data_source.h b/include/perfetto/tracing/internal/track_event_data_source.h
index 0a636ef..d1e17a6 100644
--- a/include/perfetto/tracing/internal/track_event_data_source.h
+++ b/include/perfetto/tracing/internal/track_event_data_source.h
@@ -661,6 +661,86 @@
typename TimestampTypeCheck = typename std::enable_if<
IsValidTimestamp<TimestampType>()>::type,
typename TrackTypeCheck =
+ typename std::enable_if<IsValidTrack<TrackType>()>::type>
+ static perfetto::EventContext WriteTrackEvent(
+ typename Base::TraceContext& ctx,
+ const CategoryType& category,
+ const EventNameType& event_name,
+ perfetto::protos::pbzero::TrackEvent::Type type,
+ const TrackType& track,
+ const TimestampType& timestamp) PERFETTO_NO_INLINE {
+ using CatTraits = CategoryTraits<CategoryType>;
+ const Category* static_category =
+ CatTraits::GetStaticCategory(Registry, category);
+
+ const TrackEventTlsState& tls_state = *ctx.GetCustomTlsState();
+ TraceTimestamp trace_timestamp = ::perfetto::TraceTimestampTraits<
+ TimestampType>::ConvertTimestampToTraceTimeNs(timestamp);
+
+ TraceWriterBase* trace_writer = ctx.tls_inst_->trace_writer.get();
+ // Make sure incremental state is valid.
+ TrackEventIncrementalState* incr_state = ctx.GetIncrementalState();
+ TrackEventInternal::ResetIncrementalStateIfRequired(
+ trace_writer, incr_state, tls_state, trace_timestamp);
+
+ // Write the track descriptor before any event on the track.
+ if (track) {
+ TrackEventInternal::WriteTrackDescriptorIfNeeded(
+ track, trace_writer, incr_state, tls_state, trace_timestamp);
+ }
+
+ // Write the event itself.
+ bool on_current_thread_track =
+ (&track == &TrackEventInternal::kDefaultTrack);
+ auto event_ctx = TrackEventInternal::WriteEvent(
+ trace_writer, incr_state, tls_state, static_category, type,
+ trace_timestamp, on_current_thread_track);
+ // event name should be emitted with `TRACE_EVENT_BEGIN` macros
+ // but not with `TRACE_EVENT_END`.
+ if (type != protos::pbzero::TrackEvent::TYPE_SLICE_END) {
+ TrackEventInternal::WriteEventName(event_name, event_ctx, tls_state);
+ }
+ // Write dynamic categories (except for events that don't require
+ // categories). For counter events, the counter name (and optional
+ // category) is stored as part of the track descriptor instead being
+ // recorded with individual events.
+ if (CatTraits::kIsDynamic &&
+ type != protos::pbzero::TrackEvent::TYPE_SLICE_END &&
+ type != protos::pbzero::TrackEvent::TYPE_COUNTER) {
+ DynamicCategory dynamic_category =
+ CatTraits::GetDynamicCategory(category);
+ Category cat = Category::FromDynamicCategory(dynamic_category);
+ cat.ForEachGroupMember([&](const char* member_name, size_t name_size) {
+ event_ctx.event()->add_categories(member_name, name_size);
+ return true;
+ });
+ }
+ if (type == protos::pbzero::TrackEvent::TYPE_UNSPECIFIED) {
+ // Explicitly clear the track, so that the event is not associated
+ // with the default track, but instead uses the legacy mechanism
+ // based on the phase and pid/tid override.
+ event_ctx.event()->set_track_uuid(0);
+ } else if (!on_current_thread_track) {
+ // We emit these events using TrackDescriptors, and we cannot emit
+ // events on behalf of other processes using the TrackDescriptor
+ // format. Chrome is the only user of events with explicit process
+ // ids and currently only Chrome emits PHASE_MEMORY_DUMP events
+ // with an explicit process id, so we should be fine here.
+ // TODO(mohitms): Get rid of events with explicit process ids
+ // entirely.
+ event_ctx.event()->set_track_uuid(track.uuid);
+ }
+
+ return event_ctx;
+ }
+
+ template <typename CategoryType,
+ typename EventNameType,
+ typename TrackType = Track,
+ typename TimestampType = uint64_t,
+ typename TimestampTypeCheck = typename std::enable_if<
+ IsValidTimestamp<TimestampType>()>::type,
+ typename TrackTypeCheck =
typename std::enable_if<IsValidTrack<TrackType>()>::type,
typename... Arguments>
static void TraceForCategoryImpl(
@@ -672,8 +752,6 @@
const TimestampType& timestamp,
Arguments&&... args) PERFETTO_ALWAYS_INLINE {
using CatTraits = CategoryTraits<CategoryType>;
- const Category* static_category =
- CatTraits::GetStaticCategory(Registry, category);
TraceWithInstances(
instances, category, [&](typename Base::TraceContext ctx) {
// If this category is dynamic, first check whether it's enabled.
@@ -683,69 +761,10 @@
return;
}
- const TrackEventTlsState& tls_state = *ctx.GetCustomTlsState();
- TraceTimestamp trace_timestamp = ::perfetto::TraceTimestampTraits<
- TimestampType>::ConvertTimestampToTraceTimeNs(timestamp);
-
- TraceWriterBase* trace_writer = ctx.tls_inst_->trace_writer.get();
- // Make sure incremental state is valid.
- TrackEventIncrementalState* incr_state = ctx.GetIncrementalState();
- TrackEventInternal::ResetIncrementalStateIfRequired(
- trace_writer, incr_state, tls_state, trace_timestamp);
-
- // Write the track descriptor before any event on the track.
- if (track) {
- TrackEventInternal::WriteTrackDescriptorIfNeeded(
- track, trace_writer, incr_state, tls_state, trace_timestamp);
- }
-
- // Write the event itself.
- {
- bool on_current_thread_track =
- (&track == &TrackEventInternal::kDefaultTrack);
- auto event_ctx = TrackEventInternal::WriteEvent(
- trace_writer, incr_state, tls_state, static_category, type,
- trace_timestamp, on_current_thread_track);
- // event name should be emitted with `TRACE_EVENT_BEGIN` macros
- // but not with `TRACE_EVENT_END`.
- if (type != protos::pbzero::TrackEvent::TYPE_SLICE_END) {
- TrackEventInternal::WriteEventName(event_name, event_ctx,
- tls_state);
- }
- // Write dynamic categories (except for events that don't require
- // categories). For counter events, the counter name (and optional
- // category) is stored as part of the track descriptor instead being
- // recorded with individual events.
- if (CatTraits::kIsDynamic &&
- type != protos::pbzero::TrackEvent::TYPE_SLICE_END &&
- type != protos::pbzero::TrackEvent::TYPE_COUNTER) {
- DynamicCategory dynamic_category =
- CatTraits::GetDynamicCategory(category);
- Category cat = Category::FromDynamicCategory(dynamic_category);
- cat.ForEachGroupMember(
- [&](const char* member_name, size_t name_size) {
- event_ctx.event()->add_categories(member_name, name_size);
- return true;
- });
- }
- if (type == protos::pbzero::TrackEvent::TYPE_UNSPECIFIED) {
- // Explicitly clear the track, so that the event is not associated
- // with the default track, but instead uses the legacy mechanism
- // based on the phase and pid/tid override.
- event_ctx.event()->set_track_uuid(0);
- } else if (!on_current_thread_track) {
- // We emit these events using TrackDescriptors, and we cannot emit
- // events on behalf of other processes using the TrackDescriptor
- // format. Chrome is the only user of events with explicit process
- // ids and currently only Chrome emits PHASE_MEMORY_DUMP events
- // with an explicit process id, so we should be fine here.
- // TODO(mohitms): Get rid of events with explicit process ids
- // entirely.
- event_ctx.event()->set_track_uuid(track.uuid);
- }
- WriteTrackEventArgs(std::move(event_ctx),
- std::forward<Arguments>(args)...);
- } // event_ctx
+ auto event_ctx = WriteTrackEvent(ctx, category, event_name, type,
+ track, timestamp);
+ WriteTrackEventArgs(std::move(event_ctx),
+ std::forward<Arguments>(args)...);
});
}
diff --git a/include/perfetto/tracing/internal/track_event_legacy.h b/include/perfetto/tracing/internal/track_event_legacy.h
index 95b40ca..2ad4813 100644
--- a/include/perfetto/tracing/internal/track_event_legacy.h
+++ b/include/perfetto/tracing/internal/track_event_legacy.h
@@ -17,6 +17,11 @@
#ifndef INCLUDE_PERFETTO_TRACING_INTERNAL_TRACK_EVENT_LEGACY_H_
#define INCLUDE_PERFETTO_TRACING_INTERNAL_TRACK_EVENT_LEGACY_H_
+#include "perfetto/base/build_config.h"
+#include "perfetto/tracing/event_context.h"
+#include "perfetto/tracing/track.h"
+#include "protos/perfetto/trace/track_event/track_event.pbzero.h"
+
#ifndef PERFETTO_ENABLE_LEGACY_TRACE_EVENTS
#define PERFETTO_ENABLE_LEGACY_TRACE_EVENTS 0
#endif
diff --git a/include/perfetto/tracing/internal/write_track_event_args.h b/include/perfetto/tracing/internal/write_track_event_args.h
index 7d2aca9..fe5d149 100644
--- a/include/perfetto/tracing/internal/write_track_event_args.h
+++ b/include/perfetto/tracing/internal/write_track_event_args.h
@@ -73,6 +73,24 @@
return IsValidTraceLambdaTakingReferenceImpl<T>(nullptr);
}
+template <typename T>
+static constexpr bool IsFieldMetadataTypeImpl(
+ typename std::enable_if<
+ std::is_base_of<protozero::proto_utils::FieldMetadataBase,
+ T>::value>::type* = nullptr) {
+ return true;
+}
+
+template <typename T>
+static constexpr bool IsFieldMetadataTypeImpl(...) {
+ return false;
+}
+
+template <typename T>
+static constexpr bool IsFieldMetadataType() {
+ return IsFieldMetadataTypeImpl<T>(nullptr);
+}
+
} // namespace
// Write an old-style lambda taking an EventContext (without a reference)
@@ -95,13 +113,15 @@
ArgValue&& arg_value,
Args&&... args);
-template <typename FieldMetadataType, typename ArgValue, typename... Args>
-PERFETTO_ALWAYS_INLINE void WriteTrackEventArgs(
- EventContext event_ctx,
- protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadataType>
- field_name,
- ArgValue&& arg_value,
- Args&&... args);
+template <typename FieldMetadataType,
+ typename ArgValue,
+ typename... Args,
+ typename FieldMetadataTypeCheck = typename std::enable_if<
+ IsFieldMetadataType<FieldMetadataType>()>::type>
+PERFETTO_ALWAYS_INLINE void WriteTrackEventArgs(EventContext event_ctx,
+ FieldMetadataType field_name,
+ ArgValue&& arg_value,
+ Args&&... args);
template <typename ArgumentFunction,
typename... Args,
@@ -119,13 +139,14 @@
}
// Write one typed message and recursively write the rest of the arguments.
-template <typename FieldMetadataType, typename ArgValue, typename... Args>
-PERFETTO_ALWAYS_INLINE void WriteTrackEventArgs(
- EventContext event_ctx,
- protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadataType>
- field_name,
- ArgValue&& arg_value,
- Args&&... args) {
+template <typename FieldMetadataType,
+ typename ArgValue,
+ typename... Args,
+ typename FieldMetadataTypeCheck>
+PERFETTO_ALWAYS_INLINE void WriteTrackEventArgs(EventContext event_ctx,
+ FieldMetadataType field_name,
+ ArgValue&& arg_value,
+ Args&&... args) {
static_assert(std::is_base_of<protozero::proto_utils::FieldMetadataBase,
FieldMetadataType>::value,
"");
diff --git a/include/perfetto/tracing/traced_proto.h b/include/perfetto/tracing/traced_proto.h
index 042b96e..6e7b27b 100644
--- a/include/perfetto/tracing/traced_proto.h
+++ b/include/perfetto/tracing/traced_proto.h
@@ -102,7 +102,7 @@
// repeated and non-repeated complex fields are supported.
template <typename FieldMetadata>
TracedProto<typename FieldMetadata::cpp_field_type> WriteNestedMessage(
- protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>) {
+ FieldMetadata) {
static_assert(std::is_base_of<MessageType,
typename FieldMetadata::message_type>::value,
"Field should belong to the current message");
@@ -121,8 +121,7 @@
// string), but requires the |field| to be non-repeateable (i.e. optional).
// For repeatable fields, AppendValue or AppendFrom should be used.
template <typename FieldMetadata, typename ValueType>
- void Set(protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>,
- ValueType&& value) {
+ void Set(FieldMetadata, ValueType&& value) {
static_assert(std::is_base_of<MessageType,
typename FieldMetadata::message_type>::value,
"Field should belong to the current message");
@@ -148,9 +147,7 @@
// current message. If the field is not repeated, Set() should be used
// instead.
template <typename FieldMetadata, typename ValueType>
- void AppendValue(
- protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>,
- ValueType&& value) {
+ void AppendValue(FieldMetadata, ValueType&& value) {
static_assert(std::is_base_of<MessageType,
typename FieldMetadata::message_type>::value,
"Field should belong to the current message");
@@ -174,9 +171,7 @@
// current message. If the field is not repeated, Set() should be used
// instead.
template <typename FieldMetadata, typename ValueType>
- void AppendFrom(
- protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>,
- ValueType&& value) {
+ void AppendFrom(FieldMetadata, ValueType&& value) {
static_assert(std::is_base_of<MessageType,
typename FieldMetadata::message_type>::value,
"Field should belong to the current message");
@@ -199,8 +194,7 @@
// above and make these methods private.
template <typename FieldMetadata>
TracedProto<typename FieldMetadata::cpp_field_type> WriteNestedMessage() {
- return WriteNestedMessage(
- protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>());
+ return WriteNestedMessage(FieldMetadata());
}
private:
@@ -401,10 +395,9 @@
}
template <typename MessageType, typename FieldMetadataType, typename ValueType>
-void WriteTracedProtoField(
- TracedProto<MessageType>& message,
- protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadataType>,
- ValueType&& value) {
+void WriteTracedProtoField(TracedProto<MessageType>& message,
+ FieldMetadataType,
+ ValueType&& value) {
static_assert(
std::is_base_of<protozero::proto_utils::FieldMetadataBase,
FieldMetadataType>::value,
diff --git a/include/perfetto/tracing/traced_value.h b/include/perfetto/tracing/traced_value.h
index 366db4b..5a26753 100644
--- a/include/perfetto/tracing/traced_value.h
+++ b/include/perfetto/tracing/traced_value.h
@@ -286,11 +286,10 @@
// Create a |TracedDictionary| which will populate the given field of the
// given |message|.
template <typename MessageType, typename FieldMetadata>
- inline TracedDictionary(
- MessageType* message,
- protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>,
- EventContext* event_context,
- internal::CheckedScope* parent_scope)
+ inline TracedDictionary(MessageType* message,
+ FieldMetadata,
+ EventContext* event_context,
+ internal::CheckedScope* parent_scope)
: message_(message),
field_id_(FieldMetadata::kFieldId),
event_context_(event_context),
diff --git a/src/ipc/host_impl.cc b/src/ipc/host_impl.cc
index 45030ed..e9a50d1 100644
--- a/src/ipc/host_impl.cc
+++ b/src/ipc/host_impl.cc
@@ -147,6 +147,12 @@
PERFETTO_DCHECK(client_connection->send_fd_cb_fuchsia);
}
+void HostImpl::SetSocketSendTimeoutMs(uint32_t timeout_ms) {
+ PERFETTO_DCHECK_THREAD(thread_checker_);
+ // Should be less than the watchdog period (30s).
+ socket_tx_timeout_ms_ = timeout_ms;
+}
+
void HostImpl::OnNewIncomingConnection(
base::UnixSocket*,
std::unique_ptr<base::UnixSocket> new_conn) {
@@ -156,8 +162,7 @@
clients_by_socket_[new_conn.get()] = client.get();
client->id = client_id;
client->sock = std::move(new_conn);
- // Watchdog is 30 seconds, so set the socket timeout to 10 seconds.
- client->sock->SetTxTimeout(10000);
+ client->sock->SetTxTimeout(socket_tx_timeout_ms_);
clients_[client_id] = std::move(client);
}
diff --git a/src/ipc/host_impl.h b/src/ipc/host_impl.h
index 2f0a4f8..788b81a 100644
--- a/src/ipc/host_impl.h
+++ b/src/ipc/host_impl.h
@@ -33,6 +33,8 @@
namespace perfetto {
namespace ipc {
+constexpr uint32_t kDefaultIpcTxTimeoutMs = 10000;
+
class HostImpl : public Host, public base::UnixSocket::EventListener {
public:
HostImpl(const char* socket_name, base::TaskRunner*);
@@ -45,6 +47,7 @@
void AdoptConnectedSocket_Fuchsia(
base::ScopedSocketHandle,
std::function<bool(int)> send_fd_cb) override;
+ void SetSocketSendTimeoutMs(uint32_t timeout_ms) override;
// base::UnixSocket::EventListener implementation.
void OnNewIncomingConnection(base::UnixSocket*,
@@ -94,6 +97,7 @@
std::map<base::UnixSocket*, ClientConnection*> clients_by_socket_;
ServiceID last_service_id_ = 0;
ClientID last_client_id_ = 0;
+ uint32_t socket_tx_timeout_ms_ = kDefaultIpcTxTimeoutMs;
PERFETTO_THREAD_CHECKER(thread_checker_)
base::WeakPtrFactory<HostImpl> weak_ptr_factory_; // Keep last.
};
diff --git a/src/protozero/protoc_plugin/protozero_plugin.cc b/src/protozero/protoc_plugin/protozero_plugin.cc
index 6a80bf6..bcfbf28 100644
--- a/src/protozero/protoc_plugin/protozero_plugin.cc
+++ b/src/protozero/protoc_plugin/protozero_plugin.cc
@@ -913,14 +913,7 @@
$cpp_type$,
$message_cpp_type$>;
-// Ceci n'est pas une pipe.
-// This is actually a variable of FieldMetadataHelper<FieldMetadata<...>>
-// type (and users are expected to use it as such, hence kCamelCase name).
-// It is declared as a function to keep protozero bindings header-only as
-// inline constexpr variables are not available until C++17 (while inline
-// functions are).
-// TODO(altimin): Use inline variable instead after adopting C++17.
-static constexpr $field_metadata_type$ $field_metadata_var$() { return {}; }
+static constexpr $field_metadata_type$ $field_metadata_var${};
)";
stub_h_->Print(code_stub, "field_id", std::to_string(field->number()),
diff --git a/src/trace_processor/prelude/functions/BUILD.gn b/src/trace_processor/prelude/functions/BUILD.gn
index 6927947..02cb90f 100644
--- a/src/trace_processor/prelude/functions/BUILD.gn
+++ b/src/trace_processor/prelude/functions/BUILD.gn
@@ -26,6 +26,8 @@
"create_view_function.h",
"import.cc",
"import.h",
+ "layout_functions.cc",
+ "layout_functions.h",
"pprof_functions.cc",
"pprof_functions.h",
"register_function.cc",
diff --git a/src/trace_processor/prelude/functions/layout_functions.cc b/src/trace_processor/prelude/functions/layout_functions.cc
new file mode 100644
index 0000000..2210355
--- /dev/null
+++ b/src/trace_processor/prelude/functions/layout_functions.cc
@@ -0,0 +1,203 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/trace_processor/prelude/functions/layout_functions.h"
+
+#include <queue>
+#include <vector>
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/basic_types.h"
+#include "src/trace_processor/sqlite/sqlite_utils.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto::trace_processor {
+
+namespace {
+
+constexpr char kFunctionName[] = "INTERNAL_LAYOUT";
+
+// A helper class for tracking which depths are available at a given time
+// and which slices are occupying each depths.
+class SlicePacker {
+ public:
+ SlicePacker() = default;
+
+ // |dur| can be 0 for instant events and -1 for slices which do not end.
+ base::Status AddSlice(int64_t ts, int64_t dur) {
+ if (last_call_ == LastCall::kAddSlice) {
+ return base::ErrStatus(R"(
+Incorrect window clause (observed two consecutive calls to "step" function).
+The window clause should be "rows between unbounded preceding and current row".
+)");
+ }
+ last_call_ = LastCall::kAddSlice;
+ if (ts < last_seen_ts_) {
+ return base::ErrStatus(R"(
+Passed slices are in incorrect order: %s requires timestamps to be sorted.
+Please specify "ORDER BY ts" in the window clause.
+)",
+ kFunctionName);
+ }
+ last_seen_ts_ = ts;
+ ProcessPrecedingEvents(ts);
+ // If the event is instant, do not mark this depth as occupied as it
+ // becomes immediately available again.
+ bool is_busy = dur != 0;
+ size_t depth = SelectAvailableDepth(is_busy);
+ // If the slice has an end and is not an instant, schedule this depth
+ // to be marked available again when it ends.
+ if (dur > 0) {
+ slice_ends_.push({ts + dur, depth});
+ }
+ last_depth_ = depth;
+ return base::OkStatus();
+ }
+
+ size_t GetLastDepth() {
+ last_call_ = LastCall::kQuery;
+ return last_depth_;
+ }
+
+ private:
+ struct SliceEnd {
+ int64_t ts;
+ size_t depth;
+ };
+
+ struct SliceEndGreater {
+ bool operator()(const SliceEnd& lhs, const SliceEnd& rhs) {
+ return lhs.ts > rhs.ts;
+ }
+ };
+
+ void ProcessPrecedingEvents(int64_t ts) {
+ while (!slice_ends_.empty() && slice_ends_.top().ts <= ts) {
+ is_depth_busy_[slice_ends_.top().depth] = false;
+ slice_ends_.pop();
+ }
+ }
+
+ size_t SelectAvailableDepth(bool new_state) {
+ for (size_t i = 0; i < is_depth_busy_.size(); ++i) {
+ if (!is_depth_busy_[i]) {
+ is_depth_busy_[i] = new_state;
+ return i;
+ }
+ }
+ size_t depth = is_depth_busy_.size();
+ is_depth_busy_.push_back(new_state);
+ return depth;
+ }
+
+ enum class LastCall {
+ kAddSlice,
+ kQuery,
+ };
+ // The first call will be "add slice" and the calls are expected to
+ // interleave, so set initial value to "query".
+ LastCall last_call_ = LastCall::kQuery;
+
+ int64_t last_seen_ts_ = 0;
+ std::vector<bool> is_depth_busy_;
+ // A list of currently open slices, ordered by end timestamp (ascending).
+ std::priority_queue<SliceEnd, std::vector<SliceEnd>, SliceEndGreater>
+ slice_ends_;
+ size_t last_depth_ = 0;
+};
+
+base::StatusOr<SlicePacker*> GetOrCreateAggregationContext(
+ sqlite3_context* ctx) {
+ SlicePacker** packer = static_cast<SlicePacker**>(
+ sqlite3_aggregate_context(ctx, sizeof(SlicePacker*)));
+ if (!packer) {
+ return base::ErrStatus("Failed to allocate aggregate context");
+ }
+
+ if (!*packer) {
+ *packer = new SlicePacker();
+ }
+ return *packer;
+}
+
+base::Status Step(sqlite3_context* ctx, size_t argc, sqlite3_value** argv) {
+ base::StatusOr<SlicePacker*> slice_packer =
+ GetOrCreateAggregationContext(ctx);
+ RETURN_IF_ERROR(slice_packer.status());
+
+ base::StatusOr<SqlValue> ts =
+ sqlite_utils::ExtractArgument(argc, argv, "ts", 0, SqlValue::kLong);
+ RETURN_IF_ERROR(ts.status());
+
+ base::StatusOr<SqlValue> dur =
+ sqlite_utils::ExtractArgument(argc, argv, "dur", 1, SqlValue::kLong);
+ RETURN_IF_ERROR(dur.status());
+
+ return slice_packer.value()->AddSlice(ts->AsLong(), dur.value().AsLong());
+}
+
+void StepWrapper(sqlite3_context* ctx, int argc, sqlite3_value** argv) {
+ PERFETTO_CHECK(argc >= 0);
+
+ base::Status status = Step(ctx, static_cast<size_t>(argc), argv);
+ if (!status.ok()) {
+ sqlite_utils::SetSqliteError(ctx, kFunctionName, status);
+ return;
+ }
+}
+
+void FinalWrapper(sqlite3_context* ctx) {
+ SlicePacker** slice_packer = static_cast<SlicePacker**>(
+ sqlite3_aggregate_context(ctx, sizeof(SlicePacker*)));
+ if (!slice_packer || !*slice_packer) {
+ return;
+ }
+ sqlite3_result_int64(ctx,
+ static_cast<int64_t>((*slice_packer)->GetLastDepth()));
+ delete *slice_packer;
+}
+
+void ValueWrapper(sqlite3_context* ctx) {
+ base::StatusOr<SlicePacker*> slice_packer =
+ GetOrCreateAggregationContext(ctx);
+ if (!slice_packer.ok()) {
+ sqlite_utils::SetSqliteError(ctx, kFunctionName, slice_packer.status());
+ return;
+ }
+ sqlite3_result_int64(
+ ctx, static_cast<int64_t>(slice_packer.value()->GetLastDepth()));
+}
+
+void InverseWrapper(sqlite3_context* ctx, int, sqlite3_value**) {
+ sqlite_utils::SetSqliteError(ctx, kFunctionName, base::ErrStatus(R"(
+The inverse step is not supported: the window clause should be
+"BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW".
+)"));
+}
+
+} // namespace
+
+base::Status LayoutFunctions::Register(sqlite3* db,
+ TraceProcessorContext* context) {
+ int flags = SQLITE_UTF8 | SQLITE_DETERMINISTIC;
+ int ret = sqlite3_create_window_function(
+ db, kFunctionName, 2, flags, context, StepWrapper, FinalWrapper,
+ ValueWrapper, InverseWrapper, nullptr);
+ if (ret != SQLITE_OK) {
+ return base::ErrStatus("Unable to register function with name %s",
+ kFunctionName);
+ }
+ return base::OkStatus();
+}
+
+} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/prelude/functions/layout_functions.h b/src/trace_processor/prelude/functions/layout_functions.h
new file mode 100644
index 0000000..a88d8ab
--- /dev/null
+++ b/src/trace_processor/prelude/functions/layout_functions.h
@@ -0,0 +1,40 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TRACE_PROCESSOR_PRELUDE_FUNCTIONS_LAYOUT_FUNCTIONS_H_
+#define SRC_TRACE_PROCESSOR_PRELUDE_FUNCTIONS_LAYOUT_FUNCTIONS_H_
+
+#include <sqlite3.h>
+
+#include "perfetto/base/status.h"
+
+namespace perfetto::trace_processor {
+
+class TraceProcessorContext;
+
+// Implements INTERNAL_LAYOUT(ts, dur) window aggregate function.
+// This function takes a set of slices (ordered by ts) and computes depths
+// allowing them to be displayed on a single track in a non-overlapping manner,
+// while trying to minimising total height.
+//
+// TODO(altimin): this should support grouping sets of sets of slices (aka
+// "tracks") by passing 'track_id' parameter. The complication is that we will
+// need to know the max depth for each "track", so it's punted for now.
+struct LayoutFunctions {
+ static base::Status Register(sqlite3* db, TraceProcessorContext* context);
+};
+
+} // namespace perfetto::trace_processor
+
+#endif // SRC_TRACE_PROCESSOR_PRELUDE_FUNCTIONS_LAYOUT_FUNCTIONS_H_
diff --git a/src/trace_processor/prelude/functions/pprof_functions.cc b/src/trace_processor/prelude/functions/pprof_functions.cc
index 7d7e56e..23b2f23 100644
--- a/src/trace_processor/prelude/functions/pprof_functions.cc
+++ b/src/trace_processor/prelude/functions/pprof_functions.cc
@@ -54,18 +54,6 @@
return std::unique_ptr<T>(ptr);
}
-void SetSqliteError(sqlite3_context* ctx, const base::Status& status) {
- PERFETTO_CHECK(!status.ok());
- sqlite3_result_error(ctx, status.c_message(), -1);
-}
-
-void SetSqliteError(sqlite3_context* ctx,
- const std::string& function_name,
- const base::Status& status) {
- SetSqliteError(ctx, base::ErrStatus("%s: %s", function_name.c_str(),
- status.c_message()));
-}
-
class AggregateContext {
public:
static base::StatusOr<std::unique_ptr<AggregateContext>>
@@ -196,7 +184,7 @@
base::Status status = Step(ctx, static_cast<size_t>(argc), argv);
if (!status.ok()) {
- SetSqliteError(ctx, kFunctionName, status);
+ sqlite_utils::SetSqliteError(ctx, kFunctionName, status);
}
}
diff --git a/src/trace_processor/sqlite/sqlite_utils.h b/src/trace_processor/sqlite/sqlite_utils.h
index cc9cf41..c3b08bb 100644
--- a/src/trace_processor/sqlite/sqlite_utils.h
+++ b/src/trace_processor/sqlite/sqlite_utils.h
@@ -214,6 +214,18 @@
return base::OkStatus();
}
+inline void SetSqliteError(sqlite3_context* ctx, const base::Status& status) {
+ PERFETTO_CHECK(!status.ok());
+ sqlite3_result_error(ctx, status.c_message(), -1);
+}
+
+inline void SetSqliteError(sqlite3_context* ctx,
+ const std::string& function_name,
+ const base::Status& status) {
+ SetSqliteError(ctx, base::ErrStatus("%s: %s", function_name.c_str(),
+ status.c_message()));
+}
+
// Exracts the given type from the SqlValue if |value| can fit
// in the provided optional. Note that SqlValue::kNull will always
// succeed and cause base::nullopt to be set.
diff --git a/src/trace_processor/stdlib/android/monitor_contention.sql b/src/trace_processor/stdlib/android/monitor_contention.sql
index a075464..09860e7 100644
--- a/src/trace_processor/stdlib/android/monitor_contention.sql
+++ b/src/trace_processor/stdlib/android/monitor_contention.sql
@@ -152,7 +152,11 @@
-- @column ts timestamp of lock contention start
-- @column dur duration of lock contention
-- @column track_id thread track id of blocked thread
+-- @column is_blocked_main_thread whether the blocked thread is the main thread
+-- @column is_blocking_main_thread whether the blocking thread is the main thread
-- @column binder_reply_id slice id of binder reply slice if lock contention was part of a binder txn
+-- @column binder_reply_ts timestamp of binder reply slice if lock contention was part of a binder txn
+-- @column binder_reply_tid tid of binder reply slice if lock contention was part of a binder txn
CREATE TABLE android_monitor_contention
AS
SELECT
@@ -173,7 +177,11 @@
slice.ts,
slice.dur,
slice.track_id,
- binder_reply.id AS binder_reply_id
+ thread.is_main_thread AS is_blocked_thread_main,
+ blocking_thread.is_main_thread AS is_blocking_thread_main,
+ binder_reply.id AS binder_reply_id,
+ binder_reply.ts AS binder_reply_ts,
+ binder_reply_thread.tid AS binder_reply_tid
FROM slice
JOIN thread_track
ON thread_track.id = slice.track_id
@@ -183,6 +191,8 @@
USING (upid)
LEFT JOIN internal_broken_android_monitor_contention ON internal_broken_android_monitor_contention.id = slice.id
LEFT JOIN ANCESTOR_SLICE(slice.id) binder_reply ON binder_reply.name = 'binder reply'
+LEFT JOIN thread_track binder_reply_thread_track ON binder_reply.track_id = binder_reply_thread_track.id
+LEFT JOIN thread binder_reply_thread ON binder_reply_thread_track.utid = binder_reply_thread.utid
JOIN thread blocking_thread ON blocking_thread.name = blocking_thread_name AND blocking_thread.upid = thread.upid
WHERE slice.name LIKE 'monitor contention%'
AND slice.dur != -1
@@ -209,7 +219,11 @@
-- @column ts timestamp of lock contention start
-- @column dur duration of lock contention
-- @column track_id thread track id of blocked thread
+-- @column is_blocked_main_thread whether the blocked thread is the main thread
+-- @column is_blocking_main_thread whether the blocking thread is the main thread
-- @column binder_reply_id slice id of binder reply slice if lock contention was part of a binder txn
+-- @column binder_reply_ts timestamp of binder reply slice if lock contention was part of a binder txn
+-- @column binder_reply_tid tid of binder reply slice if lock contention was part of a binder txn
CREATE TABLE android_monitor_contention_chain
AS
SELECT parent.id AS parent_id, child.* FROM android_monitor_contention child
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index 48a854e..ee0fd9c 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -49,6 +49,7 @@
#include "src/trace_processor/prelude/functions/create_function.h"
#include "src/trace_processor/prelude/functions/create_view_function.h"
#include "src/trace_processor/prelude/functions/import.h"
+#include "src/trace_processor/prelude/functions/layout_functions.h"
#include "src/trace_processor/prelude/functions/pprof_functions.h"
#include "src/trace_processor/prelude/functions/register_function.h"
#include "src/trace_processor/prelude/functions/sqlite3_str_split.h"
@@ -768,6 +769,11 @@
if (!status.ok())
PERFETTO_ELOG("%s", status.c_message());
}
+ {
+ base::Status status = LayoutFunctions::Register(db, &context_);
+ if (!status.ok())
+ PERFETTO_ELOG("%s", status.c_message());
+ }
auto stdlib_modules = GetStdlibModules();
for (auto module_it = stdlib_modules.GetIterator(); module_it; ++module_it) {
diff --git a/src/trace_processor/util/proto_to_args_parser.h b/src/trace_processor/util/proto_to_args_parser.h
index 5e54775..7252a0f 100644
--- a/src/trace_processor/util/proto_to_args_parser.h
+++ b/src/trace_processor/util/proto_to_args_parser.h
@@ -99,7 +99,7 @@
template <typename FieldMetadata>
typename FieldMetadata::cpp_field_type::Decoder* GetInternedMessage(
- protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>,
+ FieldMetadata,
uint64_t iid) {
static_assert(std::is_base_of<protozero::proto_utils::FieldMetadataBase,
FieldMetadata>::value,
diff --git a/src/tracing/ipc/service/service_ipc_host_impl.cc b/src/tracing/ipc/service/service_ipc_host_impl.cc
index aab46dd..85029a2 100644
--- a/src/tracing/ipc/service/service_ipc_host_impl.cc
+++ b/src/tracing/ipc/service/service_ipc_host_impl.cc
@@ -31,6 +31,10 @@
namespace perfetto {
+namespace {
+constexpr uint32_t kProducerSocketTxTimeoutMs = 10;
+}
+
// TODO(fmayer): implement per-uid connection limit (b/69093705).
// Implements the publicly exposed factory method declared in
@@ -98,6 +102,16 @@
return false;
}
+ // Lower the timeout for blocking socket sends to producers as we shouldn't
+ // normally exhaust the kernel send buffer unless the producer is
+ // unresponsive. We'll drop the connection if the timeout is hit (see
+ // UnixSocket::Send). Context in b/236813972, b/193234818.
+ // Consumer port continues using the default timeout (10s) as there are
+ // generally fewer consumer processes, and they're better behaved. Also the
+ // consumer port ipcs might exhaust the send buffer under normal operation
+ // due to large messages such as ReadBuffersResponse.
+ producer_ipc_port_->SetSocketSendTimeoutMs(kProducerSocketTxTimeoutMs);
+
// TODO(fmayer): add a test that destroyes the ServiceIPCHostImpl soon after
// Start() and checks that no spurious callbacks are issued.
bool producer_service_exposed = producer_ipc_port_->ExposeService(
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
index 1f4f279..ee11f6b 100644
--- a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
+++ b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
@@ -1 +1 @@
-5a89fcf84d965f0fb6fa10f376fb6366ab6608974d5bf81cbf58fa784b4c0c83
\ No newline at end of file
+3142c75d3c652460c6f76261b59191afb89be996a234a9e5700bb4147b18537f
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_dismiss_1.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_dismiss_1.png.sha256
index eeec9ab..677e059 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_dismiss_1.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_dismiss_1.png.sha256
@@ -1 +1 @@
-0f96be819cae4e8ae4fa80e3b8bfdcd46176a9357172bf5e92b6311d55c74e2e
\ No newline at end of file
+81d485072c6494fa2fb7b23693c3f6ae197534e4fd39d2c4647f735ab2a5fafc
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_dismiss_2.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_dismiss_2.png.sha256
index 9427a25..102a59b 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_dismiss_2.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_dismiss_2.png.sha256
@@ -1 +1 @@
-5e6bc6ab0108752c5eb14d6bc1c4b8e3d1283121591a8db45a8a66a2ab2cb00a
\ No newline at end of file
+0690737a791ad7fc869a041574e220eed6cca864f5d93740c44ff2e36b4fe4d5
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
index dab9123..b2a0167 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
@@ -1 +1 @@
-53c2f27d6ffb0a10593c331167f92e025daec37b7802fe6f8f1beb056291062e
\ No newline at end of file
+717822b63455dab14a57811379e6e406537d80aa08746903d558aff463ea359a
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
index 684c3da..4b292da 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
@@ -1 +1 @@
-3bdbffb6d4ab142b0665d50bc929606edb55f50c056bf17f7f0b337d1cb8b608
\ No newline at end of file
+3efbdefb1d110c11a82d109f4f7c6edf599a4866d5c6c6bdf80011762244b206
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_switch_page_no_dialog.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_switch_page_no_dialog.png.sha256
index 9094b6c..98dcc39 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_switch_page_no_dialog.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_switch_page_no_dialog.png.sha256
@@ -1 +1 @@
-f76316e3870b30a13b55b7c95f4f7ddea45c1eb33ef49f98ee664c38b35494ab
\ No newline at end of file
+c9769de63b7caa32bda648b08a7e302a80e8bf1a9138c9e508faec5713e0964c
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_navigate_navigate_back_and_forward.png.sha256 b/test/data/ui-screenshots/ui-routing_navigate_navigate_back_and_forward.png.sha256
index 42d454e..8e2378f 100644
--- a/test/data/ui-screenshots/ui-routing_navigate_navigate_back_and_forward.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_navigate_navigate_back_and_forward.png.sha256
@@ -1 +1 @@
-59a6d1a1b5af0a3b2724e20e4c4290bb8cc3ede400f2bc1fad08ca8f52a3d0d0
\ No newline at end of file
+e4461c82b2051197fab373f50d44ffa6f103d24a10e582a62e67f56f77c0ec3a
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/android/tests.py b/test/trace_processor/diff_tests/android/tests.py
index 7f6390d..573bed9 100644
--- a/test/trace_processor/diff_tests/android/tests.py
+++ b/test/trace_processor/diff_tests/android/tests.py
@@ -233,8 +233,8 @@
LIMIT 1;
""",
out=Csv("""
- "blocking_method","blocked_method","short_blocking_method","short_blocked_method","blocking_src","blocked_src","waiter_count","blocked_utid","blocked_thread_name","blocking_utid","blocking_thread_name","upid","process_name","id","ts","dur","track_id","binder_reply_id"
- "float com.android.server.wm.WindowManagerService.getCurrentAnimatorScale()","android.app.ActivityTaskManager$RootTaskInfo com.android.server.wm.ActivityTaskManagerService.getFocusedRootTaskInfo()","com.android.server.wm.WindowManagerService.getCurrentAnimatorScale","com.android.server.wm.ActivityTaskManagerService.getFocusedRootTaskInfo","WindowManagerService.java:3511","ActivityTaskManagerService.java:1977",2,555,"binder:642_3",527,"android.anim",279,"system_server",69099,146987786843,24888520,1317,69097
+ "blocking_method","blocked_method","short_blocking_method","short_blocked_method","blocking_src","blocked_src","waiter_count","blocked_utid","blocked_thread_name","blocking_utid","blocking_thread_name","upid","process_name","id","ts","dur","track_id","is_blocked_thread_main","is_blocking_thread_main","binder_reply_id","binder_reply_ts","binder_reply_tid"
+ "float com.android.server.wm.WindowManagerService.getCurrentAnimatorScale()","android.app.ActivityTaskManager$RootTaskInfo com.android.server.wm.ActivityTaskManagerService.getFocusedRootTaskInfo()","com.android.server.wm.WindowManagerService.getCurrentAnimatorScale","com.android.server.wm.ActivityTaskManagerService.getFocusedRootTaskInfo","WindowManagerService.java:3511","ActivityTaskManagerService.java:1977",2,555,"binder:642_3",527,"android.anim",279,"system_server",69099,146987786843,24888520,1317,0,0,69097,146987701011,1559
"""))
def test_monitor_contention_chain_extraction(self):
@@ -261,12 +261,16 @@
ts,
dur,
track_id,
- IIF(binder_reply_id IS NULL, "", binder_reply_id) AS binder_reply_id
+ is_blocked_thread_main,
+ is_blocking_thread_main,
+ IIF(binder_reply_id IS NULL, "", binder_reply_id) AS binder_reply_id,
+ IIF(binder_reply_ts IS NULL, "", binder_reply_ts) AS binder_reply_ts,
+ IIF(binder_reply_tid IS NULL, "", binder_reply_tid) AS binder_reply_tid
FROM android_monitor_contention_chain
ORDER BY dur DESC
LIMIT 1;
""",
out=Csv("""
- "parent_id","blocking_method","blocked_method","short_blocking_method","short_blocked_method","blocking_src","blocked_src","waiter_count","blocked_utid","blocked_thread_name","blocking_utid","blocking_thread_name","upid","process_name","id","ts","dur","track_id","binder_reply_id"
- "","void java.lang.Object.wait(long, int)","void android.opengl.GLSurfaceView$GLThread.requestRenderAndNotify(java.lang.Runnable)","java.lang.Object.wait","android.opengl.GLSurfaceView$GLThread.requestRenderAndNotify","Object.java:-2","GLSurfaceView.java:1658",0,313,"droid.gallery3d",1769,"GLThread 33",313,"com.android.gallery3d",289064,155411562446,51012448,2036,""
+ "parent_id","blocking_method","blocked_method","short_blocking_method","short_blocked_method","blocking_src","blocked_src","waiter_count","blocked_utid","blocked_thread_name","blocking_utid","blocking_thread_name","upid","process_name","id","ts","dur","track_id","is_blocked_thread_main","is_blocking_thread_main","binder_reply_id","binder_reply_ts","binder_reply_tid"
+ "","void java.lang.Object.wait(long, int)","void android.opengl.GLSurfaceView$GLThread.requestRenderAndNotify(java.lang.Runnable)","java.lang.Object.wait","android.opengl.GLSurfaceView$GLThread.requestRenderAndNotify","Object.java:-2","GLSurfaceView.java:1658",0,313,"droid.gallery3d",1769,"GLThread 33",313,"com.android.gallery3d",289064,155411562446,51012448,2036,1,0,"","",""
"""))
diff --git a/test/trace_processor/diff_tests/functions/tests.py b/test/trace_processor/diff_tests/functions/tests.py
index aa66328..d68159a 100644
--- a/test/trace_processor/diff_tests/functions/tests.py
+++ b/test/trace_processor/diff_tests/functions/tests.py
@@ -37,7 +37,6 @@
class Functions(TestSuite):
-
def test_first_non_null_frame(self):
return DiffTestBlueprint(
trace=TextProto(r"""
@@ -333,7 +332,7 @@
A (0x0)
"""))
-def test_annotated_callstack(self):
+ def test_annotated_callstack(self):
return DiffTestBlueprint(
trace=DataPath("perf_sample_annotations.pftrace"),
query="""
@@ -375,3 +374,111 @@
main (0x63da9c354c)
__libc_init (0x74ff4a0728)
"""))
+
+ def test_layout(self):
+ return DiffTestBlueprint(
+ trace=TextProto(r"""
+ """),
+ query="""
+ CREATE TABLE TEST(start INTEGER, end INTEGER);
+
+ INSERT INTO TEST
+ VALUES
+ (1, 5),
+ (2, 4),
+ (3, 8),
+ (6, 7),
+ (6, 7),
+ (6, 7);
+
+ WITH custom_slices as (
+ SELECT
+ start as ts,
+ end - start as dur
+ FROM test
+ )
+ SELECT
+ ts,
+ INTERNAL_LAYOUT(ts, dur) over (
+ order by ts
+ rows between unbounded preceding and current row
+ ) as depth
+ FROM custom_slices
+ """,
+ out=Csv("""
+ "ts","depth"
+ 1,0
+ 2,1
+ 3,2
+ 6,0
+ 6,1
+ 6,3
+ """))
+
+ def test_layout_with_instant_events(self):
+ return DiffTestBlueprint(
+ trace=TextProto(r"""
+ """),
+ query="""
+ CREATE TABLE TEST(start INTEGER, end INTEGER);
+
+ INSERT INTO TEST
+ VALUES
+ (1, 5),
+ (2, 2),
+ (3, 3),
+ (4, 4);
+
+ WITH custom_slices as (
+ SELECT
+ start as ts,
+ end - start as dur
+ FROM test
+ )
+ SELECT
+ ts,
+ INTERNAL_LAYOUT(ts, dur) over (
+ order by ts
+ rows between unbounded preceding and current row
+ ) as depth
+ FROM custom_slices
+ """,
+ out=Csv("""
+ "ts","depth"
+ 1,0
+ 2,1
+ 3,1
+ 4,1
+ """))
+
+ def test_layout_with_events_without_end(self):
+ return DiffTestBlueprint(
+ trace=TextProto(r"""
+ """),
+ query="""
+ CREATE TABLE TEST(ts INTEGER, dur INTEGER);
+
+ INSERT INTO TEST
+ VALUES
+ (1, -1),
+ (2, -1),
+ (3, 5),
+ (4, 1),
+ (5, 1);
+
+ SELECT
+ ts,
+ INTERNAL_LAYOUT(ts, dur) over (
+ order by ts
+ rows between unbounded preceding and current row
+ ) as depth
+ FROM test
+ """,
+ out=Csv("""
+ "ts","depth"
+ 1,0
+ 2,1
+ 3,2
+ 4,3
+ 5,3
+ """))
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 0a30805..1e602fc 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -24,6 +24,7 @@
tableColumnEquals,
toggleEnabled,
} from '../frontend/pivot_table_types';
+import {DebugTrackV2Config} from '../tracks/debug/slice_track';
import {randomColor} from './colorizer';
import {
@@ -62,7 +63,7 @@
} from './state';
import {toNs} from './time';
-const DEBUG_SLICE_TRACK_KIND = 'DebugSliceTrack';
+export const DEBUG_SLICE_TRACK_KIND = 'DebugSliceTrack';
type StateDraft = Draft<State>;
@@ -275,11 +276,12 @@
};
},
- addDebugTrack(state: StateDraft, args: {engineId: string, name: string}):
+ addDebugTrack(
+ state: StateDraft,
+ args: {engineId: string, name: string, config: DebugTrackV2Config}):
void {
if (state.debugTrackId !== undefined) return;
const trackId = generateNextId(state);
- state.debugTrackId = trackId;
this.addTrack(state, {
id: trackId,
engineId: args.engineId,
@@ -287,18 +289,15 @@
name: args.name,
trackSortKey: PrimaryTrackSortKey.DEBUG_SLICE_TRACK,
trackGroup: SCROLLING_TRACK_GROUP,
- config: {
- maxDepth: 1,
- },
+ config: args.config,
});
this.toggleTrackPinned(state, {trackId});
},
- removeDebugTrack(state: StateDraft, _: {}): void {
- const {debugTrackId} = state;
- if (debugTrackId === undefined) return;
- removeTrack(state, debugTrackId);
- state.debugTrackId = undefined;
+ removeDebugTrack(state: StateDraft, args: {trackId: string}): void {
+ const track = state.tracks[args.trackId];
+ assertTrue(track.kind === DEBUG_SLICE_TRACK_KIND);
+ removeTrack(state, args.trackId);
},
removeVisualisedArgTracks(state: StateDraft, args: {trackIds: string[]}) {
@@ -782,6 +781,23 @@
state.pendingScrollId = args.scroll ? args.id : undefined;
},
+ selectDebugSlice(state: StateDraft, args: {
+ id: number,
+ sqlTableName: string,
+ startS: number,
+ durationS: number,
+ trackId: string,
+ }): void {
+ state.currentSelection = {
+ kind: 'DEBUG_SLICE',
+ id: args.id,
+ sqlTableName: args.sqlTableName,
+ startS: args.startS,
+ durationS: args.durationS,
+ trackId: args.trackId,
+ };
+ },
+
clearPendingScrollId(state: StateDraft, _: {}): void {
state.pendingScrollId = undefined;
},
diff --git a/ui/src/common/engine.ts b/ui/src/common/engine.ts
index 30c9423..8b6cb46 100644
--- a/ui/src/common/engine.ts
+++ b/ui/src/common/engine.ts
@@ -463,4 +463,8 @@
query(sqlQuery: string, tag?: string): Promise<QueryResult>&QueryResult {
return this.engine.query(sqlQuery, tag || this.tag);
}
+
+ get engineId(): string {
+ return this.engine.id;
+ }
}
diff --git a/ui/src/common/event_set.ts b/ui/src/common/event_set.ts
new file mode 100644
index 0000000..87e62d5
--- /dev/null
+++ b/ui/src/common/event_set.ts
@@ -0,0 +1,153 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// A single value. These are often retived from trace_processor so
+// need to map to the related sqlite type:
+// null = NULL, string = TEXT, number = INTEGER/REAL, boolean = INTEGER
+export type Primitive = null|string|boolean|number;
+
+export const NullType = null;
+export const NumType = 0 as const;
+export const StrType = 'str' as const;
+export const IdType = 'id' as const;
+export const BoolType = true as const;
+
+// Values may be of any of the above types:
+type KeyType =
+ typeof NumType|typeof StrType|typeof NullType|typeof IdType|typeof BoolType;
+
+// KeySet is a specification for the key/value pairs on an Event.
+// - Every event must have a string ID.
+// - In addition Events may have 1 or more key/value pairs.
+// The *specification* for the key/value pair has to be *precisely* one
+// of the KeySet constants above. So:
+// const thisTypeChecks: KeySet = { id: IdType, foo: StrType };
+// const thisDoesNot: KeySet = { id: IdType, foo: "bar" };
+// Since although are is a string it's not a KeySet.
+export type KeySet = {
+ readonly id: typeof IdType,
+ readonly [key: string]: KeyType,
+};
+
+export interface EmptyKeySet extends KeySet {
+ readonly id: typeof IdType;
+}
+;
+
+// A particular key/value pair on an Event matches the relevant entry
+// on the KeySet if the KeyType and the value type 'match':
+// IdType => string
+// StrType => string
+// BoolType => boolean
+// NullType => null
+// NumType => number
+type IsExactly<P, Q> = P extends Q ? (Q extends P ? any : never) : never;
+type IsId<T> = T extends IsExactly<T, typeof IdType>? string : never;
+type IsStr<T> = T extends IsExactly<T, typeof StrType>? string : never;
+type IsNum<T> = T extends IsExactly<T, typeof NumType>? number : never;
+type IsBool<T> = T extends IsExactly<T, typeof BoolType>? boolean : never;
+type IsNull<T> = T extends IsExactly<T, typeof NullType>? null : never;
+type MapType<T> = IsId<T>|IsStr<T>|IsNum<T>|IsBool<T>|IsNull<T>;
+type ConformingValue<T> = T extends MapType<T>? MapType<T>: void;
+
+// A single trace Event.
+// Events have:
+// - A globally unique identifier `id`.
+// - Zero or more key/value pairs.
+// Note: Events do *not* have to have all possible keys/value pairs for
+// the given id. It is expected that users will only materialise the
+// key/value pairs relevant to the specific use case at hand.
+export type UntypedEvent = {
+ readonly id: string,
+ readonly [key: string]: Primitive,
+};
+
+export type Event<K extends KeySet> = {
+ [Property in keyof K]: ConformingValue<K[Property]>;
+};
+
+type KeyUnion<P, Q> = P&Q;
+
+// An EventSet is a:
+// - ordered
+// - immutable
+// - subset
+// of events in the trace.
+export interface EventSet<P extends KeySet> {
+ // All possible keys for Events in this EventSet.
+ readonly keys: KeySet;
+
+ // Methods for refining the set.
+ // Note: these are all synchronous - we expect the cost (and hence
+ // any asynchronous queries) to be deferred to analysis time.
+ filter(...filters: Filter[]): EventSet<P>;
+ sort(...sorts: Sort[]): EventSet<P>;
+ union<Q extends KeySet>(other: EventSet<Q>): EventSet<KeyUnion<P, Q>>;
+ intersect<Q extends KeySet>(other: EventSet<Q>): EventSet<KeyUnion<P, Q>>;
+
+ // Methods for analysing the set.
+ // Note: these are all asynchronous - it's expected that these will
+ // often have to do queries.
+ count(): Promise<number>;
+ isEmpty(): Promise<boolean>;
+ materialise<T extends P>(keys: T, offset?: number, limit?: number):
+ Promise<ConcreteEventSet<T>>;
+}
+
+export type UntypedEventSet = EventSet<KeySet>;
+
+// An expression that operates on an Event and produces a Primitive as
+// output. Expressions have to work both in JavaScript and in SQL.
+// In SQL users can use buildQueryFragment to convert the expression
+// into a snippet of SQL. For JavaScript they call execute(). In both
+// cases you need to know which keys the expression uses, for this call
+// `freeVariables`.
+export interface Expr {
+ // Return a fragment of SQL that can be used to evaluate the
+ // expression. `binding` maps key names to column names in the
+ // resulting SQL. The caller must ensure that binding includes at
+ // least all the keys from `freeVariables`.
+ buildQueryFragment(binding: Map<string, string>): string;
+
+ // Run the expression on an Event. The caller must ensure that event
+ // has all the keys from `freeVariables` materialised.
+ execute(event: UntypedEvent): Primitive;
+
+ // Returns the set of keys used in this expression.
+ // For example in an expression representing `(foo + 4) * bar`
+ // freeVariables would return the set {'foo', 'bar'}.
+ freeVariables(): Set<string>;
+}
+
+// A filter is a (normally boolean) expression.
+export type Filter = Expr;
+
+// Sorting direction.
+export enum Direction {
+ ASC,
+ DESC,
+}
+
+// A sort is an expression combined with a direction:
+export interface Sort {
+ direction: Direction;
+ expression: Expr;
+}
+
+// An EventSet where the Event are accesible synchronously.
+interface ConcreteEventSet<T extends KeySet> extends EventSet<T> {
+ readonly events: Event<T>[];
+}
+
+export type UntypedConcreteEventSet = ConcreteEventSet<KeySet>;
diff --git a/ui/src/common/event_set_nocompile_test.ts b/ui/src/common/event_set_nocompile_test.ts
new file mode 100644
index 0000000..e5f74d7
--- /dev/null
+++ b/ui/src/common/event_set_nocompile_test.ts
@@ -0,0 +1,71 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+ BoolType,
+ Event,
+ IdType,
+ KeySet,
+ NullType,
+ NumType,
+ StrType,
+} from './event_set';
+
+export function keySetMustHaveId(): KeySet {
+ // @ts-expect-error
+ const ks: KeySet = {};
+ return ks;
+}
+
+export function keySetMustHaveCorrectIdType(): KeySet {
+ const ks: KeySet = {
+ // @ts-expect-error
+ id: StrType,
+ };
+ return ks;
+}
+
+export function eventMustHaveAllKeys(): Event<KeySet> {
+ const ks = {
+ id: IdType,
+ foo: StrType,
+ };
+
+ // @ts-expect-error
+ const event: Event<typeof ks> = {
+ id: 'myid',
+ };
+
+ return event;
+}
+
+export function eventMayHaveNonKeyTypeValues(): Event<KeySet> {
+ const ks = {
+ id: IdType,
+ foo: StrType,
+ bar: NumType,
+ baz: BoolType,
+ xyzzy: NullType,
+ };
+
+ const event: Event<typeof ks> = {
+ id: 'myid',
+ foo: 'foo',
+ bar: 32,
+ baz: false,
+ xyzzy: null,
+ };
+
+ return event;
+}
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index c72c3c6..3f6f5d2 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -18,6 +18,7 @@
PivotTree,
TableColumn,
} from '../frontend/pivot_table_types';
+import {Direction} from './event_set';
/**
* A plain js object, holding objects of type |Class| keyed by string id.
@@ -306,6 +307,14 @@
id: number;
}
+export interface DebugSliceSelection {
+ kind: 'DEBUG_SLICE';
+ id: number;
+ sqlTableName: string;
+ startS: number;
+ durationS: number;
+}
+
export interface CounterSelection {
kind: 'COUNTER';
leftTs: number;
@@ -368,7 +377,8 @@
export type Selection =
(NoteSelection|SliceSelection|CounterSelection|HeapProfileSelection|
CpuProfileSampleSelection|ChromeSliceSelection|ThreadStateSelection|
- AreaSelection|PerfSamplesSelection|LogSelection)&{trackId?: string};
+ AreaSelection|PerfSamplesSelection|LogSelection|DebugSliceSelection)&
+ {trackId?: string};
export type SelectionKind = Selection['kind']; // 'THREAD_STATE' | 'SLICE' ...
export interface Pagination {
@@ -437,7 +447,7 @@
tracks: string[];
}
-export type SortDirection = 'DESC'|'ASC';
+export type SortDirection = keyof typeof Direction;
export interface PivotTableState {
// Currently selected area, if null, pivot table is not going to be visible.
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 186620e..421874b 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -498,7 +498,6 @@
publishFtraceCounters(counters);
}
- globals.dispatch(Actions.removeDebugTrack({}));
globals.dispatch(Actions.sortThreadTracks({}));
globals.dispatch(Actions.maybeExpandOnlyTrackGroup({}));
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index 0f205ba..c1a877f 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -317,15 +317,18 @@
group by parent_id, name
)
select
- parent_id as parentId,
+ t.parent_id as parentId,
+ p.name as parentName,
t.name as name,
t.trackIds as trackIds,
max_layout_depth(t.trackCount, t.trackIds) as maxDepth
from global_tracks_grouped AS t
- order by t.name;
+ left join track p on (t.parent_id = p.id)
+ order by p.name, t.name;
`);
const it = rawGlobalAsyncTracks.iter({
name: STR_NULL,
+ parentName: STR_NULL,
parentId: NUM_NULL,
trackIds: STR,
maxDepth: NUM,
@@ -336,6 +339,7 @@
for (; it.valid(); it.next()) {
const kind = ASYNC_SLICE_TRACK_KIND;
const rawName = it.name === null ? undefined : it.name;
+ const rawParentName = it.parentName === null ? undefined : it.name;
const name = TrackDecider.getTrackName({name: rawName, kind});
const rawTrackIds = it.trackIds;
const trackIds = rawTrackIds.split(',').map((v) => Number(v));
@@ -349,21 +353,24 @@
trackGroup = uuidv4();
parentIdToGroupId.set(parentTrackId, trackGroup);
+ const parentName =
+ TrackDecider.getTrackName({name: rawParentName, kind});
+
const summaryTrackId = uuidv4();
this.tracksToAdd.push({
id: summaryTrackId,
engineId: this.engineId,
kind: NULL_TRACK_KIND,
trackSortKey: PrimaryTrackSortKey.NULL_TRACK,
- name,
trackGroup: undefined,
+ name: parentName,
config: {},
});
this.addTrackGroupActions.push(Actions.addTrackGroup({
engineId: this.engineId,
summaryTrackId,
- name,
+ name: parentName,
id: trackGroup,
collapsed: true,
}));
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index cd6f234..ac3cad0 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -21,7 +21,7 @@
UNEXPECTED_PINK_COLOR,
} from '../common/colorizer';
import {NUM} from '../common/query_result';
-import {SelectionKind} from '../common/state';
+import {Selection, SelectionKind} from '../common/state';
import {fromNs, toNs} from '../common/time';
import {checkerboardExcept} from './checkerboard';
@@ -204,13 +204,6 @@
// TODO(hjd): Replace once we have cancellable query sequences.
private isDestroyed = false;
- // TODO(hjd): Remove when updating selection.
- // We shouldn't know here about CHROME_SLICE. Maybe should be set by
- // whatever deals with that. Dunno the namespace of selection is weird. For
- // most cases in non-ambiguous (because most things are a 'slice'). But some
- // others (e.g. THREAD_SLICE) have their own ID namespace so we need this.
- protected selectionKinds: SelectionKind[] = ['SLICE', 'CHROME_SLICE'];
-
// Extension points.
// Each extension point should take a dedicated argument type (e.g.,
// OnSliceOverArgs {slice?: T['slice']}) so it makes future extensions
@@ -266,6 +259,16 @@
this.onUpdatedSlices(this.slices);
}
+ protected isSelectionHandled(selection: Selection): boolean {
+ // TODO(hjd): Remove when updating selection.
+ // We shouldn't know here about CHROME_SLICE. Maybe should be set by
+ // whatever deals with that. Dunno the namespace of selection is weird. For
+ // most cases in non-ambiguous (because most things are a 'slice'). But some
+ // others (e.g. THREAD_SLICE) have their own ID namespace so we need this.
+ const supportedSelectionKinds: SelectionKind[] = ['SLICE', 'CHROME_SLICE'];
+ return supportedSelectionKinds.includes(selection.kind);
+ }
+
renderCanvas(ctx: CanvasRenderingContext2D): void {
// TODO(hjd): fonts and colors should come from the CSS and not hardcoded
// here.
@@ -299,7 +302,7 @@
let selection = globals.state.currentSelection;
- if (!selection || !this.selectionKinds.includes(selection.kind)) {
+ if (!selection || !this.isSelectionHandled(selection)) {
selection = null;
}
diff --git a/ui/src/frontend/debug.ts b/ui/src/frontend/debug.ts
new file mode 100644
index 0000000..8451e99
--- /dev/null
+++ b/ui/src/frontend/debug.ts
@@ -0,0 +1,36 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as m from 'mithril';
+
+import {Actions} from '../common/actions';
+import {getSchema} from '../common/schema';
+
+import {globals} from './globals';
+
+declare global {
+ interface Window {
+ m: typeof m;
+ getSchema: typeof getSchema;
+ globals: typeof globals;
+ Actions: typeof Actions;
+ }
+}
+
+export function registerDebugGlobals() {
+ window.getSchema = getSchema;
+ window.m = m;
+ window.globals = globals;
+ window.Actions = Actions;
+}
diff --git a/ui/src/frontend/details_panel.ts b/ui/src/frontend/details_panel.ts
index 3dd234c..c9faeaa 100644
--- a/ui/src/frontend/details_panel.ts
+++ b/ui/src/frontend/details_panel.ts
@@ -19,6 +19,7 @@
import {LogExists, LogExistsKey} from '../common/logs';
import {addSelectionChangeObserver} from '../common/selection_observer';
import {Selection} from '../common/state';
+import {DebugSliceDetailsTab} from '../tracks/debug/details_tab';
import {AggregationPanel} from './aggregation_panel';
import {ChromeSliceDetailsPanel} from './chrome_slice_panel';
@@ -214,6 +215,16 @@
},
});
break;
+ case 'DEBUG_SLICE':
+ bottomTabList.addTab({
+ kind: DebugSliceDetailsTab.kind,
+ tag: currentSelectionTag,
+ config: {
+ sqlTableName: newSelection.sqlTableName,
+ id: newSelection.id,
+ },
+ });
+ break;
default:
bottomTabList.closeTabByTag(currentSelectionTag);
}
diff --git a/ui/src/frontend/help_modal.ts b/ui/src/frontend/help_modal.ts
index 3242b2c..8047377 100644
--- a/ui/src/frontend/help_modal.ts
+++ b/ui/src/frontend/help_modal.ts
@@ -14,51 +14,88 @@
// limitations under the License.
import * as m from 'mithril';
+import {getErrorMessage} from '../common/errors';
import {globals} from './globals';
+import {
+ KeyboardLayoutMap,
+ nativeKeyboardLayoutMap,
+ NotSupportedError,
+} from './keyboard_layout_map';
import {showModal} from './modal';
+import {KeyMapping} from './pan_and_zoom_handler';
+import {Spinner} from './widgets/spinner';
export function toggleHelp() {
globals.logging.logEvent('User Actions', 'Show help');
showHelp();
}
-function keycap(key: string) {
- return m('.keycap', key);
+function keycap(glyph: m.Children): m.Children {
+ return m('.keycap', glyph);
}
-function showHelp() {
- const ctrlOrCmd =
- window.navigator.platform.indexOf('Mac') !== -1 ? 'Cmd' : 'Ctrl';
- showModal({
- title: 'Perfetto Help',
- content: m(
+// A fallback keyboard map based on the QWERTY keymap. Converts keyboard event
+// codes to their associated glyphs on an English QWERTY keyboard.
+class EnglishQwertyKeyboardLayoutMap implements KeyboardLayoutMap {
+ get(code: string): string {
+ // Converts 'KeyX' -> 'x'
+ return code.replace(/^Key([A-Z])$/, '$1').toLowerCase();
+ }
+}
+
+class KeyMappingsHelp implements m.ClassComponent {
+ private keyMap?: KeyboardLayoutMap;
+
+ oninit() {
+ nativeKeyboardLayoutMap()
+ .then((keyMap: KeyboardLayoutMap) => {
+ this.keyMap = keyMap;
+ globals.rafScheduler.scheduleFullRedraw();
+ })
+ .catch((e) => {
+ if (e instanceof NotSupportedError ||
+ getErrorMessage(e).includes('SecurityError')) {
+ // Keyboard layout is unavailable. Since showing the keyboard
+ // mappings correct for the user's keyboard layout is a nice-to-
+ // have, and users with non-QWERTY layouts are usually aware of the
+ // fact that the are using non-QWERTY layouts, we resort to showing
+ // English QWERTY mappings as a best-effort approach.
+ // The alternative would be to show key mappings for all keyboard
+ // layouts which is not feasible.
+ this.keyMap = new EnglishQwertyKeyboardLayoutMap();
+ globals.rafScheduler.scheduleFullRedraw();
+ } else {
+ // Something unexpected happened. Either the browser doesn't conform
+ // to the keyboard API spec, or the keyboard API spec has changed!
+ throw e;
+ }
+ });
+ }
+
+ view(_: m.Vnode): m.Children {
+ const ctrlOrCmd =
+ window.navigator.platform.indexOf('Mac') !== -1 ? 'Cmd' : 'Ctrl';
+
+ return m(
'.help',
m('h2', 'Navigation'),
m(
'table',
m(
'tr',
- m('td', keycap('w'), '/', keycap('s')),
+ m('td',
+ this.codeToKeycap(KeyMapping.KEY_ZOOM_IN),
+ '/',
+ this.codeToKeycap(KeyMapping.KEY_ZOOM_OUT)),
m('td', 'Zoom in/out'),
),
m(
'tr',
- m('td', keycap('a'), '/', keycap('d')),
- m('td', 'Pan left/right'),
- ),
- ),
- m('h2', 'Navigation (Dvorak)'),
- m(
- 'table',
- m(
- 'tr',
- m('td', keycap(','), '/', keycap('o')),
- m('td', 'Zoom in/out'),
- ),
- m(
- 'tr',
- m('td', keycap('a'), '/', keycap('e')),
+ m('td',
+ this.codeToKeycap(KeyMapping.KEY_PAN_LEFT),
+ '/',
+ this.codeToKeycap(KeyMapping.KEY_PAN_RIGHT)),
m('td', 'Pan left/right'),
),
),
@@ -130,7 +167,22 @@
m('td', keycap(ctrlOrCmd), ' + ', keycap('b')),
m('td', 'Toggle display of sidebar')),
m('tr', m('td', keycap('?')), m('td', 'Show help')),
- )),
+ ));
+ }
+
+ private codeToKeycap(code: string): m.Children {
+ if (this.keyMap) {
+ return keycap(this.keyMap.get(code));
+ } else {
+ return keycap(m(Spinner));
+ }
+ }
+}
+
+function showHelp() {
+ showModal({
+ title: 'Perfetto Help',
+ content: () => m(KeyMappingsHelp),
buttons: [],
});
}
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 94a1ec5..e58a03b 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -32,6 +32,7 @@
import {AnalyzePage} from './analyze_page';
import {initCssConstants} from './css_constants';
+import {registerDebugGlobals} from './debug';
import {maybeShowErrorDialog} from './error_dialog';
import {installFileDropHandler} from './file_drop_handler';
import {FlagsPage} from './flags_page';
@@ -287,10 +288,8 @@
if (extensionPort) extensionPort.postMessage(data);
};
- // Put these variables in the global scope for better debugging.
- (window as {} as {m: {}}).m = m;
- (window as {} as {globals: {}}).globals = globals;
- (window as {} as {Actions: {}}).Actions = Actions;
+ // Put debug variables in the global scope for better debugging.
+ registerDebugGlobals();
// Prevent pinch zoom.
document.body.addEventListener('wheel', (e: MouseEvent) => {
diff --git a/ui/src/frontend/keyboard_event_handler.ts b/ui/src/frontend/keyboard_event_handler.ts
index bc1ae88..7da04de 100644
--- a/ui/src/frontend/keyboard_event_handler.ts
+++ b/ui/src/frontend/keyboard_event_handler.ts
@@ -245,6 +245,13 @@
}
} else if (selection.kind === 'LOG') {
// TODO(hjd): Make focus selection work for logs.
+ } else if (selection.kind === 'DEBUG_SLICE') {
+ startTs = selection.startS;
+ if (selection.durationS > 0) {
+ endTs = startTs + selection.durationS;
+ } else {
+ endTs = startTs + INSTANT_FOCUS_DURATION_S;
+ }
}
return {startTs, endTs};
diff --git a/ui/src/frontend/keyboard_layout_map.ts b/ui/src/frontend/keyboard_layout_map.ts
new file mode 100644
index 0000000..d5d2317
--- /dev/null
+++ b/ui/src/frontend/keyboard_layout_map.ts
@@ -0,0 +1,38 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+//
+// 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.
+
+// A keyboard layout map that converts key codes to their equivalent glyphs for
+// a given keyboard layout (e.g. 'KeyX' -> 'x').
+export interface KeyboardLayoutMap {
+ get(code: string): string|undefined;
+}
+
+export class NotSupportedError extends Error {}
+
+// Fetch the user's keyboard layout map.
+// This function is merely a wrapper around the keyboard API, which throws a
+// specific error when used in browsers that don't support it.
+export async function nativeKeyboardLayoutMap(): Promise<KeyboardLayoutMap> {
+ // Browser's that don't support the Keyboard API won't have a keyboard
+ // property in their window.navigator object.
+ // Note: it seems this is also what Chrome does when the website is accessed
+ // through an insecure connection.
+ if ('keyboard' in window.navigator) {
+ // Typescript's dom library doesn't know about this feature, so we must
+ // take some liberties when it comes to relaxing types
+ const keyboard = (window.navigator as any).keyboard;
+ return await keyboard.getLayoutMap();
+ } else {
+ throw new NotSupportedError('Keyboard API is not supported');
+ }
+}
diff --git a/ui/src/frontend/modal.ts b/ui/src/frontend/modal.ts
index 8c6ec2e..37f112e 100644
--- a/ui/src/frontend/modal.ts
+++ b/ui/src/frontend/modal.ts
@@ -58,11 +58,9 @@
import {assertExists, assertTrue} from '../base/logging';
import {globals} from './globals';
-type AnyAttrsVnode = m.Vnode<unknown, {}>;
-
export interface ModalDefinition {
title: string;
- content: AnyAttrsVnode;
+ content: m.Children|(() => m.Children);
vAlign?: 'MIDDLE' /* default */ | 'TOP';
buttons?: Button[];
close?: boolean;
@@ -151,11 +149,19 @@
'button[aria-label=Close Modal]',
{onclick: () => attrs.parent.close()},
m.trust('✕'),
+ ),
),
- ),
- m('main', attrs.content),
+ m('main', this.renderContent(attrs.content)),
m('footer', buttons),
- ));
+ ));
+ }
+
+ private renderContent(content: m.Children|(() => m.Children)): m.Children {
+ if (typeof content === 'function') {
+ return content();
+ } else {
+ return content;
+ }
}
oncreate(vnode: m.VnodeDOM<ModalImplAttrs>) {
diff --git a/ui/src/frontend/pan_and_zoom_handler.ts b/ui/src/frontend/pan_and_zoom_handler.ts
index 2a0610e..5f2413c 100644
--- a/ui/src/frontend/pan_and_zoom_handler.ts
+++ b/ui/src/frontend/pan_and_zoom_handler.ts
@@ -46,15 +46,32 @@
const DRAG_CURSOR = 'default';
const PAN_CURSOR = 'move';
+// Use key mapping based on the 'KeyboardEvent.code' property vs the
+// 'KeyboardEvent.key', because the the former corresponds to the physical key
+// position rather than the glyph printed on top of it, and is unaffected by
+// the user's keyboard layout.
+// For example, 'KeyW' always corresponds to the key at the physical location of
+// the 'w' key on an English QWERTY keyboard, regardless of the user's keyboard
+// layout, or at least the layout they have configured in their OS.
+// Seeing as most users use the keys in the English QWERTY "WASD" position for
+// controlling kb+mouse applications like games, it's a good bet that these are
+// the keys most poeple are going to find natural for navigating the UI.
+// See https://www.w3.org/TR/uievents-code/#key-alphanumeric-writing-system
+export enum KeyMapping {
+ KEY_PAN_LEFT = 'KeyA',
+ KEY_PAN_RIGHT = 'KeyD',
+ KEY_ZOOM_IN = 'KeyW',
+ KEY_ZOOM_OUT = 'KeyS',
+}
+
enum Pan {
None = 0,
Left = -1,
Right = 1
}
function keyToPan(e: KeyboardEvent): Pan {
- const key = e.key.toLowerCase();
- if (['a'].includes(key)) return Pan.Left;
- if (['d', 'e'].includes(key)) return Pan.Right;
+ if (e.code === KeyMapping.KEY_PAN_LEFT) return Pan.Left;
+ if (e.code === KeyMapping.KEY_PAN_RIGHT) return Pan.Right;
return Pan.None;
}
@@ -64,9 +81,8 @@
Out = -1
}
function keyToZoom(e: KeyboardEvent): Zoom {
- const key = e.key.toLowerCase();
- if (['w', ','].includes(key)) return Zoom.In;
- if (['s', 'o'].includes(key)) return Zoom.Out;
+ if (e.code === KeyMapping.KEY_ZOOM_IN) return Zoom.In;
+ if (e.code === KeyMapping.KEY_ZOOM_OUT) return Zoom.Out;
return Zoom.None;
}
diff --git a/ui/src/frontend/query_result_tab.ts b/ui/src/frontend/query_result_tab.ts
index e000ba9..8e9e4c8 100644
--- a/ui/src/frontend/query_result_tab.ts
+++ b/ui/src/frontend/query_result_tab.ts
@@ -12,7 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import * as m from 'mithril';
+import {v4 as uuidv4} from 'uuid';
+
+import {assertExists} from '../base/logging';
import {QueryResponse, runQuery} from '../common/queries';
+import {QueryError} from '../common/query_result';
+import {
+ AddDebugTrackMenu,
+ uuidToViewName,
+} from '../tracks/debug/add_debug_track_menu';
+
import {
addTab,
BottomTab,
@@ -22,6 +32,8 @@
} from './bottom_tab';
import {globals} from './globals';
import {QueryTable} from './query_table';
+import {Button} from './widgets/button';
+import {Popup, PopupPosition} from './widgets/popup';
export function runQueryInNewTab(query: string, title: string, tag?: string) {
@@ -47,6 +59,7 @@
static readonly kind = 'org.perfetto.QueryResultTab';
queryResponse?: QueryResponse;
+ sqlViewName?: string;
static create(args: NewBottomTabArgs): QueryResultTab {
return new QueryResultTab(args);
@@ -58,10 +71,38 @@
if (this.config.prefetchedResponse !== undefined) {
this.queryResponse = this.config.prefetchedResponse;
} else {
- runQuery(this.config.query, this.engine).then((result: QueryResponse) => {
- this.queryResponse = result;
- globals.rafScheduler.scheduleFullRedraw();
- });
+ runQuery(this.config.query, this.engine)
+ .then(async (result: QueryResponse) => {
+ this.queryResponse = result;
+ globals.rafScheduler.scheduleFullRedraw();
+
+ if (result.error !== undefined) {
+ return;
+ }
+
+ const uuid = uuidv4();
+ const viewId = uuidToViewName(uuid);
+ // Assuming that it was a SELECT query, try creating a view to allow
+ // us to reuse it for further queries.
+ // TODO(altimin): This should get the actual query that was used to
+ // generate the results from the SQL query iterator.
+ try {
+ const createViewResult = await this.engine.query(
+ `create view ${viewId} as ${this.config.query}`);
+ if (createViewResult.error()) {
+ // If it failed, do nothing.
+ return;
+ }
+ } catch (e) {
+ if (e instanceof QueryError) {
+ // If it failed, do nothing.
+ return;
+ }
+ throw e;
+ }
+ this.sqlViewName = viewId;
+ globals.rafScheduler.scheduleFullRedraw();
+ });
}
}
@@ -71,11 +112,25 @@
return `${this.config.title}${suffix}`;
}
- viewTab(): void {
+ viewTab(): m.Child {
return m(QueryTable, {
query: this.config.query,
resp: this.queryResponse,
onClose: () => closeTab(this.uuid),
+ contextButtons: [
+ this.sqlViewName === undefined ?
+ null :
+ m(Popup,
+ {
+ trigger: m(Button, {label: 'Show debug track', minimal: true}),
+ position: PopupPosition.Top,
+ },
+ m(AddDebugTrackMenu, {
+ sqlViewName: this.sqlViewName,
+ columns: assertExists(this.queryResponse).columns,
+ engine: this.engine,
+ })),
+ ],
});
}
diff --git a/ui/src/frontend/query_table.ts b/ui/src/frontend/query_table.ts
index 0d7eba5..9331749 100644
--- a/ui/src/frontend/query_table.ts
+++ b/ui/src/frontend/query_table.ts
@@ -117,19 +117,47 @@
}
}
-interface QueryTableAttrs {
- query: string;
- resp?: QueryResponse;
- onClose: () => void;
+interface QueryTableContentAttrs {
+ resp: QueryResponse;
}
-export class QueryTable extends Panel<QueryTableAttrs> {
+class QueryTableContent implements m.ClassComponent<QueryTableContentAttrs> {
private previousResponse?: QueryResponse;
- onbeforeupdate(vnode: m.CVnode<QueryTableAttrs>) {
+ onbeforeupdate(vnode: m.CVnode<QueryTableContentAttrs>) {
return vnode.attrs.resp !== this.previousResponse;
}
+ view(vnode: m.CVnode<QueryTableContentAttrs>) {
+ const resp = vnode.attrs.resp;
+ this.previousResponse = resp;
+ const cols = [];
+ for (const col of resp.columns) {
+ cols.push(m('td', col));
+ }
+ const tableHeader = m('tr', cols);
+
+ const rows =
+ resp.rows.map((row) => m(QueryTableRow, {row, columns: resp.columns}));
+
+ if (resp.error) {
+ return m('.query-error', `SQL error: ${resp.error}`);
+ } else {
+ return m(
+ '.query-table-container.x-scrollable',
+ m('table.query-table', m('thead', tableHeader), m('tbody', rows)));
+ }
+ }
+}
+
+interface QueryTableAttrs {
+ query: string;
+ onClose: () => void;
+ resp?: QueryResponse;
+ contextButtons?: m.Child[];
+}
+
+export class QueryTable extends Panel<QueryTableAttrs> {
view(vnode: m.CVnode<QueryTableAttrs>) {
const resp = vnode.attrs.resp;
@@ -139,6 +167,7 @@
`Query - running`),
m('span.code.text-select', vnode.attrs.query),
m('span.spacer'),
+ ...(vnode.attrs.contextButtons ?? []),
m(Button, {
label: 'Copy query',
minimal: true,
@@ -170,18 +199,6 @@
return m('div', ...headers);
}
- this.previousResponse = resp;
- const cols = [];
- for (const col of resp.columns) {
- cols.push(m('td', col));
- }
- const tableHeader = m('tr', cols);
-
- const rows = [];
- for (let i = 0; i < resp.rows.length; i++) {
- rows.push(m(QueryTableRow, {row: resp.rows[i], columns: resp.columns}));
- }
-
if (resp.statementWithOutputCount > 1) {
headers.push(
m('header.overview',
@@ -190,14 +207,7 @@
`statement are displayed in the table below.`));
}
- return m(
- 'div',
- ...headers,
- resp.error ? m('.query-error', `SQL error: ${resp.error}`) :
- m('.query-table-container.x-scrollable',
- m('table.query-table',
- m('thead', tableHeader),
- m('tbody', rows))));
+ return m('div', ...headers, m(QueryTableContent, {resp}));
}
renderCanvas() {}
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index e4e7f6a..bd63228 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -152,18 +152,6 @@
};
}
-function showDebugTrack(): (_: Event) => void {
- return (e: Event) => {
- e.preventDefault();
- globals.dispatch(Actions.addDebugTrack({
- // The debug track will only be shown once we have a currentEngineId which
- // is not undefined.
- engineId: assertExists(globals.state.engine).id,
- name: 'Debug Slices',
- }));
- };
-}
-
const EXAMPLE_ANDROID_TRACE_URL =
'https://storage.googleapis.com/perfetto-misc/example_android_trace_15s';
@@ -316,12 +304,6 @@
summary: 'Compute summary statistics',
items: [
{
- t: 'Show Debug Track',
- a: showDebugTrack(),
- i: 'view_day',
- isVisible: () => globals.state.engine !== undefined,
- },
- {
t: 'Record metatrace',
a: recordMetatrace,
i: 'fiber_smart_record',
diff --git a/ui/src/frontend/widgets/duration.ts b/ui/src/frontend/widgets/duration.ts
new file mode 100644
index 0000000..259c8d8
--- /dev/null
+++ b/ui/src/frontend/widgets/duration.ts
@@ -0,0 +1,27 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as m from 'mithril';
+
+import {fromNs, timeToCode} from '../../common/time';
+
+interface DurationAttrs {
+ dur: number;
+}
+
+export class Duration implements m.ClassComponent<DurationAttrs> {
+ view(vnode: m.Vnode<DurationAttrs>) {
+ return timeToCode(fromNs(vnode.attrs.dur));
+ }
+}
diff --git a/ui/src/frontend/widgets/timestamp.ts b/ui/src/frontend/widgets/timestamp.ts
new file mode 100644
index 0000000..8805b70
--- /dev/null
+++ b/ui/src/frontend/widgets/timestamp.ts
@@ -0,0 +1,28 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as m from 'mithril';
+
+import {timeToCode} from '../../common/time';
+import {toTraceTime, TPTimestamp} from '../sql_types';
+
+interface TimestampAttrs {
+ ts: TPTimestamp;
+}
+
+export class Timestamp implements m.ClassComponent<TimestampAttrs> {
+ view(vnode: m.Vnode<TimestampAttrs>) {
+ return timeToCode(toTraceTime(vnode.attrs.ts));
+ }
+}
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index 3bfae60..0953d2f 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -317,7 +317,8 @@
title = `${threadInfo.threadName} [${threadInfo.tid}]`;
}
}
- const right = Math.min(timeScale.timeToPx(visibleWindowTime.end), rectEnd);
+ const right =
+ Math.min(timeScale.timeToPx(visibleWindowTime.end), rectEnd);
const left = Math.max(rectStart, 0);
const visibleWidth = Math.max(right - left, 1);
title = cropText(title, charWidth, visibleWidth);
diff --git a/ui/src/tracks/debug/add_debug_track_menu.ts b/ui/src/tracks/debug/add_debug_track_menu.ts
new file mode 100644
index 0000000..e4a74bb
--- /dev/null
+++ b/ui/src/tracks/debug/add_debug_track_menu.ts
@@ -0,0 +1,117 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as m from 'mithril';
+
+import {EngineProxy} from '../../common/engine';
+import {Button} from '../../frontend/widgets/button';
+import {Select} from '../../frontend/widgets/select';
+import {TextInput} from '../../frontend/widgets/text_input';
+import {Tree, TreeNode} from '../../frontend/widgets/tree';
+import {addDebugTrack, SliceColumns} from './slice_track';
+
+export const ARG_PREFIX = 'arg_';
+
+export function uuidToViewName(uuid: string): string {
+ return `view_${uuid.split('-').join('_')}`;
+}
+
+interface AddDebugTrackMenuAttrs {
+ sqlViewName: string;
+ columns: string[];
+ engine: EngineProxy;
+}
+
+export class AddDebugTrackMenu implements
+ m.ClassComponent<AddDebugTrackMenuAttrs> {
+ name: string = '';
+ sliceColumns: SliceColumns;
+
+ constructor(vnode: m.Vnode<AddDebugTrackMenuAttrs>) {
+ const chooseDefaultOption = (name: string) => {
+ for (const column of vnode.attrs.columns) {
+ if (column === name) return column;
+ }
+ for (const column of vnode.attrs.columns) {
+ if (column.endsWith(`_${name}`)) return column;
+ }
+ return vnode.attrs.columns[0];
+ };
+
+ this.sliceColumns = {
+ ts: chooseDefaultOption('ts'),
+ dur: chooseDefaultOption('dur'),
+ name: chooseDefaultOption('name'),
+ };
+ }
+
+ view(vnode: m.Vnode<AddDebugTrackMenuAttrs>) {
+ const renderSelect = (name: 'ts'|'dur'|'name') => {
+ const options = [];
+ for (const column of vnode.attrs.columns) {
+ options.push(
+ m('option',
+ {
+ selected: this.sliceColumns[name] === column ? true : undefined,
+ },
+ column));
+ }
+ return m(TreeNode, {
+ left: name,
+ right: m(
+ Select,
+ {
+ oninput: (e: Event) => {
+ if (!e.target) return;
+ this.sliceColumns[name] = (e.target as HTMLSelectElement).value;
+ },
+ },
+ options),
+ });
+ };
+ return [
+ m(
+ Tree,
+ m(TreeNode, {
+ left: 'Name',
+ right: m(TextInput, {
+ onkeydown: (e: KeyboardEvent) => {
+ // Allow Esc to close popup.
+ if (e.key === 'Escape') return;
+ e.stopPropagation();
+ },
+ oninput: (e: KeyboardEvent) => {
+ if (!e.target) return;
+ this.name = (e.target as HTMLInputElement).value;
+ },
+ }),
+ }),
+ renderSelect('ts'),
+ renderSelect('dur'),
+ renderSelect('name'),
+ ),
+ m(Button, {
+ label: 'Show',
+ onclick: () => {
+ addDebugTrack(
+ vnode.attrs.engine,
+ vnode.attrs.sqlViewName,
+ this.name,
+ this.sliceColumns,
+ vnode.attrs.columns);
+ },
+ }),
+ ];
+ }
+}
diff --git a/ui/src/tracks/debug/details_tab.ts b/ui/src/tracks/debug/details_tab.ts
new file mode 100644
index 0000000..d30eaa9
--- /dev/null
+++ b/ui/src/tracks/debug/details_tab.ts
@@ -0,0 +1,119 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as m from 'mithril';
+
+import {ColumnType} from '../../common/query_result';
+import {
+ BottomTab,
+ bottomTabRegistry,
+ NewBottomTabArgs,
+} from '../../frontend/bottom_tab';
+import {globals} from '../../frontend/globals';
+import {TPTimestamp} from '../../frontend/sql_types';
+import {Duration} from '../../frontend/widgets/duration';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {Tree, TreeNode} from '../../frontend/widgets/tree';
+import {ARG_PREFIX} from './add_debug_track_menu';
+
+interface DebugSliceDetalsTabConfig {
+ sqlTableName: string;
+ id: number;
+}
+
+function SqlValueToString(val: ColumnType) {
+ if (val instanceof Uint8Array) {
+ return `<blob length=${val.length}>`;
+ }
+ if (val === null) {
+ return 'NULL';
+ }
+ return val.toString();
+}
+
+function dictToTree(dict: {[key: string]: m.Child}): m.Children {
+ const children: m.Child[] = [];
+ for (const key of Object.keys(dict)) {
+ children.push(m(TreeNode, {
+ left: key,
+ right: dict[key],
+ }));
+ }
+ return m(Tree, children);
+}
+
+export class DebugSliceDetailsTab extends BottomTab<DebugSliceDetalsTabConfig> {
+ static readonly kind = 'org.perfetto.DebugSliceDetailsTab';
+
+ data: {[key: string]: ColumnType}|undefined;
+
+ static create(args: NewBottomTabArgs): DebugSliceDetailsTab {
+ return new DebugSliceDetailsTab(args);
+ }
+
+ constructor(args: NewBottomTabArgs) {
+ super(args);
+
+ this.engine
+ .query(`select * from ${this.config.sqlTableName} where id = ${
+ this.config.id}`)
+ .then((queryResult) => {
+ this.data = queryResult.firstRow({});
+ globals.rafScheduler.scheduleFullRedraw();
+ });
+ }
+
+ viewTab() {
+ if (this.data === undefined) {
+ return m('h2', 'Loading');
+ }
+ const left = dictToTree({
+ 'Name': this.data['name'] as string,
+ 'Start time': m(Timestamp, {ts: this.data['ts'] as TPTimestamp}),
+ 'Duration': m(Duration, {dur: this.data['dur'] as number}),
+ 'Debug slice id': `${this.config.sqlTableName}[${this.config.id}]`,
+ });
+ const args: {[key: string]: m.Child} = {};
+ for (const key of Object.keys(this.data)) {
+ if (key.startsWith(ARG_PREFIX)) {
+ args[key.substr(ARG_PREFIX.length)] = SqlValueToString(this.data[key]);
+ }
+ }
+ return m(
+ 'div.details-panel',
+ m('header.overview', m('span', 'Debug Slice')),
+ m('.details-table-multicolumn',
+ {
+ style: {
+ 'user-select': 'text',
+ },
+ },
+ m('.half-width-panel', left),
+ m('.half-width-panel', dictToTree(args))));
+ }
+
+ getTitle(): string {
+ return `Current Selection`;
+ }
+
+ isLoading() {
+ return this.data === undefined;
+ }
+
+ renderTabCanvas() {
+ return;
+ }
+}
+
+bottomTabRegistry.register(DebugSliceDetailsTab);
diff --git a/ui/src/tracks/debug/index.ts b/ui/src/tracks/debug/index.ts
new file mode 100644
index 0000000..e68b451
--- /dev/null
+++ b/ui/src/tracks/debug/index.ts
@@ -0,0 +1,25 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {PluginContext} from '../../common/plugin_api';
+import {DebugTrackV2} from './slice_track';
+
+function activate(ctx: PluginContext) {
+ ctx.registerTrack(DebugTrackV2);
+}
+
+export const plugin = {
+ pluginId: 'perfetto.DebugSlices',
+ activate,
+};
diff --git a/ui/src/tracks/debug/slice_track.ts b/ui/src/tracks/debug/slice_track.ts
new file mode 100644
index 0000000..6622a03
--- /dev/null
+++ b/ui/src/tracks/debug/slice_track.ts
@@ -0,0 +1,141 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as m from 'mithril';
+
+import {Actions, DEBUG_SLICE_TRACK_KIND} from '../../common/actions';
+import {EngineProxy} from '../../common/engine';
+import {Selection} from '../../common/state';
+import {OnSliceClickArgs} from '../../frontend/base_slice_track';
+import {globals} from '../../frontend/globals';
+import {
+ NamedSliceTrack,
+ NamedSliceTrackTypes,
+} from '../../frontend/named_slice_track';
+import {NewTrackArgs} from '../../frontend/track';
+import {TrackButton, TrackButtonAttrs} from '../../frontend/track_panel';
+import {ARG_PREFIX} from './add_debug_track_menu';
+
+// Names of the columns of the underlying view to be used as ts / dur / name.
+export interface SliceColumns {
+ ts: string;
+ dur: string;
+ name: string;
+}
+
+export interface DebugTrackV2Config {
+ sqlTableName: string;
+ columns: SliceColumns;
+}
+
+interface DebugTrackV2Types extends NamedSliceTrackTypes {
+ config: DebugTrackV2Config;
+}
+
+export class DebugTrackV2 extends NamedSliceTrack<DebugTrackV2Types> {
+ static readonly kind = DEBUG_SLICE_TRACK_KIND;
+
+ static create(args: NewTrackArgs) {
+ return new DebugTrackV2(args);
+ }
+
+ constructor(args: NewTrackArgs) {
+ super(args);
+ }
+
+ async initSqlTable(tableName: string): Promise<void> {
+ await this.engine.query(`
+ create view ${tableName} as
+ select
+ id,
+ ts,
+ dur,
+ name,
+ depth
+ from ${this.config.sqlTableName}
+ `);
+ }
+
+ isSelectionHandled(selection: Selection) {
+ if (selection.kind !== 'DEBUG_SLICE') {
+ return false;
+ }
+ return selection.sqlTableName === this.config.sqlTableName;
+ }
+
+ onSliceClick(args: OnSliceClickArgs<DebugTrackV2Types['slice']>) {
+ globals.dispatch(Actions.selectDebugSlice({
+ id: args.slice.id,
+ sqlTableName: this.config.sqlTableName,
+ startS: args.slice.startS,
+ durationS: args.slice.durationS,
+ trackId: this.trackId,
+ }));
+ }
+
+ getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
+ return [m(TrackButton, {
+ action: () => {
+ globals.dispatch(Actions.removeDebugTrack({trackId: this.trackId}));
+ },
+ i: 'close',
+ tooltip: 'Close',
+ showButton: true,
+ })];
+ }
+}
+
+let debugTrackCount = 0;
+
+export async function addDebugTrack(
+ engine: EngineProxy,
+ sqlViewName: string,
+ trackName: string,
+ sliceColumns: SliceColumns,
+ argColumns: string[]) {
+ // QueryResultTab has successfully created a view corresponding to |uuid|.
+ // To prepare displaying it as a track, we materialize it and compute depths.
+ const debugTrackId = ++debugTrackCount;
+ const sqlTableName = `materialized_${debugTrackId}_${sqlViewName}`;
+ // TODO(altimin): Support removing this table when the track is closed.
+ await engine.query(`
+ create table ${sqlTableName} as
+ with prepared_data as (
+ select
+ row_number() over () as id,
+ ${sliceColumns.ts} as ts,
+ cast(${sliceColumns.dur} as int) as dur,
+ printf('%s', ${sliceColumns.name}) as name
+ ${argColumns.length > 0 ? ',' : ''}
+ ${argColumns.map((c) => `${c} as ${ARG_PREFIX}${c}`).join(',')}
+ from ${sqlViewName}
+ )
+ select
+ *,
+ internal_layout(ts, dur) over (
+ order by ${sliceColumns.ts}
+ rows between unbounded preceding and current row
+ ) as depth
+ from prepared_data
+ order by ts;`);
+
+ globals.dispatch(Actions.addDebugTrack({
+ engineId: engine.engineId,
+ name: trackName.trim() || `Debug Track ${debugTrackId}`,
+ config: {
+ sqlTableName,
+ columns: sliceColumns,
+ },
+ }));
+}
diff --git a/ui/src/tracks/debug_slices/index.ts b/ui/src/tracks/debug_slices/index.ts
deleted file mode 100644
index 46c1fdd..0000000
--- a/ui/src/tracks/debug_slices/index.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-// 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 m from 'mithril';
-
-import {Actions} from '../../common/actions';
-import {PluginContext} from '../../common/plugin_api';
-import {NUM, NUM_NULL, STR} from '../../common/query_result';
-import {fromNs, toNs} from '../../common/time';
-import {
- TrackController,
-} from '../../controller/track_controller';
-import {globals} from '../../frontend/globals';
-import {NewTrackArgs, Track} from '../../frontend/track';
-import {TrackButton, TrackButtonAttrs} from '../../frontend/track_panel';
-import {ChromeSliceTrack} from '../chrome_slices';
-
-export const DEBUG_SLICE_TRACK_KIND = 'DebugSliceTrack';
-
-export interface Config {
- maxDepth: number;
-}
-
-import {Data} from '../chrome_slices';
-export {Data} from '../chrome_slices';
-
-class DebugSliceTrackController extends TrackController<Config, Data> {
- static readonly kind = DEBUG_SLICE_TRACK_KIND;
-
- async onReload() {
- const rawResult = await this.query(
- `select ifnull(max(depth), 1) as maxDepth from debug_slices`);
- const maxDepth = rawResult.firstRow({maxDepth: NUM}).maxDepth;
- globals.dispatch(
- Actions.updateTrackConfig({id: this.trackId, config: {maxDepth}}));
- }
-
- async onBoundsChange(start: number, end: number, resolution: number):
- Promise<Data> {
- const queryRes = await this.query(`select
- ifnull(id, -1) as id,
- CAST(ifnull(name, '[null]') AS text) as name,
- ts,
- iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur) as dur,
- ifnull(depth, 0) as depth
- from debug_slices
- where (ts + dur) >= ${toNs(start)} and ts <= ${toNs(end)}`);
-
- const numRows = queryRes.numRows();
-
- const slices: Data = {
- start,
- end,
- resolution,
- length: numRows,
- strings: [],
- sliceIds: new Float64Array(numRows),
- starts: new Float64Array(numRows),
- ends: new Float64Array(numRows),
- depths: new Uint16Array(numRows),
- titles: new Uint16Array(numRows),
- isInstant: new Uint16Array(numRows),
- isIncomplete: new Uint16Array(numRows),
- };
-
- const stringIndexes = new Map<string, number>();
- function internString(str: string) {
- let idx = stringIndexes.get(str);
- if (idx !== undefined) return idx;
- idx = slices.strings.length;
- slices.strings.push(str);
- stringIndexes.set(str, idx);
- return idx;
- }
-
- const it = queryRes.iter(
- {id: NUM, name: STR, ts: NUM_NULL, dur: NUM_NULL, depth: NUM});
- for (let row = 0; it.valid(); it.next(), row++) {
- let sliceStart: number;
- let sliceEnd: number;
- if (it.ts === null || it.dur === null) {
- sliceStart = sliceEnd = -1;
- } else {
- sliceStart = it.ts;
- sliceEnd = sliceStart + it.dur;
- }
- slices.sliceIds[row] = it.id;
- slices.starts[row] = fromNs(sliceStart);
- slices.ends[row] = fromNs(sliceEnd);
- slices.depths[row] = it.depth;
- const sliceName = it.name;
- slices.titles[row] = internString(sliceName);
- slices.isInstant[row] = 0;
- slices.isIncomplete[row] = 0;
- }
-
- return slices;
- }
-}
-
-export class DebugSliceTrack extends ChromeSliceTrack {
- static readonly kind = DEBUG_SLICE_TRACK_KIND;
- static create(args: NewTrackArgs): Track {
- return new DebugSliceTrack(args);
- }
-
- getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
- const buttons: Array<m.Vnode<TrackButtonAttrs>> = [];
- buttons.push(m(TrackButton, {
- action: () => {
- globals.dispatch(Actions.requestTrackReload({}));
- },
- i: 'refresh',
- tooltip: 'Refresh tracks',
- showButton: true,
- }));
- buttons.push(m(TrackButton, {
- action: () => {
- globals.dispatch(Actions.removeDebugTrack({}));
- },
- i: 'close',
- tooltip: 'Close',
- showButton: true,
- }));
- return buttons;
- }
-}
-
-function activate(ctx: PluginContext) {
- ctx.registerTrack(DebugSliceTrack);
- ctx.registerTrackController(DebugSliceTrackController);
-}
-
-export const plugin = {
- pluginId: 'perfetto.DebugSlices',
- activate,
-};