Merge "ui: tp: plumb errors in NotifyEndOfFile into UI" into main
diff --git a/Android.bp b/Android.bp
index 69c599d..a0b685c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -5481,6 +5481,7 @@
"protos/perfetto/metrics/chrome/dropped_frames.proto",
"protos/perfetto/metrics/chrome/frame_times.proto",
"protos/perfetto/metrics/chrome/histogram_hashes.proto",
+ "protos/perfetto/metrics/chrome/histogram_summaries.proto",
"protos/perfetto/metrics/chrome/long_latency.proto",
"protos/perfetto/metrics/chrome/media_metric.proto",
"protos/perfetto/metrics/chrome/performance_mark_hashes.proto",
@@ -6790,6 +6791,7 @@
"protos/perfetto/trace/ftrace/fastrpc.proto",
"protos/perfetto/trace/ftrace/fence.proto",
"protos/perfetto/trace/ftrace/filemap.proto",
+ "protos/perfetto/trace/ftrace/fs.proto",
"protos/perfetto/trace/ftrace/ftrace.proto",
"protos/perfetto/trace/ftrace/ftrace_event.proto",
"protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -7220,6 +7222,7 @@
"protos/perfetto/trace/ftrace/fastrpc.proto",
"protos/perfetto/trace/ftrace/fence.proto",
"protos/perfetto/trace/ftrace/filemap.proto",
+ "protos/perfetto/trace/ftrace/fs.proto",
"protos/perfetto/trace/ftrace/ftrace.proto",
"protos/perfetto/trace/ftrace/ftrace_event.proto",
"protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -7312,6 +7315,7 @@
"external/perfetto/protos/perfetto/trace/ftrace/fastrpc.gen.cc",
"external/perfetto/protos/perfetto/trace/ftrace/fence.gen.cc",
"external/perfetto/protos/perfetto/trace/ftrace/filemap.gen.cc",
+ "external/perfetto/protos/perfetto/trace/ftrace/fs.gen.cc",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace.gen.cc",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.gen.cc",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.gen.cc",
@@ -7404,6 +7408,7 @@
"external/perfetto/protos/perfetto/trace/ftrace/fastrpc.gen.h",
"external/perfetto/protos/perfetto/trace/ftrace/fence.gen.h",
"external/perfetto/protos/perfetto/trace/ftrace/filemap.gen.h",
+ "external/perfetto/protos/perfetto/trace/ftrace/fs.gen.h",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace.gen.h",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.gen.h",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.gen.h",
@@ -7492,6 +7497,7 @@
"protos/perfetto/trace/ftrace/fastrpc.proto",
"protos/perfetto/trace/ftrace/fence.proto",
"protos/perfetto/trace/ftrace/filemap.proto",
+ "protos/perfetto/trace/ftrace/fs.proto",
"protos/perfetto/trace/ftrace/ftrace.proto",
"protos/perfetto/trace/ftrace/ftrace_event.proto",
"protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -7583,6 +7589,7 @@
"external/perfetto/protos/perfetto/trace/ftrace/fastrpc.pb.cc",
"external/perfetto/protos/perfetto/trace/ftrace/fence.pb.cc",
"external/perfetto/protos/perfetto/trace/ftrace/filemap.pb.cc",
+ "external/perfetto/protos/perfetto/trace/ftrace/fs.pb.cc",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace.pb.cc",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.pb.cc",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.pb.cc",
@@ -7674,6 +7681,7 @@
"external/perfetto/protos/perfetto/trace/ftrace/fastrpc.pb.h",
"external/perfetto/protos/perfetto/trace/ftrace/fence.pb.h",
"external/perfetto/protos/perfetto/trace/ftrace/filemap.pb.h",
+ "external/perfetto/protos/perfetto/trace/ftrace/fs.pb.h",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace.pb.h",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.pb.h",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.pb.h",
@@ -7762,6 +7770,7 @@
"protos/perfetto/trace/ftrace/fastrpc.proto",
"protos/perfetto/trace/ftrace/fence.proto",
"protos/perfetto/trace/ftrace/filemap.proto",
+ "protos/perfetto/trace/ftrace/fs.proto",
"protos/perfetto/trace/ftrace/ftrace.proto",
"protos/perfetto/trace/ftrace/ftrace_event.proto",
"protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -7854,6 +7863,7 @@
"external/perfetto/protos/perfetto/trace/ftrace/fastrpc.pbzero.cc",
"external/perfetto/protos/perfetto/trace/ftrace/fence.pbzero.cc",
"external/perfetto/protos/perfetto/trace/ftrace/filemap.pbzero.cc",
+ "external/perfetto/protos/perfetto/trace/ftrace/fs.pbzero.cc",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace.pbzero.cc",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.pbzero.cc",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.cc",
@@ -7946,6 +7956,7 @@
"external/perfetto/protos/perfetto/trace/ftrace/fastrpc.pbzero.h",
"external/perfetto/protos/perfetto/trace/ftrace/fence.pbzero.h",
"external/perfetto/protos/perfetto/trace/ftrace/filemap.pbzero.h",
+ "external/perfetto/protos/perfetto/trace/ftrace/fs.pbzero.h",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace.pbzero.h",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.pbzero.h",
"external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h",
@@ -13224,6 +13235,7 @@
"src/trace_processor/metrics/sql/chrome/chrome_args_class_names.sql",
"src/trace_processor/metrics/sql/chrome/chrome_event_metadata.sql",
"src/trace_processor/metrics/sql/chrome/chrome_histogram_hashes.sql",
+ "src/trace_processor/metrics/sql/chrome/chrome_histogram_summaries.sql",
"src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals.sql",
"src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_base.sql",
"src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_template.sql",
@@ -15152,6 +15164,7 @@
"protos/perfetto/trace/ftrace/fastrpc.proto",
"protos/perfetto/trace/ftrace/fence.proto",
"protos/perfetto/trace/ftrace/filemap.proto",
+ "protos/perfetto/trace/ftrace/fs.proto",
"protos/perfetto/trace/ftrace/ftrace.proto",
"protos/perfetto/trace/ftrace/ftrace_event.proto",
"protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -16493,6 +16506,7 @@
"protos/perfetto/trace/ftrace/fastrpc.proto",
"protos/perfetto/trace/ftrace/fence.proto",
"protos/perfetto/trace/ftrace/filemap.proto",
+ "protos/perfetto/trace/ftrace/fs.proto",
"protos/perfetto/trace/ftrace/ftrace.proto",
"protos/perfetto/trace/ftrace/ftrace_event.proto",
"protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
diff --git a/BUILD b/BUILD
index d4f1f1b..d4d3aff 100644
--- a/BUILD
+++ b/BUILD
@@ -2541,6 +2541,7 @@
"src/trace_processor/metrics/sql/chrome/chrome_args_class_names.sql",
"src/trace_processor/metrics/sql/chrome/chrome_event_metadata.sql",
"src/trace_processor/metrics/sql/chrome/chrome_histogram_hashes.sql",
+ "src/trace_processor/metrics/sql/chrome/chrome_histogram_summaries.sql",
"src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals.sql",
"src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_base.sql",
"src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_template.sql",
@@ -5230,6 +5231,7 @@
"protos/perfetto/metrics/chrome/dropped_frames.proto",
"protos/perfetto/metrics/chrome/frame_times.proto",
"protos/perfetto/metrics/chrome/histogram_hashes.proto",
+ "protos/perfetto/metrics/chrome/histogram_summaries.proto",
"protos/perfetto/metrics/chrome/long_latency.proto",
"protos/perfetto/metrics/chrome/media_metric.proto",
"protos/perfetto/metrics/chrome/performance_mark_hashes.proto",
@@ -5625,6 +5627,7 @@
"protos/perfetto/trace/ftrace/fastrpc.proto",
"protos/perfetto/trace/ftrace/fence.proto",
"protos/perfetto/trace/ftrace/filemap.proto",
+ "protos/perfetto/trace/ftrace/fs.proto",
"protos/perfetto/trace/ftrace/ftrace.proto",
"protos/perfetto/trace/ftrace/ftrace_event.proto",
"protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
diff --git a/protos/perfetto/metrics/chrome/BUILD.gn b/protos/perfetto/metrics/chrome/BUILD.gn
index bf47885..c0b0c36 100644
--- a/protos/perfetto/metrics/chrome/BUILD.gn
+++ b/protos/perfetto/metrics/chrome/BUILD.gn
@@ -27,6 +27,7 @@
"dropped_frames.proto",
"frame_times.proto",
"histogram_hashes.proto",
+ "histogram_summaries.proto",
"long_latency.proto",
"media_metric.proto",
"performance_mark_hashes.proto",
diff --git a/protos/perfetto/metrics/chrome/all_chrome_metrics.proto b/protos/perfetto/metrics/chrome/all_chrome_metrics.proto
index 596bc7f..ebc0ac5 100644
--- a/protos/perfetto/metrics/chrome/all_chrome_metrics.proto
+++ b/protos/perfetto/metrics/chrome/all_chrome_metrics.proto
@@ -23,6 +23,7 @@
import "protos/perfetto/metrics/chrome/dropped_frames.proto";
import "protos/perfetto/metrics/chrome/frame_times.proto";
import "protos/perfetto/metrics/chrome/histogram_hashes.proto";
+import "protos/perfetto/metrics/chrome/histogram_summaries.proto";
import "protos/perfetto/metrics/chrome/long_latency.proto";
import "protos/perfetto/metrics/chrome/media_metric.proto";
import "protos/perfetto/metrics/chrome/performance_mark_hashes.proto";
@@ -53,4 +54,5 @@
optional ChromeUnsymbolizedArgs chrome_unsymbolized_args = 1014;
optional ChromeArgsClassNames chrome_args_class_names = 1015;
optional ChromeScrollJankV3 chrome_scroll_jank_v3 = 1017;
+ optional ChromeHistogramSummaries chrome_histogram_summaries = 1018;
}
diff --git a/protos/perfetto/metrics/chrome/histogram_summaries.proto b/protos/perfetto/metrics/chrome/histogram_summaries.proto
new file mode 100644
index 0000000..57dad94
--- /dev/null
+++ b/protos/perfetto/metrics/chrome/histogram_summaries.proto
@@ -0,0 +1,43 @@
+
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+message HistogramSummary {
+ // The name of the histogram event
+ optional string name = 1;
+ // The avarage value of the histogram event
+ optional int64 mean = 2;
+ // The number of the histogram event in the trace track
+ optional uint32 count = 3;
+ // The sum of value of the histogram event
+ optional int64 sum = 4;
+ // The maximum value of the histogram event
+ optional int64 max = 5;
+ // The 90 percentile value of the histogram event
+ optional int64 p90 = 6;
+ // The 50 percentile (median) value of the histogram event
+ optional int64 p50 = 7;
+}
+
+// The list of the summary of Chrome Histograms in trace track events.
+// This includes the statistic information of each histograms from Chrome.
+message ChromeHistogramSummaries {
+ repeated HistogramSummary histogram_summary = 1;
+}
diff --git a/protos/perfetto/trace/ftrace/all_protos.gni b/protos/perfetto/trace/ftrace/all_protos.gni
index 7319dd4..8565125 100644
--- a/protos/perfetto/trace/ftrace/all_protos.gni
+++ b/protos/perfetto/trace/ftrace/all_protos.gni
@@ -42,6 +42,7 @@
"fastrpc.proto",
"fence.proto",
"filemap.proto",
+ "fs.proto",
"ftrace.proto",
"g2d.proto",
"google_icc_trace.proto",
diff --git a/protos/perfetto/trace/ftrace/fs.proto b/protos/perfetto/trace/ftrace/fs.proto
new file mode 100644
index 0000000..4cc3531
--- /dev/null
+++ b/protos/perfetto/trace/ftrace/fs.proto
@@ -0,0 +1,15 @@
+// Autogenerated by:
+// ../../src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
+// Do not edit.
+
+syntax = "proto2";
+package perfetto.protos;
+
+message DoSysOpenFtraceEvent {
+ optional string filename = 1;
+ optional int32 flags = 2;
+ optional int32 mode = 3;
+}
+message OpenExecFtraceEvent {
+ optional string filename = 1;
+}
diff --git a/protos/perfetto/trace/ftrace/ftrace_event.proto b/protos/perfetto/trace/ftrace/ftrace_event.proto
index b363eb0..40e7113 100644
--- a/protos/perfetto/trace/ftrace/ftrace_event.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_event.proto
@@ -42,6 +42,7 @@
import "protos/perfetto/trace/ftrace/fastrpc.proto";
import "protos/perfetto/trace/ftrace/fence.proto";
import "protos/perfetto/trace/ftrace/filemap.proto";
+import "protos/perfetto/trace/ftrace/fs.proto";
import "protos/perfetto/trace/ftrace/ftrace.proto";
import "protos/perfetto/trace/ftrace/g2d.proto";
import "protos/perfetto/trace/ftrace/google_icc_trace.proto";
@@ -683,5 +684,7 @@
DevfreqFrequencyFtraceEvent devfreq_frequency = 541;
KprobeEvent kprobe_event = 542;
ParamSetValueCpmFtraceEvent param_set_value_cpm = 543;
+ DoSysOpenFtraceEvent do_sys_open = 544;
+ OpenExecFtraceEvent open_exec = 545;
}
}
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 5e07a0c..6104064 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -8805,6 +8805,19 @@
// End of protos/perfetto/trace/ftrace/filemap.proto
+// Begin of protos/perfetto/trace/ftrace/fs.proto
+
+message DoSysOpenFtraceEvent {
+ optional string filename = 1;
+ optional int32 flags = 2;
+ optional int32 mode = 3;
+}
+message OpenExecFtraceEvent {
+ optional string filename = 1;
+}
+
+// End of protos/perfetto/trace/ftrace/fs.proto
+
// Begin of protos/perfetto/trace/ftrace/ftrace.proto
message PrintFtraceEvent {
@@ -11424,6 +11437,8 @@
DevfreqFrequencyFtraceEvent devfreq_frequency = 541;
KprobeEvent kprobe_event = 542;
ParamSetValueCpmFtraceEvent param_set_value_cpm = 543;
+ DoSysOpenFtraceEvent do_sys_open = 544;
+ OpenExecFtraceEvent open_exec = 545;
}
}
diff --git a/src/tools/ftrace_proto_gen/event_list b/src/tools/ftrace_proto_gen/event_list
index 8c99f49..7eeadfb 100644
--- a/src/tools/ftrace_proto_gen/event_list
+++ b/src/tools/ftrace_proto_gen/event_list
@@ -537,3 +537,5 @@
sched/sched_wakeup_task_attr
devfreq/devfreq_frequency
cpm_trace/param_set_value_cpm
+fs/do_sys_open
+fs/open_exec
diff --git a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
index 53085f9..c9d3970 100644
--- a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
@@ -24,7 +24,7 @@
namespace trace_processor {
namespace {
-std::array<FtraceMessageDescriptor, 544> descriptors{{
+std::array<FtraceMessageDescriptor, 546> descriptors{{
{nullptr, 0, {}},
{nullptr, 0, {}},
{nullptr, 0, {}},
@@ -6016,6 +6016,24 @@
{"timestamp", ProtoSchemaType::kInt64},
},
},
+ {
+ "do_sys_open",
+ 3,
+ {
+ {},
+ {"filename", ProtoSchemaType::kString},
+ {"flags", ProtoSchemaType::kInt32},
+ {"mode", ProtoSchemaType::kInt32},
+ },
+ },
+ {
+ "open_exec",
+ 1,
+ {
+ {},
+ {"filename", ProtoSchemaType::kString},
+ },
+ },
}};
} // namespace
diff --git a/src/trace_processor/importers/proto/android_probes_parser.cc b/src/trace_processor/importers/proto/android_probes_parser.cc
index 2da16c9..7c3e9f9 100644
--- a/src/trace_processor/importers/proto/android_probes_parser.cc
+++ b/src/trace_processor/importers/proto/android_probes_parser.cc
@@ -147,8 +147,10 @@
TrackTracker::Group::kPower, batt_power_id);
auto current = evt.current_ua();
auto voltage = evt.voltage_uv();
- context_->event_tracker->PushCounter(
- ts, static_cast<double>(current * voltage / 1000000000), track);
+ // Current is negative when discharging, but we want the power counter to
+ // always be positive, so take the absolute value.
+ auto power = std::abs(static_cast<double>(current * voltage / 1000000000));
+ context_->event_tracker->PushCounter(ts, power, track);
}
}
diff --git a/src/trace_processor/metrics/sql/chrome/BUILD.gn b/src/trace_processor/metrics/sql/chrome/BUILD.gn
index 30962b8..7d10aeb 100644
--- a/src/trace_processor/metrics/sql/chrome/BUILD.gn
+++ b/src/trace_processor/metrics/sql/chrome/BUILD.gn
@@ -26,6 +26,7 @@
"chrome_args_class_names.sql",
"chrome_event_metadata.sql",
"chrome_histogram_hashes.sql",
+ "chrome_histogram_summaries.sql",
"chrome_input_to_browser_intervals.sql",
"chrome_input_to_browser_intervals_base.sql",
"chrome_input_to_browser_intervals_template.sql",
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_histogram_summaries.sql b/src/trace_processor/metrics/sql/chrome/chrome_histogram_summaries.sql
new file mode 100644
index 0000000..7946491
--- /dev/null
+++ b/src/trace_processor/metrics/sql/chrome/chrome_histogram_summaries.sql
@@ -0,0 +1,49 @@
+--
+-- Copyright 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
+--
+-- https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+INCLUDE PERFETTO MODULE chrome.histograms;
+
+DROP VIEW IF EXISTS HistogramSummaryTable;
+CREATE PERFETTO VIEW HistogramSummaryTable AS
+SELECT
+ hist.name AS histname,
+ CAST(AVG(hist.value) AS INTEGER) AS mean_histval,
+ COUNT(*) AS hist_count,
+ CAST(SUM(hist.value) AS INTEGER) AS sum_histval,
+ CAST(MAX(hist.value) AS INTEGER) AS max_histval,
+ CAST(PERCENTILE(hist.value, 90) AS INTEGER) AS p90_histval,
+ CAST(PERCENTILE(hist.value, 50) AS INTEGER) AS p50_histval
+FROM chrome_histograms hist
+GROUP BY hist.name;
+
+DROP VIEW IF EXISTS chrome_histogram_summaries_output;
+CREATE PERFETTO VIEW chrome_histogram_summaries_output AS
+SELECT ChromeHistogramSummaries(
+ 'histogram_summary', (
+ SELECT RepeatedField(
+ HistogramSummary(
+ 'name', histname,
+ 'mean', mean_histval,
+ 'count', hist_count,
+ 'sum', sum_histval,
+ 'max', max_histval,
+ 'p90', p90_histval,
+ 'p50', p50_histval
+ )
+ )
+ FROM HistogramSummaryTable
+ )
+);
diff --git a/src/traced/probes/ftrace/event_info.cc b/src/traced/probes/ftrace/event_info.cc
index 2cd9b20..f93e6e4 100644
--- a/src/traced/probes/ftrace/event_info.cc
+++ b/src/traced/probes/ftrace/event_info.cc
@@ -5045,6 +5045,32 @@
kUnsetFtraceId,
98,
kUnsetSize},
+ {"do_sys_open",
+ "fs",
+ {
+ {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+ "filename", 1, ProtoSchemaType::kString,
+ TranslationStrategy::kInvalidTranslationStrategy},
+ {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+ "flags", 2, ProtoSchemaType::kInt32,
+ TranslationStrategy::kInvalidTranslationStrategy},
+ {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+ "mode", 3, ProtoSchemaType::kInt32,
+ TranslationStrategy::kInvalidTranslationStrategy},
+ },
+ kUnsetFtraceId,
+ 544,
+ kUnsetSize},
+ {"open_exec",
+ "fs",
+ {
+ {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+ "filename", 1, ProtoSchemaType::kString,
+ TranslationStrategy::kInvalidTranslationStrategy},
+ },
+ kUnsetFtraceId,
+ 545,
+ kUnsetSize},
{"print",
"ftrace",
{
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/fs/do_sys_open/format b/src/traced/probes/ftrace/test/data/synthetic/events/fs/do_sys_open/format
new file mode 100644
index 0000000..d6bac92
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/fs/do_sys_open/format
@@ -0,0 +1,13 @@
+name: do_sys_open
+ID: 685
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:__data_loc char[] filename; offset:8; size:4; signed:1;
+ field:int flags; offset:12; size:4; signed:1;
+ field:int mode; offset:16; size:4; signed:1;
+
+print fmt: ""%s" %x %o", __get_str(filename), REC->flags, REC->mode
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/fs/open_exec/format b/src/traced/probes/ftrace/test/data/synthetic/events/fs/open_exec/format
new file mode 100644
index 0000000..9f6fe3d
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/fs/open_exec/format
@@ -0,0 +1,11 @@
+name: open_exec
+ID: 686
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:__data_loc char[] filename; offset:8; size:4; signed:1;
+
+print fmt: ""%s"", __get_str(filename)
diff --git a/test/trace_processor/diff_tests/parser/power/tests_linux_sysfs_power.py b/test/trace_processor/diff_tests/parser/power/tests_linux_sysfs_power.py
index 046d1c4..ccdae4f 100644
--- a/test/trace_processor/diff_tests/parser/power/tests_linux_sysfs_power.py
+++ b/test/trace_processor/diff_tests/parser/power/tests_linux_sysfs_power.py
@@ -147,7 +147,7 @@
packet {
timestamp: 4000000
battery {
- current_ua: 510000
+ current_ua: -510000
voltage_uv: 12000000
}
}
diff --git a/ui/release/channels.json b/ui/release/channels.json
index c2c76c2..5f8615b 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -6,7 +6,7 @@
},
{
"name": "canary",
- "rev": "4817ff8af4289f905c36a8a1ba6a583afc569af4"
+ "rev": "2db61efa59d1e2eecb6975854c14b2a122fbfa8a"
},
{
"name": "autopush",
diff --git a/ui/src/common/track_helper.ts b/ui/src/common/track_helper.ts
index 3087228..e9ef6fb 100644
--- a/ui/src/common/track_helper.ts
+++ b/ui/src/common/track_helper.ts
@@ -95,6 +95,6 @@
const {start, end} = this.latestTimespan;
const resolution = this.latestResolution;
this.data_ = await this.doFetch(start, end, resolution);
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
}
}
diff --git a/ui/src/core/app_impl.ts b/ui/src/core/app_impl.ts
index 3336045..7662755 100644
--- a/ui/src/core/app_impl.ts
+++ b/ui/src/core/app_impl.ts
@@ -32,7 +32,7 @@
import {createProxy, getOrCreate} from '../base/utils';
import {PageManagerImpl} from './page_manager';
import {PageHandler} from '../public/page';
-import {setPerfHooks} from './perf';
+import {PerfManager} from './perf_manager';
import {ServiceWorkerController} from '../frontend/service_worker_controller';
import {FeatureFlagManager, FlagSettings} from '../public/feature_flag';
import {featureFlags} from './feature_flags';
@@ -59,6 +59,7 @@
readonly pageMgr = new PageManagerImpl();
readonly sidebarMgr: SidebarManagerImpl;
readonly pluginMgr: PluginManagerImpl;
+ readonly perfMgr = new PerfManager();
readonly analytics: AnalyticsInternal;
readonly serviceWorkerController: ServiceWorkerController;
httpRpc = {
@@ -67,7 +68,6 @@
};
initialRouteArgs: RouteArgs;
isLoadingTrace = false; // Set when calling openTrace().
- perfDebugging = false; // Enables performance debugging of tracks/panels.
readonly initArgs: AppInitArgs;
readonly embeddedMode: boolean;
readonly testingMode: boolean;
@@ -296,17 +296,8 @@
return this.appCtx.extraSqlPackages;
}
- get perfDebugging(): boolean {
- return this.appCtx.perfDebugging;
- }
-
- setPerfDebuggingEnabled(enabled: boolean) {
- this.appCtx.perfDebugging = enabled;
- setPerfHooks(
- () => this.perfDebugging,
- () => this.setPerfDebuggingEnabled(!this.perfDebugging),
- );
- raf.scheduleFullRedraw();
+ get perfDebugging(): PerfManager {
+ return this.appCtx.perfMgr;
}
get serviceWorkerController(): ServiceWorkerController {
diff --git a/ui/src/core/perf.ts b/ui/src/core/perf.ts
deleted file mode 100644
index 6e9afaf..0000000
--- a/ui/src/core/perf.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-// Copyright (C) 2018 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';
-
-const hooks = {
- isDebug: () => false,
- toggleDebug: () => {},
-};
-
-export function setPerfHooks(isDebug: () => boolean, toggleDebug: () => void) {
- hooks.isDebug = isDebug;
- hooks.toggleDebug = toggleDebug;
-}
-
-// Shorthand for if globals perf debug mode is on.
-export const perfDebug = () => hooks.isDebug();
-
-// Returns performance.now() if perfDebug is enabled, otherwise 0.
-// This is needed because calling performance.now is generally expensive
-// and should not be done for every frame.
-export const debugNow = () => (perfDebug() ? performance.now() : 0);
-
-// Returns execution time of |fn| if perf debug mode is on. Returns 0 otherwise.
-export function measure(fn: () => void): number {
- const start = debugNow();
- fn();
- return debugNow() - start;
-}
-
-// Stores statistics about samples, and keeps a fixed size buffer of most recent
-// samples.
-export class RunningStatistics {
- private _count = 0;
- private _mean = 0;
- private _lastValue = 0;
- private _ptr = 0;
-
- private buffer: number[] = [];
-
- constructor(private _maxBufferSize = 10) {}
-
- addValue(value: number) {
- this._lastValue = value;
- if (this.buffer.length >= this._maxBufferSize) {
- this.buffer[this._ptr++] = value;
- if (this._ptr >= this.buffer.length) {
- this._ptr -= this.buffer.length;
- }
- } else {
- this.buffer.push(value);
- }
-
- this._mean = (this._mean * this._count + value) / (this._count + 1);
- this._count++;
- }
-
- get mean() {
- return this._mean;
- }
- get count() {
- return this._count;
- }
- get bufferMean() {
- return this.buffer.reduce((sum, v) => sum + v, 0) / this.buffer.length;
- }
- get bufferSize() {
- return this.buffer.length;
- }
- get maxBufferSize() {
- return this._maxBufferSize;
- }
- get last() {
- return this._lastValue;
- }
-}
-
-// Returns a summary string representation of a RunningStatistics object.
-export function runningStatStr(stat: RunningStatistics) {
- return (
- `Last: ${stat.last.toFixed(2)}ms | ` +
- `Avg: ${stat.mean.toFixed(2)}ms | ` +
- `Avg${stat.maxBufferSize}: ${stat.bufferMean.toFixed(2)}ms`
- );
-}
-
-export interface PerfStatsSource {
- renderPerfStats(): m.Children;
-}
-
-// Globals singleton class that renders performance stats for the whole app.
-class PerfDisplay {
- private containers: PerfStatsSource[] = [];
-
- addContainer(container: PerfStatsSource) {
- this.containers.push(container);
- }
-
- removeContainer(container: PerfStatsSource) {
- const i = this.containers.indexOf(container);
- this.containers.splice(i, 1);
- }
-
- renderPerfStats(src: PerfStatsSource) {
- if (!perfDebug()) return;
- const perfDisplayEl = document.querySelector('.perf-stats');
- if (!perfDisplayEl) return;
- m.render(perfDisplayEl, [
- m('section', src.renderPerfStats()),
- m(
- 'button.close-button',
- {
- onclick: hooks.toggleDebug,
- },
- m('i.material-icons', 'close'),
- ),
- this.containers.map((c, i) =>
- m('section', m('div', `Panel Container ${i + 1}`), c.renderPerfStats()),
- ),
- ]);
- }
-}
-
-export const perfDisplay = new PerfDisplay();
diff --git a/ui/src/core/perf_manager.ts b/ui/src/core/perf_manager.ts
new file mode 100644
index 0000000..e63e7e8
--- /dev/null
+++ b/ui/src/core/perf_manager.ts
@@ -0,0 +1,145 @@
+// Copyright (C) 2018 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 {raf} from './raf_scheduler';
+import {PerfStats, PerfStatsContainer, runningStatStr} from './perf_stats';
+
+export class PerfManager {
+ private _enabled = false;
+ readonly containers: PerfStatsContainer[] = [];
+
+ get enabled(): boolean {
+ return this._enabled;
+ }
+
+ set enabled(enabled: boolean) {
+ this._enabled = enabled;
+ raf.setPerfStatsEnabled(true);
+ this.containers.forEach((c) => c.setPerfStatsEnabled(enabled));
+ }
+
+ addContainer(container: PerfStatsContainer): Disposable {
+ this.containers.push(container);
+ return {
+ [Symbol.dispose]: () => {
+ const i = this.containers.indexOf(container);
+ this.containers.splice(i, 1);
+ },
+ };
+ }
+
+ renderPerfStats(): m.Children {
+ if (!this._enabled) return;
+ // The rendering of the perf stats UI is atypical. The main issue is that we
+ // want to redraw the mithril component even if there is no full DOM redraw
+ // happening (and we don't want to force redraws as a side effect). So we
+ // return here just a container and handle its rendering ourselves.
+ const perfMgr = this;
+ let removed = false;
+ return m('.perf-stats', {
+ oncreate(vnode: m.VnodeDOM) {
+ const animationFrame = (dom: Element) => {
+ if (removed) return;
+ m.render(dom, m(PerfStatsUi, {perfMgr}));
+ requestAnimationFrame(() => animationFrame(dom));
+ };
+ animationFrame(vnode.dom);
+ },
+ onremove() {
+ removed = true;
+ },
+ });
+ }
+}
+
+// The mithril component that draws the contents of the perf stats box.
+
+interface PerfStatsUiAttrs {
+ perfMgr: PerfManager;
+}
+
+class PerfStatsUi implements m.ClassComponent<PerfStatsUiAttrs> {
+ view({attrs}: m.Vnode<PerfStatsUiAttrs>) {
+ return m(
+ '.perf-stats',
+ {},
+ m('section', this.renderRafSchedulerStats()),
+ m(
+ 'button.close-button',
+ {
+ onclick: () => (attrs.perfMgr.enabled = false),
+ },
+ m('i.material-icons', 'close'),
+ ),
+ attrs.perfMgr.containers.map((c, i) =>
+ m('section', m('div', `Panel Container ${i + 1}`), c.renderPerfStats()),
+ ),
+ );
+ }
+
+ renderRafSchedulerStats() {
+ return m(
+ 'div',
+ m('div', [
+ m(
+ 'button',
+ {onclick: () => raf.scheduleCanvasRedraw()},
+ 'Do Canvas Redraw',
+ ),
+ ' | ',
+ m(
+ 'button',
+ {onclick: () => raf.scheduleFullRedraw()},
+ 'Do Full Redraw',
+ ),
+ ]),
+ m('div', 'Raf Timing ' + '(Total may not add up due to imprecision)'),
+ m(
+ 'table',
+ this.statTableHeader(),
+ this.statTableRow('Actions', raf.perfStats.rafActions),
+ this.statTableRow('Dom', raf.perfStats.rafDom),
+ this.statTableRow('Canvas', raf.perfStats.rafCanvas),
+ this.statTableRow('Total', raf.perfStats.rafTotal),
+ ),
+ m(
+ 'div',
+ 'Dom redraw: ' +
+ `Count: ${raf.perfStats.domRedraw.count} | ` +
+ runningStatStr(raf.perfStats.domRedraw),
+ ),
+ );
+ }
+
+ statTableHeader() {
+ return m(
+ 'tr',
+ m('th', ''),
+ m('th', 'Last (ms)'),
+ m('th', 'Avg (ms)'),
+ m('th', 'Avg-10 (ms)'),
+ );
+ }
+
+ statTableRow(title: string, stat: PerfStats) {
+ return m(
+ 'tr',
+ m('td', title),
+ m('td', stat.last.toFixed(2)),
+ m('td', stat.mean.toFixed(2)),
+ m('td', stat.bufferMean.toFixed(2)),
+ );
+ }
+}
diff --git a/ui/src/core/perf_stats.ts b/ui/src/core/perf_stats.ts
new file mode 100644
index 0000000..3f1eda0
--- /dev/null
+++ b/ui/src/core/perf_stats.ts
@@ -0,0 +1,78 @@
+// 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';
+
+// The interface that every container (e.g. Track Panels) that exposes granular
+// per-container masurements implements to be perf-stats-aware.
+export interface PerfStatsContainer {
+ setPerfStatsEnabled(enable: boolean): void;
+ renderPerfStats(): m.Children;
+}
+
+// Stores statistics about samples, and keeps a fixed size buffer of most recent
+// samples.
+export class PerfStats {
+ private _count = 0;
+ private _mean = 0;
+ private _lastValue = 0;
+ private _ptr = 0;
+
+ private buffer: number[] = [];
+
+ constructor(private _maxBufferSize = 10) {}
+
+ addValue(value: number) {
+ this._lastValue = value;
+ if (this.buffer.length >= this._maxBufferSize) {
+ this.buffer[this._ptr++] = value;
+ if (this._ptr >= this.buffer.length) {
+ this._ptr -= this.buffer.length;
+ }
+ } else {
+ this.buffer.push(value);
+ }
+
+ this._mean = (this._mean * this._count + value) / (this._count + 1);
+ this._count++;
+ }
+
+ get mean() {
+ return this._mean;
+ }
+ get count() {
+ return this._count;
+ }
+ get bufferMean() {
+ return this.buffer.reduce((sum, v) => sum + v, 0) / this.buffer.length;
+ }
+ get bufferSize() {
+ return this.buffer.length;
+ }
+ get maxBufferSize() {
+ return this._maxBufferSize;
+ }
+ get last() {
+ return this._lastValue;
+ }
+}
+
+// Returns a summary string representation of a RunningStatistics object.
+export function runningStatStr(stat: PerfStats) {
+ return (
+ `Last: ${stat.last.toFixed(2)}ms | ` +
+ `Avg: ${stat.mean.toFixed(2)}ms | ` +
+ `Avg${stat.maxBufferSize}: ${stat.bufferMean.toFixed(2)}ms`
+ );
+}
diff --git a/ui/src/core/perf_unittest.ts b/ui/src/core/perf_stats_unittest.ts
similarity index 86%
rename from ui/src/core/perf_unittest.ts
rename to ui/src/core/perf_stats_unittest.ts
index 5ba357c..1b24bf5 100644
--- a/ui/src/core/perf_unittest.ts
+++ b/ui/src/core/perf_stats_unittest.ts
@@ -12,10 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {RunningStatistics} from './perf';
+import {PerfStats} from './perf_stats';
test('buffer size is accurate before reaching max capacity', () => {
- const buf = new RunningStatistics(10);
+ const buf = new PerfStats(10);
for (let i = 0; i < 10; i++) {
buf.addValue(i);
@@ -24,7 +24,7 @@
});
test('buffer size is accurate after reaching max capacity', () => {
- const buf = new RunningStatistics(10);
+ const buf = new PerfStats(10);
for (let i = 0; i < 10; i++) {
buf.addValue(i);
@@ -37,7 +37,7 @@
});
test('buffer mean is accurate before reaching max capacity', () => {
- const buf = new RunningStatistics(10);
+ const buf = new PerfStats(10);
buf.addValue(1);
buf.addValue(2);
@@ -47,7 +47,7 @@
});
test('buffer mean is accurate after reaching max capacity', () => {
- const buf = new RunningStatistics(10);
+ const buf = new PerfStats(10);
for (let i = 0; i < 20; i++) {
buf.addValue(2);
diff --git a/ui/src/core/raf_scheduler.ts b/ui/src/core/raf_scheduler.ts
index c6ca0fc..b23379f 100644
--- a/ui/src/core/raf_scheduler.ts
+++ b/ui/src/core/raf_scheduler.ts
@@ -12,39 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import m from 'mithril';
-import {
- debugNow,
- measure,
- perfDebug,
- perfDisplay,
- PerfStatsSource,
- RunningStatistics,
- runningStatStr,
-} from './perf';
+import {PerfStats} from './perf_stats';
-function statTableHeader() {
- return m(
- 'tr',
- m('th', ''),
- m('th', 'Last (ms)'),
- m('th', 'Avg (ms)'),
- m('th', 'Avg-10 (ms)'),
- );
-}
-
-function statTableRow(title: string, stat: RunningStatistics) {
- return m(
- 'tr',
- m('td', title),
- m('td', stat.last.toFixed(2)),
- m('td', stat.mean.toFixed(2)),
- m('td', stat.bufferMean.toFixed(2)),
- );
-}
-
-export type ActionCallback = (nowMs: number) => void;
-export type RedrawCallback = (nowMs: number) => void;
+export type AnimationCallback = (lastFrameMs: number) => void;
+export type RedrawCallback = () => void;
// This class orchestrates all RAFs in the UI. It ensures that there is only
// one animation frame handler overall and that callbacks are called in
@@ -54,146 +25,134 @@
// - redraw callbacks that will repaint canvases.
// This class guarantees that, on each frame, redraw callbacks are called after
// all action callbacks.
-export class RafScheduler implements PerfStatsSource {
- private actionCallbacks = new Set<ActionCallback>();
+export class RafScheduler {
+ // These happen at the beginning of any animation frame. Used by Animation.
+ private animationCallbacks = new Set<AnimationCallback>();
+
+ // These happen during any animaton frame, after the (optional) DOM redraw.
private canvasRedrawCallbacks = new Set<RedrawCallback>();
- private _syncDomRedraw: RedrawCallback = (_) => {};
+
+ // These happen at the end of full (DOM) animation frames.
+ private postRedrawCallbacks = new Array<RedrawCallback>();
+ private syncDomRedrawFn: () => void = () => {};
private hasScheduledNextFrame = false;
private requestedFullRedraw = false;
private isRedrawing = false;
private _shutdown = false;
- private _beforeRedraw: () => void = () => {};
- private _afterRedraw: () => void = () => {};
- private _pendingCallbacks: RedrawCallback[] = [];
+ private recordPerfStats = false;
- private perfStats = {
- rafActions: new RunningStatistics(),
- rafCanvas: new RunningStatistics(),
- rafDom: new RunningStatistics(),
- rafTotal: new RunningStatistics(),
- domRedraw: new RunningStatistics(),
+ readonly perfStats = {
+ rafActions: new PerfStats(),
+ rafCanvas: new PerfStats(),
+ rafDom: new PerfStats(),
+ rafTotal: new PerfStats(),
+ domRedraw: new PerfStats(),
};
- start(cb: ActionCallback) {
- this.actionCallbacks.add(cb);
- this.maybeScheduleAnimationFrame();
+ // Called by frontend/index.ts. syncDomRedrawFn is a function that invokes
+ // m.render() of the root UiMain component.
+ initialize(syncDomRedrawFn: () => void) {
+ this.syncDomRedrawFn = syncDomRedrawFn;
}
- stop(cb: ActionCallback) {
- this.actionCallbacks.delete(cb);
- }
-
- addRedrawCallback(cb: RedrawCallback) {
- this.canvasRedrawCallbacks.add(cb);
- }
-
- removeRedrawCallback(cb: RedrawCallback) {
- this.canvasRedrawCallbacks.delete(cb);
- }
-
- addPendingCallback(cb: RedrawCallback) {
- this._pendingCallbacks.push(cb);
+ // Schedule re-rendering of virtual DOM and canvas.
+ // If a callback is passed it will be executed after the DOM redraw has
+ // completed.
+ scheduleFullRedraw(cb?: RedrawCallback) {
+ this.requestedFullRedraw = true;
+ cb && this.postRedrawCallbacks.push(cb);
+ this.maybeScheduleAnimationFrame(true);
}
// Schedule re-rendering of canvas only.
- scheduleRedraw() {
+ scheduleCanvasRedraw() {
this.maybeScheduleAnimationFrame(true);
}
+ startAnimation(cb: AnimationCallback) {
+ this.animationCallbacks.add(cb);
+ this.maybeScheduleAnimationFrame();
+ }
+
+ stopAnimation(cb: AnimationCallback) {
+ this.animationCallbacks.delete(cb);
+ }
+
+ addCanvasRedrawCallback(cb: RedrawCallback): Disposable {
+ this.canvasRedrawCallbacks.add(cb);
+ const canvasRedrawCallbacks = this.canvasRedrawCallbacks;
+ return {
+ [Symbol.dispose]() {
+ canvasRedrawCallbacks.delete(cb);
+ },
+ };
+ }
+
shutdown() {
this._shutdown = true;
}
- set domRedraw(cb: RedrawCallback) {
- this._syncDomRedraw = cb;
- }
-
- set beforeRedraw(cb: () => void) {
- this._beforeRedraw = cb;
- }
-
- set afterRedraw(cb: () => void) {
- this._afterRedraw = cb;
- }
-
- // Schedule re-rendering of virtual DOM and canvas.
- scheduleFullRedraw() {
- this.requestedFullRedraw = true;
- this.maybeScheduleAnimationFrame(true);
- }
-
- // Schedule a full redraw to happen after a short delay (50 ms).
- // This is done to prevent flickering / visual noise and allow the UI to fetch
- // the initial data from the Trace Processor.
- // There is a chance that someone else schedules a full redraw in the
- // meantime, forcing the flicker, but in practice it works quite well and
- // avoids a lot of complexity for the callers.
- scheduleDelayedFullRedraw() {
- // 50ms is half of the responsiveness threshold (100ms):
- // https://web.dev/rail/#response-process-events-in-under-50ms
- const delayMs = 50;
- setTimeout(() => this.scheduleFullRedraw(), delayMs);
- }
-
- syncDomRedraw(nowMs: number) {
- const redrawStart = debugNow();
- this._syncDomRedraw(nowMs);
- if (perfDebug()) {
- this.perfStats.domRedraw.addValue(debugNow() - redrawStart);
- }
+ setPerfStatsEnabled(enabled: boolean) {
+ this.recordPerfStats = enabled;
+ this.scheduleFullRedraw();
}
get hasPendingRedraws(): boolean {
return this.isRedrawing || this.hasScheduledNextFrame;
}
- private syncCanvasRedraw(nowMs: number) {
- const redrawStart = debugNow();
- if (this.isRedrawing) return;
- this._beforeRedraw();
- this.isRedrawing = true;
- for (const redraw of this.canvasRedrawCallbacks) redraw(nowMs);
- this.isRedrawing = false;
- this._afterRedraw();
- for (const cb of this._pendingCallbacks) {
- cb(nowMs);
+ private syncDomRedraw() {
+ const redrawStart = performance.now();
+ this.syncDomRedrawFn();
+ if (this.recordPerfStats) {
+ this.perfStats.domRedraw.addValue(performance.now() - redrawStart);
}
- this._pendingCallbacks.splice(0, this._pendingCallbacks.length);
- if (perfDebug()) {
- this.perfStats.rafCanvas.addValue(debugNow() - redrawStart);
+ }
+
+ private syncCanvasRedraw() {
+ const redrawStart = performance.now();
+ if (this.isRedrawing) return;
+ this.isRedrawing = true;
+ this.canvasRedrawCallbacks.forEach((cb) => cb());
+ this.isRedrawing = false;
+ if (this.recordPerfStats) {
+ this.perfStats.rafCanvas.addValue(performance.now() - redrawStart);
}
}
private maybeScheduleAnimationFrame(force = false) {
if (this.hasScheduledNextFrame) return;
- if (this.actionCallbacks.size !== 0 || force) {
+ if (this.animationCallbacks.size !== 0 || force) {
this.hasScheduledNextFrame = true;
window.requestAnimationFrame(this.onAnimationFrame.bind(this));
}
}
- private onAnimationFrame(nowMs: number) {
+ private onAnimationFrame(lastFrameMs: number) {
if (this._shutdown) return;
- const rafStart = debugNow();
this.hasScheduledNextFrame = false;
-
const doFullRedraw = this.requestedFullRedraw;
this.requestedFullRedraw = false;
- const actionTime = measure(() => {
- for (const action of this.actionCallbacks) action(nowMs);
- });
+ const tStart = performance.now();
+ this.animationCallbacks.forEach((cb) => cb(lastFrameMs));
+ const tAnim = performance.now();
+ doFullRedraw && this.syncDomRedraw();
+ const tDom = performance.now();
+ this.syncCanvasRedraw();
+ const tCanvas = performance.now();
- const domTime = measure(() => {
- if (doFullRedraw) this.syncDomRedraw(nowMs);
- });
- const canvasTime = measure(() => this.syncCanvasRedraw(nowMs));
-
- const totalRafTime = debugNow() - rafStart;
- this.updatePerfStats(actionTime, domTime, canvasTime, totalRafTime);
- perfDisplay.renderPerfStats(this);
-
+ const animTime = tAnim - tStart;
+ const domTime = tDom - tAnim;
+ const canvasTime = tCanvas - tDom;
+ const totalTime = tCanvas - tStart;
+ this.updatePerfStats(animTime, domTime, canvasTime, totalTime);
this.maybeScheduleAnimationFrame();
+
+ if (doFullRedraw && this.postRedrawCallbacks.length > 0) {
+ const pendingCbs = this.postRedrawCallbacks.splice(0); // splice = clear.
+ pendingCbs.forEach((cb) => cb());
+ }
}
private updatePerfStats(
@@ -202,42 +161,12 @@
canvasTime: number,
totalRafTime: number,
) {
- if (!perfDebug()) return;
+ if (!this.recordPerfStats) return;
this.perfStats.rafActions.addValue(actionsTime);
this.perfStats.rafDom.addValue(domTime);
this.perfStats.rafCanvas.addValue(canvasTime);
this.perfStats.rafTotal.addValue(totalRafTime);
}
-
- renderPerfStats() {
- return m(
- 'div',
- m('div', [
- m('button', {onclick: () => this.scheduleRedraw()}, 'Do Canvas Redraw'),
- ' | ',
- m(
- 'button',
- {onclick: () => this.scheduleFullRedraw()},
- 'Do Full Redraw',
- ),
- ]),
- m('div', 'Raf Timing ' + '(Total may not add up due to imprecision)'),
- m(
- 'table',
- statTableHeader(),
- statTableRow('Actions', this.perfStats.rafActions),
- statTableRow('Dom', this.perfStats.rafDom),
- statTableRow('Canvas', this.perfStats.rafCanvas),
- statTableRow('Total', this.perfStats.rafTotal),
- ),
- m(
- 'div',
- 'Dom redraw: ' +
- `Count: ${this.perfStats.domRedraw.count} | ` +
- runningStatStr(this.perfStats.domRedraw),
- ),
- );
- }
}
export const raf = new RafScheduler();
diff --git a/ui/src/core/scroll_helper.ts b/ui/src/core/scroll_helper.ts
index 59b7b11..c732b91 100644
--- a/ui/src/core/scroll_helper.ts
+++ b/ui/src/core/scroll_helper.ts
@@ -35,7 +35,7 @@
// See comments in ScrollToArgs for the intended semantics.
scrollTo(args: ScrollToArgs) {
const {time, track} = args;
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
if (time !== undefined) {
if (time.end === undefined) {
diff --git a/ui/src/core/timeline.ts b/ui/src/core/timeline.ts
index d91503c..bc8a613 100644
--- a/ui/src/core/timeline.ts
+++ b/ui/src/core/timeline.ts
@@ -46,7 +46,7 @@
set highlightedSliceId(x) {
this._highlightedSliceId = x;
- raf.scheduleFullRedraw();
+ raf.scheduleCanvasRedraw();
}
get hoveredNoteTimestamp() {
@@ -55,7 +55,7 @@
set hoveredNoteTimestamp(x) {
this._hoveredNoteTimestamp = x;
- raf.scheduleFullRedraw();
+ raf.scheduleCanvasRedraw();
}
get hoveredUtid() {
@@ -64,7 +64,7 @@
set hoveredUtid(x) {
this._hoveredUtid = x;
- raf.scheduleFullRedraw();
+ raf.scheduleCanvasRedraw();
}
get hoveredPid() {
@@ -73,7 +73,7 @@
set hoveredPid(x) {
this._hoveredPid = x;
- raf.scheduleFullRedraw();
+ raf.scheduleCanvasRedraw();
}
// This is used to calculate the tracks within a Y range for area selection.
@@ -95,7 +95,7 @@
.scale(ratio, centerPoint, MIN_DURATION)
.fitWithin(this.traceInfo.start, this.traceInfo.end);
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
}
panVisibleWindow(delta: number) {
@@ -103,7 +103,7 @@
.translate(delta)
.fitWithin(this.traceInfo.start, this.traceInfo.end);
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
}
// Given a timestamp, if |ts| is not currently in view move the view to
@@ -136,7 +136,7 @@
deselectArea() {
this._selectedArea = undefined;
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
}
get selectedArea(): Area | undefined {
@@ -160,7 +160,7 @@
.clampDuration(MIN_DURATION)
.fitWithin(this.traceInfo.start, this.traceInfo.end);
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
}
// Get the bounds of the visible window as a high-precision time span
@@ -174,7 +174,7 @@
set hoverCursorTimestamp(t: time | undefined) {
this._hoverCursorTimestamp = t;
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
}
// Offset between t=0 and the configured time domain.
diff --git a/ui/src/core/trace_impl.ts b/ui/src/core/trace_impl.ts
index 2ae2d16..abed7f5 100644
--- a/ui/src/core/trace_impl.ts
+++ b/ui/src/core/trace_impl.ts
@@ -50,6 +50,7 @@
import {featureFlags} from './feature_flags';
import {SerializedAppState} from './state_serialization_schema';
import {PostedTrace} from './trace_source';
+import {PerfManager} from './perf_manager';
/**
* Handles the per-trace state of the UI
@@ -460,6 +461,10 @@
}
}
+ get perfDebugging(): PerfManager {
+ return this.appImpl.perfDebugging;
+ }
+
get trash(): DisposableStack {
return this.traceCtx.trash;
}
diff --git a/ui/src/frontend/animation.ts b/ui/src/frontend/animation.ts
index c8428c4..74cf065 100644
--- a/ui/src/frontend/animation.ts
+++ b/ui/src/frontend/animation.ts
@@ -31,12 +31,12 @@
}
this.startMs = nowMs;
this.endMs = nowMs + durationMs;
- raf.start(this.boundOnAnimationFrame);
+ raf.startAnimation(this.boundOnAnimationFrame);
}
stop() {
this.endMs = 0;
- raf.stop(this.boundOnAnimationFrame);
+ raf.stopAnimation(this.boundOnAnimationFrame);
}
get startTimeMs(): number {
@@ -45,7 +45,7 @@
private onAnimationFrame(nowMs: number) {
if (nowMs >= this.endMs) {
- raf.stop(this.boundOnAnimationFrame);
+ raf.stopAnimation(this.boundOnAnimationFrame);
return;
}
this.onAnimationStep(Math.max(Math.round(nowMs - this.startMs), 0));
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index c09ccbc..b5d57fa 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -867,7 +867,7 @@
this.countersKey = countersKey;
this.counters = data;
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
}
private async createTableAndFetchLimits(
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 7ef1cd4..0ca6c01 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -694,7 +694,7 @@
this.onUpdatedSlices(slices);
this.slices = slices;
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
}
private rowToSliceInternal(row: RowT): CastInternal<SliceT> {
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 67aa000..4c87e4d 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -63,19 +63,18 @@
});
function routeChange(route: Route) {
- raf.scheduleFullRedraw();
- maybeOpenTraceFromRoute(route);
- if (route.fragment) {
- // This needs to happen after the next redraw call. It's not enough
- // to use setTimeout(..., 0); since that may occur before the
- // redraw scheduled above.
- raf.addPendingCallback(() => {
+ raf.scheduleFullRedraw(() => {
+ if (route.fragment) {
+ // This needs to happen after the next redraw call. It's not enough
+ // to use setTimeout(..., 0); since that may occur before the
+ // redraw scheduled above.
const e = document.getElementById(route.fragment);
if (e) {
e.scrollIntoView();
}
- });
- }
+ }
+ });
+ maybeOpenTraceFromRoute(route);
}
function setupContentSecurityPolicy() {
@@ -226,12 +225,12 @@
const router = new Router();
router.onRouteChanged = routeChange;
- raf.domRedraw = () => {
+ raf.initialize(() =>
m.render(
document.body,
m(UiMain, pages.renderPageForCurrentRoute(AppImpl.instance.trace)),
- );
- };
+ ),
+ );
if (
(location.origin.startsWith('http://localhost:') ||
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index 21dc29a..ac5b015 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -89,11 +89,11 @@
onmousemove: (e: MouseEvent) => {
this.mouseDragging = true;
this.hoveredX = currentTargetOffset(e).x - TRACK_SHELL_WIDTH;
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
},
onmouseenter: (e: MouseEvent) => {
this.hoveredX = currentTargetOffset(e).x - TRACK_SHELL_WIDTH;
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
},
onmouseout: () => {
this.hoveredX = null;
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index e3798e1..8eb41ec 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -241,7 +241,7 @@
const cb = (vizTime: HighPrecisionTimeSpan) => {
this.trace.timeline.updateVisibleTimeHP(vizTime);
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
};
const pixelBounds = this.extractBounds(this.timeScale);
const timeScale = this.timeScale;
@@ -445,6 +445,6 @@
this.overviewData.get(key)!.push(value);
}
}
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
}
}
diff --git a/ui/src/frontend/pan_and_zoom_handler.ts b/ui/src/frontend/pan_and_zoom_handler.ts
index 4536b9e..0009335 100644
--- a/ui/src/frontend/pan_and_zoom_handler.ts
+++ b/ui/src/frontend/pan_and_zoom_handler.ts
@@ -259,12 +259,12 @@
private onWheel(e: WheelEvent) {
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
this.onPanned(e.deltaX * HORIZONTAL_WHEEL_PAN_SPEED);
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
} else if (e.ctrlKey && this.mousePositionX !== null) {
const sign = e.deltaY < 0 ? -1 : 1;
const deltaY = sign * Math.log2(1 + Math.abs(e.deltaY));
this.onZoomed(this.mousePositionX, deltaY * WHEEL_ZOOM_SPEED);
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
}
}
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index ab6de73..760e098 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -16,13 +16,10 @@
import {findRef, toHTMLElement} from '../base/dom_utils';
import {assertExists, assertFalse} from '../base/logging';
import {
- PerfStatsSource,
- RunningStatistics,
- debugNow,
- perfDebug,
- perfDisplay,
+ PerfStats,
+ PerfStatsContainer,
runningStatStr,
-} from '../core/perf';
+} from '../core/perf_stats';
import {raf} from '../core/raf_scheduler';
import {SimpleResizeObserver} from '../base/resize_observer';
import {canvasClip} from '../base/canvas_utils';
@@ -94,7 +91,7 @@
}
export class PanelContainer
- implements m.ClassComponent<PanelContainerAttrs>, PerfStatsSource
+ implements m.ClassComponent<PanelContainerAttrs>, PerfStatsContainer
{
private readonly trace: TraceImpl;
private attrs: PanelContainerAttrs;
@@ -105,11 +102,12 @@
// Updated every render cycle in the oncreate/onupdate hook
private panelInfos: PanelInfo[] = [];
- private panelPerfStats = new WeakMap<Panel, RunningStatistics>();
+ private perfStatsEnabled = false;
+ private panelPerfStats = new WeakMap<Panel, PerfStats>();
private perfStats = {
totalPanels: 0,
panelsOnCanvas: 0,
- renderStats: new RunningStatistics(10),
+ renderStats: new PerfStats(10),
};
private ctx?: CanvasRenderingContext2D;
@@ -122,16 +120,8 @@
constructor({attrs}: m.CVnode<PanelContainerAttrs>) {
this.attrs = attrs;
this.trace = attrs.trace;
- const onRedraw = () => this.renderCanvas();
- raf.addRedrawCallback(onRedraw);
- this.trash.defer(() => {
- raf.removeRedrawCallback(onRedraw);
- });
-
- perfDisplay.addContainer(this);
- this.trash.defer(() => {
- perfDisplay.removeContainer(this);
- });
+ this.trash.use(raf.addCanvasRedrawCallback(() => this.renderCanvas()));
+ this.trash.use(attrs.trace.perfDebugging.addContainer(this));
}
getPanelsInRegion(
@@ -352,7 +342,7 @@
const ctx = this.ctx;
const vc = this.virtualCanvas;
- const redrawStart = debugNow();
+ const redrawStart = performance.now();
ctx.resetTransform();
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
@@ -367,7 +357,7 @@
this.drawTopLayerOnCanvas(ctx, vc);
// Collect performance as the last thing we do.
- const redrawDur = debugNow() - redrawStart;
+ const redrawDur = performance.now() - redrawStart;
this.updatePerfStats(
redrawDur,
this.panelInfos.length,
@@ -407,12 +397,12 @@
ctx.save();
ctx.translate(0, panelTop);
canvasClip(ctx, 0, 0, panelWidth, panelHeight);
- const beforeRender = debugNow();
+ const beforeRender = performance.now();
panel.renderCanvas(ctx, panelSize);
this.updatePanelStats(
i,
panel,
- debugNow() - beforeRender,
+ performance.now() - beforeRender,
ctx,
panelSize,
);
@@ -505,10 +495,10 @@
ctx: CanvasRenderingContext2D,
size: Size2D,
) {
- if (!perfDebug()) return;
+ if (!this.perfStatsEnabled) return;
let renderStats = this.panelPerfStats.get(panel);
if (renderStats === undefined) {
- renderStats = new RunningStatistics();
+ renderStats = new PerfStats();
this.panelPerfStats.set(panel, renderStats);
}
renderStats.addValue(renderTime);
@@ -537,12 +527,16 @@
totalPanels: number,
panelsOnCanvas: number,
) {
- if (!perfDebug()) return;
+ if (!this.perfStatsEnabled) return;
this.perfStats.renderStats.addValue(renderTime);
this.perfStats.totalPanels = totalPanels;
this.perfStats.panelsOnCanvas = panelsOnCanvas;
}
+ setPerfStatsEnabled(enable: boolean): void {
+ this.perfStatsEnabled = enable;
+ }
+
renderPerfStats() {
return [
m(
diff --git a/ui/src/frontend/pivot_table.ts b/ui/src/frontend/pivot_table.ts
index a096140..925cfed 100644
--- a/ui/src/frontend/pivot_table.ts
+++ b/ui/src/frontend/pivot_table.ts
@@ -34,11 +34,6 @@
sliceAggregationColumns,
tables,
} from '../core/pivot_table_query_generator';
-import {
- PopupMenuButton,
- popupMenuIcon,
- PopupMenuItem,
-} from '../widgets/popup_menu';
import {ReorderableCell, ReorderableCellGroup} from './reorderable_cells';
import {AttributeModalHolder} from './tables/attribute_modal_holder';
import {DurationWidget} from './widgets/duration';
@@ -49,6 +44,9 @@
import {TraceImpl} from '../core/trace_impl';
import {PivotTableManager} from '../core/pivot_table_manager';
import {extensions} from '../public/lib/extensions';
+import {MenuItem, PopupMenu2} from '../widgets/menu';
+import {Button} from '../widgets/button';
+import {popupMenuIcon} from '../widgets/table';
interface PathItem {
tree: PivotTree;
@@ -293,15 +291,14 @@
return m('tr', overallValuesRow);
}
- sortingItem(aggregationIndex: number, order: SortDirection): PopupMenuItem {
+ sortingItem(aggregationIndex: number, order: SortDirection): m.Child {
const pivotMgr = this.pivotMgr;
- return {
- itemType: 'regular',
- text: order === 'DESC' ? 'Highest first' : 'Lowest first',
- callback() {
+ return m(MenuItem, {
+ label: order === 'DESC' ? 'Highest first' : 'Lowest first',
+ onclick: () => {
pivotMgr.setSortColumn(aggregationIndex, order);
},
- };
+ });
}
readableAggregationName(aggregation: Aggregation) {
@@ -317,20 +314,21 @@
aggregation: Aggregation,
index: number,
nameOverride?: string,
- ): PopupMenuItem {
- return {
- itemType: 'regular',
- text: nameOverride ?? readableColumnName(aggregation.column),
- callback: () => this.pivotMgr.addAggregation(aggregation, index),
- };
+ ): m.Child {
+ return m(MenuItem, {
+ label: nameOverride ?? readableColumnName(aggregation.column),
+ onclick: () => {
+ this.pivotMgr.addAggregation(aggregation, index);
+ },
+ });
}
aggregationPopupTableGroup(
table: string,
columns: string[],
index: number,
- ): PopupMenuItem | undefined {
- const items = [];
+ ): m.Child | undefined {
+ const items: m.Child[] = [];
for (const column of columns) {
const tableColumn: TableColumn = {kind: 'regular', table, column};
items.push(
@@ -345,12 +343,7 @@
return undefined;
}
- return {
- itemType: 'group',
- itemId: `aggregations-${table}`,
- text: `Add ${table} aggregation`,
- children: items,
- };
+ return m(MenuItem, {label: `Add ${table} aggregation`}, items);
}
renderAggregationHeaderCell(
@@ -358,7 +351,7 @@
index: number,
removeItem: boolean,
): ReorderableCell {
- const popupItems: PopupMenuItem[] = [];
+ const popupItems: m.Child[] = [];
if (aggregation.sortDirection === undefined) {
popupItems.push(
this.sortingItem(index, 'DESC'),
@@ -381,22 +374,26 @@
continue;
}
const pivotMgr = this.pivotMgr;
- popupItems.push({
- itemType: 'regular',
- text: otherAgg,
- callback() {
- pivotMgr.setAggregationFunction(index, otherAgg);
- },
- });
+ popupItems.push(
+ m(MenuItem, {
+ label: otherAgg,
+ onclick: () => {
+ pivotMgr.setAggregationFunction(index, otherAgg);
+ },
+ }),
+ );
}
}
if (removeItem) {
- popupItems.push({
- itemType: 'regular',
- text: 'Remove',
- callback: () => this.pivotMgr.removeAggregation(index),
- });
+ popupItems.push(
+ m(MenuItem, {
+ label: 'Remove',
+ onclick: () => {
+ this.pivotMgr.removeAggregation(index);
+ },
+ }),
+ );
}
let hasCount = false;
@@ -429,10 +426,15 @@
extraClass: '.aggregation' + markFirst(index),
content: [
this.readableAggregationName(aggregation),
- m(PopupMenuButton, {
- icon: popupMenuIcon(aggregation.sortDirection),
- items: popupItems,
- }),
+ m(
+ PopupMenu2,
+ {
+ trigger: m(Button, {
+ icon: popupMenuIcon(aggregation.sortDirection),
+ }),
+ },
+ popupItems,
+ ),
],
};
}
@@ -445,27 +447,27 @@
selectedPivots: Set<string>,
): ReorderableCell {
const pivotMgr = this.pivotMgr;
- const items: PopupMenuItem[] = [
- {
- itemType: 'regular',
- text: 'Add argument pivot',
- callback: () => {
+ const items: m.Child[] = [
+ m(MenuItem, {
+ label: 'Add argument pivot',
+ onclick: () => {
this.attributeModalHolder.start();
},
- },
+ }),
];
if (queryResult.metadata.pivotColumns.length > 1) {
- items.push({
- itemType: 'regular',
- text: 'Remove',
- callback() {
- pivotMgr.setPivotSelected({column: pivot, selected: false});
- },
- });
+ items.push(
+ m(MenuItem, {
+ label: 'Remove',
+ onclick: () => {
+ pivotMgr.setPivotSelected({column: pivot, selected: false});
+ },
+ }),
+ );
}
for (const table of tables) {
- const group: PopupMenuItem[] = [];
+ const group: m.Child[] = [];
for (const columnName of table.columns) {
const column: TableColumn = {
kind: 'regular',
@@ -475,26 +477,30 @@
if (selectedPivots.has(columnKey(column))) {
continue;
}
- group.push({
- itemType: 'regular',
- text: columnName,
- callback() {
- pivotMgr.setPivotSelected({column, selected: true});
- },
- });
+ group.push(
+ m(MenuItem, {
+ label: columnName,
+ onclick: () => {
+ pivotMgr.setPivotSelected({column, selected: true});
+ },
+ }),
+ );
}
- items.push({
- itemType: 'group',
- itemId: `pivot-${table.name}`,
- text: `Add ${table.displayName} pivot`,
- children: group,
- });
+ items.push(
+ m(
+ MenuItem,
+ {
+ label: `Add ${table.displayName} pivot`,
+ },
+ group,
+ ),
+ );
}
return {
content: [
readableColumnName(pivot),
- m(PopupMenuButton, {icon: 'more_horiz', items}),
+ m(PopupMenu2, {trigger: m(Button, {icon: 'more_horiz'})}, items),
],
};
}
@@ -551,20 +557,20 @@
}),
m(
'td.menu',
- m(PopupMenuButton, {
- icon: 'menu',
- items: [
- {
- itemType: 'regular',
- text: state.constrainToArea
- ? 'Query data for the whole timeline'
- : 'Constrain to selected area',
- callback: () => {
- this.pivotMgr.setConstrainedToArea(!state.constrainToArea);
- },
+ m(
+ PopupMenu2,
+ {
+ trigger: m(Button, {icon: 'menu'}),
+ },
+ m(MenuItem, {
+ label: state.constrainToArea
+ ? 'Query data for the whole timeline'
+ : 'Constrain to selected area',
+ onclick: () => {
+ this.pivotMgr.setConstrainedToArea(!state.constrainToArea);
},
- ],
- }),
+ }),
+ ),
),
),
),
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 7814674..fcae30c 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -133,15 +133,15 @@
...pos,
timescale,
});
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
},
onTrackContentMouseOut: () => {
trackRenderer?.track.onMouseOut?.();
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
},
onTrackContentClick: (pos, bounds) => {
const timescale = this.getTimescaleForBounds(bounds);
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
return (
trackRenderer?.track.onMouseClick?.({
...pos,
diff --git a/ui/src/frontend/ui_main.ts b/ui/src/frontend/ui_main.ts
index 954da1a..c67f5b7 100644
--- a/ui/src/frontend/ui_main.ts
+++ b/ui/src/frontend/ui_main.ts
@@ -171,7 +171,8 @@
{
id: 'perfetto.TogglePerformanceMetrics',
name: 'Toggle performance metrics',
- callback: () => app.setPerfDebuggingEnabled(!app.perfDebugging),
+ callback: () =>
+ (app.perfDebugging.enabled = !app.perfDebugging.enabled),
},
{
id: 'perfetto.ShareTrace',
@@ -652,7 +653,7 @@
children,
m(CookieConsent),
maybeRenderFullscreenModalDialog(),
- AppImpl.instance.perfDebugging && m('.perf-stats'),
+ AppImpl.instance.perfDebugging.renderPerfStats(),
),
);
}
diff --git a/ui/src/frontend/value.ts b/ui/src/frontend/value.ts
index 40ad1f4..a57f2ea 100644
--- a/ui/src/frontend/value.ts
+++ b/ui/src/frontend/value.ts
@@ -14,7 +14,8 @@
import m from 'mithril';
import {Tree, TreeNode} from '../widgets/tree';
-import {PopupMenuButton, PopupMenuItem} from '../widgets/popup_menu';
+import {PopupMenu2} from '../widgets/menu';
+import {Button} from '../widgets/button';
// This file implements a component for rendering JSON-like values (with
// customisation options like context menu and action buttons).
@@ -109,7 +110,7 @@
// Customisation parameters which apply to any Value (e.g. context menu).
interface ValueParams {
- contextMenu?: PopupMenuItem[];
+ contextMenu?: m.Child[];
}
// Customisation parameters which apply for a primitive value (e.g. showing
@@ -137,10 +138,15 @@
const left = [
name,
value.contextMenu
- ? m(PopupMenuButton, {
- icon: 'arrow_drop_down',
- items: value.contextMenu,
- })
+ ? m(
+ PopupMenu2,
+ {
+ trigger: m(Button, {
+ icon: 'arrow_drop_down',
+ }),
+ },
+ value.contextMenu,
+ )
: null,
];
if (isArray(value)) {
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 75f2aba..9509c1d 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -145,7 +145,7 @@
const rect = dom.getBoundingClientRect();
const centerPoint = zoomPx / (rect.width - TRACK_SHELL_WIDTH);
timeline.zoomVisibleWindow(1 - zoomRatio, centerPoint);
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
},
editSelection: (currentPx: number) => {
if (this.timelineWidthPx === undefined) return false;
@@ -257,7 +257,7 @@
}
this.showPanningHint = true;
}
- raf.scheduleRedraw();
+ raf.scheduleCanvasRedraw();
},
endSelection: (edit: boolean) => {
this.selectedContainer = undefined;
diff --git a/ui/src/frontend/widgets/sql/table/state.ts b/ui/src/frontend/widgets/sql/table/state.ts
index 1f513b8..4540214 100644
--- a/ui/src/frontend/widgets/sql/table/state.ts
+++ b/ui/src/frontend/widgets/sql/table/state.ts
@@ -331,8 +331,15 @@
this.rowCount = undefined;
}
- // Run a delayed UI update to avoid flickering if the query returns quickly.
- raf.scheduleDelayedFullRedraw();
+ // Schedule a full redraw to happen after a short delay (50 ms).
+ // This is done to prevent flickering / visual noise and allow the UI to fetch
+ // the initial data from the Trace Processor.
+ // There is a chance that someone else schedules a full redraw in the
+ // meantime, forcing the flicker, but in practice it works quite well and
+ // avoids a lot of complexity for the callers.
+ // 50ms is half of the responsiveness threshold (100ms):
+ // https://web.dev/rail/#response-process-events-in-under-50ms
+ setTimeout(() => raf.scheduleFullRedraw(), 50);
if (!filtersMatch) {
this.rowCount = await this.loadRowCount();
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
index 7bbcace..327a179 100644
--- a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
@@ -43,7 +43,6 @@
import {LazyTreeNode, Tree, TreeNode} from '../../widgets/tree';
import {VegaView} from '../../widgets/vega_view';
import {PageAttrs} from '../../public/page';
-import {PopupMenuButton} from '../../widgets/popup_menu';
import {TableShowcase} from './table_showcase';
import {TreeTable, TreeTableAttrs} from '../../frontend/widgets/treetable';
import {Intent} from '../../widgets/common';
@@ -904,30 +903,6 @@
},
}),
m(WidgetShowcase, {
- label: 'PopupMenu',
- renderWidget: () => {
- return m(PopupMenuButton, {
- icon: 'description',
- items: [
- {itemType: 'regular', text: 'New', callback: () => {}},
- {itemType: 'regular', text: 'Open', callback: () => {}},
- {itemType: 'regular', text: 'Save', callback: () => {}},
- {itemType: 'regular', text: 'Delete', callback: () => {}},
- {
- itemType: 'group',
- text: 'Share',
- itemId: 'foo',
- children: [
- {itemType: 'regular', text: 'Friends', callback: () => {}},
- {itemType: 'regular', text: 'Family', callback: () => {}},
- {itemType: 'regular', text: 'Everyone', callback: () => {}},
- ],
- },
- ],
- });
- },
- }),
- m(WidgetShowcase, {
label: 'Menu',
renderWidget: () =>
m(
diff --git a/ui/src/widgets/popup_menu.ts b/ui/src/widgets/popup_menu.ts
deleted file mode 100644
index 737815c..0000000
--- a/ui/src/widgets/popup_menu.ts
+++ /dev/null
@@ -1,198 +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 m from 'mithril';
-import {SortDirection} from '../base/comparison_utils';
-import {scheduleFullRedraw} from './raf';
-
-export interface RegularPopupMenuItem {
- itemType: 'regular';
- // Display text
- text: string;
- // Action on menu item click
- callback: () => void;
-}
-
-// Helper function for simplifying defining menus.
-export function menuItem(
- text: string,
- action: () => void,
-): RegularPopupMenuItem {
- return {
- itemType: 'regular',
- text,
- callback: action,
- };
-}
-
-export interface GroupPopupMenuItem {
- itemType: 'group';
- text: string;
- itemId: string;
- children: PopupMenuItem[];
-}
-
-export type PopupMenuItem = RegularPopupMenuItem | GroupPopupMenuItem;
-
-export interface PopupMenuButtonAttrs {
- // Icon for button opening a menu
- icon: string;
- // List of popup menu items
- items: PopupMenuItem[];
-}
-
-// To ensure having at most one popup menu on the screen at a time, we need to
-// listen to click events on the whole page and close currently opened popup, if
-// there's any. This class, used as a singleton, does exactly that.
-class PopupHolder {
- // Invariant: global listener should be register if and only if this.popup is
- // not undefined.
- popup: PopupMenuButton | undefined = undefined;
- initialized = false;
- listener: (e: MouseEvent) => void;
-
- constructor() {
- this.listener = (e: MouseEvent) => {
- // Only handle those events that are not part of dropdown menu themselves.
- const hasDropdown =
- e.composedPath().find(PopupHolder.isDropdownElement) !== undefined;
- if (!hasDropdown) {
- this.ensureHidden();
- }
- };
- }
-
- static isDropdownElement(target: EventTarget) {
- if (target instanceof HTMLElement) {
- return target.tagName === 'DIV' && target.classList.contains('dropdown');
- }
- return false;
- }
-
- ensureHidden() {
- if (this.popup !== undefined) {
- this.popup.setVisible(false);
- }
- }
-
- clear() {
- if (this.popup !== undefined) {
- this.popup = undefined;
- window.removeEventListener('click', this.listener);
- }
- }
-
- showPopup(popup: PopupMenuButton) {
- this.ensureHidden();
- this.popup = popup;
- window.addEventListener('click', this.listener);
- }
-}
-
-// Singleton instance of PopupHolder
-const popupHolder = new PopupHolder();
-
-// For a table column that can be sorted; the standard popup icon should
-// reflect the current sorting direction. This function returns an icon
-// corresponding to optional SortDirection according to which the column is
-// sorted. (Optional because column might be unsorted)
-export function popupMenuIcon(sortDirection?: SortDirection) {
- switch (sortDirection) {
- case undefined:
- return 'more_horiz';
- case 'DESC':
- return 'arrow_drop_down';
- case 'ASC':
- return 'arrow_drop_up';
- }
-}
-
-// Component that displays a button that shows a popup menu on click.
-export class PopupMenuButton implements m.ClassComponent<PopupMenuButtonAttrs> {
- popupShown = false;
- expandedGroups: Set<string> = new Set();
-
- setVisible(visible: boolean) {
- this.popupShown = visible;
- if (this.popupShown) {
- popupHolder.showPopup(this);
- } else {
- popupHolder.clear();
- }
- scheduleFullRedraw();
- }
-
- renderItem(item: PopupMenuItem): m.Child {
- switch (item.itemType) {
- case 'regular':
- return m(
- 'button.open-menu',
- {
- onclick: () => {
- item.callback();
- // Hide the menu item after the action has been invoked
- this.setVisible(false);
- },
- },
- item.text,
- );
- case 'group':
- const isExpanded = this.expandedGroups.has(item.itemId);
- return m(
- 'div',
- m(
- 'button.open-menu.disallow-selection',
- {
- onclick: () => {
- if (this.expandedGroups.has(item.itemId)) {
- this.expandedGroups.delete(item.itemId);
- } else {
- this.expandedGroups.add(item.itemId);
- }
- scheduleFullRedraw();
- },
- },
- // Show text with up/down arrow, depending on expanded state.
- item.text + (isExpanded ? ' \u25B2' : ' \u25BC'),
- ),
- isExpanded
- ? m(
- 'div.nested-menu',
- item.children.map((item) => this.renderItem(item)),
- )
- : null,
- );
- }
- }
-
- view(vnode: m.Vnode<PopupMenuButtonAttrs, this>) {
- return m(
- '.dropdown',
- m(
- '.dropdown-button',
- {
- onclick: () => {
- this.setVisible(!this.popupShown);
- },
- },
- vnode.children,
- m('i.material-icons', vnode.attrs.icon),
- ),
- m(
- this.popupShown ? '.popup-menu.opened' : '.popup-menu.closed',
- vnode.attrs.items.map((item) => this.renderItem(item)),
- ),
- );
- }
-}
diff --git a/ui/src/widgets/table.ts b/ui/src/widgets/table.ts
index ee195d0..1389907 100644
--- a/ui/src/widgets/table.ts
+++ b/ui/src/widgets/table.ts
@@ -22,17 +22,28 @@
SortDirection,
withDirection,
} from '../base/comparison_utils';
-import {
- menuItem,
- PopupMenuButton,
- popupMenuIcon,
- PopupMenuItem,
-} from './popup_menu';
import {scheduleFullRedraw} from './raf';
+import {MenuItem, PopupMenu2} from './menu';
+import {Button} from './button';
+
+// For a table column that can be sorted; the standard popup icon should
+// reflect the current sorting direction. This function returns an icon
+// corresponding to optional SortDirection according to which the column is
+// sorted. (Optional because column might be unsorted)
+export function popupMenuIcon(sortDirection?: SortDirection) {
+ switch (sortDirection) {
+ case undefined:
+ return 'more_horiz';
+ case 'DESC':
+ return 'arrow_drop_down';
+ case 'ASC':
+ return 'arrow_drop_up';
+ }
+}
export interface ColumnDescriptorAttrs<T> {
// Context menu items displayed on the column header.
- contextMenu?: PopupMenuItem[];
+ contextMenu?: m.Child[];
// Unique column ID, used to identify which column is currently sorted.
columnId?: string;
@@ -49,7 +60,7 @@
name: string;
render: (row: T) => m.Child;
id: string;
- contextMenu?: PopupMenuItem[];
+ contextMenu?: m.Child[];
ordering?: ComparisonFn<T>;
constructor(
@@ -81,7 +92,7 @@
export function numberColumn<T>(
name: string,
getter: (t: T) => number,
- contextMenu?: PopupMenuItem[],
+ contextMenu?: m.Child[],
): ColumnDescriptor<T> {
return new ColumnDescriptor<T>(name, getter, {contextMenu, sortKey: getter});
}
@@ -89,7 +100,7 @@
export function stringColumn<T>(
name: string,
getter: (t: T) => string,
- contextMenu?: PopupMenuItem[],
+ contextMenu?: m.Child[],
): ColumnDescriptor<T> {
return new ColumnDescriptor<T>(name, getter, {contextMenu, sortKey: getter});
}
@@ -191,33 +202,42 @@
if (column.ordering !== undefined) {
const ordering = column.ordering;
currDirection = directionOnIndex(column.id, vnode.attrs.data.sortingInfo);
- const newItems: PopupMenuItem[] = [];
+ const newItems: m.Child[] = [];
if (currDirection !== 'ASC') {
newItems.push(
- menuItem('Sort ascending', () => {
- vnode.attrs.data.reorder({
- columnId: column.id,
- direction: 'ASC',
- ordering,
- });
+ m(MenuItem, {
+ label: 'Sort ascending',
+ onclick: () => {
+ vnode.attrs.data.reorder({
+ columnId: column.id,
+ direction: 'ASC',
+ ordering,
+ });
+ },
}),
);
}
if (currDirection !== 'DESC') {
newItems.push(
- menuItem('Sort descending', () => {
- vnode.attrs.data.reorder({
- columnId: column.id,
- direction: 'DESC',
- ordering,
- });
+ m(MenuItem, {
+ label: 'Sort descending',
+ onclick: () => {
+ vnode.attrs.data.reorder({
+ columnId: column.id,
+ direction: 'DESC',
+ ordering,
+ });
+ },
}),
);
}
if (currDirection !== undefined) {
newItems.push(
- menuItem('Restore original order', () => {
- vnode.attrs.data.resetOrder();
+ m(MenuItem, {
+ label: 'Restore original order',
+ onclick: () => {
+ vnode.attrs.data.resetOrder();
+ },
}),
);
}
@@ -227,12 +247,14 @@
return m(
'td',
column.name,
- items === undefined
- ? null
- : m(PopupMenuButton, {
- icon: popupMenuIcon(currDirection),
- items,
- }),
+ items &&
+ m(
+ PopupMenu2,
+ {
+ trigger: m(Button, {icon: popupMenuIcon(currDirection)}),
+ },
+ items,
+ ),
);
}