Merge "tp: Add RegisterSqlPackage() and start sunsetting RegisterSqlModule()" into main
diff --git a/Android.bp b/Android.bp
index 0efdda3..2341f3b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -2467,6 +2467,8 @@
":perfetto_src_trace_processor_export_json",
":perfetto_src_trace_processor_importers_android_bugreport_android_bugreport",
":perfetto_src_trace_processor_importers_android_bugreport_android_log_event",
+ ":perfetto_src_trace_processor_importers_art_method_art_method",
+ ":perfetto_src_trace_processor_importers_art_method_art_method_event",
":perfetto_src_trace_processor_importers_common_common",
":perfetto_src_trace_processor_importers_common_parser_types",
":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
@@ -12394,6 +12396,20 @@
],
}
+// GN: //src/trace_processor/importers/art_method:art_method
+filegroup {
+ name: "perfetto_src_trace_processor_importers_art_method_art_method",
+ srcs: [
+ "src/trace_processor/importers/art_method/art_method_parser_impl.cc",
+ "src/trace_processor/importers/art_method/art_method_tokenizer.cc",
+ ],
+}
+
+// GN: //src/trace_processor/importers/art_method:art_method_event
+filegroup {
+ name: "perfetto_src_trace_processor_importers_art_method_art_method_event",
+}
+
// GN: //src/trace_processor/importers/common:common
filegroup {
name: "perfetto_src_trace_processor_importers_common_common",
@@ -15583,6 +15599,8 @@
":perfetto_src_trace_processor_importers_android_bugreport_android_bugreport",
":perfetto_src_trace_processor_importers_android_bugreport_android_log_event",
":perfetto_src_trace_processor_importers_android_bugreport_unittests",
+ ":perfetto_src_trace_processor_importers_art_method_art_method",
+ ":perfetto_src_trace_processor_importers_art_method_art_method_event",
":perfetto_src_trace_processor_importers_common_common",
":perfetto_src_trace_processor_importers_common_parser_types",
":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
@@ -16653,6 +16671,8 @@
":perfetto_src_trace_processor_export_json",
":perfetto_src_trace_processor_importers_android_bugreport_android_bugreport",
":perfetto_src_trace_processor_importers_android_bugreport_android_log_event",
+ ":perfetto_src_trace_processor_importers_art_method_art_method",
+ ":perfetto_src_trace_processor_importers_art_method_art_method_event",
":perfetto_src_trace_processor_importers_common_common",
":perfetto_src_trace_processor_importers_common_parser_types",
":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
@@ -16901,6 +16921,7 @@
":perfetto_src_trace_processor_db_compare",
":perfetto_src_trace_processor_db_minimal",
":perfetto_src_trace_processor_importers_android_bugreport_android_log_event",
+ ":perfetto_src_trace_processor_importers_art_method_art_method_event",
":perfetto_src_trace_processor_importers_common_common",
":perfetto_src_trace_processor_importers_common_parser_types",
":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
@@ -17072,6 +17093,8 @@
":perfetto_src_trace_processor_export_json",
":perfetto_src_trace_processor_importers_android_bugreport_android_bugreport",
":perfetto_src_trace_processor_importers_android_bugreport_android_log_event",
+ ":perfetto_src_trace_processor_importers_art_method_art_method",
+ ":perfetto_src_trace_processor_importers_art_method_art_method_event",
":perfetto_src_trace_processor_importers_common_common",
":perfetto_src_trace_processor_importers_common_parser_types",
":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
diff --git a/BUILD b/BUILD
index 5a733b4..4e69402 100644
--- a/BUILD
+++ b/BUILD
@@ -223,6 +223,8 @@
":src_trace_processor_export_json",
":src_trace_processor_importers_android_bugreport_android_bugreport",
":src_trace_processor_importers_android_bugreport_android_log_event",
+ ":src_trace_processor_importers_art_method_art_method",
+ ":src_trace_processor_importers_art_method_art_method_event",
":src_trace_processor_importers_common_common",
":src_trace_processor_importers_common_parser_types",
":src_trace_processor_importers_common_trace_parser_hdr",
@@ -1515,6 +1517,25 @@
],
)
+# GN target: //src/trace_processor/importers/art_method:art_method
+perfetto_filegroup(
+ name = "src_trace_processor_importers_art_method_art_method",
+ srcs = [
+ "src/trace_processor/importers/art_method/art_method_parser_impl.cc",
+ "src/trace_processor/importers/art_method/art_method_parser_impl.h",
+ "src/trace_processor/importers/art_method/art_method_tokenizer.cc",
+ "src/trace_processor/importers/art_method/art_method_tokenizer.h",
+ ],
+)
+
+# GN target: //src/trace_processor/importers/art_method:art_method_event
+perfetto_filegroup(
+ name = "src_trace_processor_importers_art_method_art_method_event",
+ srcs = [
+ "src/trace_processor/importers/art_method/art_method_event.h",
+ ],
+)
+
# GN target: //src/trace_processor/importers/common:common
perfetto_filegroup(
name = "src_trace_processor_importers_common_common",
@@ -6357,6 +6378,8 @@
":src_trace_processor_export_json",
":src_trace_processor_importers_android_bugreport_android_bugreport",
":src_trace_processor_importers_android_bugreport_android_log_event",
+ ":src_trace_processor_importers_art_method_art_method",
+ ":src_trace_processor_importers_art_method_art_method_event",
":src_trace_processor_importers_common_common",
":src_trace_processor_importers_common_parser_types",
":src_trace_processor_importers_common_trace_parser_hdr",
@@ -6556,6 +6579,8 @@
":src_trace_processor_export_json",
":src_trace_processor_importers_android_bugreport_android_bugreport",
":src_trace_processor_importers_android_bugreport_android_log_event",
+ ":src_trace_processor_importers_art_method_art_method",
+ ":src_trace_processor_importers_art_method_art_method_event",
":src_trace_processor_importers_common_common",
":src_trace_processor_importers_common_parser_types",
":src_trace_processor_importers_common_trace_parser_hdr",
@@ -6812,6 +6837,8 @@
":src_trace_processor_export_json",
":src_trace_processor_importers_android_bugreport_android_bugreport",
":src_trace_processor_importers_android_bugreport_android_log_event",
+ ":src_trace_processor_importers_art_method_art_method",
+ ":src_trace_processor_importers_art_method_art_method_event",
":src_trace_processor_importers_common_common",
":src_trace_processor_importers_common_parser_types",
":src_trace_processor_importers_common_trace_parser_hdr",
diff --git a/python/tools/check_ratchet.py b/python/tools/check_ratchet.py
index 573e6a3..0c845d2 100755
--- a/python/tools/check_ratchet.py
+++ b/python/tools/check_ratchet.py
@@ -37,7 +37,7 @@
from dataclasses import dataclass
EXPECTED_ANY_COUNT = 59
-EXPECTED_RUN_METRIC_COUNT = 5
+EXPECTED_RUN_METRIC_COUNT = 4
ROOT_DIR = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index 331634c..89c5ee8 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -168,6 +168,7 @@
"../protozero",
"db",
"importers/android_bugreport",
+ "importers/art_method",
"importers/common",
"importers/etw:full",
"importers/ftrace:full",
diff --git a/src/trace_processor/forwarding_trace_parser.cc b/src/trace_processor/forwarding_trace_parser.cc
index 4200209..babde0c 100644
--- a/src/trace_processor/forwarding_trace_parser.cc
+++ b/src/trace_processor/forwarding_trace_parser.cc
@@ -68,6 +68,7 @@
case kZipFile:
case kAndroidLogcatTraceType:
case kGeckoTraceType:
+ case kArtMethodTraceType:
return TraceSorter::SortingMode::kFullSort;
case kProtoTraceType:
diff --git a/src/trace_processor/importers/art_method/BUILD.gn b/src/trace_processor/importers/art_method/BUILD.gn
new file mode 100644
index 0000000..57f6c62
--- /dev/null
+++ b/src/trace_processor/importers/art_method/BUILD.gn
@@ -0,0 +1,45 @@
+# Copyright (C) 2024 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("../../../../gn/test.gni")
+
+source_set("art_method_event") {
+ sources = [ "art_method_event.h" ]
+ deps = [
+ "../../../../gn:default_deps",
+ "../../containers",
+ ]
+}
+
+source_set("art_method") {
+ sources = [
+ "art_method_parser_impl.cc",
+ "art_method_parser_impl.h",
+ "art_method_tokenizer.cc",
+ "art_method_tokenizer.h",
+ ]
+ deps = [
+ ":art_method_event",
+ "../../../../gn:default_deps",
+ "../../../../protos/perfetto/common:zero",
+ "../../../base",
+ "../../containers",
+ "../../importers/common",
+ "../../sorter",
+ "../../storage",
+ "../../types",
+ "../../util",
+ "../../util:trace_blob_view_reader",
+ ]
+}
diff --git a/src/trace_processor/importers/art_method/art_method_event.h b/src/trace_processor/importers/art_method/art_method_event.h
new file mode 100644
index 0000000..1a5edb2
--- /dev/null
+++ b/src/trace_processor/importers/art_method/art_method_event.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_EVENT_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_EVENT_H_
+
+#include <cstdint>
+#include <optional>
+
+#include "src/trace_processor/containers/string_pool.h"
+
+namespace perfetto::trace_processor::art_method {
+
+struct alignas(8) ArtMethodEvent {
+ uint32_t tid;
+ StringPool::Id method;
+ enum { kEnter, kExit } action;
+ std::optional<StringPool::Id> pathname;
+ std::optional<uint32_t> line_number;
+};
+
+} // namespace perfetto::trace_processor::art_method
+
+#endif // SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_EVENT_H_
diff --git a/src/trace_processor/importers/art_method/art_method_parser_impl.cc b/src/trace_processor/importers/art_method/art_method_parser_impl.cc
new file mode 100644
index 0000000..f80d2f4
--- /dev/null
+++ b/src/trace_processor/importers/art_method/art_method_parser_impl.cc
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/art_method/art_method_parser_impl.h"
+
+#include <cstdint>
+
+#include "src/trace_processor/importers/art_method/art_method_event.h"
+#include "src/trace_processor/importers/common/args_tracker.h"
+#include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/slice_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/importers/common/track_tracker.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/types/variadic.h"
+
+namespace perfetto::trace_processor::art_method {
+
+ArtMethodParserImpl::ArtMethodParserImpl(TraceProcessorContext* context)
+ : context_(context),
+ pathname_id_(context->storage->InternString("pathname")),
+ line_number_id_(context->storage->InternString("line_number")) {}
+
+ArtMethodParserImpl::~ArtMethodParserImpl() = default;
+
+void ArtMethodParserImpl::ParseArtMethodEvent(int64_t ts, ArtMethodEvent e) {
+ UniqueTid utid = context_->process_tracker->GetOrCreateThread(e.tid);
+ TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
+ switch (e.action) {
+ case ArtMethodEvent::kEnter:
+ context_->slice_tracker->Begin(
+ ts, track_id, kNullStringId, e.method,
+ [this, &e](ArgsTracker::BoundInserter* i) {
+ if (e.pathname) {
+ i->AddArg(pathname_id_, Variadic::String(*e.pathname));
+ }
+ if (e.line_number) {
+ i->AddArg(line_number_id_, Variadic::Integer(*e.line_number));
+ }
+ });
+ break;
+ case ArtMethodEvent::kExit:
+ context_->slice_tracker->End(ts, track_id);
+ break;
+ }
+}
+
+} // namespace perfetto::trace_processor::art_method
diff --git a/src/trace_processor/importers/art_method/art_method_parser_impl.h b/src/trace_processor/importers/art_method/art_method_parser_impl.h
new file mode 100644
index 0000000..09759cd
--- /dev/null
+++ b/src/trace_processor/importers/art_method/art_method_parser_impl.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_PARSER_IMPL_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_PARSER_IMPL_H_
+
+#include <cstdint>
+
+#include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/importers/art_method/art_method_event.h"
+#include "src/trace_processor/importers/common/trace_parser.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::art_method {
+
+class ArtMethodParserImpl : public ArtMethodParser {
+ public:
+ explicit ArtMethodParserImpl(TraceProcessorContext*);
+ ~ArtMethodParserImpl() override;
+
+ void ParseArtMethodEvent(int64_t ts, ArtMethodEvent) override;
+
+ private:
+ TraceProcessorContext* const context_;
+
+ StringPool::Id pathname_id_;
+ StringPool::Id line_number_id_;
+};
+
+} // namespace perfetto::trace_processor::art_method
+
+#endif // SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_PARSER_IMPL_H_
diff --git a/src/trace_processor/importers/art_method/art_method_tokenizer.cc b/src/trace_processor/importers/art_method/art_method_tokenizer.cc
new file mode 100644
index 0000000..fd32c78
--- /dev/null
+++ b/src/trace_processor/importers/art_method/art_method_tokenizer.cc
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/art_method/art_method_tokenizer.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <utility>
+
+#include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/ext/base/utils.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/art_method/art_method_event.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/status_macros.h"
+#include "src/trace_processor/util/trace_blob_view_reader.h"
+
+#include "protos/perfetto/common/builtin_clock.pbzero.h"
+
+namespace perfetto::trace_processor::art_method {
+namespace {
+
+constexpr uint32_t kTraceMagic = 0x574f4c53; // 'SLOW'
+
+std::string_view ReadLine(util::TraceBlobViewReader& reader,
+ util::TraceBlobViewReader::Iterator& it) {
+ size_t begin = it.file_offset();
+ if (!it.MaybeFindAndAdvance('\n')) {
+ return {};
+ }
+ auto x = reader.SliceOff(begin, it.file_offset() - begin);
+ std::string_view str(reinterpret_cast<const char*>(x->data()), x->size());
+ PERFETTO_CHECK(it.MaybeAdvance(1));
+ return str;
+}
+
+std::string ConstructPathname(const std::string& class_name,
+ const std::string& pathname) {
+ size_t index = class_name.rfind('/');
+ if (index != std::string::npos && base::EndsWith(pathname, ".java")) {
+ return class_name.substr(0, index + 1) + pathname;
+ }
+ return pathname;
+}
+
+uint64_t ToLong(const TraceBlobView& tbv) {
+ uint64_t x = 0;
+ memcpy(base::AssumeLittleEndian(&x), tbv.data(), tbv.size());
+ return x;
+}
+
+uint32_t ToInt(const TraceBlobView& tbv) {
+ uint32_t x = 0;
+ memcpy(base::AssumeLittleEndian(&x), tbv.data(), tbv.size());
+ return x;
+}
+
+uint16_t ToShort(const TraceBlobView& tbv) {
+ uint16_t x = 0;
+ memcpy(base::AssumeLittleEndian(&x), tbv.data(), tbv.size());
+ return x;
+}
+
+} // namespace
+
+ArtMethodTokenizer::ArtMethodTokenizer(TraceProcessorContext* ctx)
+ : context_(ctx) {}
+ArtMethodTokenizer::~ArtMethodTokenizer() = default;
+
+base::Status ArtMethodTokenizer::Parse(TraceBlobView blob) {
+ reader_.PushBack(std::move(blob));
+ auto it = reader_.GetIterator();
+ for (bool cnt = true; cnt;) {
+ switch (mode_) {
+ case kHeaderDetection: {
+ ASSIGN_OR_RETURN(cnt, ParseHeaderDetection(it));
+ break;
+ }
+ case kHeaderVersion: {
+ ASSIGN_OR_RETURN(cnt, ParseHeaderVersion(it));
+ break;
+ }
+ case kHeaderOptions: {
+ ASSIGN_OR_RETURN(cnt, ParseHeaderOptions(it));
+ break;
+ }
+ case kHeaderThreads: {
+ ASSIGN_OR_RETURN(cnt, ParseHeaderThreads(it));
+ break;
+ }
+ case kHeaderMethods: {
+ ASSIGN_OR_RETURN(cnt, ParseHeaderMethods(it));
+ break;
+ }
+ case kDataHeader: {
+ ASSIGN_OR_RETURN(cnt, ParseDataHeader(it));
+ break;
+ }
+ case kData: {
+ size_t s = it.file_offset();
+ for (size_t i = s;; i += record_size_) {
+ auto record = reader_.SliceOff(i, record_size_);
+ if (!record) {
+ PERFETTO_CHECK(it.MaybeAdvance(i - s));
+ cnt = false;
+ break;
+ }
+
+ ArtMethodEvent evt{};
+ evt.tid = version_ == 1 ? record->data()[0]
+ : ToShort(record->slice_off(0, 2));
+ uint32_t methodid_action = ToInt(record->slice_off(2, 4));
+ uint32_t ts_delta = clock_ == kDual ? ToInt(record->slice_off(10, 4))
+ : ToInt(record->slice_off(6, 4));
+
+ uint32_t action = methodid_action & 0x03;
+ uint32_t method_id = methodid_action & ~0x03u;
+
+ const auto& m = method_map_[method_id];
+ evt.method = m.name;
+ evt.pathname = m.pathname;
+ evt.line_number = m.line_number;
+ switch (action) {
+ case 0:
+ evt.action = ArtMethodEvent::kEnter;
+ break;
+ case 1:
+ case 2:
+ evt.action = ArtMethodEvent::kExit;
+ break;
+ }
+ ASSIGN_OR_RETURN(int64_t ts,
+ context_->clock_tracker->ToTraceTime(
+ protos::pbzero::BUILTIN_CLOCK_MONOTONIC,
+ (ts_ + ts_delta) * 1000));
+ context_->sorter->PushArtMethodEvent(ts, evt);
+ }
+ break;
+ }
+ }
+ }
+ reader_.PopFrontUntil(it.file_offset());
+ return base::OkStatus();
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::ParseHeaderDetection(Iterator& it) {
+ auto smagic = reader_.SliceOff(it.file_offset(), 4);
+ if (!smagic) {
+ return false;
+ }
+ uint32_t magic = ToInt(*smagic);
+ if (magic == kTraceMagic) {
+ return base::ErrStatus(
+ "ART Method trace is in streaming format: this is not supported");
+ }
+ auto line = ReadLine(reader_, it);
+ if (line.empty()) {
+ return false;
+ }
+ context_->clock_tracker->SetTraceTimeClock(
+ protos::pbzero::BUILTIN_CLOCK_MONOTONIC);
+ RETURN_IF_ERROR(ParseHeaderSectionLine(line));
+ return true;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::ParseHeaderVersion(Iterator& it) {
+ auto version_str = ReadLine(reader_, it);
+ if (version_str.empty()) {
+ return false;
+ }
+ auto version = base::StringToInt32(std::string(version_str));
+ if (!version || *version < 1 || *version > 3) {
+ return base::ErrStatus("ART Method trace: trace version (%s) not supported",
+ std::string(version_str).c_str());
+ }
+ version_ = static_cast<uint32_t>(*version);
+ mode_ = kHeaderOptions;
+ return true;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::ParseHeaderOptions(Iterator& it) {
+ for (auto l = ReadLine(reader_, it); !l.empty(); l = ReadLine(reader_, it)) {
+ if (l[0] == '*') {
+ RETURN_IF_ERROR(ParseHeaderSectionLine(l));
+ return true;
+ }
+ auto res = base::SplitString(std::string(l), "=");
+ if (res.size() != 2) {
+ return base::ErrStatus("ART method tracing: unable to parse option");
+ }
+ if (res[0] == "clock") {
+ if (res[1] == "dual") {
+ clock_ = kDual;
+ } else if (res[1] == "wall") {
+ clock_ = kWall;
+ } else if (res[1] == "thread-cpu") {
+ return base::ErrStatus(
+ "ART method tracing: thread-cpu clock is *not* supported. Use wall "
+ "or dual clocks");
+ } else {
+ return base::ErrStatus("ART method tracing: unknown clock %s",
+ res[1].c_str());
+ }
+ }
+ }
+ return false;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::ParseHeaderThreads(Iterator& it) {
+ for (auto l = ReadLine(reader_, it); !l.empty(); l = ReadLine(reader_, it)) {
+ if (l[0] == '*') {
+ RETURN_IF_ERROR(ParseHeaderSectionLine(l));
+ return true;
+ }
+ }
+ return false;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::ParseHeaderMethods(Iterator& it) {
+ for (auto l = ReadLine(reader_, it); !l.empty(); l = ReadLine(reader_, it)) {
+ if (l[0] == '*') {
+ RETURN_IF_ERROR(ParseHeaderSectionLine(l));
+ return true;
+ }
+ auto tokens = base::SplitString(std::string(l), "\t");
+ auto id = base::StringToUInt32(tokens[0], 16);
+ if (!id) {
+ return base::ErrStatus(
+ "ART method trace: unable to parse method id as integer: %s",
+ tokens[0].c_str());
+ }
+
+ std::string class_name = tokens[1];
+ std::string method_name;
+ std::string signature;
+ std::optional<StringId> pathname;
+ std::optional<uint32_t> line_number;
+ if (tokens.size() == 6) {
+ method_name = tokens[2];
+ signature = tokens[3];
+ pathname = context_->storage->InternString(
+ base::StringView(ConstructPathname(class_name, tokens[4])));
+ line_number = base::StringToUInt32(tokens[5]);
+ } else if (tokens.size() > 2) {
+ if (base::StartsWith(tokens[3], "(")) {
+ method_name = tokens[2];
+ signature = tokens[3];
+ if (tokens.size() >= 5) {
+ pathname =
+ context_->storage->InternString(base::StringView(tokens[4]));
+ }
+ } else {
+ pathname = context_->storage->InternString(base::StringView(tokens[2]));
+ line_number = base::StringToUInt32(tokens[3]);
+ }
+ }
+ base::StackString<2048> slice_name("%s.%s: %s", class_name.c_str(),
+ method_name.c_str(), signature.c_str());
+ method_map_[*id] = {
+ context_->storage->InternString(slice_name.string_view()),
+ pathname,
+ line_number,
+ };
+ }
+ return false;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::ParseDataHeader(Iterator& it) {
+ size_t begin = it.file_offset();
+ if (!it.MaybeAdvance(32)) {
+ return false;
+ }
+ auto header = reader_.SliceOff(begin, it.file_offset() - begin);
+ uint32_t magic = ToInt(header->slice_off(0, 4));
+ if (magic != kTraceMagic) {
+ return base::ErrStatus("ART Method trace: expected pre-data magic");
+ }
+ uint16_t version = ToShort(header->slice_off(4, 2));
+ if (version != version_) {
+ return base::ErrStatus(
+ "ART Method trace: trace version does not match data version");
+ }
+ ts_ = static_cast<int64_t>(ToLong(header->slice_off(8, 8)));
+ switch (version_) {
+ case 1:
+ record_size_ = 9;
+ break;
+ case 2:
+ record_size_ = 10;
+ break;
+ case 3:
+ record_size_ = ToShort(header->slice_off(16, 2));
+ break;
+ default:
+ PERFETTO_FATAL("Illegal version %u", version_);
+ }
+ mode_ = kData;
+ return true;
+}
+
+base::Status ArtMethodTokenizer::ParseHeaderSectionLine(std::string_view line) {
+ if (line == "*version") {
+ mode_ = kHeaderVersion;
+ return base::OkStatus();
+ }
+ if (line == "*threads") {
+ mode_ = kHeaderThreads;
+ return base::OkStatus();
+ }
+ if (line == "*methods") {
+ mode_ = kHeaderMethods;
+ return base::OkStatus();
+ }
+ if (line == "*end") {
+ mode_ = kDataHeader;
+ return base::OkStatus();
+ }
+ return base::ErrStatus(
+ "ART Method trace: unexpected line (%s) when expecting section header "
+ "(line starting with *)",
+ std::string(line).c_str());
+}
+
+base::Status ArtMethodTokenizer::NotifyEndOfFile() {
+ // DNS: also add a check here for whether our state machine reached the end
+ // too.
+ if (!reader_.empty() || mode_ != kData) {
+ return base::ErrStatus("ART Method trace: trace is incomplete");
+ }
+ return base::OkStatus();
+}
+
+} // namespace perfetto::trace_processor::art_method
diff --git a/src/trace_processor/importers/art_method/art_method_tokenizer.h b/src/trace_processor/importers/art_method/art_method_tokenizer.h
new file mode 100644
index 0000000..91a0eed
--- /dev/null
+++ b/src/trace_processor/importers/art_method/art_method_tokenizer.h
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_TOKENIZER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_TOKENIZER_H_
+
+#include <cstdint>
+#include <limits>
+#include <optional>
+#include <string_view>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/trace_blob_view_reader.h"
+
+namespace perfetto::trace_processor::art_method {
+
+class ArtMethodTokenizer : public ChunkedTraceReader {
+ public:
+ explicit ArtMethodTokenizer(TraceProcessorContext*);
+ ~ArtMethodTokenizer() override;
+
+ base::Status Parse(TraceBlobView) override;
+ base::Status NotifyEndOfFile() override;
+
+ private:
+ using Iterator = util::TraceBlobViewReader::Iterator;
+ struct Method {
+ StringId name;
+ std::optional<StringId> pathname;
+ std::optional<uint32_t> line_number;
+ };
+
+ base::StatusOr<bool> ParseHeaderDetection(Iterator&);
+ base::StatusOr<bool> ParseHeaderVersion(Iterator&);
+ base::StatusOr<bool> ParseHeaderOptions(Iterator&);
+ base::StatusOr<bool> ParseHeaderThreads(Iterator&);
+ base::StatusOr<bool> ParseHeaderMethods(Iterator&);
+ base::StatusOr<bool> ParseDataHeader(Iterator&);
+
+ base::Status ParseHeaderSectionLine(std::string_view);
+
+ TraceProcessorContext* const context_;
+ util::TraceBlobViewReader reader_;
+ enum {
+ kHeaderDetection,
+ kHeaderVersion,
+ kHeaderOptions,
+ kHeaderThreads,
+ kHeaderMethods,
+ kDataHeader,
+ kData,
+ } mode_ = kHeaderDetection;
+ enum {
+ kWall,
+ kDual,
+ } clock_ = kWall;
+
+ uint32_t version_ = std::numeric_limits<uint32_t>::max();
+ int64_t ts_ = std::numeric_limits<int64_t>::max();
+ uint32_t record_size_ = std::numeric_limits<uint32_t>::max();
+ base::FlatHashMap<uint32_t, Method> method_map_;
+};
+
+} // namespace perfetto::trace_processor::art_method
+
+#endif // SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_TOKENIZER_H_
diff --git a/src/trace_processor/importers/common/trace_parser.cc b/src/trace_processor/importers/common/trace_parser.cc
index 01345cc..12f3589 100644
--- a/src/trace_processor/importers/common/trace_parser.cc
+++ b/src/trace_processor/importers/common/trace_parser.cc
@@ -27,6 +27,7 @@
ProtoTraceParser::~ProtoTraceParser() = default;
SpeRecordParser::~SpeRecordParser() = default;
GeckoTraceParser::~GeckoTraceParser() = default;
+ArtMethodParser::~ArtMethodParser() = default;
} // namespace trace_processor
} // namespace perfetto
diff --git a/src/trace_processor/importers/common/trace_parser.h b/src/trace_processor/importers/common/trace_parser.h
index 34475d6..735d27a 100644
--- a/src/trace_processor/importers/common/trace_parser.h
+++ b/src/trace_processor/importers/common/trace_parser.h
@@ -30,6 +30,9 @@
namespace gecko_importer {
struct GeckoEvent;
}
+namespace art_method {
+struct ArtMethodEvent;
+}
struct AndroidLogEvent;
class PacketSequenceStateGeneration;
@@ -97,6 +100,12 @@
virtual void ParseGeckoEvent(int64_t, gecko_importer::GeckoEvent) = 0;
};
+class ArtMethodParser {
+ public:
+ virtual ~ArtMethodParser();
+ virtual void ParseArtMethodEvent(int64_t, art_method::ArtMethodEvent) = 0;
+};
+
} // namespace perfetto::trace_processor
#endif // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACE_PARSER_H_
diff --git a/src/trace_processor/importers/perf/spe_tokenizer.cc b/src/trace_processor/importers/perf/spe_tokenizer.cc
index 8186bbb..6c637b6 100644
--- a/src/trace_processor/importers/perf/spe_tokenizer.cc
+++ b/src/trace_processor/importers/perf/spe_tokenizer.cc
@@ -20,6 +20,7 @@
#include <cstring>
#include <memory>
#include <optional>
+#include <utility>
#include "perfetto/base/logging.h"
#include "perfetto/base/status.h"
@@ -56,7 +57,7 @@
}
bool SpeTokenizer::ProcessRecord() {
- for (auto it = buffer_.begin(); it;) {
+ for (auto it = buffer_.GetIterator(); it;) {
uint8_t byte_0 = *it;
// Must be true (we passed the for loop condition).
it.MaybeAdvance(1);
diff --git a/src/trace_processor/sorter/BUILD.gn b/src/trace_processor/sorter/BUILD.gn
index eb2da54..327d80b 100644
--- a/src/trace_processor/sorter/BUILD.gn
+++ b/src/trace_processor/sorter/BUILD.gn
@@ -30,6 +30,7 @@
"../../../include/perfetto/trace_processor:storage",
"../../base",
"../importers/android_bugreport:android_log_event",
+ "../importers/art_method:art_method_event",
"../importers/common:parser_types",
"../importers/common:trace_parser_hdr",
"../importers/fuchsia:fuchsia_record",
diff --git a/src/trace_processor/sorter/trace_sorter.cc b/src/trace_processor/sorter/trace_sorter.cc
index e2fc178..0ee3ef7 100644
--- a/src/trace_processor/sorter/trace_sorter.cc
+++ b/src/trace_processor/sorter/trace_sorter.cc
@@ -28,6 +28,7 @@
#include "perfetto/public/compiler.h"
#include "perfetto/trace_processor/trace_blob_view.h"
#include "src/trace_processor/importers/android_bugreport/android_log_event.h"
+#include "src/trace_processor/importers/art_method/art_method_event.h"
#include "src/trace_processor/importers/common/parser_types.h"
#include "src/trace_processor/importers/common/trace_parser.h"
#include "src/trace_processor/importers/fuchsia/fuchsia_record.h"
@@ -267,6 +268,10 @@
context.gecko_trace_parser->ParseGeckoEvent(
event.ts, token_buffer_.Extract<gecko_importer::GeckoEvent>(id));
return;
+ case TimestampedEvent::Type::kArtMethodEvent:
+ context.art_method_parser->ParseArtMethodEvent(
+ event.ts, token_buffer_.Extract<art_method::ArtMethodEvent>(id));
+ return;
case TimestampedEvent::Type::kInlineSchedSwitch:
case TimestampedEvent::Type::kInlineSchedWaking:
case TimestampedEvent::Type::kEtwEvent:
@@ -300,6 +305,7 @@
case TimestampedEvent::Type::kAndroidLogEvent:
case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
case TimestampedEvent::Type::kGeckoEvent:
+ case TimestampedEvent::Type::kArtMethodEvent:
PERFETTO_FATAL("Invalid event type");
}
PERFETTO_FATAL("For GCC");
@@ -335,6 +341,7 @@
case TimestampedEvent::Type::kAndroidLogEvent:
case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
case TimestampedEvent::Type::kGeckoEvent:
+ case TimestampedEvent::Type::kArtMethodEvent:
PERFETTO_FATAL("Invalid event type");
}
PERFETTO_FATAL("For GCC");
@@ -389,6 +396,10 @@
base::ignore_result(
token_buffer_.Extract<gecko_importer::GeckoEvent>(id));
return;
+ case TimestampedEvent::Type::kArtMethodEvent:
+ base::ignore_result(
+ token_buffer_.Extract<art_method::ArtMethodEvent>(id));
+ return;
}
PERFETTO_FATAL("For GCC");
}
diff --git a/src/trace_processor/sorter/trace_sorter.h b/src/trace_processor/sorter/trace_sorter.h
index 1ff3d18..4cb7951 100644
--- a/src/trace_processor/sorter/trace_sorter.h
+++ b/src/trace_processor/sorter/trace_sorter.h
@@ -35,6 +35,7 @@
#include "perfetto/trace_processor/ref_counted.h"
#include "perfetto/trace_processor/trace_blob_view.h"
#include "src/trace_processor/importers/android_bugreport/android_log_event.h"
+#include "src/trace_processor/importers/art_method/art_method_event.h"
#include "src/trace_processor/importers/common/parser_types.h"
#include "src/trace_processor/importers/common/trace_parser.h"
#include "src/trace_processor/importers/fuchsia/fuchsia_record.h"
@@ -247,11 +248,18 @@
}
inline void PushGeckoEvent(int64_t timestamp,
- gecko_importer::GeckoEvent event) {
- TraceTokenBuffer::Id id = token_buffer_.Append(std::move(event));
+ const gecko_importer::GeckoEvent& event) {
+ TraceTokenBuffer::Id id = token_buffer_.Append(event);
AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kGeckoEvent, id);
}
+ inline void PushArtMethodEvent(int64_t timestamp,
+ const art_method::ArtMethodEvent& event) {
+ TraceTokenBuffer::Id id = token_buffer_.Append(event);
+ AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kArtMethodEvent,
+ id);
+ }
+
inline void PushInlineFtraceEvent(
uint32_t cpu,
int64_t timestamp,
@@ -333,11 +341,12 @@
kTracePacket,
kTrackEvent,
kGeckoEvent,
+ kArtMethodEvent,
kMax = kGeckoEvent,
};
// Number of bits required to store the max element in |Type|.
- static constexpr uint32_t kMaxTypeBits = 4;
+ static constexpr uint32_t kMaxTypeBits = 6;
static_assert(static_cast<uint8_t>(Type::kMax) <= (1 << kMaxTypeBits),
"Max type does not fit inside storage");
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index 743a6a2..b7f013c 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -46,6 +46,8 @@
#include "perfetto/trace_processor/trace_processor.h"
#include "src/trace_processor/importers/android_bugreport/android_log_event_parser_impl.h"
#include "src/trace_processor/importers/android_bugreport/android_log_reader.h"
+#include "src/trace_processor/importers/art_method/art_method_parser_impl.h"
+#include "src/trace_processor/importers/art_method/art_method_tokenizer.h"
#include "src/trace_processor/importers/common/clock_tracker.h"
#include "src/trace_processor/importers/common/trace_file_tracker.h"
#include "src/trace_processor/importers/common/trace_parser.h"
@@ -442,6 +444,11 @@
#endif
}
+ context_.reader_registry->RegisterTraceReader<art_method::ArtMethodTokenizer>(
+ kArtMethodTraceType);
+ context_.art_method_parser =
+ std::make_unique<art_method::ArtMethodParserImpl>(&context_);
+
if (context_.config.analyze_trace_proto_content) {
context_.content_analyzer =
std::make_unique<ProtoContentAnalyzer>(&context_);
diff --git a/src/trace_processor/trace_reader_registry.cc b/src/trace_processor/trace_reader_registry.cc
index 4b1b6c3..30f5205 100644
--- a/src/trace_processor/trace_reader_registry.cc
+++ b/src/trace_processor/trace_reader_registry.cc
@@ -52,6 +52,7 @@
case kAndroidLogcatTraceType:
case kAndroidDumpstateTraceType:
case kGeckoTraceType:
+ case kArtMethodTraceType:
return false;
}
PERFETTO_FATAL("For GCC");
diff --git a/src/trace_processor/types/trace_processor_context.h b/src/trace_processor/types/trace_processor_context.h
index 84682eb..7a84bb9 100644
--- a/src/trace_processor/types/trace_processor_context.h
+++ b/src/trace_processor/types/trace_processor_context.h
@@ -31,6 +31,7 @@
class AndroidLogEventParser;
class ArgsTracker;
class ArgsTranslationTable;
+class ArtMethodParser;
class AsyncTrackSetTracker;
class ChunkedTraceReader;
class ClockConverter;
@@ -175,6 +176,7 @@
std::unique_ptr<InstrumentsRowParser> instruments_row_parser;
std::unique_ptr<AndroidLogEventParser> android_log_event_parser;
std::unique_ptr<GeckoTraceParser> gecko_trace_parser;
+ std::unique_ptr<ArtMethodParser> art_method_parser;
// This field contains the list of proto descriptors that can be used by
// reflection-based parsers.
diff --git a/src/trace_processor/util/bump_allocator.h b/src/trace_processor/util/bump_allocator.h
index 985b5bc..9e92471 100644
--- a/src/trace_processor/util/bump_allocator.h
+++ b/src/trace_processor/util/bump_allocator.h
@@ -54,7 +54,7 @@
public:
// The limit on the total number of bits which can be used to represent
// the chunk id.
- static constexpr uint64_t kMaxIdBits = 60;
+ static constexpr uint64_t kMaxIdBits = 58;
// The limit on the total amount of memory which can be allocated.
static constexpr uint64_t kAllocLimit = 1ull << kMaxIdBits;
diff --git a/src/trace_processor/util/trace_blob_view_reader.h b/src/trace_processor/util/trace_blob_view_reader.h
index 69e5aa3..158881f 100644
--- a/src/trace_processor/util/trace_blob_view_reader.h
+++ b/src/trace_processor/util/trace_blob_view_reader.h
@@ -20,6 +20,7 @@
#include <cstddef>
#include <cstdint>
#include <cstdio>
+#include <cstring>
#include <optional>
#include "perfetto/base/logging.h"
@@ -46,12 +47,56 @@
public:
class Iterator {
public:
- Iterator(const Iterator&) = default;
+ ~Iterator() = default;
+
+ Iterator(const Iterator&) = delete;
+ Iterator& operator=(const Iterator&) = delete;
+
Iterator(Iterator&&) = default;
- Iterator& operator=(const Iterator&) = default;
Iterator& operator=(Iterator&&) = default;
- ~Iterator() = default;
+ // Tries to advance the iterator |size| bytes forward. Returns true if
+ // the advance was successful and false if it would overflow the iterator.
+ // If false is returned, the state of the iterator is not changed.
+ bool MaybeAdvance(size_t delta) {
+ file_offset_ += delta;
+ if (PERFETTO_LIKELY(file_offset_ < iter_->end_offset())) {
+ return true;
+ }
+ if (file_offset_ == end_offset_) {
+ return true;
+ }
+ if (file_offset_ > end_offset_) {
+ file_offset_ -= delta;
+ return false;
+ }
+ do {
+ ++iter_;
+ } while (file_offset_ >= iter_->end_offset());
+ return true;
+ }
+
+ // Tries to find a byte equal to |chr| in the iterator and, if found,
+ // advance to it. Returns true if the byte was found and could be advanced
+ // to and false if no such byte was found before the end of the iterator. If
+ // false is returned, the state of the iterator is not changed.
+ bool MaybeFindAndAdvance(uint8_t chr) {
+ size_t off = file_offset_;
+ while (off < end_offset_) {
+ size_t iter_off = off - iter_->start_offset;
+ size_t iter_rem = iter_->data.size() - iter_off;
+ const auto* p = reinterpret_cast<const uint8_t*>(
+ memchr(iter_->data.data() + iter_off, chr, iter_rem));
+ if (p) {
+ file_offset_ =
+ iter_->start_offset + static_cast<size_t>(p - iter_->data.data());
+ return true;
+ }
+ off = iter_->end_offset();
+ ++iter_;
+ }
+ return false;
+ }
uint8_t operator*() const {
PERFETTO_DCHECK(file_offset_ < iter_->end_offset());
@@ -62,45 +107,20 @@
size_t file_offset() const { return file_offset_; }
- bool MaybeAdvance(size_t delta) {
- if (delta == 0) {
- return true;
- }
- if (delta > end_offset_ - file_offset_) {
- return false;
- }
- file_offset_ += delta;
- if (PERFETTO_LIKELY(file_offset_ < iter_->end_offset())) {
- return true;
- }
- while (file_offset_ > iter_->end_offset()) {
- ++iter_;
- }
- if (file_offset_ == iter_->end_offset()) {
- ++iter_;
- }
-
- return true;
- }
-
private:
friend TraceBlobViewReader;
Iterator(base::CircularQueue<Entry>::Iterator iter,
size_t file_offset,
size_t end_offset)
- : iter_(std::move(iter)),
- file_offset_(file_offset),
- end_offset_(end_offset) {}
+ : iter_(iter), file_offset_(file_offset), end_offset_(end_offset) {}
+
base::CircularQueue<Entry>::Iterator iter_;
size_t file_offset_;
size_t end_offset_;
};
- Iterator begin() const {
- return Iterator(data_.begin(), start_offset(), end_offset());
- }
- Iterator end() const {
- return Iterator(data_.end(), end_offset(), end_offset());
+ Iterator GetIterator() const {
+ return {data_.begin(), start_offset(), end_offset()};
}
// Adds a `TraceBlobView` at the back.
@@ -129,6 +149,7 @@
//
// NOTE: If `offset` < 'file_offset()' this method will CHECK fail.
std::optional<TraceBlobView> SliceOff(size_t offset, size_t length) const;
+
// Returns the offset to the start of the available data.
size_t start_offset() const {
return data_.empty() ? end_offset_ : data_.front().start_offset;
diff --git a/src/trace_processor/util/trace_type.cc b/src/trace_processor/util/trace_type.cc
index 62f586f..e5690bb 100644
--- a/src/trace_processor/util/trace_type.cc
+++ b/src/trace_processor/util/trace_type.cc
@@ -37,10 +37,9 @@
constexpr char kFuchsiaMagic[] = {'\x10', '\x00', '\x04', '\x46',
'\x78', '\x54', '\x16', '\x00'};
constexpr char kPerfMagic[] = {'P', 'E', 'R', 'F', 'I', 'L', 'E', '2'};
-
constexpr char kZipMagic[] = {'P', 'K', '\x03', '\x04'};
-
constexpr char kGzipMagic[] = {'\x1f', '\x8b'};
+constexpr char kArtMethodStreamingMagic[] = {'S', 'L', 'O', 'W'};
constexpr uint8_t kTracePacketTag =
protozero::proto_utils::MakeTagLengthDelimited(
@@ -130,6 +129,8 @@
return "android_bugreport";
case kGeckoTraceType:
return "gecko";
+ case kArtMethodTraceType:
+ return "art_method";
case kUnknownTraceType:
return "unknown";
}
@@ -157,6 +158,10 @@
return kGzipTraceType;
}
+ if (MatchesMagic(data, size, kArtMethodStreamingMagic)) {
+ return kArtMethodTraceType;
+ }
+
std::string start(reinterpret_cast<const char*>(data),
std::min<size_t>(size, kGuessTraceMaxLookahead));
@@ -168,6 +173,10 @@
if (base::StartsWith(start_minus_white_space, "[{\""))
return kJsonTraceType;
+ // ART method traces (non-streaming).
+ if (base::StartsWith(start, "*version\n"))
+ return kArtMethodTraceType;
+
// Systrace with header but no leading HTML.
if (base::Contains(start, "# tracer"))
return kSystraceTraceType;
diff --git a/src/trace_processor/util/trace_type.h b/src/trace_processor/util/trace_type.h
index 963e8d8..5a29c61 100644
--- a/src/trace_processor/util/trace_type.h
+++ b/src/trace_processor/util/trace_type.h
@@ -39,6 +39,7 @@
kZipFile,
kInstrumentsXmlTraceType,
kGeckoTraceType,
+ kArtMethodTraceType,
};
constexpr size_t kGuessTraceMaxLookahead = 64;
diff --git a/test/data/art-method-tracing.trace.sha256 b/test/data/art-method-tracing.trace.sha256
new file mode 100644
index 0000000..38910a6
--- /dev/null
+++ b/test/data/art-method-tracing.trace.sha256
@@ -0,0 +1 @@
+ebd46d41eaa4656ad06535dacc1d3c6f6018a180f89c546515fed4f7e1df2337
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index c9e7a10..f8183da 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -60,6 +60,7 @@
from diff_tests.parser.android.tests_surfaceflinger_transactions import SurfaceFlingerTransactions
from diff_tests.parser.android.tests_viewcapture import ViewCapture
from diff_tests.parser.android.tests_windowmanager import WindowManager
+from diff_tests.parser.art_method.tests import ArtMethodParser
from diff_tests.parser.atrace.tests import Atrace
from diff_tests.parser.atrace.tests_error_handling import AtraceErrorHandling
from diff_tests.parser.chrome.tests import ChromeParser
@@ -67,9 +68,9 @@
from diff_tests.parser.chrome.tests_v8 import ChromeV8Parser
from diff_tests.parser.cros.tests import Cros
from diff_tests.parser.fs.tests import Fs
-from diff_tests.parser.gecko.tests import GeckoParser
from diff_tests.parser.ftrace.ftrace_crop_tests import FtraceCrop
from diff_tests.parser.fuchsia.tests import Fuchsia
+from diff_tests.parser.gecko.tests import GeckoParser
from diff_tests.parser.graphics.tests import GraphicsParser
from diff_tests.parser.graphics.tests_drm_related_ftrace_events import GraphicsDrmRelatedFtraceEvents
from diff_tests.parser.graphics.tests_gpu_trace import GraphicsGpuTrace
@@ -241,7 +242,9 @@
'AndroidInputEvent').fetch(),
*Instruments(index_path, 'parser/instruments', 'Instruments').fetch(),
*Gzip(index_path, 'parser/gzip', 'Gzip').fetch(),
- *GeckoParser(index_path, 'parser/gecko', 'Gecko').fetch(),
+ *GeckoParser(index_path, 'parser/gecko', 'GeckoParser').fetch(),
+ *ArtMethodParser(index_path, 'parser/art_method',
+ 'ArtMethodParser').fetch(),
]
metrics_tests = [
diff --git a/test/trace_processor/diff_tests/parser/art_method/tests.py b/test/trace_processor/diff_tests/parser/art_method/tests.py
new file mode 100644
index 0000000..4074b0f
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/art_method/tests.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+# 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 a
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import DataPath
+from python.generators.diff_tests.testing import Csv
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class ArtMethodParser(TestSuite):
+
+ def test_art_method_smoke(self):
+ return DiffTestBlueprint(
+ trace=DataPath('art-method-tracing.trace'),
+ query="""
+ SELECT ts, dur, name, extract_arg(arg_set_id, 'pathname') AS pathname
+ FROM slice
+ LIMIT 10
+ """,
+ out=Csv('''
+ "ts","dur","name","pathname"
+ 430421819465000,-1,"com.android.internal.os.ZygoteInit.main: ([Ljava/lang/String;)V","ZygoteInit.java"
+ 430421819468000,-1,"com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run: ()V","RuntimeInit.java"
+ 430421819469000,-1,"java.lang.reflect.Method.invoke: (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;","Method.java"
+ 430421819472000,-1,"android.app.ActivityThread.main: ([Ljava/lang/String;)V","ActivityThread.java"
+ 430421819473000,-1,"android.os.Looper.loop: ()V","Looper.java"
+ 430421819473000,-1,"android.os.Looper.loopOnce: (Landroid/os/Looper;JI)Z","Looper.java"
+ 430421819475000,-1,"android.os.MessageQueue.next: ()Landroid/os/Message;","MessageQueue.java"
+ 430421819476000,-1,"android.os.MessageQueue.nativePollOnce: (JI)V","MessageQueue.java"
+ 430421819490000,-1,"java.lang.Thread.run: ()V","Thread.java"
+ 430421819508000,-1,"java.lang.Daemons$Daemon.run: ()V","Daemons.java"
+ '''))
diff --git a/ui/src/base/utils.ts b/ui/src/base/utils.ts
index f2e90a5..b0c0fc6 100644
--- a/ui/src/base/utils.ts
+++ b/ui/src/base/utils.ts
@@ -36,3 +36,22 @@
// Make field K required in T
export type RequiredField<T, K extends keyof T> = Omit<T, K> &
Required<Pick<T, K>>;
+
+// The lowest common denoninator between Map<> and WeakMap<>.
+// This is just to avoid duplication of the getOrCreate below.
+interface MapLike<K, V> {
+ get(key: K): V | undefined;
+ set(key: K, value: V): this;
+}
+
+export function getOrCreate<K, V>(
+ map: MapLike<K, V>,
+ key: K,
+ factory: () => V,
+): V {
+ let value = map.get(key);
+ if (value !== undefined) return value;
+ value = factory();
+ map.set(key, value);
+ return value;
+}
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index d025c23..3942f83 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -27,25 +27,6 @@
type StateDraft = Draft<State>;
-function clearTraceState(state: StateDraft) {
- const nextId = state.nextId;
- const recordConfig = state.recordConfig;
- const recordingTarget = state.recordingTarget;
- const fetchChromeCategories = state.fetchChromeCategories;
- const extensionInstalled = state.extensionInstalled;
- const availableAdbDevices = state.availableAdbDevices;
- const chromeCategories = state.chromeCategories;
-
- Object.assign(state, createEmptyState());
- state.nextId = nextId;
- state.recordConfig = recordConfig;
- state.recordingTarget = recordingTarget;
- state.fetchChromeCategories = fetchChromeCategories;
- state.extensionInstalled = extensionInstalled;
- state.availableAdbDevices = availableAdbDevices;
- state.chromeCategories = chromeCategories;
-}
-
function generateNextId(draft: StateDraft): string {
const nextId = String(Number(draft.nextId) + 1);
draft.nextId = nextId;
@@ -53,8 +34,27 @@
}
export const StateActions = {
+ clearState(state: StateDraft, _args: {}) {
+ const nextId = state.nextId;
+ const recordConfig = state.recordConfig;
+ const recordingTarget = state.recordingTarget;
+ const fetchChromeCategories = state.fetchChromeCategories;
+ const extensionInstalled = state.extensionInstalled;
+ const availableAdbDevices = state.availableAdbDevices;
+ const chromeCategories = state.chromeCategories;
+
+ Object.assign(state, createEmptyState());
+ state.nextId = nextId;
+ state.recordConfig = recordConfig;
+ state.recordingTarget = recordingTarget;
+ state.fetchChromeCategories = fetchChromeCategories;
+ state.extensionInstalled = extensionInstalled;
+ state.availableAdbDevices = availableAdbDevices;
+ state.chromeCategories = chromeCategories;
+ },
+
openTraceFromFile(state: StateDraft, args: {file: File}): void {
- clearTraceState(state);
+ this.clearState(state, {});
const id = generateNextId(state);
state.engine = {
id,
@@ -63,7 +63,7 @@
},
openTraceFromBuffer(state: StateDraft, args: PostedTrace): void {
- clearTraceState(state);
+ this.clearState(state, {});
const id = generateNextId(state);
state.engine = {
id,
@@ -75,7 +75,7 @@
state: StateDraft,
args: {url: string; serializedAppState?: SerializedAppState},
): void {
- clearTraceState(state);
+ this.clearState(state, {});
const id = generateNextId(state);
state.engine = {
id,
@@ -88,7 +88,7 @@
},
openTraceFromHttpRpc(state: StateDraft, _args: {}): void {
- clearTraceState(state);
+ this.clearState(state, {});
const id = generateNextId(state);
state.engine = {
id,
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
index c4ca69c..3cef09d 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/common/empty_state.ts
@@ -47,7 +47,6 @@
return {
version: STATE_VERSION,
nextId: '-1',
- queries: {},
recordConfig: AUTOLOAD_STARTED_CONFIG_FLAG.get()
? autosaveConfigStore.get()
@@ -55,8 +54,6 @@
displayConfigAsPbtxt: false,
lastLoadedConfig: {type: 'NONE'},
- traceConversionInProgress: false,
-
perfDebug: false,
sidebarVisible: true,
hoveredUtid: -1,
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 6173528..38cab82 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -174,8 +174,6 @@
debugTrackId?: string;
lastTrackReloadRequest?: number;
- queries: ObjectById<QueryConfig>;
- traceConversionInProgress: boolean;
flamegraphModalDismissed: boolean;
// Show track perf debugging overlay
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 0133f2b..037b63a 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -13,21 +13,19 @@
// limitations under the License.
import {assertExists, assertTrue} from '../base/logging';
-import {Duration, time, Time, TimeSpan} from '../base/time';
+import {time, Time, TimeSpan} from '../base/time';
import {Actions} from '../common/actions';
import {cacheTrace} from '../common/cache_manager';
import {
getEnabledMetatracingCategories,
isMetatracingEnabled,
} from '../common/metatracing';
-import {EngineConfig, PendingDeeplinkState} from '../common/state';
+import {EngineConfig} from '../common/state';
import {featureFlags, Flag} from '../core/feature_flags';
-import {globals, QuantizedLoad, ThreadDesc} from '../frontend/globals';
+import {globals, ThreadDesc} from '../frontend/globals';
import {
- clearOverviewData,
publishHasFtrace,
publishMetricError,
- publishOverviewData,
publishThreads,
} from '../frontend/publish';
import {addQueryResultsTab} from '../public/lib/query_table/query_result_tab';
@@ -62,6 +60,7 @@
import {TraceImpl} from '../core/trace_impl';
import {SerializedAppState} from '../public/state_serialization_schema';
import {TraceSource} from '../public/trace_source';
+import {RouteArgs} from '../core/route_schema';
type States = 'init' | 'loading_trace' | 'ready';
@@ -303,10 +302,8 @@
}
const traceDetails = await getTraceInfo(engine, traceSource);
- const trace = TraceImpl.newInstance(engine, traceDetails);
- await globals.onTraceLoad(trace);
-
- AppImpl.instance.omnibox.reset();
+ const trace = TraceImpl.createInstanceForCore(engine, traceDetails);
+ AppImpl.instance.setActiveTrace(trace);
const visibleTimeSpan = await computeVisibleTime(
traceDetails.start,
@@ -340,10 +337,6 @@
decideTabs(trace);
await listThreads(engine);
- await loadTimelineOverview(
- engine,
- new TimeSpan(traceDetails.start, traceDetails.end),
- );
{
// Check if we have any ftrace events at all
@@ -359,7 +352,7 @@
const pendingDeeplink = AppImpl.instance.getAndClearInitialRouteArgs();
if (pendingDeeplink !== undefined) {
- await selectPendingDeeplink(trace, pendingDeeplink);
+ await selectInitialRouteArgs(trace, pendingDeeplink);
if (
pendingDeeplink.visStart !== undefined &&
pendingDeeplink.visEnd !== undefined
@@ -407,12 +400,9 @@
return trace;
}
-async function selectPendingDeeplink(
- trace: TraceImpl,
- link: PendingDeeplinkState,
-) {
+async function selectInitialRouteArgs(trace: TraceImpl, args: RouteArgs) {
const conditions = [];
- const {ts, dur} = link;
+ const {ts, dur} = args;
if (ts !== undefined) {
conditions.push(`ts = ${ts}`);
@@ -500,86 +490,6 @@
publishThreads(threads);
}
-async function loadTimelineOverview(engine: Engine, trace: TimeSpan) {
- clearOverviewData();
- const stepSize = Duration.max(1n, trace.duration / 100n);
- const hasSchedSql = 'select ts from sched limit 1';
- const hasSchedOverview = (await engine.query(hasSchedSql)).numRows() > 0;
- if (hasSchedOverview) {
- const stepPromises = [];
- for (
- let start = trace.start;
- start < trace.end;
- start = Time.add(start, stepSize)
- ) {
- const progress = start - trace.start;
- const ratio = Number(progress) / Number(trace.duration);
- updateStatus('Loading overview ' + `${Math.round(ratio * 100)}%`);
- const end = Time.add(start, stepSize);
- // The (async() => {})() queues all the 100 async promises in one batch.
- // Without that, we would wait for each step to be rendered before
- // kicking off the next one. That would interleave an animation frame
- // between each step, slowing down significantly the overall process.
- stepPromises.push(
- (async () => {
- const schedResult = await engine.query(
- `select cast(sum(dur) as float)/${stepSize} as load, cpu from sched ` +
- `where ts >= ${start} and ts < ${end} and utid != 0 ` +
- 'group by cpu order by cpu',
- );
- const schedData: {[key: string]: QuantizedLoad} = {};
- const it = schedResult.iter({load: NUM, cpu: NUM});
- for (; it.valid(); it.next()) {
- const load = it.load;
- const cpu = it.cpu;
- schedData[cpu] = {start, end, load};
- }
- publishOverviewData(schedData);
- })(),
- );
- } // for(start = ...)
- await Promise.all(stepPromises);
- return;
- } // if (hasSchedOverview)
-
- // Slices overview.
- const sliceResult = await engine.query(`select
- bucket,
- upid,
- ifnull(sum(utid_sum) / cast(${stepSize} as float), 0) as load
- from thread
- inner join (
- select
- ifnull(cast((ts - ${trace.start})/${stepSize} as int), 0) as bucket,
- sum(dur) as utid_sum,
- utid
- from slice
- inner join thread_track on slice.track_id = thread_track.id
- group by bucket, utid
- ) using(utid)
- where upid is not null
- group by bucket, upid`);
-
- const slicesData: {[key: string]: QuantizedLoad[]} = {};
- const it = sliceResult.iter({bucket: LONG, upid: NUM, load: NUM});
- for (; it.valid(); it.next()) {
- const bucket = it.bucket;
- const upid = it.upid;
- const load = it.load;
-
- const start = Time.add(trace.start, stepSize * bucket);
- const end = Time.add(start, stepSize);
-
- const upidStr = upid.toString();
- let loadArray = slicesData[upidStr];
- if (loadArray === undefined) {
- loadArray = slicesData[upidStr] = [];
- }
- loadArray.push({start, end, load});
- }
- publishOverviewData(slicesData);
-}
-
async function initialiseHelperViews(engine: Engine) {
updateStatus('Creating annotation counter track table');
// Create the helper tables for all the annotations related data.
diff --git a/ui/src/core/app_impl.ts b/ui/src/core/app_impl.ts
index 77092be..8e0a815 100644
--- a/ui/src/core/app_impl.ts
+++ b/ui/src/core/app_impl.ts
@@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {assertTrue} from '../base/logging';
+import {assertExists, assertTrue} from '../base/logging';
import {App} from '../public/app';
-import {TraceContext, TraceImpl} from './trace_impl';
+import {TraceImpl} from './trace_impl';
import {CommandManagerImpl} from './command_manager';
import {OmniboxManagerImpl} from './omnibox_manager';
import {raf} from './raf_scheduler';
@@ -23,18 +23,30 @@
import {NewEngineMode} from '../trace_processor/engine';
import {RouteArgs} from './route_schema';
import {Optional} from '../base/utils';
+import {SqlPackage} from '../public/extra_sql_packages';
// The pseudo plugin id used for the core instance of AppImpl.
export const CORE_PLUGIN_ID = '__core__';
+// The args that frontend/index.ts passes when calling AppImpl.initialize().
+// This is to deal with injections that would otherwise cause circular deps.
+export interface AppInitArgs {
+ rootUrl: string;
+ initialRouteArgs: RouteArgs;
+
+ // TODO(primiano): remove once State is gone.
+ // This maps to globals.dispatch(Actions.clearState({})),
+ clearState: () => void;
+}
+
/**
* Handles the global state of the ui, for anything that is not related to a
* specific trace. This is always available even before a trace is loaded (in
* contrast to TraceContext, which is bound to the lifetime of a trace).
* There is only one instance in total of this class (see instance()).
- * This class is not exposed to anybody. Both core and plugins should access
- * this via AppImpl.
+ * This class is only exposed to TraceImpl, nobody else should refer to this
+ * and should use AppImpl instead.
*/
export class AppContext {
readonly commandMgr = new CommandManagerImpl();
@@ -43,18 +55,23 @@
readonly pluginMgr: PluginManager;
newEngineMode: NewEngineMode = 'USE_HTTP_RPC_IF_AVAILABLE';
initialRouteArgs?: RouteArgs = undefined;
+ isLoadingTrace = false; // Set when calling openTrace().
+ readonly initArgs: AppInitArgs;
- // The most recently created trace context. Can be undefined before any trace
- // is loaded.
- private traceCtx?: TraceContext;
+ // This is normally empty and is injected with extra google-internal packages
+ // via is_internal_user.js
+ extraSqlPackages: SqlPackage[] = [];
- // There is only one global instance, lazily initialized on the first call.
- private static _instance: AppContext;
- static get instance() {
- return (AppContext._instance = AppContext._instance ?? new AppContext());
- }
+ // This constructor is invoked only once, when frontend/index.ts invokes
+ // AppMainImpl.initialize().
+ constructor(initArgs: AppInitArgs) {
+ this.initArgs = initArgs;
+ this.initialRouteArgs = initArgs.initialRouteArgs;
- private constructor() {
+ // The rootUrl should point to 'https://ui.perfetto.dev/v1.2.3/'. It's
+ // allowed to be empty only in unittests, because there there is no bundle
+ // hence no concrete root.
+ assertTrue(this.initArgs.rootUrl !== '' || typeof jest !== 'undefined');
this.pluginMgr = new PluginManager({
forkForPlugin: (p) => AppImpl.instance.forkForPlugin(p),
get trace() {
@@ -62,24 +79,8 @@
},
});
}
-
- get currentTraceCtx(): TraceContext | undefined {
- return this.traceCtx;
- }
-
- // Called by AppImpl.newTraceInstance().
- setActiveTrace(traceCtx: TraceContext | undefined) {
- if (this.traceCtx !== undefined) {
- // This will trigger the unregistration of trace-scoped commands and
- // sidebar menuitems (and few similar things).
- this.traceCtx[Symbol.dispose]();
- }
- this.traceCtx = traceCtx;
- }
-
- // This is set to true when calling loadTrace() and cleared when it resolves.
- isLoadingTrace = false;
}
+
/*
* Every plugin gets its own instance. This is how we keep track
* what each plugin is doing and how we can blame issues on particular
@@ -101,10 +102,22 @@
// NOT the only instance, as other AppImpl instance will be created for each
// plugin.
private static _instance: AppImpl;
+
+ // Invoked by frontend/index.ts.
+ static initialize(args: AppInitArgs) {
+ assertTrue(AppImpl._instance === undefined);
+ AppImpl._instance = new AppImpl(new AppContext(args), CORE_PLUGIN_ID);
+ }
+
+ // For testing purposes only.
+ // TODO(primiano): This is only required because today globals.ts abuses
+ // createFakeTraceImpl(). It can be removed once globals goes away.
+ static get initialized() {
+ return AppImpl._instance !== undefined;
+ }
+
static get instance(): AppImpl {
- AppImpl._instance =
- AppImpl._instance ?? new AppImpl(AppContext.instance, CORE_PLUGIN_ID);
- return AppImpl._instance;
+ return assertExists(AppImpl._instance);
}
get commands(): CommandManagerImpl {
@@ -127,20 +140,10 @@
return this.currentTrace;
}
- closeCurrentTrace() {
- this.currentTrace = undefined;
- this.appCtx.setActiveTrace(undefined);
- }
-
scheduleRedraw(): void {
raf.scheduleFullRedraw();
}
- setActiveTrace(traceImpl: TraceImpl, traceCtx: TraceContext) {
- this.appCtx.setActiveTrace(traceCtx);
- this.currentTrace = traceImpl;
- }
-
forkForPlugin(pluginId: string): AppImpl {
assertTrue(pluginId != CORE_PLUGIN_ID);
return new AppImpl(this.appCtx, pluginId);
@@ -154,16 +157,38 @@
this.appCtx.newEngineMode = mode;
}
- setInitialRouteArgs(args: RouteArgs) {
- this.appCtx.initialRouteArgs = args;
- }
-
getAndClearInitialRouteArgs(): Optional<RouteArgs> {
const args = this.appCtx.initialRouteArgs;
- this.appCtx.initialRouteArgs = {};
+ this.appCtx.initialRouteArgs = undefined;
return args;
}
+ closeCurrentTrace() {
+ // This method should be called only on the core instance, plugins don't
+ // have access to openTrace*() methods.
+ assertTrue(this.pluginId === CORE_PLUGIN_ID);
+ this.omnibox.reset(/* focus= */ false);
+
+ if (this.currentTrace !== undefined) {
+ this.appCtx.pluginMgr.onTraceClose();
+ // This will trigger the unregistration of trace-scoped commands and
+ // sidebar menuitems (and few similar things).
+ this.currentTrace[Symbol.dispose]();
+ this.currentTrace = undefined;
+ }
+ this.appCtx.initArgs.clearState();
+ }
+
+ // Called by trace_loader.ts soon after it has created a new TraceImpl.
+ setActiveTrace(traceImpl: TraceImpl) {
+ // In 99% this closeCurrentTrace() call is not needed because the real one
+ // is performed by openTrace() in this file. However in some rare cases we
+ // might end up loading a trace while another one is still loading, and this
+ // covers races in that case.
+ this.closeCurrentTrace();
+ this.currentTrace = traceImpl;
+ }
+
get isLoadingTrace() {
return this.appCtx.isLoadingTrace;
}
@@ -174,4 +199,19 @@
this.appCtx.isLoadingTrace = loading;
raf.scheduleFullRedraw();
}
+
+ get rootUrl() {
+ return this.appCtx.initArgs.rootUrl;
+ }
+
+ get extraSqlPackages(): SqlPackage[] {
+ return this.appCtx.extraSqlPackages;
+ }
+
+ // Nothing other than TraceImpl's constructor should ever refer to this.
+ // This is necessary to avoid circular dependencies between trace_impl.ts
+ // and app_impl.ts.
+ get __appCtxForTraceImplCtor() {
+ return this.appCtx;
+ }
}
diff --git a/ui/src/core/fake_trace_impl.ts b/ui/src/core/fake_trace_impl.ts
index 972905e..c582815 100644
--- a/ui/src/core/fake_trace_impl.ts
+++ b/ui/src/core/fake_trace_impl.ts
@@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {getServingRoot} from '../base/http_utils';
import {Time} from '../base/time';
import {TraceInfo} from '../public/trace_info';
import {EngineBase} from '../trace_processor/engine';
+import {AppImpl} from './app_impl';
import {TraceImpl} from './trace_impl';
export interface FakeTraceImplArgs {
@@ -24,11 +26,28 @@
allowQueries?: boolean;
}
+let appImplInitialized = false;
+
+export function initializeAppImplForTesting(): AppImpl {
+ if (!appImplInitialized) {
+ appImplInitialized = true;
+ AppImpl.initialize({
+ rootUrl: getServingRoot(), // NOTE: will be '' in unittests.
+ initialRouteArgs: {},
+ clearState: () => {},
+ });
+ }
+ return AppImpl.instance;
+}
+
// This is used:
// - For testing.
// - By globals.ts before we have an actual trace loaded, to avoid causing
// if (!= undefined) checks everywhere.
export function createFakeTraceImpl(args: FakeTraceImplArgs = {}) {
+ if (!AppImpl.initialized) {
+ initializeAppImplForTesting();
+ }
const fakeTraceInfo: TraceInfo = {
source: {type: 'URL', url: ''},
traceTitle: '',
@@ -45,7 +64,7 @@
uuid: '',
cached: false,
};
- return TraceImpl.newInstance(
+ return TraceImpl.createInstanceForCore(
new FakeEngine(args.allowQueries ?? false),
fakeTraceInfo,
);
diff --git a/ui/src/core/plugin_manager_unittest.ts b/ui/src/core/plugin_manager_unittest.ts
index ccf3406..7b423f6 100644
--- a/ui/src/core/plugin_manager_unittest.ts
+++ b/ui/src/core/plugin_manager_unittest.ts
@@ -13,9 +13,12 @@
// limitations under the License.
import {PerfettoPlugin} from '../public/plugin';
-import {createFakeTraceImpl} from './fake_trace_impl';
-import {PluginManager} from './plugin_manager';
import {AppImpl} from './app_impl';
+import {
+ createFakeTraceImpl,
+ initializeAppImplForTesting,
+} from './fake_trace_impl';
+import {PluginManager} from './plugin_manager';
function makeMockPlugin(): PerfettoPlugin {
return {
@@ -31,7 +34,8 @@
describe('PluginManger', () => {
beforeEach(() => {
mockPlugin = makeMockPlugin();
- manager = new PluginManager(AppImpl.instance);
+ initializeAppImplForTesting();
+ manager = new PluginManager(initializeAppImplForTesting());
manager.registerPlugin({
pluginId: 'foo',
plugin: mockPlugin,
@@ -46,14 +50,18 @@
});
it('invokes onTraceLoad when trace is loaded', async () => {
+ const trace = createFakeTraceImpl();
+ AppImpl.instance.setActiveTrace(trace);
+ await manager.onTraceLoad(trace);
await manager.activatePlugin('foo');
- await manager.onTraceLoad(createFakeTraceImpl());
expect(mockPlugin.onTraceLoad).toHaveBeenCalledTimes(1);
});
it('invokes onTraceLoad when plugin activated while trace loaded', async () => {
- await manager.onTraceLoad(createFakeTraceImpl());
+ const trace = createFakeTraceImpl();
+ AppImpl.instance.setActiveTrace(trace);
+ await manager.onTraceLoad(trace);
await manager.activatePlugin('foo');
expect(mockPlugin.onTraceLoad).toHaveBeenCalledTimes(1);
diff --git a/ui/src/core/selection_manager.ts b/ui/src/core/selection_manager.ts
index 1f930d7..4fd43c2 100644
--- a/ui/src/core/selection_manager.ts
+++ b/ui/src/core/selection_manager.ts
@@ -15,18 +15,13 @@
import {assertTrue, assertUnreachable} from '../base/logging';
import {
Selection,
- LegacySelection,
Area,
SelectionOpts,
SelectionManager,
AreaSelectionAggregator,
SqlSelectionResolver,
} from '../public/selection';
-import {duration, Time, time, TimeSpan} from '../base/time';
-import {
- GenericSliceDetailsTabConfig,
- GenericSliceDetailsTabConfigBase,
-} from '../public/details_panel';
+import {TimeSpan} from '../base/time';
import {raf} from './raf_scheduler';
import {exists, Optional} from '../base/utils';
import {TrackManagerImpl} from './track_manager';
@@ -49,7 +44,6 @@
// requires querying the SQL engine, which is an async operation.
export class SelectionManagerImpl implements SelectionManager {
private _selection: Selection = {kind: 'empty'};
- private _selectedDetails?: LegacySelectionDetails;
private _aggregationManager: SelectionAggregationManager;
// Incremented every time _selection changes.
private readonly selectionResolvers = new Array<SqlSelectionResolver>();
@@ -177,62 +171,10 @@
});
}
- // There is no matching addLegacy as we did not support multi-single
- // selection with the legacy selection system.
- selectLegacy(legacySelection: LegacySelection, opts?: SelectionOpts): void {
- this.setSelection(
- {
- kind: 'legacy',
- legacySelection,
- },
- opts,
- );
- }
-
- selectGenericSlice(args: {
- id: number;
- sqlTableName: string;
- start: time;
- duration: duration;
- trackUri: string;
- detailsPanelConfig: {
- kind: string;
- config: GenericSliceDetailsTabConfigBase;
- };
- }): void {
- const detailsPanelConfig: GenericSliceDetailsTabConfig = {
- id: args.id,
- ...args.detailsPanelConfig.config,
- };
- this.setSelection({
- kind: 'legacy',
- legacySelection: {
- kind: 'GENERIC_SLICE',
- id: args.id,
- sqlTableName: args.sqlTableName,
- start: args.start,
- duration: args.duration,
- trackUri: args.trackUri,
- detailsPanelConfig: {
- kind: args.detailsPanelConfig.kind,
- config: detailsPanelConfig,
- },
- },
- });
- }
-
get selection(): Selection {
return this._selection;
}
- get legacySelection(): LegacySelection | null {
- return toLegacySelection(this._selection);
- }
-
- get legacySelectionDetails(): LegacySelectionDetails | undefined {
- return this._selectedDetails;
- }
-
registerSqlSelectionResolver(resolver: SqlSelectionResolver): void {
this.selectionResolvers.push(resolver);
}
@@ -321,19 +263,37 @@
switch (this.selection.kind) {
case 'track_event':
return this.selection.trackUri;
- case 'legacy':
- return this.selection.legacySelection.trackUri;
+ // TODO(stevegolton): Handle scrolling to area and note selections.
default:
return undefined;
}
})();
- const range = this.findTimeRangeOfSelection();
+ const range = this.findFocusRangeOfSelection();
this.scrollHelper.scrollTo({
time: range ? {...range} : undefined,
track: uri ? {uri: uri, expandGroup: true} : undefined,
});
}
+ // Finds the time range range that we should actually focus on - using dummy
+ // values for instant and incomplete slices, so we don't end up super zoomed
+ // in.
+ private findFocusRangeOfSelection(): Optional<TimeSpan> {
+ const sel = this.selection;
+ if (sel.kind === 'track_event') {
+ // The focus range of slices is different to that of the actual span
+ if (sel.dur === -1n) {
+ return TimeSpan.fromTimeAndDuration(sel.ts, INCOMPLETE_SLICE_DURATION);
+ } else if (sel.dur === 0n) {
+ return TimeSpan.fromTimeAndDuration(sel.ts, INSTANT_FOCUS_DURATION);
+ } else {
+ return TimeSpan.fromTimeAndDuration(sel.ts, sel.dur);
+ }
+ } else {
+ return this.findTimeRangeOfSelection();
+ }
+ }
+
findTimeRangeOfSelection(): Optional<TimeSpan> {
const sel = this.selection;
if (sel.kind === 'area') {
@@ -358,18 +318,6 @@
return TimeSpan.fromTimeAndDuration(sel.ts, sel.dur);
}
- const legacySel = this.legacySelection;
- if (!exists(legacySel)) {
- return undefined;
- }
-
- if (legacySel.kind === 'GENERIC_SLICE') {
- return findTimeRangeOfSlice({
- ts: legacySel.start,
- dur: legacySel.duration,
- });
- }
-
return undefined;
}
@@ -377,51 +325,3 @@
return this._aggregationManager;
}
}
-
-function toLegacySelection(selection: Selection): LegacySelection | null {
- switch (selection.kind) {
- case 'area':
- case 'track_event':
- case 'empty':
- case 'note':
- return null;
- case 'union':
- for (const child of selection.selections) {
- const result = toLegacySelection(child);
- if (result !== null) {
- return result;
- }
- }
- return null;
- case 'legacy':
- return selection.legacySelection;
- default:
- assertUnreachable(selection);
- return null;
- }
-}
-
-// Returns the start and end points of a slice-like object If slice is instant
-// or incomplete, dummy time will be returned which instead.
-function findTimeRangeOfSlice(slice: {ts?: time; dur?: duration}): TimeSpan {
- if (exists(slice.ts) && exists(slice.dur)) {
- if (slice.dur === -1n) {
- return TimeSpan.fromTimeAndDuration(slice.ts, INCOMPLETE_SLICE_DURATION);
- } else if (slice.dur === 0n) {
- return TimeSpan.fromTimeAndDuration(slice.ts, INSTANT_FOCUS_DURATION);
- } else {
- return TimeSpan.fromTimeAndDuration(slice.ts, slice.dur);
- }
- } else {
- // TODO(primiano): unclear why we dont return undefined here.
- return new TimeSpan(Time.INVALID, Time.INVALID);
- }
-}
-
-export interface LegacySelectionDetails {
- ts?: time;
- dur?: duration;
- // Additional information for sched selection, used to draw the wakeup arrow.
- wakeupTs?: time;
- wakerCpu?: number;
-}
diff --git a/ui/src/core/trace_impl.ts b/ui/src/core/trace_impl.ts
index 6eae5f7..1097aa8 100644
--- a/ui/src/core/trace_impl.ts
+++ b/ui/src/core/trace_impl.ts
@@ -48,7 +48,7 @@
* This is the underlying storage for AppImpl, which instead has one instance
* per trace per plugin.
*/
-export class TraceContext implements Disposable {
+class TraceContext implements Disposable {
readonly appCtx: AppContext;
readonly engine: EngineBase;
readonly omniboxMgr = new OmniboxManagerImpl();
@@ -166,19 +166,21 @@
// engine has been set up. It obtains a new TraceImpl for the core. From that
// we can fork sibling instances (i.e. bound to the same TraceContext) for
// the various plugins.
- static newInstance(engine: EngineBase, traceInfo: TraceInfo): TraceImpl {
- const appCtx = AppContext.instance;
+ static createInstanceForCore(
+ engine: EngineBase,
+ traceInfo: TraceInfo,
+ ): TraceImpl {
const appImpl = AppImpl.instance;
- const traceCtx = new TraceContext(appCtx, engine, traceInfo);
+ const traceCtx = new TraceContext(
+ appImpl.__appCtxForTraceImplCtor,
+ engine,
+ traceInfo,
+ );
const traceImpl = new TraceImpl(appImpl, traceCtx);
- appImpl.setActiveTrace(traceImpl, traceCtx);
-
- // TODO(primiano): remove this injection once we plumb Trace everywhere.
- setScrollToFunction((x: ScrollToArgs) => traceCtx.scrollHelper.scrollTo(x));
return traceImpl;
}
- constructor(appImpl: AppImpl, ctx: TraceContext) {
+ private constructor(appImpl: AppImpl, ctx: TraceContext) {
const pluginId = appImpl.pluginId;
this.appImpl = appImpl;
this.traceCtx = ctx;
@@ -215,6 +217,9 @@
return disposable;
},
});
+
+ // TODO(primiano): remove this injection once we plumb Trace everywhere.
+ setScrollToFunction((x: ScrollToArgs) => ctx.scrollHelper.scrollTo(x));
}
scrollTo(where: ScrollToArgs): void {
@@ -322,6 +327,12 @@
scheduleRedraw(): void {
this.appImpl.scheduleRedraw();
}
+
+ [Symbol.dispose]() {
+ if (this.pluginId === CORE_PLUGIN_ID) {
+ this.traceCtx[Symbol.dispose]();
+ }
+ }
}
// Allows to take an existing class instance (`target`) and override some of its
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts b/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
index 3f5888a..6e6b909 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
@@ -12,20 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {OnSliceClickArgs} from '../../frontend/base_slice_track';
-import {GenericSliceDetailsTab} from '../../frontend/generic_slice_details_tab';
import {NAMED_ROW} from '../../frontend/named_slice_track';
-import {NUM, STR} from '../../trace_processor/query_result';
+import {LONG, NUM, STR} from '../../trace_processor/query_result';
import {Slice} from '../../public/track';
import {
- CustomSqlDetailsPanelConfig,
CustomSqlImportConfig,
CustomSqlTableDefConfig,
CustomSqlTableSliceTrack,
} from '../../frontend/tracks/custom_sql_table_slice_track';
-import {PageLoadDetailsPanel} from './page_load_details_panel';
-import {StartupDetailsPanel} from './startup_details_panel';
-import {WebContentInteractionPanel} from './web_content_interaction_details_panel';
+import {TrackEventDetails} from '../../public/selection';
+import {Duration, Time} from '../../base/time';
export const CRITICAL_USER_INTERACTIONS_KIND =
'org.chromium.CriticalUserInteraction.track';
@@ -42,28 +38,6 @@
type: string;
}
-enum CriticalUserInteractionType {
- UNKNOWN = 'Unknown',
- PAGE_LOAD = 'chrome_page_loads',
- STARTUP = 'chrome_startups',
- WEB_CONTENT_INTERACTION = 'chrome_web_content_interactions',
-}
-
-function convertToCriticalUserInteractionType(
- cujType: string,
-): CriticalUserInteractionType {
- switch (cujType) {
- case CriticalUserInteractionType.PAGE_LOAD:
- return CriticalUserInteractionType.PAGE_LOAD;
- case CriticalUserInteractionType.STARTUP:
- return CriticalUserInteractionType.STARTUP;
- case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
- return CriticalUserInteractionType.WEB_CONTENT_INTERACTION;
- default:
- return CriticalUserInteractionType.UNKNOWN;
- }
-}
-
export class CriticalUserInteractionTrack extends CustomSqlTableSliceTrack {
static readonly kind = `/critical_user_interactions`;
@@ -84,64 +58,34 @@
};
}
- getDetailsPanel(
- args: OnSliceClickArgs<CriticalUserInteractionSlice>,
- ): CustomSqlDetailsPanelConfig {
- let detailsPanel = {
- kind: GenericSliceDetailsTab.kind,
- config: {
- sqlTableName: this.tableName,
- title: 'Chrome Interaction',
- },
- };
+ async getSelectionDetails(
+ id: number,
+ ): Promise<TrackEventDetails | undefined> {
+ const query = `
+ SELECT
+ ts,
+ dur,
+ type
+ FROM (${this.getSqlSource()})
+ WHERE id = ${id}
+ `;
- switch (convertToCriticalUserInteractionType(args.slice.type)) {
- case CriticalUserInteractionType.PAGE_LOAD:
- detailsPanel = {
- kind: PageLoadDetailsPanel.kind,
- config: {
- sqlTableName: this.tableName,
- title: 'Chrome Page Load',
- },
- };
- break;
- case CriticalUserInteractionType.STARTUP:
- detailsPanel = {
- kind: StartupDetailsPanel.kind,
- config: {
- sqlTableName: this.tableName,
- title: 'Chrome Startup',
- },
- };
- break;
- case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
- detailsPanel = {
- kind: WebContentInteractionPanel.kind,
- config: {
- sqlTableName: this.tableName,
- title: 'Chrome Web Content Interaction',
- },
- };
- break;
- default:
- break;
+ const result = await this.engine.query(query);
+ if (result.numRows() === 0) {
+ return undefined;
}
- return detailsPanel;
- }
- onSliceClick(args: OnSliceClickArgs<CriticalUserInteractionSlice>) {
- const detailsPanelConfig = this.getDetailsPanel(args);
- this.trace.selection.selectGenericSlice({
- id: args.slice.scopedId,
- sqlTableName: this.tableName,
- start: args.slice.ts,
- duration: args.slice.dur,
- trackUri: this.uri,
- detailsPanelConfig: {
- kind: detailsPanelConfig.kind,
- config: detailsPanelConfig.config,
- },
+ const row = result.iter({
+ ts: LONG,
+ dur: LONG,
+ type: STR,
});
+
+ return {
+ ts: Time.fromRaw(row.ts),
+ dur: Duration.fromRaw(row.dur),
+ interactionType: row.type,
+ };
}
getSqlImports(): CustomSqlImportConfig {
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
index 23560c9..1660f10 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
@@ -12,9 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {v4 as uuidv4} from 'uuid';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {BottomTabToSCSAdapter} from '../../public/utils';
import {Trace} from '../../public/trace';
import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
import {PageLoadDetailsPanel} from './page_load_details_panel';
@@ -22,6 +19,8 @@
import {WebContentInteractionPanel} from './web_content_interaction_details_panel';
import {CriticalUserInteractionTrack} from './critical_user_interaction_track';
import {TrackNode} from '../../public/workspace';
+import {TrackEventSelection} from '../../public/selection';
+import {GenericSliceDetailsTab} from '../../frontend/generic_slice_details_tab';
class CriticalUserInteractionPlugin implements PerfettoPlugin {
async onTraceLoad(ctx: Trace): Promise<void> {
@@ -48,65 +47,24 @@
trace: ctx,
uri: CriticalUserInteractionTrack.kind,
}),
+ detailsPanel: (sel: TrackEventSelection) => {
+ switch (sel.interactionType) {
+ case 'chrome_page_loads':
+ return new PageLoadDetailsPanel(ctx, sel.eventId);
+ case 'chrome_startups':
+ return new StartupDetailsPanel(ctx, sel.eventId);
+ case 'chrome_web_content_interactions':
+ return new WebContentInteractionPanel(ctx, sel.eventId);
+ default:
+ return new GenericSliceDetailsTab(
+ ctx,
+ 'chrome_interactions',
+ sel.eventId,
+ 'Chrome Interaction',
+ );
+ }
+ },
});
-
- ctx.tabs.registerDetailsPanel(
- new BottomTabToSCSAdapter({
- tabFactory: (selection) => {
- if (
- selection.kind === 'GENERIC_SLICE' &&
- selection.detailsPanelConfig.kind === PageLoadDetailsPanel.kind
- ) {
- const config = selection.detailsPanelConfig.config;
- return new PageLoadDetailsPanel({
- config: config as GenericSliceDetailsTabConfig,
- trace: ctx,
- uuid: uuidv4(),
- });
- }
- return undefined;
- },
- }),
- );
-
- ctx.tabs.registerDetailsPanel(
- new BottomTabToSCSAdapter({
- tabFactory: (selection) => {
- if (
- selection.kind === 'GENERIC_SLICE' &&
- selection.detailsPanelConfig.kind === StartupDetailsPanel.kind
- ) {
- const config = selection.detailsPanelConfig.config;
- return new StartupDetailsPanel({
- config: config as GenericSliceDetailsTabConfig,
- trace: ctx,
- uuid: uuidv4(),
- });
- }
- return undefined;
- },
- }),
- );
-
- ctx.tabs.registerDetailsPanel(
- new BottomTabToSCSAdapter({
- tabFactory: (selection) => {
- if (
- selection.kind === 'GENERIC_SLICE' &&
- selection.detailsPanelConfig.kind ===
- WebContentInteractionPanel.kind
- ) {
- const config = selection.detailsPanelConfig.config;
- return new WebContentInteractionPanel({
- config: config as GenericSliceDetailsTabConfig,
- trace: ctx,
- uuid: uuidv4(),
- });
- }
- return undefined;
- },
- }),
- );
}
}
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/page_load_details_panel.ts b/ui/src/core_plugins/chrome_critical_user_interactions/page_load_details_panel.ts
index e01e4a8..e2d0b54 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/page_load_details_panel.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/page_load_details_panel.ts
@@ -13,29 +13,24 @@
// limitations under the License.
import m from 'mithril';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
import {
Details,
DetailsSchema,
} from '../../frontend/widgets/sql/details/details';
import {DetailsShell} from '../../widgets/details_shell';
import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
import d = DetailsSchema;
-export class PageLoadDetailsPanel extends BottomTab<GenericSliceDetailsTabConfig> {
- static readonly kind = 'org.perfetto.PageLoadDetailsPanel';
+export class PageLoadDetailsPanel implements TrackEventDetailsPanel {
private data: Details;
- static create(
- args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
- ): PageLoadDetailsPanel {
- return new PageLoadDetailsPanel(args);
- }
-
- constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
- super(args);
- this.data = new Details(this.trace, 'chrome_page_loads', this.config.id, {
+ constructor(
+ private readonly trace: Trace,
+ id: number,
+ ) {
+ this.data = new Details(this.trace, 'chrome_page_loads', id, {
'Navigation start': d.Timestamp('navigation_start_ts'),
'FCP event': d.Timestamp('fcp_ts'),
'FCP': d.Interval('navigation_start_ts', 'fcp'),
@@ -65,21 +60,13 @@
});
}
- viewTab() {
+ render() {
return m(
DetailsShell,
{
- title: this.getTitle(),
+ title: 'Chrome Page Load',
},
m(GridLayout, m(GridLayoutColumn, this.data.render())),
);
}
-
- getTitle(): string {
- return this.config.title;
- }
-
- isLoading() {
- return this.data.isLoading();
- }
}
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/startup_details_panel.ts b/ui/src/core_plugins/chrome_critical_user_interactions/startup_details_panel.ts
index ec8667f..0c26623 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/startup_details_panel.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/startup_details_panel.ts
@@ -14,8 +14,6 @@
import m from 'mithril';
import {duration, Time, time} from '../../base/time';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
import {DurationWidget} from '../../frontend/widgets/duration';
import {Timestamp} from '../../frontend/widgets/timestamp';
import {LONG, NUM, STR, STR_NULL} from '../../trace_processor/query_result';
@@ -25,6 +23,8 @@
import {SqlRef} from '../../widgets/sql_ref';
import {dictToTreeNodes, Tree} from '../../widgets/tree';
import {asUpid, Upid} from '../../trace_processor/sql_utils/core_types';
+import {Trace} from '../../public/trace';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
interface Data {
startupId: number;
@@ -35,24 +35,16 @@
upid: Upid;
}
-export class StartupDetailsPanel extends BottomTab<GenericSliceDetailsTabConfig> {
- static readonly kind = 'org.perfetto.StartupDetailsPanel';
- private loaded = false;
- private data: Data | undefined;
+export class StartupDetailsPanel implements TrackEventDetailsPanel {
+ private data?: Data;
- static create(
- args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
- ): StartupDetailsPanel {
- return new StartupDetailsPanel(args);
- }
+ constructor(
+ private readonly trace: Trace,
+ private readonly id: number,
+ ) {}
- constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
- super(args);
- this.loadData();
- }
-
- private async loadData() {
- const queryResult = await this.engine.query(`
+ async load() {
+ const queryResult = await this.trace.engine.query(`
SELECT
activity_id AS startupId,
name,
@@ -64,7 +56,7 @@
launch_cause AS launchCause,
browser_upid AS upid
FROM chrome_startups
- WHERE id = ${this.config.id};
+ WHERE id = ${this.id};
`);
const iter = queryResult.firstRow({
@@ -87,8 +79,6 @@
if (iter.launchCause) {
this.data.launchCause = iter.launchCause;
}
-
- this.loaded = true;
}
private getDetailsDictionary() {
@@ -106,20 +96,20 @@
}
details['SQL ID'] = m(SqlRef, {
table: 'chrome_startups',
- id: this.config.id,
+ id: this.id,
});
return details;
}
- viewTab() {
- if (this.isLoading()) {
+ render() {
+ if (!this.data) {
return m('h2', 'Loading');
}
return m(
DetailsShell,
{
- title: this.getTitle(),
+ title: 'Chrome Startup',
},
m(
GridLayout,
@@ -134,12 +124,4 @@
),
);
}
-
- getTitle(): string {
- return this.config.title;
- }
-
- isLoading() {
- return !this.loaded;
- }
}
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/web_content_interaction_details_panel.ts b/ui/src/core_plugins/chrome_critical_user_interactions/web_content_interaction_details_panel.ts
index cb6a7fb..25e49aa 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/web_content_interaction_details_panel.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/web_content_interaction_details_panel.ts
@@ -28,8 +28,6 @@
import m from 'mithril';
import {duration, Time, time} from '../../base/time';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
import {asUpid, Upid} from '../../trace_processor/sql_utils/core_types';
import {DurationWidget} from '../../frontend/widgets/duration';
import {Timestamp} from '../../frontend/widgets/timestamp';
@@ -39,6 +37,8 @@
import {Section} from '../../widgets/section';
import {SqlRef} from '../../widgets/sql_ref';
import {dictToTreeNodes, Tree} from '../../widgets/tree';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
interface Data {
ts: time;
@@ -48,24 +48,16 @@
upid: Upid;
}
-export class WebContentInteractionPanel extends BottomTab<GenericSliceDetailsTabConfig> {
- static readonly kind = 'org.perfetto.WebContentInteractionPanel';
- private loaded = false;
- private data: Data | undefined;
+export class WebContentInteractionPanel implements TrackEventDetailsPanel {
+ private data?: Data;
- static create(
- args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
- ): WebContentInteractionPanel {
- return new WebContentInteractionPanel(args);
- }
+ constructor(
+ private readonly trace: Trace,
+ private readonly id: number,
+ ) {}
- constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
- super(args);
- this.loadData();
- }
-
- private async loadData() {
- const queryResult = await this.engine.query(`
+ async load() {
+ const queryResult = await this.trace.engine.query(`
SELECT
ts,
dur,
@@ -73,7 +65,7 @@
total_duration_ms AS totalDurationMs,
renderer_upid AS upid
FROM chrome_web_content_interactions
- WHERE id = ${this.config.id};
+ WHERE id = ${this.id};
`);
const iter = queryResult.firstRow({
@@ -91,8 +83,6 @@
totalDurationMs: iter.totalDurationMs,
upid: asUpid(iter.upid),
};
-
- this.loaded = true;
}
private getDetailsDictionary() {
@@ -107,20 +97,20 @@
});
details['SQL ID'] = m(SqlRef, {
table: 'chrome_web_content_interactions',
- id: this.config.id,
+ id: this.id,
});
return details;
}
- viewTab() {
- if (this.isLoading()) {
+ render() {
+ if (!this.data) {
return m('h2', 'Loading');
}
return m(
DetailsShell,
{
- title: this.getTitle(),
+ title: 'Chrome Web Content Interaction',
},
m(
GridLayout,
@@ -135,12 +125,4 @@
),
);
}
-
- getTitle(): string {
- return this.config.title;
- }
-
- isLoading() {
- return !this.loaded;
- }
}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts b/ui/src/core_plugins/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts
deleted file mode 100644
index 1df20a2..0000000
--- a/ui/src/core_plugins/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2022 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 {
- NAMED_ROW,
- NamedRow,
- NamedSliceTrack,
-} from '../../frontend/named_slice_track';
-import {NewTrackArgs} from '../../frontend/track';
-import {Slice} from '../../public/track';
-
-export class ChromeTasksScrollJankTrack extends NamedSliceTrack {
- constructor(args: NewTrackArgs) {
- super(args);
- }
-
- getRowSpec(): NamedRow {
- return NAMED_ROW;
- }
-
- rowToSlice(row: NamedRow): Slice {
- return this.rowToSliceBase(row);
- }
-
- getSqlSource(): string {
- return `
- select
- s2.ts as ts,
- s2.dur as dur,
- s2.id as id,
- 0 as depth,
- s1.full_name as name
- from chrome_tasks_delaying_input_processing s1
- join slice s2 on s2.id=s1.slice_id
- `;
- }
-}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/common.ts b/ui/src/core_plugins/chrome_scroll_jank/common.ts
deleted file mode 100644
index c6232a2..0000000
--- a/ui/src/core_plugins/chrome_scroll_jank/common.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (C) 2024 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 {ObjectByKey} from '../../common/state';
-import {featureFlags} from '../../core/feature_flags';
-import {CustomSqlDetailsPanelConfig} from '../../frontend/tracks/custom_sql_table_slice_track';
-
-export const ENABLE_CHROME_SCROLL_JANK_PLUGIN = featureFlags.register({
- id: 'enableChromeScrollJankPlugin',
- name: 'Enable Chrome Scroll Jank plugin',
- description: 'Adds new tracks for scroll jank in Chrome',
- defaultValue: false,
-});
-
-export interface ScrollJankTrackSpec {
- key: string;
- sqlTableName: string;
- detailsPanelConfig: CustomSqlDetailsPanelConfig;
-}
-
-// Global state for the scroll jank plugin.
-export class ScrollJankPluginState {
- private static instance?: ScrollJankPluginState;
- private tracks: ObjectByKey<ScrollJankTrackSpec>;
-
- private constructor() {
- this.tracks = {};
- }
-
- public static getInstance(): ScrollJankPluginState {
- if (!ScrollJankPluginState.instance) {
- ScrollJankPluginState.instance = new ScrollJankPluginState();
- }
-
- return ScrollJankPluginState.instance;
- }
-
- public registerTrack(args: {
- kind: string;
- trackUri: string;
- tableName: string;
- detailsPanelConfig: CustomSqlDetailsPanelConfig;
- }): void {
- this.tracks[args.kind] = {
- key: args.trackUri,
- sqlTableName: args.tableName,
- detailsPanelConfig: args.detailsPanelConfig,
- };
- }
-
- public unregisterTrack(kind: string): void {
- delete this.tracks[kind];
- }
-
- public getTrack(kind: string): ScrollJankTrackSpec | undefined {
- return this.tracks[kind];
- }
-}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts b/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts
index 2d629ea..ae6b128 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts
@@ -13,10 +13,8 @@
// limitations under the License.
import m from 'mithril';
-import {Duration, duration, time} from '../../base/time';
+import {Duration, duration, Time, time} from '../../base/time';
import {raf} from '../../core/raf_scheduler';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
import {hasArgs, renderArguments} from '../../frontend/slice_args';
import {renderDetails} from '../../frontend/slice_details';
import {
@@ -37,7 +35,7 @@
widgetColumn,
} from '../../frontend/tables/table';
import {TreeTable, TreeTableAttrs} from '../../frontend/widgets/treetable';
-import {NUM, STR} from '../../trace_processor/query_result';
+import {LONG, NUM, STR} from '../../trace_processor/query_result';
import {DetailsShell} from '../../widgets/details_shell';
import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
import {Section} from '../../widgets/section';
@@ -51,13 +49,10 @@
getScrollJankCauseStage,
} from './scroll_jank_cause_link_utils';
import {ScrollJankCauseMap} from './scroll_jank_cause_map';
-import {
- getScrollJankSlices,
- getSliceForTrack,
- ScrollJankSlice,
-} from './scroll_jank_slice';
import {sliceRef} from '../../frontend/widgets/slice';
-import {SCROLL_JANK_V3_TRACK_KIND} from '../../public/track_kinds';
+import {JANKS_TRACK_URI, renderSliceRef} from './selection_utils';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
// Given a node in the slice tree, return a path from root to it.
function getPath(slice: SliceTreeNode): string[] {
@@ -104,15 +99,17 @@
return `${delta > 0 ? '+' : ''}${Duration.humanise(delta)}`;
}
-export class EventLatencySliceDetailsPanel extends BottomTab<GenericSliceDetailsTabConfig> {
- static readonly kind = 'dev.perfetto.EventLatencySliceDetailsPanel';
-
- private loaded = false;
+export class EventLatencySliceDetailsPanel implements TrackEventDetailsPanel {
private name = '';
private topEventLatencyId: SliceSqlId | undefined = undefined;
private sliceDetails?: SliceDetails;
- private jankySlice?: ScrollJankSlice;
+ private jankySlice?: {
+ ts: time;
+ dur: duration;
+ id: number;
+ causeOfJank: string;
+ };
// Whether this stage has caused jank. This is also true for top level
// EventLatency slices where a descendant is a cause of jank.
@@ -132,31 +129,24 @@
private tracksByTrackId: Map<number, string>;
- static create(
- args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
- ): EventLatencySliceDetailsPanel {
- return new EventLatencySliceDetailsPanel(args);
- }
-
- constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
- super(args);
-
+ constructor(
+ private readonly trace: Trace,
+ private readonly id: number,
+ ) {
this.tracksByTrackId = new Map<number, string>();
this.trace.tracks.getAllTracks().forEach((td) => {
td.tags?.trackIds?.forEach((trackId) => {
this.tracksByTrackId.set(trackId, td.uri);
});
});
-
- this.loadData();
}
- async loadData() {
- const queryResult = await this.engine.query(`
+ async load() {
+ const queryResult = await this.trace.engine.query(`
SELECT
name
- FROM ${this.config.sqlTableName}
- WHERE id = ${this.config.id}
+ FROM slice
+ WHERE id = ${this.id}
`);
const iter = queryResult.firstRow({
@@ -169,14 +159,12 @@
await this.loadJankSlice();
await this.loadRelevantThreads();
await this.loadEventLatencyBreakdown();
-
- this.loaded = true;
}
async loadSlice() {
this.sliceDetails = await getSlice(
- this.engine,
- asSliceSqlId(this.config.id),
+ this.trace.engine,
+ asSliceSqlId(this.id),
);
raf.scheduleRedraw();
}
@@ -194,14 +182,25 @@
);
}
- const possibleSlices = await getScrollJankSlices(
- this.engine,
- this.topEventLatencyId,
- );
- // We may not get any slices if the EventLatency doesn't indicate any
- // jank occurred.
- if (possibleSlices.length > 0) {
- this.jankySlice = possibleSlices[0];
+ const it = (
+ await this.trace.engine.query(`
+ SELECT ts, dur, id, cause_of_jank as causeOfJank
+ FROM chrome_janky_frame_presentation_intervals
+ WHERE event_latency_id = ${this.topEventLatencyId}`)
+ ).iter({
+ id: NUM,
+ ts: LONG,
+ dur: LONG,
+ causeOfJank: STR,
+ });
+
+ if (it.valid()) {
+ this.jankySlice = {
+ id: it.id,
+ ts: Time.fromRaw(it.ts),
+ dur: Duration.fromRaw(it.dur),
+ causeOfJank: it.causeOfJank,
+ };
}
}
@@ -214,7 +213,7 @@
if (this.sliceDetails.name === 'EventLatency' && !this.jankySlice) return;
const possibleScrollJankStage = await getScrollJankCauseStage(
- this.engine,
+ this.trace.engine,
this.topEventLatencyId,
);
if (this.sliceDetails.name === 'EventLatency') {
@@ -237,7 +236,7 @@
if (this.relevantThreadStage) {
this.relevantThreadTracks = await getEventLatencyCauseTracks(
- this.engine,
+ this.trace.engine,
this.relevantThreadStage,
);
}
@@ -248,7 +247,7 @@
return;
}
this.eventLatencyBreakdown = await getDescendantSliceTree(
- this.engine,
+ this.trace.engine,
this.topEventLatencyId,
);
@@ -264,7 +263,7 @@
AND HAS_DESCENDANT_SLICE_WITH_NAME(
id,
'SubmitCompositorFrameToPresentationCompositorFrame')`;
- const prevEventLatency = await getSliceFromConstraints(this.engine, {
+ const prevEventLatency = await getSliceFromConstraints(this.trace.engine, {
filters: [
`name = 'EventLatency'`,
`id < ${this.topEventLatencyId}`,
@@ -275,12 +274,12 @@
});
if (prevEventLatency.length > 0) {
this.prevEventLatencyBreakdown = await getDescendantSliceTree(
- this.engine,
+ this.trace.engine,
prevEventLatency[0].id,
);
}
- const nextEventLatency = await getSliceFromConstraints(this.engine, {
+ const nextEventLatency = await getSliceFromConstraints(this.trace.engine, {
filters: [
`name = 'EventLatency'`,
`id > ${this.topEventLatencyId}`,
@@ -291,7 +290,7 @@
});
if (nextEventLatency.length > 0) {
this.nextEventLatencyBreakdown = await getDescendantSliceTree(
- this.engine,
+ this.trace.engine,
nextEventLatency[0].id,
);
}
@@ -381,7 +380,7 @@
private async getOldestAncestorSliceId(): Promise<number> {
let eventLatencyId = -1;
if (!this.sliceDetails) return eventLatencyId;
- const queryResult = await this.engine.query(`
+ const queryResult = await this.trace.engine.query(`
SELECT
id
FROM ancestor_slice(${this.sliceDetails.id})
@@ -415,16 +414,15 @@
: 'EventLatency in context of other Input events',
right: this.sliceDetails ? '' : 'N/A',
}),
- m(TreeNode, {
- left: this.jankySlice
- ? getSliceForTrack(
- this.jankySlice,
- SCROLL_JANK_V3_TRACK_KIND,
- 'Jank Interval',
- )
- : 'Jank Interval',
- right: this.jankySlice ? '' : 'N/A',
- }),
+ this.jankySlice &&
+ m(TreeNode, {
+ left: renderSliceRef({
+ trace: this.trace,
+ id: this.jankySlice.id,
+ trackUri: JANKS_TRACK_URI,
+ title: this.jankySlice.causeOfJank,
+ }),
+ }),
),
);
}
@@ -498,7 +496,7 @@
);
}
- viewTab() {
+ render() {
if (this.sliceDetails) {
const slice = this.sliceDetails;
@@ -544,12 +542,4 @@
return m(DetailsShell, {title: 'Slice', description: 'Loading...'});
}
}
-
- isLoading() {
- return !this.loaded;
- }
-
- getTitle(): string {
- return `Current Selection`;
- }
}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts b/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
index 8581258..59e10c7 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
@@ -12,20 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {globals} from '../../frontend/globals';
import {NamedRow} from '../../frontend/named_slice_track';
import {NewTrackArgs} from '../../frontend/track';
-import {CHROME_EVENT_LATENCY_TRACK_KIND} from '../../public/track_kinds';
import {Slice} from '../../public/track';
import {
- CustomSqlDetailsPanelConfig,
CustomSqlTableDefConfig,
CustomSqlTableSliceTrack,
} from '../../frontend/tracks/custom_sql_table_slice_track';
-import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
import {JANK_COLOR} from './jank_colors';
-import {ScrollJankPluginState} from './common';
-import {exists} from '../../base/utils';
export const JANKY_LATENCY_NAME = 'Janky EventLatency';
@@ -35,32 +29,12 @@
private baseTable: string,
) {
super(args);
- ScrollJankPluginState.getInstance().registerTrack({
- kind: CHROME_EVENT_LATENCY_TRACK_KIND,
- trackUri: this.uri,
- tableName: this.tableName,
- detailsPanelConfig: this.getDetailsPanel(),
- });
- }
-
- async onDestroy(): Promise<void> {
- await super.onDestroy();
- ScrollJankPluginState.getInstance().unregisterTrack(
- CHROME_EVENT_LATENCY_TRACK_KIND,
- );
}
getSqlSource(): string {
return `SELECT * FROM ${this.baseTable}`;
}
- getDetailsPanel(): CustomSqlDetailsPanelConfig {
- return {
- kind: EventLatencySliceDetailsPanel.kind,
- config: {title: '', sqlTableName: this.tableName},
- };
- }
-
getSqlDataSource(): CustomSqlTableDefConfig {
return {
sqlTableName: this.baseTable,
@@ -76,22 +50,6 @@
}
}
- onUpdatedSlices(slices: Slice[]) {
- for (const slice of slices) {
- const currentSelection = this.trace.selection.legacySelection;
- const isSelected =
- exists(currentSelection) &&
- currentSelection.kind === 'GENERIC_SLICE' &&
- currentSelection.id !== undefined &&
- currentSelection.id === slice.id;
-
- const highlighted = globals.state.highlightedSliceId === slice.id;
- const hasFocus = highlighted || isSelected;
- slice.isHighlighted = !!hasFocus;
- }
- super.onUpdatedSlices(slices);
- }
-
// At the moment we will just display the slice details. However, on select,
// this behavior should be customized to show jank-related data.
}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/index.ts b/ui/src/core_plugins/chrome_scroll_jank/index.ts
index e67921f..ab0dde0 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/index.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/index.ts
@@ -12,24 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {v4 as uuidv4} from 'uuid';
import {uuidv4Sql} from '../../base/uuid';
import {generateSqlWithInternalLayout} from '../../common/internal_layout_utils';
import {featureFlags} from '../../core/feature_flags';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {BottomTabToSCSAdapter} from '../../public/utils';
-import {
- CHROME_EVENT_LATENCY_TRACK_KIND,
- CHROME_TOPLEVEL_SCROLLS_KIND,
- CHROME_SCROLL_JANK_TRACK_KIND,
- SCROLL_JANK_V3_TRACK_KIND,
-} from '../../public/track_kinds';
-import {NUM} from '../../trace_processor/query_result';
import {Trace} from '../../public/trace';
import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
-import {Engine} from '../../trace_processor/engine';
-import {ChromeTasksScrollJankTrack} from './chrome_tasks_scroll_jank_track';
-import {ENABLE_CHROME_SCROLL_JANK_PLUGIN} from './common';
import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
import {EventLatencyTrack, JANKY_LATENCY_NAME} from './event_latency_track';
import {ScrollDetailsPanel} from './scroll_details_panel';
@@ -38,9 +25,6 @@
import {TopLevelScrollTrack} from './scroll_track';
import {ScrollJankCauseMap} from './scroll_jank_cause_map';
import {TrackNode} from '../../public/workspace';
-import {getOrCreateGroupForThread} from '../../public/standard_groups';
-import {addQueryResultsTab} from '../../public/lib/query_table/query_result_tab';
-import {ThreadSliceDetailsPanel} from '../../frontend/thread_slice_details_tab';
const ENABLE_SCROLL_JANK_PLUGIN_V2 = featureFlags.register({
id: 'enableScrollJankPluginV2',
@@ -51,38 +35,6 @@
class ChromeScrollJankPlugin implements PerfettoPlugin {
async onTraceLoad(ctx: Trace): Promise<void> {
- if (ENABLE_CHROME_SCROLL_JANK_PLUGIN.get()) {
- await this.addChromeScrollJankTrack(ctx);
-
- if (!(await isChromeTrace(ctx.engine))) {
- return;
- }
-
- // Initialise the chrome_tasks_delaying_input_processing table. It will be
- // used in the tracks above.
- await ctx.engine.query(`
- INCLUDE PERFETTO MODULE deprecated.v42.common.slices;
- SELECT RUN_METRIC(
- 'chrome/chrome_tasks_delaying_input_processing.sql',
- 'duration_causing_jank_ms',
- /* duration_causing_jank_ms = */ '8');`);
-
- const query = `
- select
- s1.full_name,
- s1.duration_ms,
- s1.slice_id,
- s1.thread_dur_ms,
- s2.id,
- s2.ts,
- s2.dur,
- s2.track_id
- from chrome_tasks_delaying_input_processing s1
- join slice s2 on s1.slice_id=s2.id
- `;
- addQueryResultsTab(ctx, {query, title: 'Scroll Jank: long tasks'});
- }
-
if (ENABLE_SCROLL_JANK_PLUGIN_V2.get()) {
const group = new TrackNode({
title: 'Chrome Scroll Jank',
@@ -98,46 +50,6 @@
}
}
- private async addChromeScrollJankTrack(ctx: Trace): Promise<void> {
- const queryResult = await ctx.engine.query(`
- select
- utid,
- upid
- from thread
- where name='CrBrowserMain'
- `);
-
- if (queryResult.numRows() === 0) {
- return;
- }
-
- const it = queryResult.firstRow({
- utid: NUM,
- upid: NUM,
- });
-
- const {upid, utid} = it;
- const uri = 'perfetto.ChromeScrollJank';
- const title = 'Scroll Jank causes - long tasks';
- ctx.tracks.registerTrack({
- uri,
- title,
- tags: {
- kind: CHROME_SCROLL_JANK_TRACK_KIND,
- upid,
- utid,
- },
- track: new ChromeTasksScrollJankTrack({
- trace: ctx,
- uri,
- }),
- detailsPanel: () => new ThreadSliceDetailsPanel(ctx, 'slice'),
- });
- const group = getOrCreateGroupForThread(ctx.workspace, utid);
- const track = new TrackNode({uri, title});
- group.addChildInOrder(track);
- }
-
private async addTopLevelScrollTrack(
ctx: Trace,
group: TrackNode,
@@ -153,36 +65,17 @@
ctx.tracks.registerTrack({
uri,
title,
- tags: {
- kind: CHROME_TOPLEVEL_SCROLLS_KIND,
- },
track: new TopLevelScrollTrack({
trace: ctx,
uri,
}),
+ detailsPanel: (sel) => {
+ return new ScrollDetailsPanel(ctx, sel.eventId);
+ },
});
const track = new TrackNode({uri, title});
group.addChildInOrder(track);
-
- ctx.tabs.registerDetailsPanel(
- new BottomTabToSCSAdapter({
- tabFactory: (selection) => {
- if (
- selection.kind === 'GENERIC_SLICE' &&
- selection.detailsPanelConfig.kind === ScrollDetailsPanel.kind
- ) {
- const config = selection.detailsPanelConfig.config;
- return new ScrollDetailsPanel({
- config: config as GenericSliceDetailsTabConfig,
- trace: ctx,
- uuid: uuidv4(),
- });
- }
- return undefined;
- },
- }),
- );
}
private async addEventLatencyTrack(
@@ -291,34 +184,14 @@
ctx.tracks.registerTrack({
uri,
title,
- tags: {
- kind: CHROME_EVENT_LATENCY_TRACK_KIND,
- },
track: new EventLatencyTrack({trace: ctx, uri}, baseTable),
+ detailsPanel: (sel) => {
+ return new EventLatencySliceDetailsPanel(ctx, sel.eventId);
+ },
});
const track = new TrackNode({uri, title});
group.addChildInOrder(track);
-
- ctx.tabs.registerDetailsPanel(
- new BottomTabToSCSAdapter({
- tabFactory: (selection) => {
- if (
- selection.kind === 'GENERIC_SLICE' &&
- selection.detailsPanelConfig.kind ===
- EventLatencySliceDetailsPanel.kind
- ) {
- const config = selection.detailsPanelConfig.config;
- return new EventLatencySliceDetailsPanel({
- config: config as GenericSliceDetailsTabConfig,
- trace: ctx,
- uuid: uuidv4(),
- });
- }
- return undefined;
- },
- }),
- );
}
private async addScrollJankV3ScrollTrack(
@@ -335,54 +208,18 @@
ctx.tracks.registerTrack({
uri,
title,
- tags: {
- kind: SCROLL_JANK_V3_TRACK_KIND,
- },
track: new ScrollJankV3Track({
trace: ctx,
uri,
}),
+ detailsPanel: (sel) => new ScrollJankV3DetailsPanel(ctx, sel.eventId),
});
const track = new TrackNode({uri, title});
group.addChildInOrder(track);
-
- ctx.tabs.registerDetailsPanel(
- new BottomTabToSCSAdapter({
- tabFactory: (selection) => {
- if (
- selection.kind === 'GENERIC_SLICE' &&
- selection.detailsPanelConfig.kind === ScrollJankV3DetailsPanel.kind
- ) {
- const config = selection.detailsPanelConfig.config;
- return new ScrollJankV3DetailsPanel({
- config: config as GenericSliceDetailsTabConfig,
- trace: ctx,
- uuid: uuidv4(),
- });
- }
- return undefined;
- },
- }),
- );
}
}
-async function isChromeTrace(engine: Engine) {
- const queryResult = await engine.query(`
- select utid, upid
- from thread
- where name='CrBrowserMain'
- `);
-
- const it = queryResult.iter({
- utid: NUM,
- upid: NUM,
- });
-
- return it.valid();
-}
-
export const plugin: PluginDescriptor = {
pluginId: 'perfetto.ChromeScrollJank',
plugin: ChromeScrollJankPlugin,
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts
index c184ba7..30ab521 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts
@@ -15,19 +15,21 @@
import m from 'mithril';
import {duration, Time, time} from '../../base/time';
import {exists} from '../../base/utils';
-import {raf} from '../../core/raf_scheduler';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
import {
ColumnDescriptor,
- numberColumn,
Table,
TableData,
widgetColumn,
} from '../../frontend/tables/table';
import {DurationWidget} from '../../frontend/widgets/duration';
import {Timestamp} from '../../frontend/widgets/timestamp';
-import {LONG, NUM, STR} from '../../trace_processor/query_result';
+import {
+ LONG,
+ LONG_NULL,
+ NUM,
+ NUM_NULL,
+ STR,
+} from '../../trace_processor/query_result';
import {DetailsShell} from '../../widgets/details_shell';
import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
import {Section} from '../../widgets/section';
@@ -41,12 +43,9 @@
getPredictorJankDeltas,
getPresentedScrollDeltas,
} from './scroll_delta_graph';
-import {
- getScrollJankSlices,
- getSliceForTrack,
- ScrollJankSlice,
-} from './scroll_jank_slice';
-import {SCROLL_JANK_V3_TRACK_KIND} from '../../public/track_kinds';
+import {JANKS_TRACK_URI, renderSliceRef} from './selection_utils';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
interface Data {
// Scroll ID.
@@ -71,32 +70,27 @@
interface JankSliceDetails {
cause: string;
- jankSlice: ScrollJankSlice;
- delayDur: duration;
- delayVsync: number;
+ id: number;
+ ts: time;
+ dur?: duration;
+ delayVsync?: number;
}
-export class ScrollDetailsPanel extends BottomTab<GenericSliceDetailsTabConfig> {
- static readonly kind = 'org.perfetto.ScrollDetailsPanel';
- loaded = false;
- data: Data | undefined;
- metrics: Metrics = {};
- orderedJankSlices: JankSliceDetails[] = [];
- scrollDeltas: m.Child;
+export class ScrollDetailsPanel implements TrackEventDetailsPanel {
+ private data?: Data;
+ private metrics: Metrics = {};
+ private orderedJankSlices: JankSliceDetails[] = [];
- static create(
- args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
- ): ScrollDetailsPanel {
- return new ScrollDetailsPanel(args);
- }
+ // TODO(altimin): Don't store Mithril vnodes between render cycles.
+ private scrollDeltas: m.Child;
- constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
- super(args);
- this.loadData();
- }
+ constructor(
+ private readonly trace: Trace,
+ private readonly id: number,
+ ) {}
- private async loadData() {
- const queryResult = await this.engine.query(`
+ async load() {
+ const queryResult = await this.trace.engine.query(`
WITH scrolls AS (
SELECT
id,
@@ -107,7 +101,7 @@
THEN gesture_scroll_begin_ts + dur
ELSE ts + dur
END AS end_ts
- FROM chrome_scrolls WHERE id = ${this.config.id})
+ FROM chrome_scrolls WHERE id = ${this.id})
SELECT
id,
start_ts AS ts,
@@ -126,8 +120,6 @@
};
await this.loadMetrics();
- this.loaded = true;
- raf.scheduleFullRedraw();
}
private async loadMetrics() {
@@ -139,7 +131,7 @@
private async loadInputEventCount() {
if (exists(this.data)) {
- const queryResult = await this.engine.query(`
+ const queryResult = await this.trace.engine.query(`
SELECT
COUNT(*) AS inputEventCount
FROM slice s
@@ -159,7 +151,7 @@
private async loadFrameStats() {
if (exists(this.data)) {
- const queryResult = await this.engine.query(`
+ const queryResult = await this.trace.engine.query(`
SELECT
IFNULL(frame_count, 0) AS frameCount,
IFNULL(missed_vsyncs, 0) AS missedVsyncs,
@@ -190,39 +182,36 @@
private async loadDelayData() {
if (exists(this.data)) {
- const queryResult = await this.engine.query(`
+ const queryResult = await this.trace.engine.query(`
SELECT
+ id,
+ ts,
+ dur,
IFNULL(sub_cause_of_jank, IFNULL(cause_of_jank, 'Unknown')) AS cause,
- IFNULL(event_latency_id, 0) AS eventLatencyId,
- IFNULL(dur, 0) AS delayDur,
- IFNULL(delayed_frame_count, 0) AS delayVsync
+ event_latency_id AS eventLatencyId,
+ delayed_frame_count AS delayVsync
FROM chrome_janky_frame_presentation_intervals s
WHERE s.ts >= ${this.data.ts}
AND s.ts + s.dur <= ${this.data.ts + this.data.dur}
- ORDER by delayDur DESC;
+ ORDER by dur DESC;
`);
- const iter = queryResult.iter({
+ const it = queryResult.iter({
+ id: NUM,
+ ts: LONG,
+ dur: LONG_NULL,
cause: STR,
- eventLatencyId: NUM,
- delayDur: LONG,
- delayVsync: NUM,
+ eventLatencyId: NUM_NULL,
+ delayVsync: NUM_NULL,
});
- for (; iter.valid(); iter.next()) {
- if (iter.delayDur <= 0) {
- break;
- }
- const jankSlices = await getScrollJankSlices(
- this.engine,
- iter.eventLatencyId,
- );
-
+ for (; it.valid(); it.next()) {
this.orderedJankSlices.push({
- cause: iter.cause,
- jankSlice: jankSlices[0],
- delayDur: iter.delayDur,
- delayVsync: iter.delayVsync,
+ id: it.id,
+ ts: Time.fromRaw(it.ts),
+ dur: it.dur ?? undefined,
+ cause: it.cause,
+ delayVsync: it.delayVsync ?? undefined,
});
}
}
@@ -230,17 +219,20 @@
private async loadScrollOffsets() {
if (exists(this.data)) {
- const inputDeltas = await getInputScrollDeltas(this.engine, this.data.id);
+ const inputDeltas = await getInputScrollDeltas(
+ this.trace.engine,
+ this.data.id,
+ );
const presentedDeltas = await getPresentedScrollDeltas(
- this.engine,
+ this.trace.engine,
this.data.id,
);
const predictorDeltas = await getPredictorJankDeltas(
- this.engine,
+ this.trace.engine,
this.data.id,
);
const jankIntervals = await getJankIntervals(
- this.engine,
+ this.trace.engine,
this.data.ts,
this.data.dur,
);
@@ -310,31 +302,27 @@
private getDelayTable(): m.Child {
if (this.orderedJankSlices.length > 0) {
- interface DelayData {
- jankLink: m.Child;
- dur: m.Child;
- delayedVSyncs: number;
- }
-
- const columns: ColumnDescriptor<DelayData>[] = [
- widgetColumn<DelayData>('Cause', (x) => x.jankLink),
- widgetColumn<DelayData>('Duration', (x) => x.dur),
- numberColumn<DelayData>('Delayed Vsyncs', (x) => x.delayedVSyncs),
+ const columns: ColumnDescriptor<JankSliceDetails>[] = [
+ widgetColumn<JankSliceDetails>('Cause', (jankSlice) =>
+ renderSliceRef({
+ trace: this.trace,
+ id: jankSlice.id,
+ trackUri: JANKS_TRACK_URI,
+ title: jankSlice.cause,
+ }),
+ ),
+ widgetColumn<JankSliceDetails>('Duration', (jankSlice) =>
+ jankSlice.dur !== undefined
+ ? m(DurationWidget, {dur: jankSlice.dur})
+ : 'NULL',
+ ),
+ widgetColumn<JankSliceDetails>(
+ 'Delayed Vsyncs',
+ (jankSlice) => jankSlice.delayVsync,
+ ),
];
- const data: DelayData[] = [];
- for (const jankSlice of this.orderedJankSlices) {
- data.push({
- jankLink: getSliceForTrack(
- jankSlice.jankSlice,
- SCROLL_JANK_V3_TRACK_KIND,
- jankSlice.cause,
- ),
- dur: m(DurationWidget, {dur: jankSlice.delayDur}),
- delayedVSyncs: jankSlice.delayVsync,
- });
- }
- const tableData = new TableData(data);
+ const tableData = new TableData(this.orderedJankSlices);
return m(Table, {
data: tableData,
@@ -391,8 +379,8 @@
);
}
- viewTab() {
- if (this.isLoading() || this.data == undefined) {
+ render() {
+ if (this.data == undefined) {
return m('h2', 'Loading');
}
@@ -400,13 +388,13 @@
'Scroll ID': this.data.id,
'Start time': m(Timestamp, {ts: this.data.ts}),
'Duration': m(DurationWidget, {dur: this.data.dur}),
- 'SQL ID': m(SqlRef, {table: 'chrome_scrolls', id: this.config.id}),
+ 'SQL ID': m(SqlRef, {table: 'chrome_scrolls', id: this.id}),
});
return m(
DetailsShell,
{
- title: this.getTitle(),
+ title: 'Scroll',
},
m(
GridLayout,
@@ -437,12 +425,4 @@
),
);
}
-
- getTitle(): string {
- return this.config.title;
- }
-
- isLoading() {
- return !this.loaded;
- }
}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts
deleted file mode 100644
index 2b3e33c..0000000
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts
+++ /dev/null
@@ -1,214 +0,0 @@
-// 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 m from 'mithril';
-import {Icons} from '../../base/semantic_icons';
-import {duration, time, Time} from '../../base/time';
-import {globals} from '../../frontend/globals';
-import {SliceSqlId} from '../../trace_processor/sql_utils/core_types';
-import {Engine} from '../../trace_processor/engine';
-import {LONG, NUM} from '../../trace_processor/query_result';
-import {
- constraintsToQuerySuffix,
- SQLConstraints,
-} from '../../trace_processor/sql_utils';
-import {Anchor} from '../../widgets/anchor';
-import {ScrollJankPluginState, ScrollJankTrackSpec} from './common';
-import {
- CHROME_EVENT_LATENCY_TRACK_KIND,
- SCROLL_JANK_V3_TRACK_KIND,
-} from '../../public/track_kinds';
-import {scrollTo} from '../../public/scroll_helper';
-
-interface BasicSlice {
- // ID of slice.
- sliceId: number;
- // Timestamp of the beginning of this slice in nanoseconds.
- ts: time;
- // Duration of this slice in nanoseconds.
- dur: duration;
-}
-
-async function getSlicesFromTrack(
- engine: Engine,
- track: ScrollJankTrackSpec,
- constraints: SQLConstraints,
-): Promise<BasicSlice[]> {
- const query = await engine.query(`
- SELECT
- id AS sliceId,
- ts,
- dur AS dur
- FROM ${track.sqlTableName}
- ${constraintsToQuerySuffix(constraints)}`);
- const it = query.iter({
- sliceId: NUM,
- ts: LONG,
- dur: LONG,
- });
-
- const result: BasicSlice[] = [];
- for (; it.valid(); it.next()) {
- result.push({
- sliceId: it.sliceId as number,
- ts: Time.fromRaw(it.ts),
- dur: it.dur,
- });
- }
- return result;
-}
-
-export type ScrollJankSlice = BasicSlice;
-export async function getScrollJankSlices(
- engine: Engine,
- id: number,
-): Promise<ScrollJankSlice[]> {
- const track = ScrollJankPluginState.getInstance().getTrack(
- SCROLL_JANK_V3_TRACK_KIND,
- );
- if (track == undefined) {
- throw new Error(`${SCROLL_JANK_V3_TRACK_KIND} track is not registered.`);
- }
-
- const slices = await getSlicesFromTrack(engine, track, {
- filters: [`event_latency_id=${id}`],
- });
- return slices;
-}
-
-export type EventLatencySlice = BasicSlice;
-export async function getEventLatencySlice(
- engine: Engine,
- id: number,
-): Promise<EventLatencySlice | undefined> {
- const track = ScrollJankPluginState.getInstance().getTrack(
- CHROME_EVENT_LATENCY_TRACK_KIND,
- );
- if (track == undefined) {
- throw new Error(
- `${CHROME_EVENT_LATENCY_TRACK_KIND} track is not registered.`,
- );
- }
-
- const slices = await getSlicesFromTrack(engine, track, {
- filters: [`id=${id}`],
- });
- return slices[0];
-}
-
-export async function getEventLatencyDescendantSlice(
- engine: Engine,
- id: number,
- descendant: string | undefined,
-): Promise<EventLatencySlice | undefined> {
- const query = await engine.query(`
- SELECT
- id as sliceId,
- ts,
- dur as dur
- FROM descendant_slice(${id})
- WHERE name='${descendant}'`);
- const it = query.iter({
- sliceId: NUM,
- ts: LONG,
- dur: LONG,
- });
-
- const result: EventLatencySlice[] = [];
-
- for (; it.valid(); it.next()) {
- result.push({
- sliceId: it.sliceId as SliceSqlId,
- ts: Time.fromRaw(it.ts),
- dur: it.dur,
- });
- }
-
- const eventLatencyTrack = ScrollJankPluginState.getInstance().getTrack(
- CHROME_EVENT_LATENCY_TRACK_KIND,
- );
- if (eventLatencyTrack == undefined) {
- throw new Error(
- `${CHROME_EVENT_LATENCY_TRACK_KIND} track is not registered.`,
- );
- }
-
- if (result.length > 1) {
- throw new Error(`
- Slice table and track view ${eventLatencyTrack.sqlTableName} has more than one descendant of slice id ${id} with name ${descendant}`);
- }
- if (result.length === 0) {
- return undefined;
- }
- return result[0];
-}
-
-interface BasicScrollJankSliceRefAttrs {
- id: number;
- ts: time;
- dur: duration;
- name: string;
- kind: string;
-}
-
-export class ScrollJankSliceRef
- implements m.ClassComponent<BasicScrollJankSliceRefAttrs>
-{
- view(vnode: m.Vnode<BasicScrollJankSliceRefAttrs>) {
- return m(
- Anchor,
- {
- icon: Icons.UpdateSelection,
- onclick: () => {
- const track = ScrollJankPluginState.getInstance().getTrack(
- vnode.attrs.kind,
- );
- if (track == undefined) {
- throw new Error(`${vnode.attrs.kind} track is not registered.`);
- }
-
- const trackUri = track.key;
- globals.selectionManager.selectGenericSlice({
- id: vnode.attrs.id,
- sqlTableName: track.sqlTableName,
- start: vnode.attrs.ts,
- duration: vnode.attrs.dur,
- trackUri,
- detailsPanelConfig: track.detailsPanelConfig,
- });
-
- scrollTo({
- track: {uri: trackUri, expandGroup: true},
- time: {start: vnode.attrs.ts},
- });
- },
- },
- vnode.attrs.name,
- );
- }
-}
-
-export function getSliceForTrack(
- state: BasicSlice,
- trackKind: string,
- name: string,
-): m.Child {
- return m(ScrollJankSliceRef, {
- id: state.sliceId,
- ts: state.ts,
- dur: state.dur,
- name: name,
- kind: trackKind,
- });
-}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
index 48fd988..280fd18 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
@@ -16,8 +16,6 @@
import {duration, Time, time} from '../../base/time';
import {exists} from '../../base/utils';
import {raf} from '../../core/raf_scheduler';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
import {getSlice, SliceDetails} from '../../trace_processor/sql_utils/slice';
import {asSliceSqlId} from '../../trace_processor/sql_utils/core_types';
import {DurationWidget} from '../../frontend/widgets/duration';
@@ -30,13 +28,9 @@
import {SqlRef} from '../../widgets/sql_ref';
import {MultiParagraphText, TextParagraph} from '../../widgets/text_paragraph';
import {dictToTreeNodes, Tree, TreeNode} from '../../widgets/tree';
-import {
- EventLatencySlice,
- getEventLatencyDescendantSlice,
- getEventLatencySlice,
- getSliceForTrack,
-} from './scroll_jank_slice';
-import {CHROME_EVENT_LATENCY_TRACK_KIND} from '../../public/track_kinds';
+import {EVENT_LATENCY_TRACK_URI, renderSliceRef} from './selection_utils';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
interface Data {
name: string;
@@ -64,10 +58,8 @@
return getSlice(engine, asSliceSqlId(id));
}
-export class ScrollJankV3DetailsPanel extends BottomTab<GenericSliceDetailsTabConfig> {
- static readonly kind = 'org.perfetto.ScrollJankV3DetailsPanel';
- data: Data | undefined;
- loaded = false;
+export class ScrollJankV3DetailsPanel implements TrackEventDetailsPanel {
+ private data?: Data;
//
// Linking to associated slices
@@ -80,29 +72,34 @@
// Link to the Event Latency in the EventLatencyTrack (subset of event
// latencies associated with input events).
- private eventLatencySliceDetails?: EventLatencySlice;
+ private eventLatencySliceDetails?: {
+ ts: time;
+ dur: duration;
+ };
// Link to the scroll jank cause stage of the associated EventLatencyTrack
// slice. May be unknown.
- private causeSliceDetails?: EventLatencySlice;
+ private causeSliceDetails?: {
+ id: number;
+ ts: time;
+ dur: duration;
+ };
// Link to the scroll jank sub-cause stage of the associated EventLatencyTrack
// slice. Does not apply to all causes.
- private subcauseSliceDetails?: EventLatencySlice;
+ private subcauseSliceDetails?: {
+ id: number;
+ ts: time;
+ dur: duration;
+ };
- static create(
- args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
- ): ScrollJankV3DetailsPanel {
- return new ScrollJankV3DetailsPanel(args);
- }
+ constructor(
+ private readonly trace: Trace,
+ private readonly id: number,
+ ) {}
- constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
- super(args);
- this.loadData();
- }
-
- private async loadData() {
- const queryResult = await this.engine.query(`
+ async load() {
+ const queryResult = await this.trace.engine.query(`
SELECT
IIF(
cause_of_jank IS NOT NULL,
@@ -117,7 +114,7 @@
IFNULL(cause_of_jank, "UNKNOWN") AS causeOfJank,
IFNULL(sub_cause_of_jank, "UNKNOWN") AS subcauseOfJank
FROM chrome_janky_frame_presentation_intervals
- WHERE id = ${this.config.id}`);
+ WHERE id = ${this.id}`);
const iter = queryResult.firstRow({
name: STR,
@@ -143,7 +140,6 @@
await this.loadJankyFrames();
await this.loadSlices();
- this.loaded = true;
raf.scheduleFullRedraw();
}
@@ -164,35 +160,62 @@
private async loadSlices() {
if (exists(this.data)) {
this.sliceDetails = await getSliceDetails(
- this.engine,
+ this.trace.engine,
this.data.eventLatencyId,
);
- this.eventLatencySliceDetails = await getEventLatencySlice(
- this.engine,
- this.data.eventLatencyId,
- );
+ const it = (
+ await this.trace.engine.query(`
+ SELECT ts, dur
+ FROM slice
+ WHERE id = ${this.data.eventLatencyId}
+ `)
+ ).iter({ts: LONG, dur: LONG});
+ this.eventLatencySliceDetails = {
+ ts: Time.fromRaw(it.ts),
+ dur: it.dur,
+ };
if (this.hasCause()) {
- this.causeSliceDetails = await getEventLatencyDescendantSlice(
- this.engine,
- this.data.eventLatencyId,
- this.data.jankCause,
- );
+ const it = (
+ await this.trace.engine.query(`
+ SELECT id, ts, dur
+ FROM descendant_slice(${this.data.eventLatencyId})
+ WHERE name = "${this.data.jankCause}"
+ `)
+ ).iter({id: NUM, ts: LONG, dur: LONG});
+
+ if (it.valid()) {
+ this.causeSliceDetails = {
+ id: it.id,
+ ts: Time.fromRaw(it.ts),
+ dur: it.dur,
+ };
+ }
}
if (this.hasSubcause()) {
- this.subcauseSliceDetails = await getEventLatencyDescendantSlice(
- this.engine,
- this.data.eventLatencyId,
- this.data.jankSubcause,
- );
+ const it = (
+ await this.trace.engine.query(`
+ SELECT id, ts, dur
+ FROM descendant_slice(${this.data.eventLatencyId})
+ WHERE name = "${this.data.jankSubcause}"
+ `)
+ ).iter({id: NUM, ts: LONG, dur: LONG});
+
+ if (it.valid()) {
+ this.subcauseSliceDetails = {
+ id: it.id,
+ ts: Time.fromRaw(it.ts),
+ dur: it.dur,
+ };
+ }
}
}
}
private async loadJankyFrames() {
if (exists(this.data)) {
- const queryResult = await this.engine.query(`
+ const queryResult = await this.trace.engine.query(`
SELECT
COUNT(*) AS jankyFrames
FROM chrome_frame_info_with_delay
@@ -263,33 +286,36 @@
const result: {[key: string]: m.Child} = {};
if (exists(this.sliceDetails) && exists(this.data)) {
- result['Janked Event Latency stage'] = exists(this.causeSliceDetails)
- ? getSliceForTrack(
- this.causeSliceDetails,
- CHROME_EVENT_LATENCY_TRACK_KIND,
- this.data.jankCause,
- )
- : this.data.jankCause;
+ result['Janked Event Latency stage'] =
+ exists(this.causeSliceDetails) &&
+ renderSliceRef({
+ trace: this.trace,
+ id: this.causeSliceDetails.id,
+ trackUri: EVENT_LATENCY_TRACK_URI,
+ title: this.data.jankCause,
+ });
if (this.hasSubcause()) {
- result['Sub-cause of Jank'] = exists(this.subcauseSliceDetails)
- ? getSliceForTrack(
- this.subcauseSliceDetails,
- CHROME_EVENT_LATENCY_TRACK_KIND,
- this.data.jankSubcause,
- )
- : this.data.jankSubcause;
+ result['Sub-cause of Jank'] =
+ exists(this.subcauseSliceDetails) &&
+ renderSliceRef({
+ trace: this.trace,
+ id: this.subcauseSliceDetails.id,
+ trackUri: EVENT_LATENCY_TRACK_URI,
+ title: this.data.jankCause,
+ });
}
const children = dictToTreeNodes(result);
if (exists(this.eventLatencySliceDetails)) {
children.unshift(
m(TreeNode, {
- left: getSliceForTrack(
- this.eventLatencySliceDetails,
- CHROME_EVENT_LATENCY_TRACK_KIND,
- 'Input EventLatency in context of ScrollUpdates',
- ),
+ left: renderSliceRef({
+ trace: this.trace,
+ id: this.data.eventLatencyId,
+ trackUri: EVENT_LATENCY_TRACK_URI,
+ title: this.data.jankCause,
+ }),
right: '',
}),
);
@@ -303,7 +329,7 @@
return dictToTreeNodes(result);
}
- viewTab() {
+ render() {
if (this.data === undefined) {
return m('h2', 'Loading');
}
@@ -313,7 +339,7 @@
return m(
DetailsShell,
{
- title: this.getTitle(),
+ title: 'EventLatency',
},
m(
GridLayout,
@@ -326,12 +352,4 @@
),
);
}
-
- getTitle(): string {
- return this.config.title;
- }
-
- isLoading() {
- return !this.loaded;
- }
}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
index 81175e4..1df0c75 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
@@ -12,35 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {globals} from '../../frontend/globals';
import {NamedRow} from '../../frontend/named_slice_track';
-import {NewTrackArgs} from '../../frontend/track';
-import {SCROLL_JANK_V3_TRACK_KIND} from '../../public/track_kinds';
import {Slice} from '../../public/track';
import {
- CustomSqlDetailsPanelConfig,
CustomSqlTableDefConfig,
CustomSqlTableSliceTrack,
} from '../../frontend/tracks/custom_sql_table_slice_track';
import {JANK_COLOR} from './jank_colors';
-import {ScrollJankV3DetailsPanel} from './scroll_jank_v3_details_panel';
import {getColorForSlice} from '../../core/colorizer';
-import {ScrollJankPluginState} from './common';
const UNKNOWN_SLICE_NAME = 'Unknown';
const JANK_SLICE_NAME = ' Jank';
export class ScrollJankV3Track extends CustomSqlTableSliceTrack {
- constructor(args: NewTrackArgs) {
- super(args);
- ScrollJankPluginState.getInstance().registerTrack({
- kind: SCROLL_JANK_V3_TRACK_KIND,
- trackUri: this.uri,
- tableName: this.tableName,
- detailsPanelConfig: this.getDetailsPanel(),
- });
- }
-
getSqlDataSource(): CustomSqlTableDefConfig {
return {
columns: [
@@ -58,23 +42,6 @@
};
}
- getDetailsPanel(): CustomSqlDetailsPanelConfig {
- return {
- kind: ScrollJankV3DetailsPanel.kind,
- config: {
- sqlTableName: 'chrome_janky_frame_presentation_intervals',
- title: 'Chrome Scroll Janks',
- },
- };
- }
-
- async onDestroy(): Promise<void> {
- await super.onDestroy();
- ScrollJankPluginState.getInstance().unregisterTrack(
- SCROLL_JANK_V3_TRACK_KIND,
- );
- }
-
rowToSlice(row: NamedRow): Slice {
const slice = super.rowToSlice(row);
@@ -92,20 +59,4 @@
return {...slice, colorScheme: getColorForSlice(stage)};
}
}
-
- onUpdatedSlices(slices: Slice[]) {
- for (const slice of slices) {
- const currentSelection = globals.selectionManager.legacySelection;
- const isSelected =
- currentSelection &&
- currentSelection.kind === 'GENERIC_SLICE' &&
- currentSelection.id !== undefined &&
- currentSelection.id === slice.id;
-
- const highlighted = globals.state.highlightedSliceId === slice.id;
- const hasFocus = highlighted || isSelected;
- slice.isHighlighted = !!hasFocus;
- }
- super.onUpdatedSlices(slices);
- }
}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
index 1ee7d2c..fcd20fd 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
@@ -12,51 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {NewTrackArgs} from '../../frontend/track';
-import {CHROME_TOPLEVEL_SCROLLS_KIND} from '../../public/track_kinds';
import {
- CustomSqlDetailsPanelConfig,
CustomSqlTableDefConfig,
CustomSqlTableSliceTrack,
} from '../../frontend/tracks/custom_sql_table_slice_track';
-import {ScrollJankPluginState} from './common';
-import {ScrollDetailsPanel} from './scroll_details_panel';
export class TopLevelScrollTrack extends CustomSqlTableSliceTrack {
- public static kind = CHROME_TOPLEVEL_SCROLLS_KIND;
-
getSqlDataSource(): CustomSqlTableDefConfig {
return {
columns: [`printf("Scroll %s", CAST(id AS STRING)) AS name`, '*'],
sqlTableName: 'chrome_scrolls',
};
}
-
- getDetailsPanel(): CustomSqlDetailsPanelConfig {
- return {
- kind: ScrollDetailsPanel.kind,
- config: {
- sqlTableName: this.tableName,
- title: 'Chrome Top Level Scrolls',
- },
- };
- }
-
- constructor(args: NewTrackArgs) {
- super(args);
-
- ScrollJankPluginState.getInstance().registerTrack({
- kind: TopLevelScrollTrack.kind,
- trackUri: this.uri,
- tableName: this.tableName,
- detailsPanelConfig: this.getDetailsPanel(),
- });
- }
-
- async onDestroy(): Promise<void> {
- await super.onDestroy();
- ScrollJankPluginState.getInstance().unregisterTrack(
- TopLevelScrollTrack.kind,
- );
- }
}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/selection_utils.ts b/ui/src/core_plugins/chrome_scroll_jank/selection_utils.ts
new file mode 100644
index 0000000..4b79e05
--- /dev/null
+++ b/ui/src/core_plugins/chrome_scroll_jank/selection_utils.ts
@@ -0,0 +1,42 @@
+// Copyright (C) 2024 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 m from 'mithril';
+import {Anchor} from '../../widgets/anchor';
+import {Icons} from '../../base/semantic_icons';
+import {Trace} from '../../public/trace';
+
+export const SCROLLS_TRACK_URI = 'perfetto.ChromeScrollJank#toplevelScrolls';
+export const EVENT_LATENCY_TRACK_URI = 'perfetto.ChromeScrollJank#eventLatency';
+export const JANKS_TRACK_URI = 'perfetto.ChromeScrollJank#scrollJankV3';
+
+export function renderSliceRef(args: {
+ trace: Trace;
+ id: number;
+ trackUri: string;
+ title: m.Children;
+}) {
+ return m(
+ Anchor,
+ {
+ icon: Icons.UpdateSelection,
+ onclick: () => {
+ args.trace.selection.selectTrackEvent(args.trackUri, args.id, {
+ scrollToSelection: true,
+ });
+ },
+ },
+ args.title,
+ );
+}
diff --git a/ui/src/core_plugins/chrome_tasks/details.ts b/ui/src/core_plugins/chrome_tasks/details.ts
index 56acae1..ac96447 100644
--- a/ui/src/core_plugins/chrome_tasks/details.ts
+++ b/ui/src/core_plugins/chrome_tasks/details.ts
@@ -13,25 +13,21 @@
// limitations under the License.
import m from 'mithril';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
import {
Details,
DetailsSchema,
} from '../../frontend/widgets/sql/details/details';
import {DetailsShell} from '../../widgets/details_shell';
import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
import d = DetailsSchema;
-export class ChromeTasksDetailsTab extends BottomTab<GenericSliceDetailsTabConfig> {
- static readonly kind = 'org.chromium.ChromeTasks.TaskDetailsTab';
+export class ChromeTasksDetailsPanel implements TrackEventDetailsPanel {
+ private readonly data: Details;
- private data: Details;
-
- constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
- super(args);
-
- this.data = new Details(this.trace, 'chrome_tasks', this.config.id, {
+ constructor(trace: Trace, eventId: number) {
+ this.data = new Details(trace, 'chrome_tasks', eventId, {
'Task name': 'name',
'Start time': d.Timestamp('ts'),
'Duration': d.Interval('ts', 'dur'),
@@ -41,21 +37,13 @@
});
}
- viewTab() {
+ render() {
return m(
DetailsShell,
{
- title: this.getTitle(),
+ title: 'Chrome Tasks',
},
m(GridLayout, m(GridLayoutColumn, this.data.render())),
);
}
-
- getTitle(): string {
- return this.config.title;
- }
-
- isLoading() {
- return this.data.isLoading();
- }
}
diff --git a/ui/src/core_plugins/chrome_tasks/index.ts b/ui/src/core_plugins/chrome_tasks/index.ts
index 513da9f..9c79a0d 100644
--- a/ui/src/core_plugins/chrome_tasks/index.ts
+++ b/ui/src/core_plugins/chrome_tasks/index.ts
@@ -12,18 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {uuidv4} from '../../base/uuid';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
import {addSqlTableTab} from '../../frontend/sql_table_tab_interface';
import {asUtid} from '../../trace_processor/sql_utils/core_types';
-import {BottomTabToSCSAdapter} from '../../public/utils';
import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
import {Trace} from '../../public/trace';
import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
-import {ChromeTasksDetailsTab} from './details';
+import {ChromeTasksDetailsPanel} from './details';
import {chromeTasksTable} from './table';
import {ChromeTasksThreadTrack} from './track';
import {TrackNode} from '../../public/workspace';
+import {TrackEventSelection} from '../../public/selection';
class ChromeTasksPlugin implements PerfettoPlugin {
onActivate() {}
@@ -103,30 +101,14 @@
uri,
track: new ChromeTasksThreadTrack(ctx, uri, asUtid(utid)),
title,
+ detailsPanel: (sel: TrackEventSelection) => {
+ return new ChromeTasksDetailsPanel(ctx, sel.eventId);
+ },
});
const track = new TrackNode({uri, title});
group.addChildInOrder(track);
ctx.workspace.addChildInOrder(group);
}
-
- ctx.tabs.registerDetailsPanel(
- new BottomTabToSCSAdapter({
- tabFactory: (selection) => {
- if (
- selection.kind === 'GENERIC_SLICE' &&
- selection.detailsPanelConfig.kind === ChromeTasksDetailsTab.kind
- ) {
- const config = selection.detailsPanelConfig.config;
- return new ChromeTasksDetailsTab({
- config: config as GenericSliceDetailsTabConfig,
- trace: ctx,
- uuid: uuidv4(),
- });
- }
- return undefined;
- },
- }),
- );
}
}
diff --git a/ui/src/core_plugins/chrome_tasks/track.ts b/ui/src/core_plugins/chrome_tasks/track.ts
index e96735c..24203ea 100644
--- a/ui/src/core_plugins/chrome_tasks/track.ts
+++ b/ui/src/core_plugins/chrome_tasks/track.ts
@@ -14,11 +14,9 @@
import {Utid} from '../../trace_processor/sql_utils/core_types';
import {
- CustomSqlDetailsPanelConfig,
CustomSqlTableDefConfig,
CustomSqlTableSliceTrack,
} from '../../frontend/tracks/custom_sql_table_slice_track';
-import {ChromeTasksDetailsTab} from './details';
import {Trace} from '../../public/trace';
export class ChromeTasksThreadTrack extends CustomSqlTableSliceTrack {
@@ -37,14 +35,4 @@
whereClause: `utid = ${this.utid}`,
};
}
-
- getDetailsPanel(): CustomSqlDetailsPanelConfig {
- return {
- kind: ChromeTasksDetailsTab.kind,
- config: {
- sqlTableName: 'chrome_tasks',
- title: 'Chrome Tasks',
- },
- };
- }
}
diff --git a/ui/src/core_plugins/debug/index.ts b/ui/src/core_plugins/debug/index.ts
index 3a43977..3ff5fcb 100644
--- a/ui/src/core_plugins/debug/index.ts
+++ b/ui/src/core_plugins/debug/index.ts
@@ -12,16 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {uuidv4} from '../../base/uuid';
import {
addDebugCounterTrack,
addDebugSliceTrack,
} from '../../public/lib/debug_tracks/debug_tracks';
-import {BottomTabToSCSAdapter} from '../../public/utils';
import {Trace} from '../../public/trace';
import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
-import {DebugSliceDetailsTab} from '../../public/lib/debug_tracks/details_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
import {Optional, exists} from '../../base/utils';
class DebugTracksPlugin implements PerfettoPlugin {
@@ -65,28 +61,6 @@
}
},
});
-
- // TODO(stevegolton): While debug tracks are in their current state, we rely
- // on this plugin to provide the details panel for them. In the future, this
- // details panel will become part of the debug track's definition.
- ctx.tabs.registerDetailsPanel(
- new BottomTabToSCSAdapter({
- tabFactory: (selection) => {
- if (
- selection.kind === 'GENERIC_SLICE' &&
- selection.detailsPanelConfig.kind === DebugSliceDetailsTab.kind
- ) {
- const config = selection.detailsPanelConfig.config;
- return new DebugSliceDetailsTab({
- config: config as GenericSliceDetailsTabConfig,
- trace: ctx,
- uuid: uuidv4(),
- });
- }
- return undefined;
- },
- }),
- );
}
}
diff --git a/ui/src/core_plugins/screenshots/index.ts b/ui/src/core_plugins/screenshots/index.ts
index b7e41e2..5c8ee65 100644
--- a/ui/src/core_plugins/screenshots/index.ts
+++ b/ui/src/core_plugins/screenshots/index.ts
@@ -12,14 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {uuidv4} from '../../base/uuid';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
import {TrackNode} from '../../public/workspace';
-import {BottomTabToSCSAdapter} from '../../public/utils';
import {NUM} from '../../trace_processor/query_result';
import {Trace} from '../../public/trace';
import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
-import {ScreenshotTab} from './screenshot_panel';
+import {ScreenshotDetailsPanel} from './screenshot_panel';
import {ScreenshotsTrack} from './screenshots_track';
class ScreenshotsPlugin implements PerfettoPlugin {
@@ -45,28 +42,10 @@
tags: {
kind: ScreenshotsTrack.kind,
},
+ detailsPanel: () => new ScreenshotDetailsPanel(ctx.engine),
});
const trackNode = new TrackNode({uri, title, sortOrder: -60});
ctx.workspace.addChildInOrder(trackNode);
-
- ctx.tabs.registerDetailsPanel(
- new BottomTabToSCSAdapter({
- tabFactory: (selection) => {
- if (
- selection.kind === 'GENERIC_SLICE' &&
- selection.detailsPanelConfig.kind === ScreenshotTab.kind
- ) {
- const config = selection.detailsPanelConfig.config;
- return new ScreenshotTab({
- config: config as GenericSliceDetailsTabConfig,
- trace: ctx,
- uuid: uuidv4(),
- });
- }
- return undefined;
- },
- }),
- );
}
}
}
diff --git a/ui/src/core_plugins/screenshots/screenshot_panel.ts b/ui/src/core_plugins/screenshots/screenshot_panel.ts
index a1d8cd4..a5424af 100644
--- a/ui/src/core_plugins/screenshots/screenshot_panel.ts
+++ b/ui/src/core_plugins/screenshots/screenshot_panel.ts
@@ -15,44 +15,25 @@
import m from 'mithril';
import {assertTrue} from '../../base/logging';
import {exists} from '../../base/utils';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
import {getSlice, SliceDetails} from '../../trace_processor/sql_utils/slice';
import {asSliceSqlId} from '../../trace_processor/sql_utils/core_types';
import {Engine} from '../../trace_processor/engine';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {TrackEventSelection} from '../../public/selection';
-async function getSliceDetails(
- engine: Engine,
- id: number,
-): Promise<SliceDetails | undefined> {
- return getSlice(engine, asSliceSqlId(id));
-}
-
-export class ScreenshotTab extends BottomTab<GenericSliceDetailsTabConfig> {
- static readonly kind = 'dev.perfetto.ScreenshotDetailsPanel';
-
+export class ScreenshotDetailsPanel implements TrackEventDetailsPanel {
private sliceDetails?: SliceDetails;
- static create(
- args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
- ): ScreenshotTab {
- return new ScreenshotTab(args);
- }
+ constructor(private readonly engine: Engine) {}
- constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
- super(args);
- getSliceDetails(this.engine, this.config.id).then(
- (sliceDetails) => (this.sliceDetails = sliceDetails),
+ async load(selection: TrackEventSelection) {
+ this.sliceDetails = await getSlice(
+ this.engine,
+ asSliceSqlId(selection.eventId),
);
}
- renderTabCanvas() {}
-
- getTitle() {
- return this.config.title;
- }
-
- viewTab() {
+ render() {
if (
!exists(this.sliceDetails) ||
!exists(this.sliceDetails.args) ||
diff --git a/ui/src/core_plugins/screenshots/screenshots_track.ts b/ui/src/core_plugins/screenshots/screenshots_track.ts
index d88010e..e9decdc 100644
--- a/ui/src/core_plugins/screenshots/screenshots_track.ts
+++ b/ui/src/core_plugins/screenshots/screenshots_track.ts
@@ -13,11 +13,9 @@
// limitations under the License.
import {
- CustomSqlDetailsPanelConfig,
CustomSqlTableDefConfig,
CustomSqlTableSliceTrack,
} from '../../frontend/tracks/custom_sql_table_slice_track';
-import {ScreenshotTab} from './screenshot_panel';
export class ScreenshotsTrack extends CustomSqlTableSliceTrack {
static readonly kind = 'dev.perfetto.ScreenshotsTrack';
@@ -28,14 +26,4 @@
columns: ['*'],
};
}
-
- getDetailsPanel(): CustomSqlDetailsPanelConfig {
- return {
- kind: ScreenshotTab.kind,
- config: {
- sqlTableName: this.tableName,
- title: 'Screenshots',
- },
- };
- }
}
diff --git a/ui/src/core_plugins/test_plugin/index.ts b/ui/src/core_plugins/test_plugin/index.ts
new file mode 100644
index 0000000..0979836
--- /dev/null
+++ b/ui/src/core_plugins/test_plugin/index.ts
@@ -0,0 +1,87 @@
+// Copyright (C) 2024 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 {Trace} from '../../public/trace';
+import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
+import {
+ SimpleSliceTrack,
+ SimpleSliceTrackConfig,
+} from '../../frontend/simple_slice_track';
+import {TrackNode} from '../../public/workspace';
+import {DebugSliceDetailsPanel} from '../../public/lib/debug_tracks/details_tab';
+
+class Plugin implements PerfettoPlugin {
+ async onTraceLoad(ctx: Trace): Promise<void> {
+ const traceStartTime = ctx.traceInfo.start;
+ const traceDur = ctx.traceInfo.end - ctx.traceInfo.start;
+ await ctx.engine.query(`
+ create table example_events (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT,
+ ts INTEGER,
+ dur INTEGER,
+ arg INTEGER
+ );
+
+ insert into example_events (name, ts, dur, arg)
+ values
+ ('Foo', ${traceStartTime}, ${traceDur}, 'aaa'),
+ ('Bar', ${traceStartTime}, ${traceDur / 2n}, 'bbb'),
+ ('Baz', ${traceStartTime}, ${traceDur / 3n}, 'bbb');
+ `);
+ const config: SimpleSliceTrackConfig = {
+ data: {
+ sqlSource: 'select * from example_events',
+ },
+ columns: {ts: 'ts', dur: 'dur', name: 'name'},
+ argColumns: [],
+ };
+
+ const title = 'Test Track';
+ const uri = `/test_track`;
+ const track = new SimpleSliceTrack(ctx, {trackUri: uri}, config);
+ ctx.tracks.registerTrack({
+ uri,
+ title,
+ track,
+ detailsPanel: ({eventId}) =>
+ new DebugSliceDetailsPanel(ctx, track.sqlTableName, eventId),
+ });
+
+ this.addNestedTracks(ctx, uri);
+ }
+
+ private addNestedTracks(ctx: Trace, uri: string): void {
+ const trackRoot = new TrackNode({uri, title: 'Root'});
+ const track1 = new TrackNode({uri, title: '1'});
+ const track2 = new TrackNode({uri, title: '2'});
+ const track11 = new TrackNode({uri, title: '1.1'});
+ const track12 = new TrackNode({uri, title: '1.2'});
+ const track121 = new TrackNode({uri, title: '1.2.1'});
+ const track21 = new TrackNode({uri, title: '2.1'});
+
+ ctx.workspace.addChildInOrder(trackRoot);
+ trackRoot.addChildLast(track1);
+ trackRoot.addChildLast(track2);
+ track1.addChildLast(track11);
+ track1.addChildLast(track12);
+ track12.addChildLast(track121);
+ track2.addChildLast(track21);
+ }
+}
+
+export const plugin: PluginDescriptor = {
+ pluginId: 'dev.perfetto.ExampleNestedTracks',
+ plugin: Plugin,
+};
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 79aae89..18005df 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -21,7 +21,7 @@
import {cropText} from '../base/string_utils';
import {colorCompare} from '../public/color';
import {UNEXPECTED_PINK} from '../core/colorizer';
-import {LegacySelection, TrackEventDetails} from '../public/selection';
+import {TrackEventDetails} from '../public/selection';
import {featureFlags} from '../core/feature_flags';
import {raf} from '../core/raf_scheduler';
import {Track} from '../public/track';
@@ -277,10 +277,6 @@
}
}
- protected isSelectionHandled(_selection: LegacySelection): boolean {
- return false;
- }
-
private getTitleFont(): string {
const size = this.sliceLayout.titleSizePx ?? 12;
return `${size}px Roboto Condensed`;
@@ -397,21 +393,11 @@
visibleWindow.end.toTime('ceil'),
);
- let selectedId: number | undefined = undefined;
const selection = globals.selectionManager.selection;
- switch (selection.kind) {
- case 'track_event':
- if (selection.trackUri === this.uri) {
- selectedId = selection.eventId;
- }
- break;
- case 'legacy':
- const legacySelection = selection.legacySelection;
- if (this.isSelectionHandled(legacySelection)) {
- selectedId = (legacySelection as {id: number}).id;
- }
- break;
- }
+ const selectedId =
+ selection.kind === 'track_event' && selection.trackUri === this.uri
+ ? selection.eventId
+ : undefined;
if (selectedId === undefined) {
this.selectedSlice = undefined;
diff --git a/ui/src/frontend/generic_slice_details_tab.ts b/ui/src/frontend/generic_slice_details_tab.ts
index 289e217..2389134 100644
--- a/ui/src/frontend/generic_slice_details_tab.ts
+++ b/ui/src/frontend/generic_slice_details_tab.ts
@@ -13,8 +13,7 @@
// limitations under the License.
import m from 'mithril';
-import {GenericSliceDetailsTabConfig} from '../public/details_panel';
-import {raf} from '../core/raf_scheduler';
+import {Columns, TrackEventDetailsPanel} from '../public/details_panel';
import {ColumnType} from '../trace_processor/query_result';
import {sqlValueToReadableString} from '../trace_processor/sql_utils';
import {DetailsShell} from '../widgets/details_shell';
@@ -22,7 +21,7 @@
import {Section} from '../widgets/section';
import {SqlRef} from '../widgets/sql_ref';
import {dictToTree, Tree, TreeNode} from '../widgets/tree';
-import {BottomTab, NewBottomTabArgs} from '../public/lib/bottom_tab';
+import {Trace} from '../public/trace';
export {
ColumnConfig,
@@ -34,41 +33,36 @@
// A details tab, which fetches slice-like object from a given SQL table by id
// and renders it according to the provided config, specifying which columns
// need to be rendered and how.
-export class GenericSliceDetailsTab extends BottomTab<GenericSliceDetailsTabConfig> {
- static readonly kind = 'dev.perfetto.GenericSliceDetailsTab';
+export class GenericSliceDetailsTab implements TrackEventDetailsPanel {
+ private data?: {[key: string]: ColumnType};
- data: {[key: string]: ColumnType} | undefined;
+ constructor(
+ private readonly trace: Trace,
+ private readonly sqlTableName: string,
+ private readonly id: number,
+ private readonly title: string,
+ private readonly columns?: Columns,
+ ) {}
- static create(
- args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
- ): GenericSliceDetailsTab {
- return new GenericSliceDetailsTab(args);
+ async load() {
+ const result = await this.trace.engine.query(
+ `select * from ${this.sqlTableName} where id = ${this.id}`,
+ );
+
+ this.data = result.firstRow({});
}
- constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
- super(args);
-
- this.engine
- .query(
- `select * from ${this.config.sqlTableName} where id = ${this.config.id}`,
- )
- .then((queryResult) => {
- this.data = queryResult.firstRow({});
- raf.scheduleFullRedraw();
- });
- }
-
- viewTab() {
- if (this.data === undefined) {
+ render() {
+ if (!this.data) {
return m('h2', 'Loading');
}
const args: {[key: string]: m.Child} = {};
- if (this.config.columns !== undefined) {
- for (const key of Object.keys(this.config.columns)) {
+ if (this.columns !== undefined) {
+ for (const key of Object.keys(this.columns)) {
let argKey = key;
- if (this.config.columns[key].displayName !== undefined) {
- argKey = this.config.columns[key].displayName!;
+ if (this.columns[key].displayName !== undefined) {
+ argKey = this.columns[key].displayName!;
}
args[argKey] = sqlValueToReadableString(this.data[key]);
}
@@ -83,7 +77,7 @@
return m(
DetailsShell,
{
- title: this.config.title,
+ title: this.title,
},
m(
GridLayout,
@@ -95,8 +89,8 @@
m(TreeNode, {
left: 'SQL ID',
right: m(SqlRef, {
- table: this.config.sqlTableName,
- id: this.config.id,
+ table: this.sqlTableName,
+ id: this.id,
}),
}),
]),
@@ -104,12 +98,4 @@
),
);
}
-
- getTitle(): string {
- return this.config.title;
- }
-
- isLoading() {
- return this.data === undefined;
- }
}
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index aedc10c..b58e5f9 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -14,7 +14,6 @@
import {assertExists} from '../base/logging';
import {createStore, Store} from '../base/store';
-import {time} from '../base/time';
import {Actions, DeferredAction} from '../common/actions';
import {CommandManagerImpl} from '../core/command_manager';
import {
@@ -37,13 +36,6 @@
type DispatchMultiple = (actions: DeferredAction[]) => void;
type TrackDataStore = Map<string, {}>;
-export interface QuantizedLoad {
- start: time;
- end: time;
- load: number;
-}
-type OverviewStore = Map<string, QuantizedLoad[]>;
-
export interface ThreadDesc {
utid: number;
tid: number;
@@ -54,23 +46,11 @@
}
type ThreadMap = Map<number, ThreadDesc>;
-interface SqlModule {
- readonly name: string;
- readonly sql: string;
-}
-
-interface SqlPackage {
- readonly name: string;
- readonly modules: SqlModule[];
-}
-
/**
* Global accessors for state/dispatch in the frontend.
*/
class Globals {
- readonly root = getServingRoot();
-
- private _trace: TraceImpl;
+ private _initialFakeTrace?: TraceImpl;
private _testing = false;
private _dispatchMultiple?: DispatchMultiple = undefined;
private _store = createStore<State>(createEmptyState());
@@ -80,7 +60,6 @@
// TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
private _trackDataStore?: TrackDataStore = undefined;
- private _overviewStore?: OverviewStore = undefined;
private _threadMap?: ThreadMap = undefined;
private _bufferUsage?: number = undefined;
private _recordingLog?: string = undefined;
@@ -89,46 +68,25 @@
private _embeddedMode?: boolean = undefined;
private _hideSidebar?: boolean = undefined;
private _hasFtrace: boolean = false;
- private _currentTraceId = '';
httpRpcState: HttpRpcState = {connected: false};
showPanningHint = false;
permalinkHash?: string;
- extraSqlPackages: SqlPackage[] = [];
-
- get workspace(): Workspace {
- return this._trace?.workspace;
- }
-
- // This is the app's equivalent of a plugin's onTraceLoad() function.
- // TODO(primiano): right now this is used to inject the TracImpl class into
- // globals, so it can hop consistently all its accessors to it. Once globals
- // is gone, figure out what to do with createSearchOverviewTrack().
- async onTraceLoad(trace: TraceImpl): Promise<void> {
- this._trace = trace;
- this._currentTraceId = trace.engine.engineId;
- }
// TODO(hjd): Remove once we no longer need to update UUID on redraw.
private _publishRedraw?: () => void = undefined;
- constructor() {
- // TODO(primiano): we do this to avoid making all our members possibly
- // undefined, which would cause a drama of if (!=undefined) all over the
- // code. This is not pretty, but this entire file is going to be nuked from
- // orbit soon.
- this._trace = createFakeTraceImpl();
-
- // We just want an empty instance of TraceImpl but don't want to mark it
- // as the current trace, otherwise this will trigger the plugins' OnLoad().
- AppImpl.instance.closeCurrentTrace();
- }
-
initialize(
dispatchMultiple: DispatchMultiple,
initAnalytics: () => Analytics,
) {
this._dispatchMultiple = dispatchMultiple;
+ // TODO(primiano): we do this to avoid making all our members possibly
+ // undefined, which would cause a drama of if (!=undefined) all over the
+ // code. This is not pretty, but this entire file is going to be nuked from
+ // orbit soon.
+ this._initialFakeTrace = createFakeTraceImpl();
+
setPerfHooks(
() => this.state.perfDebug,
() => this.dispatch(Actions.togglePerfDebug({})),
@@ -155,10 +113,13 @@
// initialize() is only called ever once. (But then i'm going to kill this
// entire file soon).
this._trackDataStore = new Map<string, {}>();
- this._overviewStore = new Map<string, QuantizedLoad[]>();
this._threadMap = new Map<number, ThreadDesc>();
}
+ get root() {
+ return AppImpl.instance.rootUrl;
+ }
+
get publishRedraw(): () => void {
return this._publishRedraw || (() => {});
}
@@ -184,15 +145,16 @@
}
get trace() {
- return this._trace;
+ const trace = AppImpl.instance.trace;
+ return trace ?? assertExists(this._initialFakeTrace);
}
get timeline() {
- return this._trace.timeline;
+ return this.trace.timeline;
}
get searchManager() {
- return this._trace.search;
+ return this.trace.search;
}
get logging() {
@@ -203,17 +165,17 @@
return assertExists(this._serviceWorkerController);
}
+ get workspace(): Workspace {
+ return this.trace.workspace;
+ }
+
// TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
// TODO(primiano): this should be really renamed to traceInfo, but doing so
// creates extra churn. Not worth it as we are going to get rid of this file
// soon.
get traceContext() {
- return this._trace.traceInfo;
- }
-
- get overviewStore(): OverviewStore {
- return assertExists(this._overviewStore);
+ return this.trace.traceInfo;
}
get trackDataStore(): TrackDataStore {
@@ -248,10 +210,6 @@
return this._hasFtrace;
}
- get currentTraceId() {
- return this._currentTraceId;
- }
-
getConversionJobStatus(name: ConversionJobName): ConversionJobStatus {
return this.getJobStatusMap().get(name) ?? ConversionJobStatus.NotRunning;
}
@@ -300,6 +258,10 @@
this._recordingLog = recordingLog;
}
+ get extraSqlPackages() {
+ return AppImpl.instance.extraSqlPackages;
+ }
+
// This variable is set by the is_internal_user.js script if the user is a
// googler. This is used to avoid exposing features that are not ready yet
// for public consumption. The gated features themselves are not secret.
@@ -336,19 +298,19 @@
}
get tabManager() {
- return this._trace.tabs;
+ return this.trace.tabs;
}
get trackManager() {
- return this._trace.tracks;
+ return this.trace.tracks;
}
get selectionManager() {
- return this._trace.selection;
+ return this.trace.selection;
}
get noteManager() {
- return this._trace.notes;
+ return this.trace.notes;
}
}
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 61ebc68..be030fb 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -63,6 +63,7 @@
import {AppImpl} from '../core/app_impl';
import {setAddSqlTableTabImplFunction} from './sql_table_tab_interface';
import {addSqlTableTabImpl} from './sql_table_tab';
+import {getServingRoot} from '../base/http_utils';
const EXTENSION_ID = 'lfmkphfpdbjijhpomgecfikhfohaoine';
@@ -204,11 +205,18 @@
}
function main() {
+ // Setup content security policy before anything else.
+ setupContentSecurityPolicy();
+
+ AppImpl.initialize({
+ rootUrl: getServingRoot(),
+ initialRouteArgs: Router.parseUrl(window.location.href).args,
+ clearState: () => globals.dispatch(Actions.clearState({})),
+ });
+
// Wire up raf for widgets.
setScheduleFullRedraw(() => raf.scheduleFullRedraw());
- setupContentSecurityPolicy();
-
// Load the css. The load is asynchronous and the CSS is not ready by the time
// appendChild returns.
const cssLoadPromise = defer<void>();
@@ -341,7 +349,6 @@
maybeChangeRpcPortFromFragment();
CheckHttpRpcConnection().then(() => {
const route = Router.parseUrl(window.location.href);
- AppImpl.instance.setInitialRouteArgs(route.args);
if (!globals.embeddedMode) {
installFileDropHandler();
}
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index 3545ba1..719746a 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import m from 'mithril';
-import {Time, TimeSpan, time} from '../base/time';
+import {Duration, Time, TimeSpan, duration, time} from '../base/time';
import {colorForCpu} from '../core/colorizer';
import {timestampFormat, TimestampFormat} from '../core/timestamp_format';
import {
@@ -25,7 +25,6 @@
import {InnerDragStrategy} from './drag/inner_drag_strategy';
import {OuterDragStrategy} from './drag/outer_drag_strategy';
import {DragGestureHandler} from '../base/drag_gesture_handler';
-import {globals} from './globals';
import {
getMaxMajorTicks,
MIN_PX_PER_STEP,
@@ -36,23 +35,37 @@
import {Panel} from './panel_container';
import {TimeScale} from '../base/time_scale';
import {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
+import {TraceImpl} from '../core/trace_impl';
+import {LONG, NUM} from '../trace_processor/query_result';
+import {raf} from '../core/raf_scheduler';
+import {getOrCreate} from '../base/utils';
+
+const tracesData = new WeakMap<TraceImpl, OverviewDataLoader>();
export class OverviewTimelinePanel implements Panel {
private static HANDLE_SIZE_PX = 5;
readonly kind = 'panel';
readonly selectable = false;
-
private width = 0;
private gesture?: DragGestureHandler;
private timeScale?: TimeScale;
private dragStrategy?: DragStrategy;
private readonly boundOnMouseMove = this.onMouseMove.bind(this);
+ private readonly overviewData: OverviewDataLoader;
+
+ constructor(private trace: TraceImpl) {
+ this.overviewData = getOrCreate(
+ tracesData,
+ trace,
+ () => new OverviewDataLoader(trace),
+ );
+ }
// Must explicitly type now; arguments types are no longer auto-inferred.
// https://github.com/Microsoft/TypeScript/issues/1373
onupdate({dom}: m.CVnodeDOM) {
this.width = dom.getBoundingClientRect().width;
- const traceTime = globals.traceContext;
+ const traceTime = this.trace.traceInfo;
if (this.width > TRACK_SHELL_WIDTH) {
const pxBounds = {left: TRACK_SHELL_WIDTH, right: this.width};
const hpTraceTime = HighPrecisionTimeSpan.fromTime(
@@ -103,16 +116,17 @@
renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
if (this.width === undefined) return;
if (this.timeScale === undefined) return;
+
const headerHeight = 20;
const tracksHeight = size.height - headerHeight;
const traceContext = new TimeSpan(
- globals.traceContext.start,
- globals.traceContext.end,
+ this.trace.traceInfo.start,
+ this.trace.traceInfo.end,
);
if (size.width > TRACK_SHELL_WIDTH && traceContext.duration > 0n) {
const maxMajorTicks = getMaxMajorTicks(this.width - TRACK_SHELL_WIDTH);
- const offset = globals.trace.timeline.timestampOffset();
+ const offset = this.trace.timeline.timestampOffset();
const tickGen = generateTicks(traceContext, maxMajorTicks, offset);
// Draw time labels
@@ -124,7 +138,7 @@
if (xPos > this.width) break;
if (type === TickType.MAJOR) {
ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
- const domainTime = globals.trace.timeline.toDomainTime(time);
+ const domainTime = this.trace.timeline.toDomainTime(time);
renderTimestamp(ctx, domainTime, xPos + 5, 18, MIN_PX_PER_STEP);
} else if (type == TickType.MEDIUM) {
ctx.fillRect(xPos - 1, 0, 1, 8);
@@ -135,12 +149,13 @@
}
// Draw mini-tracks with quanitzed density for each process.
- if (globals.overviewStore.size > 0) {
- const numTracks = globals.overviewStore.size;
+ const overviewData = this.overviewData.overviewData;
+ if (overviewData.size > 0) {
+ const numTracks = overviewData.size;
let y = 0;
const trackHeight = (tracksHeight - 1) / numTracks;
- for (const key of globals.overviewStore.keys()) {
- const loads = globals.overviewStore.get(key)!;
+ for (const key of overviewData.keys()) {
+ const loads = overviewData.get(key)!;
for (let i = 0; i < loads.length; i++) {
const xStart = Math.floor(this.timeScale.timeToPx(loads[i].start));
const xEnd = Math.ceil(this.timeScale.timeToPx(loads[i].end));
@@ -159,9 +174,7 @@
ctx.fillRect(0, size.height - 1, this.width, 1);
// Draw semi-opaque rects that occlude the non-visible time range.
- const [vizStartPx, vizEndPx] = OverviewTimelinePanel.extractBounds(
- this.timeScale,
- );
+ const [vizStartPx, vizEndPx] = this.extractBounds(this.timeScale);
ctx.fillStyle = OVERVIEW_TIMELINE_NON_VISIBLE_COLOR;
ctx.fillRect(
@@ -203,9 +216,7 @@
private chooseCursor(x: number) {
if (this.timeScale === undefined) return 'default';
- const [startBound, endBound] = OverviewTimelinePanel.extractBounds(
- this.timeScale,
- );
+ const [startBound, endBound] = this.extractBounds(this.timeScale);
if (
OverviewTimelinePanel.inBorderRange(x, startBound) ||
OverviewTimelinePanel.inBorderRange(x, endBound)
@@ -227,7 +238,7 @@
onDragStart(x: number) {
if (this.timeScale === undefined) return;
- const pixelBounds = OverviewTimelinePanel.extractBounds(this.timeScale);
+ const pixelBounds = this.extractBounds(this.timeScale);
if (
OverviewTimelinePanel.inBorderRange(x, pixelBounds[0]) ||
OverviewTimelinePanel.inBorderRange(x, pixelBounds[1])
@@ -245,8 +256,8 @@
this.dragStrategy = undefined;
}
- private static extractBounds(timeScale: TimeScale): [number, number] {
- const vizTime = globals.timeline.visibleWindow;
+ private extractBounds(timeScale: TimeScale): [number, number] {
+ const vizTime = this.trace.timeline.visibleWindow;
return [
Math.floor(timeScale.hpTimeToPx(vizTime.start)),
Math.ceil(timeScale.hpTimeToPx(vizTime.end)),
@@ -308,3 +319,126 @@
const {dhhmmss} = timecode;
ctx.fillText(dhhmmss, x, y, minWidth);
}
+
+interface QuantizedLoad {
+ start: time;
+ end: time;
+ load: number;
+}
+
+// Kicks of a sequence of promises that load the overiew data in steps.
+// Each step schedules an animation frame.
+class OverviewDataLoader {
+ overviewData = new Map<string, QuantizedLoad[]>();
+
+ constructor(private trace: TraceImpl) {
+ this.beginLoad();
+ }
+
+ async beginLoad() {
+ const traceSpan = new TimeSpan(
+ this.trace.traceInfo.start,
+ this.trace.traceInfo.end,
+ );
+ const engine = this.trace.engine;
+ const stepSize = Duration.max(1n, traceSpan.duration / 100n);
+ const hasSchedSql = 'select ts from sched limit 1';
+ const hasSchedOverview = (await engine.query(hasSchedSql)).numRows() > 0;
+ if (hasSchedOverview) {
+ await this.loadSchedOverview(traceSpan, stepSize);
+ } else {
+ await this.loadSliceOverview(traceSpan, stepSize);
+ }
+ }
+
+ async loadSchedOverview(traceSpan: TimeSpan, stepSize: duration) {
+ const stepPromises = [];
+ for (
+ let start = traceSpan.start;
+ start < traceSpan.end;
+ start = Time.add(start, stepSize)
+ ) {
+ const progress = start - traceSpan.start;
+ const ratio = Number(progress) / Number(traceSpan.duration);
+ this.trace.omnibox.showStatusMessage(
+ 'Loading overview ' + `${Math.round(ratio * 100)}%`,
+ );
+ const end = Time.add(start, stepSize);
+ // The (async() => {})() queues all the 100 async promises in one batch.
+ // Without that, we would wait for each step to be rendered before
+ // kicking off the next one. That would interleave an animation frame
+ // between each step, slowing down significantly the overall process.
+ stepPromises.push(
+ (async () => {
+ const schedResult = await this.trace.engine.query(
+ `select cast(sum(dur) as float)/${stepSize} as load, cpu from sched ` +
+ `where ts >= ${start} and ts < ${end} and utid != 0 ` +
+ 'group by cpu order by cpu',
+ );
+ const schedData: {[key: string]: QuantizedLoad} = {};
+ const it = schedResult.iter({load: NUM, cpu: NUM});
+ for (; it.valid(); it.next()) {
+ const load = it.load;
+ const cpu = it.cpu;
+ schedData[cpu] = {start, end, load};
+ }
+ this.appendData(schedData);
+ })(),
+ );
+ } // for(start = ...)
+ await Promise.all(stepPromises);
+ }
+
+ async loadSliceOverview(traceSpan: TimeSpan, stepSize: duration) {
+ // Slices overview.
+ const sliceResult = await this.trace.engine.query(`select
+ bucket,
+ upid,
+ ifnull(sum(utid_sum) / cast(${stepSize} as float), 0) as load
+ from thread
+ inner join (
+ select
+ ifnull(cast((ts - ${traceSpan.start})/${stepSize} as int), 0) as bucket,
+ sum(dur) as utid_sum,
+ utid
+ from slice
+ inner join thread_track on slice.track_id = thread_track.id
+ group by bucket, utid
+ ) using(utid)
+ where upid is not null
+ group by bucket, upid`);
+
+ const slicesData: {[key: string]: QuantizedLoad[]} = {};
+ const it = sliceResult.iter({bucket: LONG, upid: NUM, load: NUM});
+ for (; it.valid(); it.next()) {
+ const bucket = it.bucket;
+ const upid = it.upid;
+ const load = it.load;
+
+ const start = Time.add(traceSpan.start, stepSize * bucket);
+ const end = Time.add(start, stepSize);
+
+ const upidStr = upid.toString();
+ let loadArray = slicesData[upidStr];
+ if (loadArray === undefined) {
+ loadArray = slicesData[upidStr] = [];
+ }
+ loadArray.push({start, end, load});
+ }
+ this.appendData(slicesData);
+ }
+
+ appendData(data: {[key: string]: QuantizedLoad | QuantizedLoad[]}) {
+ for (const [key, value] of Object.entries(data)) {
+ if (!this.overviewData.has(key)) {
+ this.overviewData.set(key, []);
+ }
+ if (value instanceof Array) {
+ this.overviewData.get(key)!.push(...value);
+ } else {
+ this.overviewData.get(key)!.push(value);
+ }
+ }
+ raf.scheduleRedraw();
+ }
+}
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
index ab30a19..a32cb86 100644
--- a/ui/src/frontend/publish.ts
+++ b/ui/src/frontend/publish.ts
@@ -15,28 +15,7 @@
import {ConversionJobStatusUpdate} from '../common/conversion_jobs';
import {raf} from '../core/raf_scheduler';
import {HttpRpcState} from '../trace_processor/http_rpc_engine';
-import {globals, QuantizedLoad, ThreadDesc} from './globals';
-
-export function publishOverviewData(data: {
- [key: string]: QuantizedLoad | QuantizedLoad[];
-}) {
- for (const [key, value] of Object.entries(data)) {
- if (!globals.overviewStore.has(key)) {
- globals.overviewStore.set(key, []);
- }
- if (value instanceof Array) {
- globals.overviewStore.get(key)!.push(...value);
- } else {
- globals.overviewStore.get(key)!.push(value);
- }
- }
- raf.scheduleRedraw();
-}
-
-export function clearOverviewData() {
- globals.overviewStore.clear();
- raf.scheduleRedraw();
-}
+import {globals, ThreadDesc} from './globals';
export function publishTrackData(args: {id: string; data: {}}) {
globals.setTrackData(args.id, args.data);
diff --git a/ui/src/frontend/simple_slice_track.ts b/ui/src/frontend/simple_slice_track.ts
index 484b834..ccc5ceb 100644
--- a/ui/src/frontend/simple_slice_track.ts
+++ b/ui/src/frontend/simple_slice_track.ts
@@ -14,7 +14,6 @@
import {TrackContext} from '../public/track';
import {
- CustomSqlDetailsPanelConfig,
CustomSqlTableDefConfig,
CustomSqlTableSliceTrack,
} from './tracks/custom_sql_table_slice_track';
@@ -23,10 +22,7 @@
SqlDataSource,
} from '../public/lib/debug_tracks/debug_tracks';
import {uuidv4Sql} from '../base/uuid';
-import {
- ARG_PREFIX,
- DebugSliceDetailsTab,
-} from '../public/lib/debug_tracks/details_tab';
+import {ARG_PREFIX} from '../public/lib/debug_tracks/details_tab';
import {createPerfettoTable} from '../trace_processor/sql_utils';
import {Trace} from '../public/trace';
@@ -38,7 +34,7 @@
export class SimpleSliceTrack extends CustomSqlTableSliceTrack {
private config: SimpleSliceTrackConfig;
- private sqlTableName: string;
+ public readonly sqlTableName: string;
constructor(trace: Trace, ctx: TrackContext, config: SimpleSliceTrackConfig) {
super({
@@ -66,18 +62,6 @@
};
}
- getDetailsPanel(): CustomSqlDetailsPanelConfig {
- // We currently borrow the debug slice details tab.
- // TODO: Don't do this!
- return {
- kind: DebugSliceDetailsTab.kind,
- config: {
- sqlTableName: this.sqlTableName,
- title: 'Debug Slice',
- },
- };
- }
-
private createTableQuery(
data: SqlDataSource,
sliceColumns: SliceColumns,
diff --git a/ui/src/frontend/tracks/custom_sql_table_slice_track.ts b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
index f97142b..6b7fa59 100644
--- a/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
+++ b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
@@ -13,10 +13,7 @@
// limitations under the License.
import {generateSqlWithInternalLayout} from '../../common/internal_layout_utils';
-import {LegacySelection} from '../../public/selection';
-import {OnSliceClickArgs} from '../base_slice_track';
import {GenericSliceDetailsTabConfigBase} from '../generic_slice_details_tab';
-import {globals} from '../globals';
import {NAMED_ROW, NamedRow, NamedSliceTrack} from '../named_slice_track';
import {NewTrackArgs} from '../track';
import {createView} from '../../trace_processor/sql_utils';
@@ -69,11 +66,6 @@
| CustomSqlTableDefConfig
| Promise<CustomSqlTableDefConfig>;
- // Override by subclasses.
- abstract getDetailsPanel(
- args: OnSliceClickArgs<Slice>,
- ): CustomSqlDetailsPanelConfig;
-
getSqlImports(): CustomSqlImportConfig {
return {
modules: [] as string[],
@@ -109,32 +101,6 @@
return `SELECT * FROM ${this.tableName}`;
}
- isSelectionHandled(selection: LegacySelection) {
- if (selection.kind !== 'GENERIC_SLICE') {
- return false;
- }
- return selection.trackUri === this.uri;
- }
-
- onSliceClick(args: OnSliceClickArgs<Slice>) {
- if (this.getDetailsPanel(args) === undefined) {
- return;
- }
-
- const detailsPanelConfig = this.getDetailsPanel(args);
- globals.selectionManager.selectGenericSlice({
- id: args.slice.id,
- sqlTableName: this.tableName,
- start: args.slice.ts,
- duration: args.slice.dur,
- trackUri: this.uri,
- detailsPanelConfig: {
- kind: detailsPanelConfig.kind,
- config: detailsPanelConfig.config,
- },
- });
- }
-
async loadImports() {
for (const importModule of this.getSqlImports().modules) {
await this.engine.query(`INCLUDE PERFETTO MODULE ${importModule};`);
diff --git a/ui/src/frontend/ui_main.ts b/ui/src/frontend/ui_main.ts
index 08b145c..a7ab13f 100644
--- a/ui/src/frontend/ui_main.ts
+++ b/ui/src/frontend/ui_main.ts
@@ -82,7 +82,8 @@
// loaded (including the case of no trace at the beginning).
export class UiMain implements m.ClassComponent {
view({children}: m.CVnode) {
- return [m(UiMainPerTrace, {key: globals.currentTraceId}, children)];
+ const currentTraceId = AppImpl.instance.trace?.engine.engineId ?? '';
+ return [m(UiMainPerTrace, {key: currentTraceId}, children)];
}
}
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 46908d9..68645c4 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -91,7 +91,7 @@
// Used to prevent global deselection if a pan/drag select occurred.
private keepCurrentSelection = false;
- private overviewTimelinePanel = new OverviewTimelinePanel();
+ private overviewTimelinePanel: OverviewTimelinePanel;
private timeAxisPanel = new TimeAxisPanel();
private timeSelectionPanel = new TimeSelectionPanel();
private notesPanel = new NotesPanel();
@@ -102,6 +102,7 @@
constructor(vnode: m.CVnode<PageWithTraceAttrs>) {
this.tickmarkPanel = new TickmarkPanel(vnode.attrs.trace);
+ this.overviewTimelinePanel = new OverviewTimelinePanel(vnode.attrs.trace);
}
oncreate(vnode: m.CVnodeDOM<PageWithTraceAttrs>) {
diff --git a/ui/src/plugins/com.android.InputEvents/index.ts b/ui/src/plugins/com.android.InputEvents/index.ts
index 7a529d3..9116ad3 100644
--- a/ui/src/plugins/com.android.InputEvents/index.ts
+++ b/ui/src/plugins/com.android.InputEvents/index.ts
@@ -21,6 +21,7 @@
} from '../../frontend/simple_slice_track';
import {TrackNode} from '../../public/workspace';
import {getOrCreateUserInteractionGroup} from '../../public/standard_groups';
+import {DebugSliceDetailsPanel} from '../../public/lib/debug_tracks/details_tab';
class InputEvents implements PerfettoPlugin {
private readonly SQL_SOURCE = `
@@ -33,12 +34,12 @@
`;
async onTraceLoad(ctx: Trace): Promise<void> {
- const cnt = await(ctx.engine.query(`
+ const cnt = await ctx.engine.query(`
SELECT
count(*) as cnt
FROM slice
WHERE name GLOB 'UnwantedInteractionBlocker::notifyMotion*'
- `));
+ `);
if (cnt.firstRow({cnt: LONG}).cnt == 0n) {
return;
}
@@ -51,13 +52,16 @@
columns: {ts: 'ts', dur: 'dur', name: 'name'},
argColumns: [],
};
- await ctx.engine.query("INCLUDE PERFETTO MODULE android.input;");
+ await ctx.engine.query('INCLUDE PERFETTO MODULE android.input;');
const uri = 'com.android.InputEvents#InputEventsTrack';
const title = 'Input Events';
+ const track = new SimpleSliceTrack(ctx, {trackUri: uri}, config);
ctx.tracks.registerTrack({
uri,
title: title,
- track: new SimpleSliceTrack(ctx, {trackUri: uri}, config)
+ track,
+ detailsPanel: ({eventId}) =>
+ new DebugSliceDetailsPanel(ctx, track.sqlTableName, eventId),
});
const node = new TrackNode({uri, title});
const group = getOrCreateUserInteractionGroup(ctx.workspace);
diff --git a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
index 73bc032..0968acd 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
@@ -25,6 +25,7 @@
SimpleCounterTrackConfig,
} from '../../frontend/simple_counter_track';
import {TrackNode} from '../../public/workspace';
+import {DebugSliceDetailsPanel} from '../../public/lib/debug_tracks/details_tab';
interface ContainedTrace {
uuid: string;
@@ -1184,13 +1185,16 @@
};
const uri = `/long_battery_tracing_${name}`;
+ const track = new SimpleSliceTrack(ctx, {trackUri: uri}, config);
ctx.tracks.registerTrack({
uri,
title: name,
- track: new SimpleSliceTrack(ctx, {trackUri: uri}, config),
+ track,
+ detailsPanel: ({eventId}) =>
+ new DebugSliceDetailsPanel(ctx, track.sqlTableName, eventId),
});
- const track = new TrackNode({uri, title: name});
- this.addTrack(ctx, track, groupName);
+ const trackNode = new TrackNode({uri, title: name});
+ this.addTrack(ctx, trackNode, groupName);
}
addCounterTrack(
@@ -1513,8 +1517,8 @@
for (; slicesIt.valid(); slicesIt.next()) {
this.addSliceTrack(
ctx,
- it.name,
- `select ts dur, slice_name as name from pixel_modem_counters
+ slicesIt.track_name,
+ `select ts, dur, slice_name as name from pixel_modem_slices
where track_name = '${slicesIt.track_name}'`,
groupName,
);
diff --git a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
index e734528..a53dd2a 100644
--- a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
@@ -20,6 +20,7 @@
SimpleSliceTrackConfig,
} from '../../frontend/simple_slice_track';
import {TrackNode} from '../../public/workspace';
+import {DebugSliceDetailsPanel} from '../../public/lib/debug_tracks/details_tab';
class AndroidStartup implements PerfettoPlugin {
async onTraceLoad(ctx: Trace): Promise<void> {
const e = ctx.engine;
@@ -43,13 +44,16 @@
};
const uri = `/android_startups`;
const title = 'Android App Startups';
+ const track = new SimpleSliceTrack(ctx, {trackUri: uri}, config);
ctx.tracks.registerTrack({
uri,
title: 'Android App Startups',
- track: new SimpleSliceTrack(ctx, {trackUri: uri}, config),
+ track,
+ detailsPanel: ({eventId}) =>
+ new DebugSliceDetailsPanel(ctx, track.sqlTableName, eventId),
});
- const track = new TrackNode({title, uri});
- ctx.workspace.addChildInOrder(track);
+ const trackNode = new TrackNode({title, uri});
+ ctx.workspace.addChildInOrder(trackNode);
}
}
diff --git a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
index 4199c44..fc660e2 100644
--- a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
+++ b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
@@ -17,6 +17,7 @@
import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
import {SimpleSliceTrack} from '../../frontend/simple_slice_track';
import {TrackNode} from '../../public/workspace';
+import {DebugSliceDetailsPanel} from '../../public/lib/debug_tracks/details_tab';
class TraceMetadata implements PerfettoPlugin {
async onTraceLoad(ctx: Trace): Promise<void> {
const res = await ctx.engine.query(`
@@ -28,27 +29,30 @@
}
const uri = `/clock_snapshots`;
const title = 'Clock Snapshots';
+ const track = new SimpleSliceTrack(
+ ctx,
+ {trackUri: uri},
+ {
+ data: {
+ sqlSource: `
+ select ts, 0 as dur, 'Snapshot' as name
+ from clock_snapshot
+ `,
+ columns: ['ts', 'dur', 'name'],
+ },
+ columns: {ts: 'ts', dur: 'dur', name: 'name'},
+ argColumns: [],
+ },
+ );
ctx.tracks.registerTrack({
uri,
title,
- track: new SimpleSliceTrack(
- ctx,
- {trackUri: uri},
- {
- data: {
- sqlSource: `
- select ts, 0 as dur, 'Snapshot' as name
- from clock_snapshot
- `,
- columns: ['ts', 'dur', 'name'],
- },
- columns: {ts: 'ts', dur: 'dur', name: 'name'},
- argColumns: [],
- },
- ),
+ track,
+ detailsPanel: ({eventId}) =>
+ new DebugSliceDetailsPanel(ctx, track.sqlTableName, eventId),
});
- const track = new TrackNode({uri, title});
- ctx.workspace.addChildInOrder(track);
+ const trackNode = new TrackNode({uri, title});
+ ctx.workspace.addChildInOrder(trackNode);
}
}
diff --git a/ui/src/public/extra_sql_packages.ts b/ui/src/public/extra_sql_packages.ts
new file mode 100644
index 0000000..17feab7
--- /dev/null
+++ b/ui/src/public/extra_sql_packages.ts
@@ -0,0 +1,25 @@
+// Copyright (C) 2024 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.
+
+// Interfaces for extra SQL packages injected via google-internal deployments.
+
+export interface SqlModule {
+ readonly name: string;
+ readonly sql: string;
+}
+
+export interface SqlPackage {
+ readonly name: string;
+ readonly modules: SqlModule[];
+}
diff --git a/ui/src/public/lib/debug_tracks/debug_tracks.ts b/ui/src/public/lib/debug_tracks/debug_tracks.ts
index f85290f..387b8f2 100644
--- a/ui/src/public/lib/debug_tracks/debug_tracks.ts
+++ b/ui/src/public/lib/debug_tracks/debug_tracks.ts
@@ -19,9 +19,10 @@
sqlValueToReadableString,
} from '../../../trace_processor/sql_utils';
import {DebugCounterTrack} from './counter_track';
-import {ARG_PREFIX} from './details_tab';
+import {ARG_PREFIX, DebugSliceDetailsPanel} from './details_tab';
import {TrackNode} from '../../workspace';
import {Trace} from '../../trace';
+import {TrackEventSelection} from '../../selection';
let trackCounter = 0; // For reproducible ids.
@@ -123,6 +124,9 @@
uri,
title: trackName,
track: new DebugSliceTrack(trace, {trackUri: uri}, tableName),
+ detailsPanel: (sel: TrackEventSelection) => {
+ return new DebugSliceDetailsPanel(trace, tableName, sel.eventId);
+ },
});
// Create the actions to add this track to the tracklist
diff --git a/ui/src/public/lib/debug_tracks/details_tab.ts b/ui/src/public/lib/debug_tracks/details_tab.ts
index 615f842..fe7c61e 100644
--- a/ui/src/public/lib/debug_tracks/details_tab.ts
+++ b/ui/src/public/lib/debug_tracks/details_tab.ts
@@ -14,8 +14,6 @@
import m from 'mithril';
import {duration, Time, time} from '../../../base/time';
-import {BottomTab, NewBottomTabArgs} from '../bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../../frontend/generic_slice_details_tab';
import {hasArgs, renderArguments} from '../../../frontend/slice_args';
import {getSlice, SliceDetails} from '../../../trace_processor/sql_utils/slice';
import {
@@ -49,6 +47,8 @@
import {getThreadName} from '../../../trace_processor/sql_utils/thread';
import {getProcessName} from '../../../trace_processor/sql_utils/process';
import {sliceRef} from '../../../frontend/widgets/slice';
+import {TrackEventDetailsPanel} from '../../details_panel';
+import {Trace} from '../../trace';
export const ARG_PREFIX = 'arg_';
@@ -78,10 +78,8 @@
return children;
}
-export class DebugSliceDetailsTab extends BottomTab<GenericSliceDetailsTabConfig> {
- static readonly kind = 'dev.perfetto.DebugSliceDetailsTab';
-
- data?: {
+export class DebugSliceDetailsPanel implements TrackEventDetailsPanel {
+ private data?: {
name: string;
ts: time;
dur: duration;
@@ -91,14 +89,14 @@
// tables. These values will be set if the relevant columns exist and
// are consistent (e.g. 'ts' and 'dur' for this slice correspond to values
// in these well-known tables).
- threadState?: ThreadState;
- slice?: SliceDetails;
+ private threadState?: ThreadState;
+ private slice?: SliceDetails;
- static create(
- args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
- ): DebugSliceDetailsTab {
- return new DebugSliceDetailsTab(args);
- }
+ constructor(
+ private readonly trace: Trace,
+ private readonly tableName: string,
+ private readonly eventId: number,
+ ) {}
private async maybeLoadThreadState(
id: number | undefined,
@@ -110,7 +108,7 @@
if (id === undefined) return undefined;
if (utid === undefined) return undefined;
- const threadState = await getThreadState(this.engine, id);
+ const threadState = await getThreadState(this.trace.engine, id);
if (threadState === undefined) return undefined;
if (
table === 'thread_state' ||
@@ -150,7 +148,7 @@
if (id === undefined) return undefined;
if (table !== 'slice' && trackId === undefined) return undefined;
- const slice = await getSlice(this.engine, asSliceSqlId(id));
+ const slice = await getSlice(this.trace.engine, asSliceSqlId(id));
if (slice === undefined) return undefined;
if (
table === 'slice' ||
@@ -193,9 +191,9 @@
);
}
- private async loadData() {
- const queryResult = await this.engine.query(
- `select * from ${this.config.sqlTableName} where id = ${this.config.id}`,
+ async load() {
+ const queryResult = await this.trace.engine.query(
+ `select * from ${this.tableName} where id = ${this.eventId}`,
);
const row = queryResult.firstRow({
ts: LONG,
@@ -237,12 +235,7 @@
this.trace.scheduleRedraw();
}
- constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
- super(args);
- this.loadData();
- }
-
- viewTab() {
+ render() {
if (this.data === undefined) {
return m('h2', 'Loading');
}
@@ -250,7 +243,7 @@
'Name': this.data['name'] as string,
'Start time': m(Timestamp, {ts: timeFromSql(this.data['ts'])}),
'Duration': m(DurationWidget, {dur: durationFromSql(this.data['dur'])}),
- 'Debug slice id': `${this.config.sqlTableName}[${this.config.id}]`,
+ 'Debug slice id': `${this.tableName}[${this.eventId}]`,
});
details.push(this.renderThreadStateInfo());
details.push(this.renderSliceInfo());
diff --git a/ui/src/public/lib/debug_tracks/slice_track.ts b/ui/src/public/lib/debug_tracks/slice_track.ts
index 4cfc74f..214f32b 100644
--- a/ui/src/public/lib/debug_tracks/slice_track.ts
+++ b/ui/src/public/lib/debug_tracks/slice_track.ts
@@ -14,12 +14,10 @@
import m from 'mithril';
import {
- CustomSqlDetailsPanelConfig,
CustomSqlTableDefConfig,
CustomSqlTableSliceTrack,
} from '../../../frontend/tracks/custom_sql_table_slice_track';
import {TrackContext} from '../../track';
-import {DebugSliceDetailsTab} from './details_tab';
import {Button} from '../../../widgets/button';
import {Icons} from '../../../base/semantic_icons';
import {Trace} from '../../trace';
@@ -41,16 +39,6 @@
};
}
- getDetailsPanel(): CustomSqlDetailsPanelConfig {
- return {
- kind: DebugSliceDetailsTab.kind,
- config: {
- sqlTableName: this.sqlTableName,
- title: 'Debug Slice',
- },
- };
- }
-
getTrackShellButtons(): m.Children {
return m(Button, {
onclick: () => {
diff --git a/ui/src/public/selection.ts b/ui/src/public/selection.ts
index 4965910..69a1a87 100644
--- a/ui/src/public/selection.ts
+++ b/ui/src/public/selection.ts
@@ -16,12 +16,10 @@
import {Optional} from '../base/utils';
import {Engine} from '../trace_processor/engine';
import {ColumnDef, Sorting, ThreadStateExtra} from './aggregation';
-import {GenericSliceDetailsTabConfigBase} from './details_panel';
import {TrackDescriptor} from './track';
export interface SelectionManager {
readonly selection: Selection;
- readonly legacySelection: LegacySelection | null;
findTimeRangeOfSelection(): Optional<TimeSpan>;
clear(): void;
@@ -49,14 +47,6 @@
selectSqlEvent(sqlTableName: string, id: number, opts?: SelectionOpts): void;
/**
- * Select a legacy selection.
- *
- * @param selection - The legacy selection to select.
- * @param opts - Additional options.
- */
- selectLegacy(selection: LegacySelection, opts?: SelectionOpts): void;
-
- /**
* Create an area selection for the purposes of aggregation.
*
* @param args - The area to select.
@@ -67,20 +57,6 @@
scrollToCurrentSelection(): void;
registerAreaSelectionAggreagtor(aggr: AreaSelectionAggregator): void;
- // TODO(primiano): I don't undertsand what this generic slice is, but now
- // is exposed to plugins. For now i'm just carrying it forward.
- selectGenericSlice(args: {
- id: number;
- sqlTableName: string;
- start: time;
- duration: duration;
- trackUri: string;
- detailsPanelConfig: {
- kind: string;
- config: GenericSliceDetailsTabConfigBase;
- };
- }): void;
-
/**
* Register a new SQL selection resolver.
*
@@ -108,8 +84,7 @@
| AreaSelection
| NoteSelection
| UnionSelection
- | EmptySelection
- | LegacySelectionWrapper;
+ | EmptySelection;
/** Defines how changes to selection affect the rest of the UI state */
export interface SelectionOpts {
@@ -118,32 +93,6 @@
scrollToSelection?: boolean; // Default: false.
}
-// LEGACY Selection types:
-
-export interface LegacySelectionWrapper {
- readonly kind: 'legacy';
- readonly legacySelection: LegacySelection;
-}
-
-export type LegacySelection = GenericSliceSelection & {
- trackUri?: string;
-};
-
-export interface GenericSliceSelection {
- readonly kind: 'GENERIC_SLICE';
- readonly id: number;
- readonly sqlTableName: string;
- readonly start: time;
- readonly duration: duration;
- // NOTE: this config can be expanded for multiple details panel types.
- readonly detailsPanelConfig: {
- readonly kind: string;
- readonly config: GenericSliceDetailsTabConfigBase;
- };
-}
-
-// New Selection types:
-
export interface TrackEventSelection extends TrackEventDetails {
readonly kind: 'track_event';
readonly trackUri: string;
@@ -165,6 +114,7 @@
readonly utid?: number;
readonly tableName?: string;
readonly profileType?: ProfileType;
+ readonly interactionType?: string;
}
export interface Area {
diff --git a/ui/src/public/track.ts b/ui/src/public/track.ts
index 5f32e6b..d12faed 100644
--- a/ui/src/public/track.ts
+++ b/ui/src/public/track.ts
@@ -106,7 +106,7 @@
// Optional: A factory that returns a details panel object. This is called
// each time the selection is changed (and the selection is relevant to this
// track).
- readonly detailsPanel?: (id: TrackEventSelection) => TrackEventDetailsPanel;
+ readonly detailsPanel?: (sel: TrackEventSelection) => TrackEventDetailsPanel;
}
/**
diff --git a/ui/src/public/track_kinds.ts b/ui/src/public/track_kinds.ts
index 05671c9..e5df1ae 100644
--- a/ui/src/public/track_kinds.ts
+++ b/ui/src/public/track_kinds.ts
@@ -27,12 +27,4 @@
export const CPUSS_ESTIMATE_TRACK_KIND = 'CpuSubsystemEstimateTrack';
export const CPU_PROFILE_TRACK_KIND = 'CpuProfileTrack';
export const HEAP_PROFILE_TRACK_KIND = 'HeapProfileTrack';
-export const CHROME_TOPLEVEL_SCROLLS_KIND =
- 'org.chromium.TopLevelScrolls.scrolls';
-export const CHROME_EVENT_LATENCY_TRACK_KIND =
- 'org.chromium.ScrollJank.event_latencies';
-export const SCROLL_JANK_V3_TRACK_KIND =
- 'org.chromium.ScrollJank.scroll_jank_v3_track';
-export const CHROME_SCROLL_JANK_TRACK_KIND =
- 'org.chromium.ScrollJank.BrowserUIThreadLongTasks';
export const ANDROID_LOGS_TRACK_KIND = 'AndroidLogTrack';
diff --git a/ui/src/public/utils.ts b/ui/src/public/utils.ts
index 6e6fe02..208961a 100644
--- a/ui/src/public/utils.ts
+++ b/ui/src/public/utils.ts
@@ -13,11 +13,9 @@
// limitations under the License.
import m from 'mithril';
-import {LegacySelection, Selection} from '../public/selection';
import {BottomTab} from './lib/bottom_tab';
import {Tab} from './tab';
import {exists} from '../base/utils';
-import {DetailsPanel} from './details_panel';
import {Trace} from './trace';
import {TimeSpan} from '../base/time';
@@ -98,10 +96,6 @@
return 'Unknown';
}
-export interface BottomTabAdapterAttrs {
- tabFactory: (sel: LegacySelection) => BottomTab | undefined;
-}
-
/**
* This adapter wraps a BottomTab, converting it into a the new "current
* selection" API.
@@ -130,34 +124,6 @@
},
})
*/
-export class BottomTabToSCSAdapter implements DetailsPanel {
- private oldSelection?: Selection;
- private bottomTab?: BottomTab;
- private attrs: BottomTabAdapterAttrs;
-
- constructor(attrs: BottomTabAdapterAttrs) {
- this.attrs = attrs;
- }
-
- render(selection: Selection): m.Children {
- // Detect selection changes, assuming selection is immutable
- if (selection !== this.oldSelection) {
- this.oldSelection = selection;
- if (selection.kind === 'legacy') {
- this.bottomTab = this.attrs.tabFactory(selection.legacySelection);
- } else {
- this.bottomTab = undefined;
- }
- }
-
- return this.bottomTab?.renderPanel();
- }
-
- // Note: Must be called after render()
- isLoading(): boolean {
- return this.bottomTab?.isLoading() ?? false;
- }
-}
/**
* This adapter wraps a BottomTab, converting it to work with the Tab API.