Merge "add do_sys_open ftrace events" into main
diff --git a/BUILD b/BUILD
index 91bd49b..3cf2eee 100644
--- a/BUILD
+++ b/BUILD
@@ -502,6 +502,72 @@
linkstatic = True,
)
+# GN target: //src/traceconv:libpprofbuilder
+perfetto_cc_library(
+ name = "libpprofbuilder",
+ srcs = [
+ ":src_profiling_deobfuscator",
+ ":src_profiling_symbolizer_symbolize_database",
+ ":src_profiling_symbolizer_symbolizer",
+ ":src_trace_processor_util_build_id",
+ ":src_traceconv_pprofbuilder",
+ ":src_traceconv_utils",
+ ],
+ hdrs = [
+ ":include_perfetto_base_base",
+ ":include_perfetto_ext_base_base",
+ ":include_perfetto_profiling_pprof_builder",
+ ":include_perfetto_protozero_protozero",
+ ":include_perfetto_public_abi_base",
+ ":include_perfetto_public_base",
+ ":include_perfetto_public_protozero",
+ ":include_perfetto_trace_processor_basic_types",
+ ":include_perfetto_trace_processor_storage",
+ ":include_perfetto_trace_processor_trace_processor",
+ ],
+ visibility = PERFETTO_CONFIG.public_visibility,
+ deps = [
+ ":protos_perfetto_common_zero",
+ ":protos_perfetto_config_android_zero",
+ ":protos_perfetto_config_ftrace_zero",
+ ":protos_perfetto_config_gpu_zero",
+ ":protos_perfetto_config_inode_file_zero",
+ ":protos_perfetto_config_interceptors_zero",
+ ":protos_perfetto_config_power_zero",
+ ":protos_perfetto_config_process_stats_zero",
+ ":protos_perfetto_config_profiling_zero",
+ ":protos_perfetto_config_statsd_zero",
+ ":protos_perfetto_config_sys_stats_zero",
+ ":protos_perfetto_config_system_info_zero",
+ ":protos_perfetto_config_track_event_zero",
+ ":protos_perfetto_config_zero",
+ ":protos_perfetto_trace_android_winscope_common_zero",
+ ":protos_perfetto_trace_android_winscope_regular_zero",
+ ":protos_perfetto_trace_android_zero",
+ ":protos_perfetto_trace_chrome_zero",
+ ":protos_perfetto_trace_etw_zero",
+ ":protos_perfetto_trace_filesystem_zero",
+ ":protos_perfetto_trace_ftrace_zero",
+ ":protos_perfetto_trace_gpu_zero",
+ ":protos_perfetto_trace_interned_data_zero",
+ ":protos_perfetto_trace_minimal_zero",
+ ":protos_perfetto_trace_non_minimal_zero",
+ ":protos_perfetto_trace_perfetto_zero",
+ ":protos_perfetto_trace_power_zero",
+ ":protos_perfetto_trace_profiling_zero",
+ ":protos_perfetto_trace_ps_zero",
+ ":protos_perfetto_trace_statsd_zero",
+ ":protos_perfetto_trace_sys_stats_zero",
+ ":protos_perfetto_trace_system_info_zero",
+ ":protos_perfetto_trace_track_event_zero",
+ ":protos_perfetto_trace_translation_zero",
+ ":protos_third_party_pprof_zero",
+ ":protozero",
+ ":src_trace_processor_containers_containers",
+ ] + PERFETTO_CONFIG.deps.zlib,
+ linkstatic = True,
+)
+
# GN target: //test:client_api_example
perfetto_cc_binary(
name = "client_api_example",
@@ -6865,74 +6931,6 @@
PERFETTO_CONFIG.deps.demangle_wrapper,
)
-# GN target: //src/traceconv:libpprofbuilder
-perfetto_cc_library(
- name = "libpprofbuilder",
- srcs = [
- ":src_profiling_deobfuscator",
- ":src_profiling_symbolizer_symbolize_database",
- ":src_profiling_symbolizer_symbolizer",
- ":src_trace_processor_util_build_id",
- ":src_traceconv_pprofbuilder",
- ":src_traceconv_utils",
- ],
- hdrs = [
- ":include_perfetto_base_base",
- ":include_perfetto_ext_base_base",
- ":include_perfetto_profiling_pprof_builder",
- ":include_perfetto_protozero_protozero",
- ":include_perfetto_public_abi_base",
- ":include_perfetto_public_base",
- ":include_perfetto_public_protozero",
- ":include_perfetto_trace_processor_basic_types",
- ":include_perfetto_trace_processor_storage",
- ":include_perfetto_trace_processor_trace_processor",
- ],
- visibility = [
- "//visibility:public",
- ],
- deps = [
- ":protos_perfetto_common_zero",
- ":protos_perfetto_config_android_zero",
- ":protos_perfetto_config_ftrace_zero",
- ":protos_perfetto_config_gpu_zero",
- ":protos_perfetto_config_inode_file_zero",
- ":protos_perfetto_config_interceptors_zero",
- ":protos_perfetto_config_power_zero",
- ":protos_perfetto_config_process_stats_zero",
- ":protos_perfetto_config_profiling_zero",
- ":protos_perfetto_config_statsd_zero",
- ":protos_perfetto_config_sys_stats_zero",
- ":protos_perfetto_config_system_info_zero",
- ":protos_perfetto_config_track_event_zero",
- ":protos_perfetto_config_zero",
- ":protos_perfetto_trace_android_winscope_common_zero",
- ":protos_perfetto_trace_android_winscope_regular_zero",
- ":protos_perfetto_trace_android_zero",
- ":protos_perfetto_trace_chrome_zero",
- ":protos_perfetto_trace_etw_zero",
- ":protos_perfetto_trace_filesystem_zero",
- ":protos_perfetto_trace_ftrace_zero",
- ":protos_perfetto_trace_gpu_zero",
- ":protos_perfetto_trace_interned_data_zero",
- ":protos_perfetto_trace_minimal_zero",
- ":protos_perfetto_trace_non_minimal_zero",
- ":protos_perfetto_trace_perfetto_zero",
- ":protos_perfetto_trace_power_zero",
- ":protos_perfetto_trace_profiling_zero",
- ":protos_perfetto_trace_ps_zero",
- ":protos_perfetto_trace_statsd_zero",
- ":protos_perfetto_trace_sys_stats_zero",
- ":protos_perfetto_trace_system_info_zero",
- ":protos_perfetto_trace_track_event_zero",
- ":protos_perfetto_trace_translation_zero",
- ":protos_third_party_pprof_zero",
- ":protozero",
- ":src_trace_processor_containers_containers",
- ] + PERFETTO_CONFIG.deps.zlib,
- linkstatic = True,
-)
-
# GN target: //src/traceconv:traceconv
perfetto_cc_binary(
name = "traceconv",
diff --git a/protos/perfetto/metrics/android/startup_metric.proto b/protos/perfetto/metrics/android/startup_metric.proto
index 1de0b47..86c206c 100644
--- a/protos/perfetto/metrics/android/startup_metric.proto
+++ b/protos/perfetto/metrics/android/startup_metric.proto
@@ -303,8 +303,8 @@
// sorted by the duration in descending order.
// By checking out the top slices/threads, developers can identify specific
// slices or threads for further investigation.
- repeated TraceSliceSection trace_slice_sections = 7;
- repeated TraceThreadSection trace_thread_sections = 8;
+ optional TraceSliceSectionInfo trace_slice_sections = 7;
+ optional TraceThreadSectionInfo trace_thread_sections = 8;
// Details specific for a reason.
optional string additional_info = 9;
@@ -355,6 +355,13 @@
optional uint32 thread_tid = 6;
}
+ // Information for the SliceSections
+ message TraceSliceSectionInfo {
+ repeated TraceSliceSection slice_section = 1;
+ optional int64 start_timestamp = 2;
+ optional int64 end_timestamp = 3;
+ }
+
// Contains information for a section of a thread.
message TraceThreadSection {
optional int64 start_timestamp = 1;
@@ -371,6 +378,13 @@
optional uint32 thread_tid = 6;
}
+ // Information for the ThreadSections
+ message TraceThreadSectionInfo {
+ repeated TraceThreadSection thread_section = 1;
+ optional int64 start_timestamp = 2;
+ optional int64 end_timestamp = 3;
+ }
+
// Next id: 26
message Startup {
// Random id uniquely identifying an app startup in this trace.
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index 275e373..e2fc3a3 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -2541,8 +2541,8 @@
// sorted by the duration in descending order.
// By checking out the top slices/threads, developers can identify specific
// slices or threads for further investigation.
- repeated TraceSliceSection trace_slice_sections = 7;
- repeated TraceThreadSection trace_thread_sections = 8;
+ optional TraceSliceSectionInfo trace_slice_sections = 7;
+ optional TraceThreadSectionInfo trace_thread_sections = 8;
// Details specific for a reason.
optional string additional_info = 9;
@@ -2593,6 +2593,13 @@
optional uint32 thread_tid = 6;
}
+ // Information for the SliceSections
+ message TraceSliceSectionInfo {
+ repeated TraceSliceSection slice_section = 1;
+ optional int64 start_timestamp = 2;
+ optional int64 end_timestamp = 3;
+ }
+
// Contains information for a section of a thread.
message TraceThreadSection {
optional int64 start_timestamp = 1;
@@ -2609,6 +2616,13 @@
optional uint32 thread_tid = 6;
}
+ // Information for the ThreadSections
+ message TraceThreadSectionInfo {
+ repeated TraceThreadSection thread_section = 1;
+ optional int64 start_timestamp = 2;
+ optional int64 end_timestamp = 3;
+ }
+
// Next id: 26
message Startup {
// Random id uniquely identifying an app startup in this trace.
diff --git a/protos/perfetto/trace/android/server/windowmanagerservice.proto b/protos/perfetto/trace/android/server/windowmanagerservice.proto
index dcb4583..2c22522 100644
--- a/protos/perfetto/trace/android/server/windowmanagerservice.proto
+++ b/protos/perfetto/trace/android/server/windowmanagerservice.proto
@@ -454,6 +454,7 @@
repeated RectProto unrestricted_keep_clear_areas = 46;
repeated InsetsSourceProto mergedLocalInsetsSources = 47;
optional int32 requested_visible_types = 48;
+ optional RectProto dim_bounds = 49;
}
message IdentifierProto {
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 728882b..ed15b47 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/python/tools/check_imports.py b/python/tools/check_imports.py
index 2695450..37c1a95 100755
--- a/python/tools/check_imports.py
+++ b/python/tools/check_imports.py
@@ -115,15 +115,9 @@
(['/public/lib/colorizer'], '/core/feature_flags'),
# TODO(primiano): Record page-related technical debt.
- ('/frontend/record*', '/controller/*'),
- ('/frontend/permalink', '/controller/*'),
- ('/common/*', '/controller/record_config_types'),
- ('/controller/index', '/common/recordingV2/target_factories/index'),
- ('/common/recordingV2/*', '/controller/*'),
- ('/controller/record_controller*', '*'),
- ('/controller/adb_*', '*'),
- ('/chrome_extension/chrome_tracing_controller', '/controller/*'),
- ('/chrome_extension/chrome_tracing_controller', '/core/trace_config_utils'),
+ ('/plugins/dev.perfetto.RecordTrace/*', '/frontend/globals'),
+ ('/chrome_extension/chrome_tracing_controller',
+ '/plugins/dev.perfetto.RecordTrace/*'),
# TODO(primiano): query-table tech debt.
(
@@ -150,9 +144,6 @@
# Bigtrace deps.
('/bigtrace/*', ['/base/*', '/widgets/*', '/trace_processor/*']),
- # TODO(primiano): rationalize recordingv2. RecordingV2 is a mess of subdirs.
- ('/common/recordingV2/*', '/common/recordingV2/*'),
-
# TODO(primiano): misc tech debt.
('/public/lib/extensions', '/frontend/*'),
('/bigtrace/index', ['/core/live_reload', '/core/raf_scheduler']),
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index d9b130e..59b2446 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -2268,6 +2268,22 @@
// family) and thread creation (clone(CLONE_THREAD, ...)).
static const uint32_t kCloneThread = 0x00010000; // From kernel's sched.h.
+ if (PERFETTO_UNLIKELY(new_tid == 0)) {
+ // In the case of boot-time tracing (kernel is started with tracing
+ // enabled), the ftrace buffer will see /bin/init creating swapper/0 tasks:
+ // event {
+ // pid: 1
+ // task_newtask {
+ // pid: 0
+ // comm: "swapper/0"
+ // }
+ // }
+ // Skip these task_newtask events since they are kernel idle tasks.
+ PERFETTO_DCHECK(source_tid == 1);
+ PERFETTO_DCHECK(base::StartsWith(evt.comm().ToStdString(), "swapper"));
+ return;
+ }
+
// If the process is a fork, start a new process.
if ((clone_flags & kCloneThread) == 0) {
// This is a plain-old fork() or equivalent.
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/android/startup/slow_start_reasons.sql b/src/trace_processor/metrics/sql/android/startup/slow_start_reasons.sql
index 2d5b646..8a4a7dd 100644
--- a/src/trace_processor/metrics/sql/android/startup/slow_start_reasons.sql
+++ b/src/trace_processor/metrics/sql/android/startup/slow_start_reasons.sql
@@ -46,10 +46,13 @@
CREATE OR REPLACE PERFETTO FUNCTION get_main_thread_time_for_launch_in_runnable_state(
startup_id LONG, num_threads INT)
RETURNS PROTO AS
- SELECT RepeatedField(AndroidStartupMetric_TraceThreadSection(
- 'start_timestamp', ts, 'end_timestamp', ts + dur,
- 'thread_tid', tid, 'process_pid', pid,
- 'thread_name', thread_name))
+ SELECT AndroidStartupMetric_TraceThreadSectionInfo(
+ 'start_timestamp', MIN(ts),
+ 'end_timestamp', MAX(ts + dur),
+ 'thread_section', RepeatedField(AndroidStartupMetric_TraceThreadSection(
+ 'start_timestamp', ts, 'end_timestamp', ts + dur,
+ 'thread_tid', tid, 'process_pid', pid,
+ 'thread_name', thread_name)))
FROM (
SELECT p.pid, ts, dur, thread.tid, thread_name
FROM launch_threads_by_thread_state l, android_startup_processes p
@@ -62,10 +65,13 @@
CREATE OR REPLACE PERFETTO FUNCTION get_main_thread_time_for_launch_and_state(
startup_id LONG, state STRING, num_threads INT)
RETURNS PROTO AS
- SELECT RepeatedField(AndroidStartupMetric_TraceThreadSection(
- 'start_timestamp', ts, 'end_timestamp', ts + dur,
- 'thread_tid', tid, 'process_pid', pid,
- 'thread_name', thread_name))
+ SELECT AndroidStartupMetric_TraceThreadSectionInfo(
+ 'start_timestamp', MIN(ts),
+ 'end_timestamp', MAX(ts + dur),
+ 'thread_section', RepeatedField(AndroidStartupMetric_TraceThreadSection(
+ 'start_timestamp', ts, 'end_timestamp', ts + dur,
+ 'thread_tid', tid, 'process_pid', pid,
+ 'thread_name', thread_name)))
FROM (
SELECT p.pid, ts, dur, thread.tid, thread_name
FROM launch_threads_by_thread_state l, android_startup_processes p
@@ -78,10 +84,13 @@
CREATE OR REPLACE PERFETTO FUNCTION get_main_thread_time_for_launch_state_and_io_wait(
startup_id INT, state STRING, io_wait BOOL, num_threads INT)
RETURNS PROTO AS
- SELECT RepeatedField(AndroidStartupMetric_TraceThreadSection(
- 'start_timestamp', ts, 'end_timestamp', ts + dur,
- 'thread_tid', tid, 'process_pid', pid,
- 'thread_name', thread_name))
+ SELECT AndroidStartupMetric_TraceThreadSectionInfo(
+ 'start_timestamp', MIN(ts),
+ 'end_timestamp', MAX(ts + dur),
+ 'thread_section', RepeatedField(AndroidStartupMetric_TraceThreadSection(
+ 'start_timestamp', ts, 'end_timestamp', ts + dur,
+ 'thread_tid', tid, 'process_pid', pid,
+ 'thread_name', thread_name)))
FROM (
SELECT p.pid, ts, dur, thread.tid, thread_name
FROM launch_threads_by_thread_state l, android_startup_processes p
@@ -95,10 +104,13 @@
CREATE OR REPLACE PERFETTO FUNCTION get_thread_time_for_launch_state_and_thread(
startup_id INT, state STRING, thread_name STRING, num_threads INT)
RETURNS PROTO AS
- SELECT RepeatedField(AndroidStartupMetric_TraceThreadSection(
- 'start_timestamp', ts, 'end_timestamp', ts + dur,
- 'thread_tid', tid, 'process_pid', pid,
- 'thread_name', thread_name))
+ SELECT AndroidStartupMetric_TraceThreadSectionInfo(
+ 'start_timestamp', MIN(ts),
+ 'end_timestamp', MAX(ts + dur),
+ 'thread_section', RepeatedField(AndroidStartupMetric_TraceThreadSection(
+ 'start_timestamp', ts, 'end_timestamp', ts + dur,
+ 'thread_tid', tid, 'process_pid', pid,
+ 'thread_name', thread_name)))
FROM (
SELECT p.pid, ts, dur, thread.tid, thread_name
FROM launch_threads_by_thread_state l, android_startup_processes p
@@ -111,13 +123,16 @@
CREATE OR REPLACE PERFETTO FUNCTION get_missing_baseline_profile_for_launch(
startup_id LONG, pkg_name STRING)
RETURNS PROTO AS
- SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
- 'thread_tid', tid,
- 'process_pid', pid,
- 'start_timestamp', slice_ts,
- 'end_timestamp', slice_ts + slice_dur,
- 'slice_id', slice_id,
- 'slice_name', slice_name))
+ SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+ 'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+ 'thread_tid', tid,
+ 'process_pid', pid,
+ 'start_timestamp', slice_ts,
+ 'end_timestamp', slice_ts + slice_dur,
+ 'slice_id', slice_id,
+ 'slice_name', slice_name)),
+ 'start_timestamp', MIN(slice_ts),
+ 'end_timestamp', MAX(slice_ts + slice_dur))
FROM (
SELECT p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
FROM ANDROID_SLICES_FOR_STARTUP_AND_SLICE_NAME($startup_id,
@@ -135,13 +150,16 @@
CREATE OR REPLACE PERFETTO FUNCTION get_run_from_apk(startup_id LONG)
RETURNS PROTO AS
- SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
- 'thread_tid', tid,
- 'process_pid', pid,
- 'start_timestamp', slice_ts,
- 'end_timestamp', slice_ts + slice_dur,
- 'slice_id', slice_id,
- 'slice_name', slice_name))
+ SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+ 'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+ 'thread_tid', tid,
+ 'process_pid', pid,
+ 'start_timestamp', slice_ts,
+ 'end_timestamp', slice_ts + slice_dur,
+ 'slice_id', slice_id,
+ 'slice_name', slice_name)),
+ 'start_timestamp', MIN(slice_ts),
+ 'end_timestamp', MAX(slice_ts + slice_dur))
FROM (
SELECT p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
FROM android_thread_slices_for_all_startups l, android_startup_processes p
@@ -157,13 +175,16 @@
CREATE OR REPLACE PERFETTO FUNCTION get_unlock_running_during_launch_slice(startup_id LONG,
pid INT)
RETURNS PROTO AS
- SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
- 'thread_tid', tid,
- 'process_pid', $pid,
- 'start_timestamp', slice_ts,
- 'end_timestamp', slice_ts + slice_dur,
- 'slice_id', slice_id,
- 'slice_name', slice_name))
+ SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+ 'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+ 'thread_tid', tid,
+ 'process_pid', $pid,
+ 'start_timestamp', slice_ts,
+ 'end_timestamp', slice_ts + slice_dur,
+ 'slice_id', slice_id,
+ 'slice_name', slice_name)),
+ 'start_timestamp', MIN(slice_ts),
+ 'end_timestamp', MAX(slice_ts + slice_dur))
FROM (
SELECT tid, slice.ts as slice_ts, slice.dur as slice_dur,
slice.id as slice_id, slice.name as slice_name
@@ -180,13 +201,16 @@
CREATE OR REPLACE PERFETTO FUNCTION get_gc_activity(startup_id LONG, num_slices INT)
RETURNS PROTO AS
- SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
- 'thread_tid', tid,
- 'process_pid', pid,
- 'start_timestamp', slice_ts,
- 'end_timestamp', slice_ts + slice_dur,
- 'slice_id', slice_id,
- 'slice_name', slice_name))
+ SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+ 'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+ 'thread_tid', tid,
+ 'process_pid', pid,
+ 'start_timestamp', slice_ts,
+ 'end_timestamp', slice_ts + slice_dur,
+ 'slice_id', slice_id,
+ 'slice_name', slice_name)),
+ 'start_timestamp', MIN(slice_ts),
+ 'end_timestamp', MAX(slice_ts + slice_dur))
FROM (
SELECT p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
FROM android_thread_slices_for_all_startups slice, android_startup_processes p
@@ -204,13 +228,16 @@
CREATE OR REPLACE PERFETTO FUNCTION get_dur_on_main_thread_for_startup_and_slice(
startup_id LONG, slice_name STRING, num_slices INT)
RETURNS PROTO AS
- SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
- 'thread_tid', tid,
- 'process_pid', pid,
- 'start_timestamp', slice_ts,
- 'end_timestamp', slice_ts + slice_dur,
- 'slice_id', slice_id,
- 'slice_name', slice_name))
+ SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+ 'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+ 'thread_tid', tid,
+ 'process_pid', pid,
+ 'start_timestamp', slice_ts,
+ 'end_timestamp', slice_ts + slice_dur,
+ 'slice_id', slice_id,
+ 'slice_name', slice_name)),
+ 'start_timestamp', MIN(slice_ts),
+ 'end_timestamp', MAX(slice_ts + slice_dur))
FROM (
SELECT p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
FROM android_thread_slices_for_all_startups l,
@@ -223,11 +250,14 @@
CREATE OR REPLACE PERFETTO FUNCTION get_main_thread_binder_transactions_blocked(
startup_id LONG, threshold DOUBLE, num_slices INT)
RETURNS PROTO AS
- SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
- 'thread_tid', tid,
- 'process_pid', pid,
- 'start_timestamp', slice_ts, 'end_timestamp', slice_ts + slice_dur,
- 'slice_id', slice_id, 'slice_name', slice_name))
+ SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+ 'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+ 'thread_tid', tid,
+ 'process_pid', pid,
+ 'start_timestamp', slice_ts, 'end_timestamp', slice_ts + slice_dur,
+ 'slice_id', slice_id, 'slice_name', slice_name)),
+ 'start_timestamp', MIN(slice_ts),
+ 'end_timestamp', MAX(slice_ts + slice_dur))
FROM (
SELECT pid, request.tid as tid, request.slice_ts as slice_ts, request.slice_dur as slice_dur,
request.id as slice_id, request.slice_name as slice_name
@@ -253,11 +283,14 @@
CREATE OR REPLACE PERFETTO FUNCTION get_slices_concurrent_to_launch(
startup_id INT, slice_glob STRING, num_slices INT, pid INT)
RETURNS PROTO AS
- SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
- 'thread_tid', tid,
- 'process_pid', $pid,
- 'start_timestamp', ts, 'end_timestamp', ts + dur,
- 'slice_id', id, 'slice_name', name))
+ SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+ 'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+ 'thread_tid', tid,
+ 'process_pid', $pid,
+ 'start_timestamp', ts, 'end_timestamp', ts + dur,
+ 'slice_id', id, 'slice_name', name)),
+ 'start_timestamp', MIN(ts),
+ 'end_timestamp', MAX(ts + dur))
FROM (
SELECT thread.tid, s.ts as ts, dur, s.id, s.name FROM slice s
JOIN thread_track t ON s.track_id = t.id
@@ -275,11 +308,14 @@
CREATE OR REPLACE PERFETTO FUNCTION get_slices_for_startup_and_slice_name(
startup_id INT, slice_name STRING, num_slices INT, pid int)
RETURNS PROTO AS
- SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
- 'thread_tid', tid,
- 'process_pid', $pid,
- 'start_timestamp', slice_ts, 'end_timestamp', slice_ts + slice_dur,
- 'slice_id', slice_id, 'slice_name', slice_name))
+ SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+ 'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+ 'thread_tid', tid,
+ 'process_pid', $pid,
+ 'start_timestamp', slice_ts, 'end_timestamp', slice_ts + slice_dur,
+ 'slice_id', slice_id, 'slice_name', slice_name)),
+ 'start_timestamp', MIN(slice_ts),
+ 'end_timestamp', MAX(slice_ts + slice_dur))
FROM (
SELECT tid, slice_ts, slice_dur, slice_id, slice_name
FROM android_thread_slices_for_all_startups
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql b/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql
index 90697fa..66d2d01 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql
@@ -39,6 +39,8 @@
OR $name GLOB 'NotificationStackScrollLayout#onMeasure'
OR $name GLOB 'ExpNotRow#*'
OR $name GLOB 'GC: Wait For*'
+ OR $name GLOB 'Recomposer:*'
+ OR $name GLOB 'Compose:*'
OR (
-- Some top level handler slices
$depth = 0
diff --git a/test/cmdline_integrationtest.cc b/test/cmdline_integrationtest.cc
index 2febd90..c4a6fc7 100644
--- a/test/cmdline_integrationtest.cc
+++ b/test/cmdline_integrationtest.cc
@@ -47,6 +47,7 @@
using ::testing::ContainsRegex;
using ::testing::Each;
+using ::testing::ElementsAre;
using ::testing::ElementsAreArray;
using ::testing::Eq;
using ::testing::HasSubstr;
@@ -99,6 +100,52 @@
return trace_config;
}
+// For the regular tests.
+TraceConfig CreateTraceConfigForTest(uint32_t test_msg_count = 11,
+ uint32_t test_msg_size = 32) {
+ TraceConfig trace_config;
+ trace_config.add_buffers()->set_size_kb(1024);
+ auto* ds_config = trace_config.add_data_sources()->mutable_config();
+ ds_config->set_name("android.perfetto.FakeProducer");
+ ds_config->mutable_for_testing()->set_message_count(test_msg_count);
+ ds_config->mutable_for_testing()->set_message_size(test_msg_size);
+ return trace_config;
+}
+
+void ExpectTraceContainsTestMessages(const protos::gen::Trace& trace,
+ uint32_t count) {
+ ssize_t actual_test_packets_count = std::count_if(
+ trace.packet().begin(), trace.packet().end(),
+ [](const protos::gen::TracePacket& tp) { return tp.has_for_testing(); });
+ EXPECT_EQ(count, static_cast<uint32_t>(actual_test_packets_count));
+}
+
+void ExpectTraceContainsTestMessagesWithSize(const protos::gen::Trace& trace,
+ uint32_t message_size) {
+ for (const auto& packet : trace.packet()) {
+ if (packet.has_for_testing()) {
+ EXPECT_EQ(message_size, packet.for_testing().str().size());
+ }
+ }
+}
+
+void ExpectTraceContainsConfigWithTriggerMode(
+ const protos::gen::Trace& trace,
+ protos::gen::TraceConfig::TriggerConfig::TriggerMode trigger_mode) {
+ // GTest three level nested Property matcher is hard to read, so we use
+ // 'find_if' with lambda to ensure the trace config properly includes the
+ // trigger mode we set.
+ auto found =
+ std::find_if(trace.packet().begin(), trace.packet().end(),
+ [trigger_mode](const protos::gen::TracePacket& tp) {
+ return tp.has_trace_config() &&
+ tp.trace_config().trigger_config().trigger_mode() ==
+ trigger_mode;
+ });
+ EXPECT_NE(found, trace.packet().end())
+ << "Trace config doesn't include expected trigger mode.";
+}
+
class ScopedFileRemove {
public:
explicit ScopedFileRemove(const std::string& path) : path_(path) {}
@@ -106,6 +153,27 @@
std::string path_;
};
+bool ParseNotEmptyTraceFromFile(const std::string& trace_path,
+ protos::gen::Trace& out) {
+ std::string trace_str;
+ if (!base::ReadFile(trace_path, &trace_str))
+ return false;
+ if (trace_str.empty())
+ return false;
+ return out.ParseFromString(trace_str);
+}
+
+std::vector<std::string> GetReceivedTriggerNames(
+ const protos::gen::Trace& trace) {
+ std::vector<std::string> triggers;
+ for (const protos::gen::TracePacket& packet : trace.packet()) {
+ if (packet.has_trigger()) {
+ triggers.push_back(packet.trigger().trigger_name());
+ }
+ }
+ return triggers;
+}
+
class PerfettoCmdlineTest : public ::testing::Test {
public:
void StartServiceIfRequiredNoNewExecsAfterThis() {
@@ -190,11 +258,8 @@
// Read the trace written in the fixed location
// (/data/misc/perfetto-traces/ on Android, /tmp/ on Linux/Mac) and make
// sure it has the right contents.
- std::string trace_str;
- base::ReadFile(trace_path, &trace_str);
- ASSERT_FALSE(trace_str.empty());
protos::gen::Trace trace;
- ASSERT_TRUE(trace.ParseFromString(trace_str));
+ ASSERT_TRUE(ParseNotEmptyTraceFromFile(trace_path, trace));
uint32_t test_packets = 0;
for (const auto& p : trace.packet())
test_packets += p.has_for_testing() ? 1 : 0;
@@ -212,6 +277,11 @@
std::string stderr_;
base::TestTaskRunner task_runner_;
+ // We use these two constants to set test data payload parameters and assert
+ // it was correctly written to the trace.
+ static constexpr size_t kTestMessageCount = 11;
+ static constexpr size_t kTestMessageSize = 32;
+
private:
bool exec_allowed_ = true;
TestHelper test_helper_{&task_runner_};
@@ -350,15 +420,8 @@
}
TEST_F(PerfettoCmdlineTest, StartTracingTrigger) {
- // See |message_count| and |message_size| in the TraceConfig above.
- constexpr size_t kMessageCount = 11;
- constexpr size_t kMessageSize = 32;
- protos::gen::TraceConfig trace_config;
- trace_config.add_buffers()->set_size_kb(1024);
- auto* ds_config = trace_config.add_data_sources()->mutable_config();
- ds_config->set_name("android.perfetto.FakeProducer");
- ds_config->mutable_for_testing()->set_message_count(kMessageCount);
- ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+ protos::gen::TraceConfig trace_config =
+ CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
auto* trigger_cfg = trace_config.mutable_trigger_config();
trigger_cfg->set_trigger_mode(
protos::gen::TraceConfig::TriggerConfig::START_TRACING);
@@ -404,53 +467,25 @@
test_helper().WaitForProducerSetup();
EXPECT_EQ(0, trigger_proc.Run(&stderr_));
- // Wait for the producer to start, and then write out 11 packets.
+ // Wait for the producer to start, and then write out some test packets.
test_helper().WaitForProducerEnabled();
auto on_data_written = task_runner_.CreateCheckpoint("data_written");
fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
task_runner_.RunUntilCheckpoint("data_written");
background_trace.join();
- std::string trace_str;
- base::ReadFile(path, &trace_str);
protos::gen::Trace trace;
- ASSERT_TRUE(trace.ParseFromString(trace_str));
- size_t for_testing_packets = 0;
- size_t trigger_packets = 0;
- size_t trace_config_packets = 0;
- for (const auto& packet : trace.packet()) {
- if (packet.has_trace_config()) {
- // Ensure the trace config properly includes the trigger mode we set.
- auto kStartTrig = protos::gen::TraceConfig::TriggerConfig::START_TRACING;
- EXPECT_EQ(kStartTrig,
- packet.trace_config().trigger_config().trigger_mode());
- ++trace_config_packets;
- } else if (packet.has_trigger()) {
- // validate that the triggers are properly added to the trace.
- EXPECT_EQ("trigger_name", packet.trigger().trigger_name());
- ++trigger_packets;
- } else if (packet.has_for_testing()) {
- // Make sure that the data size is correctly set based on what we
- // requested.
- EXPECT_EQ(kMessageSize, packet.for_testing().str().size());
- ++for_testing_packets;
- }
- }
- EXPECT_EQ(trace_config_packets, 1u);
- EXPECT_EQ(trigger_packets, 1u);
- EXPECT_EQ(for_testing_packets, kMessageCount);
+ ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+ ExpectTraceContainsConfigWithTriggerMode(
+ trace, protos::gen::TraceConfig::TriggerConfig::START_TRACING);
+ EXPECT_THAT(GetReceivedTriggerNames(trace), ElementsAre("trigger_name"));
+ ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+ ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
}
TEST_F(PerfettoCmdlineTest, StopTracingTrigger) {
- // See |message_count| and |message_size| in the TraceConfig above.
- constexpr size_t kMessageCount = 11;
- constexpr size_t kMessageSize = 32;
- protos::gen::TraceConfig trace_config;
- trace_config.add_buffers()->set_size_kb(1024);
- auto* ds_config = trace_config.add_data_sources()->mutable_config();
- ds_config->set_name("android.perfetto.FakeProducer");
- ds_config->mutable_for_testing()->set_message_count(kMessageCount);
- ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+ protos::gen::TraceConfig trace_config =
+ CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
auto* trigger_cfg = trace_config.mutable_trigger_config();
trigger_cfg->set_trigger_mode(
protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
@@ -497,8 +532,8 @@
});
test_helper().WaitForProducerEnabled();
- // Wait for the producer to start, and then write out 11 packets, before the
- // trace actually starts (the trigger is seen).
+ // Wait for the producer to start, and then write out some test packets,
+ // before the trace actually starts (the trigger is seen).
auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
task_runner_.RunUntilCheckpoint("data_written_1");
@@ -507,56 +542,23 @@
background_trace.join();
- std::string trace_str;
- base::ReadFile(path, &trace_str);
protos::gen::Trace trace;
- ASSERT_TRUE(trace.ParseFromString(trace_str));
- bool seen_first_trigger = false;
- size_t for_testing_packets = 0;
- size_t trigger_packets = 0;
- size_t trace_config_packets = 0;
- for (const auto& packet : trace.packet()) {
- if (packet.has_trace_config()) {
- // Ensure the trace config properly includes the trigger mode we set.
- auto kStopTrig = protos::gen::TraceConfig::TriggerConfig::STOP_TRACING;
- EXPECT_EQ(kStopTrig,
- packet.trace_config().trigger_config().trigger_mode());
- ++trace_config_packets;
- } else if (packet.has_trigger()) {
- // validate that the triggers are properly added to the trace.
- if (!seen_first_trigger) {
- EXPECT_EQ("trigger_name", packet.trigger().trigger_name());
- seen_first_trigger = true;
- } else {
- EXPECT_EQ("trigger_name_3", packet.trigger().trigger_name());
- }
- ++trigger_packets;
- } else if (packet.has_for_testing()) {
- // Make sure that the data size is correctly set based on what we
- // requested.
- EXPECT_EQ(kMessageSize, packet.for_testing().str().size());
- ++for_testing_packets;
- }
- }
- EXPECT_EQ(trace_config_packets, 1u);
- EXPECT_EQ(trigger_packets, 2u);
- EXPECT_EQ(for_testing_packets, kMessageCount);
+ ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+ ExpectTraceContainsConfigWithTriggerMode(
+ trace, protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
+ EXPECT_THAT(GetReceivedTriggerNames(trace),
+ ElementsAre("trigger_name", "trigger_name_3"));
+ ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+ ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
}
// Dropbox on the commandline client only works on android builds. So disable
// this test on all other builds.
TEST_F(PerfettoCmdlineTest, AndroidOnly(NoDataNoFileWithoutTrigger)) {
- // See |message_count| and |message_size| in the TraceConfig above.
- constexpr size_t kMessageCount = 11;
- constexpr size_t kMessageSize = 32;
- protos::gen::TraceConfig trace_config;
- trace_config.add_buffers()->set_size_kb(1024);
+ protos::gen::TraceConfig trace_config =
+ CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
auto* incident_config = trace_config.mutable_incident_report_config();
incident_config->set_destination_package("foo.bar.baz");
- auto* ds_config = trace_config.add_data_sources()->mutable_config();
- ds_config->set_name("android.perfetto.FakeProducer");
- ds_config->mutable_for_testing()->set_message_count(kMessageCount);
- ds_config->mutable_for_testing()->set_message_size(kMessageSize);
auto* trigger_cfg = trace_config.mutable_trigger_config();
trigger_cfg->set_trigger_mode(
protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
@@ -601,15 +603,8 @@
}
TEST_F(PerfettoCmdlineTest, StopTracingTriggerFromConfig) {
- // See |message_count| and |message_size| in the TraceConfig above.
- constexpr size_t kMessageCount = 11;
- constexpr size_t kMessageSize = 32;
- protos::gen::TraceConfig trace_config;
- trace_config.add_buffers()->set_size_kb(1024);
- auto* ds_config = trace_config.add_data_sources()->mutable_config();
- ds_config->set_name("android.perfetto.FakeProducer");
- ds_config->mutable_for_testing()->set_message_count(kMessageCount);
- ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+ protos::gen::TraceConfig trace_config =
+ CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
auto* trigger_cfg = trace_config.mutable_trigger_config();
trigger_cfg->set_trigger_mode(
protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
@@ -666,8 +661,8 @@
});
test_helper().WaitForProducerEnabled();
- // Wait for the producer to start, and then write out 11 packets, before the
- // trace actually starts (the trigger is seen).
+ // Wait for the producer to start, and then write out some test packets,
+ // before the trace actually starts (the trigger is seen).
auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
task_runner_.RunUntilCheckpoint("data_written_1");
@@ -676,44 +671,20 @@
background_trace.join();
- std::string trace_str;
- base::ReadFile(path, &trace_str);
protos::gen::Trace trace;
- ASSERT_TRUE(trace.ParseFromString(trace_str));
- EXPECT_LT(static_cast<int>(kMessageCount), trace.packet_size());
- bool seen_first_trigger = false;
- for (const auto& packet : trace.packet()) {
- if (packet.has_trace_config()) {
- // Ensure the trace config properly includes the trigger mode we set.
- auto kStopTrig = protos::gen::TraceConfig::TriggerConfig::STOP_TRACING;
- EXPECT_EQ(kStopTrig,
- packet.trace_config().trigger_config().trigger_mode());
- } else if (packet.has_trigger()) {
- // validate that the triggers are properly added to the trace.
- if (!seen_first_trigger) {
- EXPECT_EQ("trigger_name", packet.trigger().trigger_name());
- seen_first_trigger = true;
- } else {
- EXPECT_EQ("trigger_name_3", packet.trigger().trigger_name());
- }
- } else if (packet.has_for_testing()) {
- // Make sure that the data size is correctly set based on what we
- // requested.
- EXPECT_EQ(kMessageSize, packet.for_testing().str().size());
- }
- }
+ ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+ EXPECT_LT(static_cast<int>(kTestMessageCount), trace.packet_size());
+ ExpectTraceContainsConfigWithTriggerMode(
+ trace, protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
+ EXPECT_THAT(GetReceivedTriggerNames(trace),
+ ElementsAre("trigger_name", "trigger_name_3"));
+ ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+ ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
}
TEST_F(PerfettoCmdlineTest, TriggerFromConfigStopsFileOpening) {
- // See |message_count| and |message_size| in the TraceConfig above.
- constexpr size_t kMessageCount = 11;
- constexpr size_t kMessageSize = 32;
- protos::gen::TraceConfig trace_config;
- trace_config.add_buffers()->set_size_kb(1024);
- auto* ds_config = trace_config.add_data_sources()->mutable_config();
- ds_config->set_name("android.perfetto.FakeProducer");
- ds_config->mutable_for_testing()->set_message_count(kMessageCount);
- ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+ protos::gen::TraceConfig trace_config =
+ CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
auto* trigger_cfg = trace_config.mutable_trigger_config();
trigger_cfg->set_trigger_mode(
protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
@@ -772,15 +743,8 @@
}
TEST_F(PerfettoCmdlineTest, AndroidOnly(CmdTriggerWithUploadFlag)) {
- // See |message_count| and |message_size| in the TraceConfig above.
- constexpr size_t kMessageCount = 2;
- constexpr size_t kMessageSize = 2;
- protos::gen::TraceConfig trace_config;
- trace_config.add_buffers()->set_size_kb(1024);
- auto* ds_config = trace_config.add_data_sources()->mutable_config();
- ds_config->set_name("android.perfetto.FakeProducer");
- ds_config->mutable_for_testing()->set_message_count(kMessageCount);
- ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+ protos::gen::TraceConfig trace_config =
+ CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
auto* trigger_cfg = trace_config.mutable_trigger_config();
trigger_cfg->set_trigger_mode(
protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
@@ -831,8 +795,8 @@
});
test_helper().WaitForProducerEnabled();
- // Wait for the producer to start, and then write out 11 packets, before the
- // trace actually starts (the trigger is seen).
+ // Wait for the producer to start, and then write out some test packets,
+ // before the trace actually starts (the trigger is seen).
auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
task_runner_.RunUntilCheckpoint("data_written_1");
@@ -841,11 +805,11 @@
background_trace.join();
- std::string trace_str;
- base::ReadFile(path, &trace_str);
protos::gen::Trace trace;
- ASSERT_TRUE(trace.ParseFromString(trace_str));
- EXPECT_LT(static_cast<int>(kMessageCount), trace.packet_size());
+ ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+ ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+ ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
+ EXPECT_LT(static_cast<int>(kTestMessageCount), trace.packet_size());
EXPECT_THAT(trace.packet(),
Contains(Property(&protos::gen::TracePacket::trigger,
Property(&protos::gen::Trigger::trigger_name,
@@ -853,14 +817,8 @@
}
TEST_F(PerfettoCmdlineTest, TriggerCloneSnapshot) {
- constexpr size_t kMessageCount = 2;
- constexpr size_t kMessageSize = 2;
- protos::gen::TraceConfig trace_config;
- trace_config.add_buffers()->set_size_kb(1024);
- auto* ds_config = trace_config.add_data_sources()->mutable_config();
- ds_config->set_name("android.perfetto.FakeProducer");
- ds_config->mutable_for_testing()->set_message_count(kMessageCount);
- ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+ protos::gen::TraceConfig trace_config =
+ CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
auto* trigger_cfg = trace_config.mutable_trigger_config();
trigger_cfg->set_trigger_mode(
protos::gen::TraceConfig::TriggerConfig::CLONE_SNAPSHOT);
@@ -910,8 +868,8 @@
});
test_helper().WaitForProducerEnabled();
- // Wait for the producer to start, and then write out 11 packets, before the
- // trace actually starts (the trigger is seen).
+ // Wait for the producer to start, and then write out some test packets,
+ // before the trace actually starts (the trigger is seen).
auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
task_runner_.RunUntilCheckpoint("data_written_1");
@@ -931,11 +889,11 @@
perfetto_proc.SendSigterm();
background_trace.join();
- std::string trace_str;
- base::ReadFile(snapshot_path, &trace_str);
protos::gen::Trace trace;
- ASSERT_TRUE(trace.ParseFromString(trace_str));
- EXPECT_LT(static_cast<int>(kMessageCount), trace.packet_size());
+ ASSERT_TRUE(ParseNotEmptyTraceFromFile(snapshot_path, trace));
+ ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+ ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
+ EXPECT_LT(static_cast<int>(kTestMessageCount), trace.packet_size());
EXPECT_THAT(trace.packet(),
Contains(Property(&protos::gen::TracePacket::trigger,
Property(&protos::gen::Trigger::trigger_name,
@@ -961,14 +919,9 @@
}
TEST_F(PerfettoCmdlineTest, CloneByName) {
- constexpr size_t kMessageCount = 2;
- protos::gen::TraceConfig trace_config;
- trace_config.add_buffers()->set_size_kb(1024);
+ protos::gen::TraceConfig trace_config =
+ CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
trace_config.set_unique_session_name("my_unique_session_name");
- auto* ds_config = trace_config.add_data_sources()->mutable_config();
- ds_config->set_name("android.perfetto.FakeProducer");
- ds_config->mutable_for_testing()->set_message_count(kMessageCount);
- ds_config->mutable_for_testing()->set_message_size(2);
// We have to construct all the processes we want to fork before we start the
// service with |StartServiceIfRequired()|. this is because it is unsafe
@@ -1026,26 +979,18 @@
EXPECT_EQ(0, perfetto_proc_clone_2.Run(&stderr_)) << "stderr: " << stderr_;
EXPECT_FALSE(base::FileExists(path_cloned_2));
- std::string cloned_trace_str;
- base::ReadFile(path_cloned, &cloned_trace_str);
protos::gen::Trace cloned_trace;
- ASSERT_TRUE(cloned_trace.ParseFromString(cloned_trace_str));
- ssize_t cloned_num_test_packets = std::count_if(
- cloned_trace.packet().begin(), cloned_trace.packet().end(),
- [](const protos::gen::TracePacket& tp) { return tp.has_for_testing(); });
- EXPECT_EQ(cloned_num_test_packets, static_cast<ssize_t>(kMessageCount));
+ ASSERT_TRUE(ParseNotEmptyTraceFromFile(path_cloned, cloned_trace));
+ ExpectTraceContainsTestMessages(cloned_trace, kTestMessageCount);
+ ExpectTraceContainsTestMessagesWithSize(cloned_trace, kTestMessageSize);
perfetto_proc.SendSigterm();
background_trace.join();
- std::string trace_str;
- base::ReadFile(path, &trace_str);
protos::gen::Trace trace;
- ASSERT_TRUE(trace.ParseFromString(trace_str));
- ssize_t num_test_packets = std::count_if(
- trace.packet().begin(), trace.packet().end(),
- [](const protos::gen::TracePacket& tp) { return tp.has_for_testing(); });
- EXPECT_EQ(num_test_packets, static_cast<ssize_t>(kMessageCount));
+ ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+ ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+ ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
}
// Regression test for b/279753347: --save-for-bugreport would create an empty
@@ -1185,10 +1130,8 @@
auto check_trace = [&](std::string fname, int expected_score) {
std::string fpath = GetBugreportTraceDir() + "/" + fname;
ASSERT_TRUE(base::FileExists(fpath)) << fpath;
- std::string trace_str;
- base::ReadFile(fpath, &trace_str);
protos::gen::Trace trace;
- ASSERT_TRUE(trace.ParseFromString(trace_str)) << fpath;
+ ASSERT_TRUE(ParseNotEmptyTraceFromFile(fpath, trace)) << fpath;
EXPECT_THAT(
trace.packet(),
Contains(Property(&protos::gen::TracePacket::trace_config,
@@ -1211,8 +1154,9 @@
auto remove_on_exit = base::OnScopeExit(remove_br_files);
const uint32_t kMsgCount = 10000;
+ const uint32_t kMsgSize = 1024;
TraceConfig cfg = CreateTraceConfigForBugreportTest(
- /*score=*/1, /*add_filter=*/false, kMsgCount, /*msg_size=*/1024);
+ /*score=*/1, /*add_filter=*/false, kMsgCount, kMsgSize);
auto session_name = "bugreport_test_" +
std::to_string(base::GetWallTimeNs().count() % 1000000);
@@ -1263,14 +1207,10 @@
std::string fpath = GetBugreportTraceDir() + "/systrace.pftrace";
ASSERT_TRUE(base::FileExists(fpath)) << fpath;
- std::string trace_str;
- base::ReadFile(fpath, &trace_str);
protos::gen::Trace trace;
- ASSERT_TRUE(trace.ParseFromString(trace_str)) << fpath;
- ssize_t num_test_packets = std::count_if(
- trace.packet().begin(), trace.packet().end(),
- [](const protos::gen::TracePacket& tp) { return tp.has_for_testing(); });
- EXPECT_EQ(num_test_packets, static_cast<ssize_t>(kMsgCount));
+ ASSERT_TRUE(ParseNotEmptyTraceFromFile(fpath, trace)) << fpath;
+ ExpectTraceContainsTestMessages(trace, kMsgCount);
+ ExpectTraceContainsTestMessagesWithSize(trace, kMsgSize);
}
} // namespace perfetto
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup.out b/test/trace_processor/diff_tests/metrics/startup/android_startup.out
index 42368b3..ec2b075 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup.out
@@ -78,11 +78,15 @@
}
launch_dur: 108
trace_thread_sections {
+ thread_section {
+ start_timestamp: 130
+ end_timestamp: 210
+ thread_name: "com.google.android.calendar"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 130
end_timestamp: 210
- thread_name: "com.google.android.calendar"
- thread_tid: 3
- process_pid: 3
}
}
startup_type: "warm"
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.out
index 2d6070c..8b53348 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.out
@@ -148,12 +148,16 @@
}
launch_dur: 999999900
trace_slice_sections {
+ slice_section {
+ start_timestamp: 340
+ end_timestamp: 390
+ slice_id: 20
+ slice_name: "CollectorTransition mark sweep GC"
+ process_pid: 3
+ thread_tid: 5
+ }
start_timestamp: 340
end_timestamp: 390
- slice_id: 20
- slice_name: "CollectorTransition mark sweep GC"
- process_pid: 3
- thread_tid: 5
}
}
slow_start_reason_with_details {
@@ -171,20 +175,24 @@
}
launch_dur: 999999900
trace_slice_sections {
- start_timestamp: 170
- end_timestamp: 500000000
- slice_id: 9
- slice_name: "OpenDexFilesFromOat(something else)"
- process_pid: 3
- thread_tid: 3
- }
- trace_slice_sections {
+ slice_section {
+ start_timestamp: 170
+ end_timestamp: 500000000
+ slice_id: 9
+ slice_name: "OpenDexFilesFromOat(something else)"
+ process_pid: 3
+ thread_tid: 3
+ }
+ slice_section {
+ start_timestamp: 150
+ end_timestamp: 165
+ slice_id: 5
+ slice_name: "OpenDexFilesFromOat(something)"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 150
- end_timestamp: 165
- slice_id: 5
- slice_name: "OpenDexFilesFromOat(something)"
- process_pid: 3
- thread_tid: 3
+ end_timestamp: 500000000
}
}
slow_start_reason_with_details {
@@ -200,12 +208,16 @@
}
launch_dur: 999999900
trace_slice_sections {
+ slice_section {
+ start_timestamp: 10000000
+ end_timestamp: 50000000
+ slice_id: 21
+ slice_name: "binder transaction"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 10000000
end_timestamp: 50000000
- slice_id: 21
- slice_name: "binder transaction"
- process_pid: 3
- thread_tid: 3
}
}
}
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.out
index a98505f..a4e1074 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.out
@@ -107,12 +107,16 @@
}
launch_dur: 999999900000000000
trace_slice_sections {
+ slice_section {
+ start_timestamp: 340000000000
+ end_timestamp: 390000000000
+ slice_id: 91
+ slice_name: "CollectorTransition mark sweep GC"
+ process_pid: 3
+ thread_tid: 5
+ }
start_timestamp: 340000000000
end_timestamp: 390000000000
- slice_id: 91
- slice_name: "CollectorTransition mark sweep GC"
- process_pid: 3
- thread_tid: 5
}
}
slow_start_reason_with_details {
@@ -129,25 +133,29 @@
}
launch_dur: 999999900000000000
trace_thread_sections {
+ thread_section {
+ start_timestamp: 155000000000
+ end_timestamp: 165000000000
+ thread_name: "Jit thread pool"
+ process_pid: 3
+ thread_tid: 4
+ }
+ thread_section {
+ start_timestamp: 170000000000
+ end_timestamp: 175000000000
+ thread_name: "Jit thread pool"
+ process_pid: 3
+ thread_tid: 4
+ }
+ thread_section {
+ start_timestamp: 185000000000
+ end_timestamp: 190000000000
+ thread_name: "Jit thread pool"
+ process_pid: 3
+ thread_tid: 4
+ }
start_timestamp: 155000000000
- end_timestamp: 165000000000
- thread_name: "Jit thread pool"
- thread_tid: 4
- process_pid: 3
- }
- trace_thread_sections {
- start_timestamp: 170000000000
- end_timestamp: 175000000000
- thread_name: "Jit thread pool"
- thread_tid: 4
- process_pid: 3
- }
- trace_thread_sections {
- start_timestamp: 185000000000
end_timestamp: 190000000000
- thread_name: "Jit thread pool"
- thread_tid: 4
- process_pid: 3
}
}
slow_start_reason_with_details {
@@ -164,28 +172,32 @@
}
launch_dur: 999999900000000000
trace_slice_sections {
- start_timestamp: 200000000000
- end_timestamp: 210000000000
- slice_id: 84
- slice_name: "JIT compiling nothing"
- process_pid: 3
- thread_tid: 3
- }
- trace_slice_sections {
+ slice_section {
+ start_timestamp: 200000000000
+ end_timestamp: 210000000000
+ slice_id: 84
+ slice_name: "JIT compiling nothing"
+ process_pid: 3
+ thread_tid: 3
+ }
+ slice_section {
+ start_timestamp: 100000000000
+ end_timestamp: 101000000000
+ slice_id: 9
+ slice_name: "JIT compiling something"
+ process_pid: 3
+ thread_tid: 4
+ }
+ slice_section {
+ start_timestamp: 101000000000
+ end_timestamp: 102000000000
+ slice_id: 10
+ slice_name: "JIT compiling something"
+ process_pid: 3
+ thread_tid: 4
+ }
start_timestamp: 100000000000
- end_timestamp: 101000000000
- slice_id: 9
- slice_name: "JIT compiling something"
- process_pid: 3
- thread_tid: 4
- }
- trace_slice_sections {
- start_timestamp: 101000000000
- end_timestamp: 102000000000
- slice_id: 10
- slice_name: "JIT compiling something"
- process_pid: 3
- thread_tid: 4
+ end_timestamp: 210000000000
}
}
}
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.out
index 2405d7e..04b8915 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.out
@@ -128,12 +128,16 @@
}
launch_dur: 108000000000
trace_slice_sections {
+ slice_section {
+ start_timestamp: 204000000000
+ end_timestamp: 205000000000
+ slice_id: 13
+ slice_name: "location=/system/framework/oat/arm/com.google.android.calendar.odex status=up-to-date filter=speed reason=install-dm"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 204000000000
end_timestamp: 205000000000
- slice_id: 13
- slice_name: "location=/system/framework/oat/arm/com.google.android.calendar.odex status=up-to-date filter=speed reason=install-dm"
- process_pid: 3
- thread_tid: 3
}
}
slow_start_reason_with_details {
@@ -149,12 +153,16 @@
}
launch_dur: 108000000000
trace_slice_sections {
+ slice_section {
+ start_timestamp: 200000000000
+ end_timestamp: 202000000000
+ slice_id: 12
+ slice_name: "location=error status=io-error-no-oat filter=run-from-apk reason=unknown"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 200000000000
end_timestamp: 202000000000
- slice_id: 12
- slice_name: "location=error status=io-error-no-oat filter=run-from-apk reason=unknown"
- process_pid: 3
- thread_tid: 3
}
}
slow_start_reason_with_details {
@@ -171,12 +179,16 @@
}
launch_dur: 108000000000
trace_slice_sections {
+ slice_section {
+ start_timestamp: 185000000000
+ end_timestamp: 187000000000
+ slice_id: 4
+ slice_name: "bindApplication"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 185000000000
end_timestamp: 187000000000
- slice_id: 4
- slice_name: "bindApplication"
- process_pid: 3
- thread_tid: 3
}
}
slow_start_reason_with_details {
@@ -193,20 +205,24 @@
}
launch_dur: 108000000000
trace_slice_sections {
+ slice_section {
+ start_timestamp: 188000000000
+ end_timestamp: 189000000000
+ slice_id: 6
+ slice_name: "inflate"
+ process_pid: 3
+ thread_tid: 3
+ }
+ slice_section {
+ start_timestamp: 191000000000
+ end_timestamp: 192000000000
+ slice_id: 8
+ slice_name: "inflate"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 188000000000
- end_timestamp: 189000000000
- slice_id: 6
- slice_name: "inflate"
- process_pid: 3
- thread_tid: 3
- }
- trace_slice_sections {
- start_timestamp: 191000000000
end_timestamp: 192000000000
- slice_id: 8
- slice_name: "inflate"
- process_pid: 3
- thread_tid: 3
}
}
slow_start_reason_with_details {
@@ -223,12 +239,16 @@
}
launch_dur: 108000000000
trace_slice_sections {
+ slice_section {
+ start_timestamp: 188000000000
+ end_timestamp: 189000000000
+ slice_id: 7
+ slice_name: "ResourcesManager#getResources"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 188000000000
end_timestamp: 189000000000
- slice_id: 7
- slice_name: "ResourcesManager#getResources"
- thread_tid: 3
- process_pid: 3
}
}
slow_start_reason_with_details {
@@ -245,11 +265,15 @@
}
launch_dur: 108000000000
trace_thread_sections {
+ thread_section {
+ start_timestamp: 205000000000
+ end_timestamp: 210000000000
+ thread_name: "com.google.android.calendar"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 205000000000
end_timestamp: 210000000000
- thread_name: "com.google.android.calendar"
- thread_tid: 3
- process_pid: 3
}
}
startup_type: "cold"
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.out
index 3dbb2aa..f2c7123 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.out
@@ -127,12 +127,16 @@
}
launch_dur: 108000000000
trace_slice_sections {
+ slice_section {
+ start_timestamp: 200000000000
+ end_timestamp: 202000000000
+ slice_id: 12
+ slice_name: "location=error status=io-error-no-oat filter=run-from-apk reason=unknown"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 200000000000
end_timestamp: 202000000000
- slice_id: 12
- slice_name: "location=error status=io-error-no-oat filter=run-from-apk reason=unknown"
- process_pid: 3
- thread_tid: 3
}
}
slow_start_reason_with_details {
@@ -149,12 +153,16 @@
}
launch_dur: 108000000000
trace_slice_sections {
+ slice_section {
+ start_timestamp: 185000000000
+ end_timestamp: 195000000000
+ slice_id: 4
+ slice_name: "bindApplication"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 185000000000
end_timestamp: 195000000000
- slice_id: 4
- slice_name: "bindApplication"
- process_pid: 3
- thread_tid: 3
}
}
slow_start_reason_with_details {
@@ -171,20 +179,24 @@
}
launch_dur: 108000000000
trace_slice_sections {
- start_timestamp: 190000000000
- end_timestamp: 192000000000
- slice_id: 8
- slice_name: "inflate"
- process_pid: 3
- thread_tid: 3
- }
- trace_slice_sections {
+ slice_section {
+ start_timestamp: 190000000000
+ end_timestamp: 192000000000
+ slice_id: 8
+ slice_name: "inflate"
+ process_pid: 3
+ thread_tid: 3
+ }
+ slice_section {
+ start_timestamp: 188000000000
+ end_timestamp: 189000000000
+ slice_id: 7
+ slice_name: "inflate"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 188000000000
- end_timestamp: 189000000000
- slice_id: 7
- slice_name: "inflate"
- process_pid: 3
- thread_tid: 3
+ end_timestamp: 192000000000
}
}
slow_start_reason_with_details {
@@ -201,12 +213,16 @@
}
launch_dur: 108000000000
trace_slice_sections {
+ slice_section {
+ start_timestamp: 187000000000
+ end_timestamp: 192000000000
+ slice_id: 5
+ slice_name: "ResourcesManager#getResources"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 187000000000
end_timestamp: 192000000000
- slice_id: 5
- slice_name: "ResourcesManager#getResources"
- thread_tid: 3
- process_pid: 3
}
}
slow_start_reason_with_details {
@@ -223,11 +239,15 @@
}
launch_dur: 108000000000
trace_thread_sections {
+ thread_section {
+ start_timestamp: 205000000000
+ end_timestamp: 210000000000
+ thread_name: "com.google.android.calendar"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 205000000000
end_timestamp: 210000000000
- thread_name: "com.google.android.calendar"
- thread_tid: 3
- process_pid: 3
}
}
startup_type: "cold"
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.out
index 4913852..6cb97e0 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.out
@@ -46,25 +46,29 @@
}
launch_dur: 100
trace_slice_sections {
+ slice_section {
+ start_timestamp: 105
+ end_timestamp: 106
+ slice_id: 6
+ slice_name: "Broadcast dispatched from android (2005:system/1000) x"
+ thread_tid: 1
+ }
+ slice_section {
+ start_timestamp: 106
+ end_timestamp: 107
+ slice_id: 8
+ slice_name: "Broadcast dispatched from android (2005:system/1000) x"
+ thread_tid: 1
+ }
+ slice_section {
+ start_timestamp: 107
+ end_timestamp: 108
+ slice_id: 10
+ slice_name: "Broadcast dispatched from android (2005:system/1000) x"
+ thread_tid: 1
+ }
start_timestamp: 105
- end_timestamp: 106
- slice_id: 6
- slice_name: "Broadcast dispatched from android (2005:system/1000) x"
- thread_tid: 1
- }
- trace_slice_sections {
- start_timestamp: 106
- end_timestamp: 107
- slice_id: 8
- slice_name: "Broadcast dispatched from android (2005:system/1000) x"
- thread_tid: 1
- }
- trace_slice_sections {
- start_timestamp: 107
end_timestamp: 108
- slice_id: 10
- slice_name: "Broadcast dispatched from android (2005:system/1000) x"
- thread_tid: 1
}
}
slow_start_reason_with_details {
@@ -81,25 +85,29 @@
}
launch_dur: 100
trace_slice_sections {
+ slice_section {
+ start_timestamp: 100
+ end_timestamp: 101
+ slice_id: 1
+ slice_name: "broadcastReceiveReg: x"
+ thread_tid: 2
+ }
+ slice_section {
+ start_timestamp: 101
+ end_timestamp: 102
+ slice_id: 2
+ slice_name: "broadcastReceiveReg: x"
+ thread_tid: 2
+ }
+ slice_section {
+ start_timestamp: 102
+ end_timestamp: 103
+ slice_id: 3
+ slice_name: "broadcastReceiveReg: x"
+ thread_tid: 2
+ }
start_timestamp: 100
- end_timestamp: 101
- slice_id: 1
- slice_name: "broadcastReceiveReg: x"
- thread_tid: 2
- }
- trace_slice_sections {
- start_timestamp: 101
- end_timestamp: 102
- slice_id: 2
- slice_name: "broadcastReceiveReg: x"
- thread_tid: 2
- }
- trace_slice_sections {
- start_timestamp: 102
end_timestamp: 103
- slice_id: 3
- slice_name: "broadcastReceiveReg: x"
- thread_tid: 2
}
}
}
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.out
index dabbb5f..f64dceb 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.out
@@ -82,12 +82,16 @@
}
launch_dur: 100000000000
trace_slice_sections {
+ slice_section {
+ start_timestamp: 112000000000
+ end_timestamp: 115000000000
+ slice_id: 1
+ slice_name: "bindApplication"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 112000000000
end_timestamp: 115000000000
- slice_id: 1
- slice_name: "bindApplication"
- process_pid: 3
- thread_tid: 3
}
}
slow_start_reason_with_details {
@@ -105,28 +109,32 @@
}
launch_dur: 100000000000
trace_slice_sections {
- start_timestamp: 140000000000
- end_timestamp: 157000000000
- slice_id: 5
- slice_name: "Lock contention on a monitor lock (owner tid: 2)"
- process_pid: 3
- thread_tid: 3
- }
- trace_slice_sections {
+ slice_section {
+ start_timestamp: 140000000000
+ end_timestamp: 157000000000
+ slice_id: 5
+ slice_name: "Lock contention on a monitor lock (owner tid: 2)"
+ process_pid: 3
+ thread_tid: 3
+ }
+ slice_section {
+ start_timestamp: 120000000000
+ end_timestamp: 130000000000
+ slice_id: 4
+ slice_name: "Lock contention on thread list lock (owner tid: 2)"
+ process_pid: 3
+ thread_tid: 3
+ }
+ slice_section {
+ start_timestamp: 155000000000
+ end_timestamp: 160000000000
+ slice_id: 6
+ slice_name: "Lock contention on a monitor lock (owner tid: 3)"
+ process_pid: 3
+ thread_tid: 4
+ }
start_timestamp: 120000000000
- end_timestamp: 130000000000
- slice_id: 4
- slice_name: "Lock contention on thread list lock (owner tid: 2)"
- process_pid: 3
- thread_tid: 3
- }
- trace_slice_sections {
- start_timestamp: 155000000000
end_timestamp: 160000000000
- slice_id: 6
- slice_name: "Lock contention on a monitor lock (owner tid: 3)"
- process_pid: 3
- thread_tid: 4
}
}
slow_start_reason_with_details {
@@ -144,20 +152,24 @@
}
launch_dur: 100000000000
trace_slice_sections {
+ slice_section {
+ start_timestamp: 140000000000
+ end_timestamp: 157000000000
+ slice_id: 5
+ slice_name: "Lock contention on a monitor lock (owner tid: 2)"
+ process_pid: 3
+ thread_tid: 3
+ }
+ slice_section {
+ start_timestamp: 155000000000
+ end_timestamp: 160000000000
+ slice_id: 6
+ slice_name: "Lock contention on a monitor lock (owner tid: 3)"
+ process_pid: 3
+ thread_tid: 4
+ }
start_timestamp: 140000000000
- end_timestamp: 157000000000
- slice_id: 5
- slice_name: "Lock contention on a monitor lock (owner tid: 2)"
- process_pid: 3
- thread_tid: 3
- }
- trace_slice_sections {
- start_timestamp: 155000000000
end_timestamp: 160000000000
- slice_id: 6
- slice_name: "Lock contention on a monitor lock (owner tid: 3)"
- process_pid: 3
- thread_tid: 4
}
}
startup_type: "cold"
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.out
index ded275e..17d426d 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.out
@@ -77,11 +77,15 @@
}
launch_dur: 7
trace_thread_sections {
+ thread_section {
+ start_timestamp: 103
+ end_timestamp: 107
+ thread_name: "com.google.android.calendar"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 103
end_timestamp: 107
- thread_name: "com.google.android.calendar"
- thread_tid: 3
- process_pid: 3
}
}
}
@@ -164,11 +168,15 @@
}
launch_dur: 7
trace_thread_sections {
+ thread_section {
+ start_timestamp: 203
+ end_timestamp: 207
+ thread_name: "com.google.android.calendar"
+ process_pid: 4
+ thread_tid: 4
+ }
start_timestamp: 203
end_timestamp: 207
- thread_name: "com.google.android.calendar"
- thread_tid: 4
- process_pid: 4
}
}
}
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_slow.out
index 942037e..0ae5b29 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_slow.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_slow.out
@@ -81,11 +81,15 @@
}
launch_dur: 108000000000
trace_thread_sections {
+ thread_section {
+ start_timestamp: 130000000000
+ end_timestamp: 210000000000
+ thread_name: "com.google.android.calendar"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 130000000000
end_timestamp: 210000000000
- thread_name: "com.google.android.calendar"
- thread_tid: 3
- process_pid: 3
}
}
slow_start_reason_with_details {
@@ -102,11 +106,15 @@
}
launch_dur: 108000000000
trace_thread_sections {
+ thread_section {
+ start_timestamp: 120000000000
+ end_timestamp: 125000000000
+ thread_name: "com.google.android.calendar"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 120000000000
end_timestamp: 125000000000
- thread_name: "com.google.android.calendar"
- thread_tid: 3
- process_pid: 3
}
}
slow_start_reason_with_details {
@@ -123,11 +131,15 @@
}
launch_dur: 108000000000
trace_thread_sections {
+ thread_section {
+ start_timestamp: 125000000000
+ end_timestamp: 130000000000
+ thread_name: "com.google.android.calendar"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 125000000000
end_timestamp: 130000000000
- thread_name: "com.google.android.calendar"
- thread_tid: 3
- process_pid: 3
}
}
slow_start_reason_with_details {
@@ -144,11 +156,15 @@
}
launch_dur: 108000000000
trace_thread_sections {
+ thread_section {
+ start_timestamp: 130000000000
+ end_timestamp: 210000000000
+ thread_name: "com.google.android.calendar"
+ process_pid: 3
+ thread_tid: 3
+ }
start_timestamp: 130000000000
end_timestamp: 210000000000
- thread_name: "com.google.android.calendar"
- thread_tid: 3
- process_pid: 3
}
}
}
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.out
index 3f52432..1f2000d 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.out
@@ -44,11 +44,15 @@
}
launch_dur: 100
trace_slice_sections {
+ slice_section {
+ start_timestamp: 130
+ end_timestamp: 133
+ slice_id: 1
+ slice_name: "KeyguardUpdateMonitor#onAuthenticationSucceeded"
+ thread_tid: 2
+ }
start_timestamp: 130
end_timestamp: 133
- slice_id: 1
- slice_name: "KeyguardUpdateMonitor#onAuthenticationSucceeded"
- thread_tid: 2
}
}
}
diff --git a/test/trace_processor/diff_tests/parser/parsing/tests.py b/test/trace_processor/diff_tests/parser/parsing/tests.py
index 54707c1..8af739d 100644
--- a/test/trace_processor/diff_tests/parser/parsing/tests.py
+++ b/test/trace_processor/diff_tests/parser/parsing/tests.py
@@ -1570,3 +1570,76 @@
5230422153284,0,1306,"[NULL]"
5230425693562,0,10,1
"""))
+
+ # Kernel idle tasks created by /sbin/init should be filtered.
+ def test_task_newtask_swapper_by_init(self):
+ return DiffTestBlueprint(
+ trace=TextProto(r"""
+ packet {
+ first_packet_on_sequence: true
+ ftrace_events {
+ cpu: 1
+ event {
+ timestamp: 1000000
+ pid: 0
+ task_newtask {
+ pid: 1
+ comm: "swapper/0"
+ clone_flags: 8389376
+ oom_score_adj: 0
+ }
+ }
+ event {
+ timestamp: 1000000
+ pid: 0
+ task_newtask {
+ pid: 2
+ comm: "swapper/0"
+ clone_flags: 8390400
+ oom_score_adj: 0
+ }
+ }
+ event {
+ timestamp: 17000000
+ pid: 1
+ task_newtask {
+ pid: 0
+ comm: "swapper/0"
+ clone_flags: 256
+ oom_score_adj: 0
+ }
+ }
+ event {
+ timestamp: 17000000
+ pid: 1
+ task_newtask {
+ pid: 0
+ comm: "swapper/0"
+ clone_flags: 256
+ oom_score_adj: 0
+ }
+ }
+ event {
+ timestamp: 17000000
+ pid: 1
+ task_newtask {
+ pid: 0
+ comm: "swapper/0"
+ clone_flags: 256
+ oom_score_adj: 0
+ }
+ }
+ }
+ trusted_uid: 9999
+ trusted_packet_sequence_id: 2
+ trusted_pid: 521
+ previous_packet_dropped: true
+ }
+ """),
+ query="""
+ SELECT utid, tid, name from thread where tid = 0
+ """,
+ out=Csv("""
+ "utid","tid","name"
+ 0,0,"swapper"
+ """))
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/tools/gen_bazel b/tools/gen_bazel
index a30f5ca..a5776ba 100755
--- a/tools/gen_bazel
+++ b/tools/gen_bazel
@@ -83,12 +83,12 @@
'//src/trace_processor:trace_processor_shell',
'//src/trace_processor:trace_processor',
'//src/traceconv:traceconv',
- '//src/traceconv:libpprofbuilder',
]
# These targets will be exported with visibility only to our allowlist.
allowlist_public_targets = [
'//src/shared_lib:libperfetto_c',
+ '//src/traceconv:libpprofbuilder',
]
# These targets are required by internal build rules but don't need to be
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/base/mithril_utils.ts b/ui/src/base/mithril_utils.ts
index b450f64..9b0615d 100644
--- a/ui/src/base/mithril_utils.ts
+++ b/ui/src/base/mithril_utils.ts
@@ -52,3 +52,36 @@
);
},
};
+
+/**
+ * Utility function to pre-bind some mithril attrs of a component, and leave
+ * the others unbound and passed at run-time.
+ * Example use case: the Page API Passes to the registered page a PageAttrs,
+ * which is {subpage:string}. Imagine you write a MyPage component that takes
+ * some extra input attrs (e.g. the App object) and you want to bind them
+ * onActivate(). The results looks like this:
+ *
+ * interface MyPageAttrs extends PageAttrs { app: App; }
+ *
+ * class MyPage extends m.classComponent<MyPageAttrs> {... view() {...} }
+ *
+ * onActivate(app: App) {
+ * pages.register(... bindMithrilApps(MyPage, {app: app});
+ * }
+ *
+ * The return value of bindMithrilApps is a mithril component that takes in
+ * input only a {subpage: string} and passes down to MyPage the combination
+ * of pre-bound and runtime attrs, that is {subpage, app}.
+ */
+export function bindMithrilAttrs<BaseAttrs, Attrs>(
+ component: m.ComponentTypes<Attrs>,
+ boundArgs: Omit<Attrs, keyof BaseAttrs>,
+): m.Component<BaseAttrs> {
+ return {
+ view(vnode: m.Vnode<BaseAttrs>) {
+ const attrs = {...vnode.attrs, ...boundArgs} as Attrs;
+ const emptyAttrs: m.CommonAttributes<Attrs, {}> = {}; // Keep tsc happy.
+ return m<Attrs, {}>(component, {...attrs, ...emptyAttrs});
+ },
+ };
+}
diff --git a/ui/src/chrome_extension/chrome_tracing_controller.ts b/ui/src/chrome_extension/chrome_tracing_controller.ts
index 916fca9..de15873 100644
--- a/ui/src/chrome_extension/chrome_tracing_controller.ts
+++ b/ui/src/chrome_extension/chrome_tracing_controller.ts
@@ -21,13 +21,13 @@
ConsumerPortResponse,
GetTraceStatsResponse,
ReadBuffersResponse,
-} from '../controller/consumer_port_types';
-import {RpcConsumerPort} from '../controller/record_controller_interfaces';
+} from '../plugins/dev.perfetto.RecordTrace/consumer_port_types';
+import {RpcConsumerPort} from '../plugins/dev.perfetto.RecordTrace/record_controller_interfaces';
import {
browserSupportsPerfettoConfig,
extractTraceConfig,
hasSystemDataSourceConfig,
-} from '../core/trace_config_utils';
+} from '../plugins/dev.perfetto.RecordTrace/trace_config_utils';
import {ITraceStats, TraceConfig} from '../protos';
import {DevToolsSocket} from './devtools_socket';
diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts
deleted file mode 100644
index cc10366..0000000
--- a/ui/src/common/constants.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-export const TRACE_SUFFIX = '.perfetto-trace';
diff --git a/ui/src/common/recordingV2/recording_error_handling.ts b/ui/src/common/recordingV2/recording_error_handling.ts
deleted file mode 100644
index ffec467..0000000
--- a/ui/src/common/recordingV2/recording_error_handling.ts
+++ /dev/null
@@ -1,141 +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 {getErrorMessage} from '../../base/errors';
-import {
- showAllowUSBDebugging,
- showConnectionLostError,
- showExtensionNotInstalled,
- showFailedToPushBinary,
- showIssueParsingTheTracedResponse,
- showNoDeviceSelected,
- showWebsocketConnectionIssue,
- showWebUSBErrorV2,
-} from '../../frontend/error_dialog';
-import {OnMessageCallback} from './recording_interfaces_v2';
-import {
- ALLOW_USB_DEBUGGING,
- BINARY_PUSH_FAILURE,
- BINARY_PUSH_UNKNOWN_RESPONSE,
- EXTENSION_NOT_INSTALLED,
- NO_DEVICE_SELECTED,
- PARSING_UNABLE_TO_DECODE_METHOD,
- PARSING_UNKNWON_REQUEST_ID,
- PARSING_UNRECOGNIZED_MESSAGE,
- PARSING_UNRECOGNIZED_PORT,
- WEBSOCKET_UNABLE_TO_CONNECT,
-} from './recording_utils';
-
-// The pattern for handling recording error can have the following nesting in
-// case of errors:
-// A. wrapRecordingError -> wraps a promise
-// B. onFailure -> has user defined logic and calls showRecordingModal
-// C. showRecordingModal -> shows UX for a given error; this is not called
-// directly by wrapRecordingError, because we want the caller (such as the
-// UI) to dictate the UX
-
-// This method takes a promise and a callback to be execute in case the promise
-// fails. It then awaits the promise and executes the callback in case of
-// failure. In the recording code it is used to wrap:
-// 1. Acessing the WebUSB API.
-// 2. Methods returning promises which can be rejected. For instance:
-// a) When the user clicks 'Add a new device' but then doesn't select a valid
-// device.
-// b) When the user starts a tracing session, but cancels it before they
-// authorize the session on the device.
-export async function wrapRecordingError<T>(
- promise: Promise<T>,
- onFailure: OnMessageCallback,
-): Promise<T | undefined> {
- try {
- return await promise;
- } catch (e) {
- // Sometimes the message is wrapped in an Error object, sometimes not, so
- // we make sure we transform it into a string.
- const errorMessage = getErrorMessage(e);
- onFailure(errorMessage);
- return undefined;
- }
-}
-
-// Shows a modal for every known type of error which can arise during recording.
-// In this way, errors occuring at different levels of the recording process
-// can be handled in a central location.
-export function showRecordingModal(message: string): void {
- if (
- [
- 'Unable to claim interface.',
- 'The specified endpoint is not part of a claimed and selected ' +
- 'alternate interface.',
- // thrown when calling the 'reset' method on a WebUSB device.
- 'Unable to reset the device.',
- ].some((partOfMessage) => message.includes(partOfMessage))
- ) {
- showWebUSBErrorV2();
- } else if (
- [
- 'A transfer error has occurred.',
- 'The device was disconnected.',
- 'The transfer was cancelled.',
- ].some((partOfMessage) => message.includes(partOfMessage)) ||
- isDeviceDisconnectedError(message)
- ) {
- showConnectionLostError();
- } else if (message === ALLOW_USB_DEBUGGING) {
- showAllowUSBDebugging();
- } else if (
- isMessageComposedOf(message, [
- BINARY_PUSH_FAILURE,
- BINARY_PUSH_UNKNOWN_RESPONSE,
- ])
- ) {
- showFailedToPushBinary(message.substring(message.indexOf(':') + 1));
- } else if (message === NO_DEVICE_SELECTED) {
- showNoDeviceSelected();
- } else if (WEBSOCKET_UNABLE_TO_CONNECT === message) {
- showWebsocketConnectionIssue(message);
- } else if (message === EXTENSION_NOT_INSTALLED) {
- showExtensionNotInstalled();
- } else if (
- isMessageComposedOf(message, [
- PARSING_UNKNWON_REQUEST_ID,
- PARSING_UNABLE_TO_DECODE_METHOD,
- PARSING_UNRECOGNIZED_PORT,
- PARSING_UNRECOGNIZED_MESSAGE,
- ])
- ) {
- showIssueParsingTheTracedResponse(message);
- } else {
- throw new Error(`${message}`);
- }
-}
-
-function isDeviceDisconnectedError(message: string) {
- return (
- message.includes('Device with serial') &&
- message.includes('was disconnected.')
- );
-}
-
-function isMessageComposedOf(message: string, issues: string[]) {
- for (const issue of issues) {
- if (message.includes(issue)) {
- return true;
- }
- }
- return false;
-}
-
-// Exception thrown by the Recording logic.
-export class RecordingError extends Error {}
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 24eceaf..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;
@@ -79,19 +79,31 @@
// The currently open trace.
currentTrace?: TraceContext;
+ private static _instance: AppContext;
+
+ static initialize(initArgs: AppInitArgs): AppContext {
+ assertTrue(AppContext._instance === undefined);
+ return (AppContext._instance = new AppContext(initArgs));
+ }
+
+ static get instance(): AppContext {
+ return assertExists(AppContext._instance);
+ }
+
// This constructor is invoked only once, when frontend/index.ts invokes
// AppMainImpl.initialize().
- constructor(initArgs: AppInitArgs) {
+ private constructor(initArgs: AppInitArgs) {
this.initArgs = initArgs;
this.initialRouteArgs = initArgs.initialRouteArgs;
- this.sidebarMgr = new SidebarManagerImpl({
- sidebarEnabled: !this.initialRouteArgs.hideSidebar,
- });
this.serviceWorkerController = new ServiceWorkerController();
this.embeddedMode = this.initialRouteArgs.mode === 'embedded';
this.testingMode =
self.location !== undefined &&
self.location.search.indexOf('testing=1') >= 0;
+ this.sidebarMgr = new SidebarManagerImpl({
+ disabled: this.embeddedMode,
+ hidden: this.initialRouteArgs.hideSidebar,
+ });
this.analytics = initAnalytics(this.testingMode, this.embeddedMode);
this.pluginMgr = new PluginManagerImpl({
forkForPlugin: (pluginId) => this.forPlugin(pluginId),
@@ -143,19 +155,16 @@
private readonly appCtx: AppContext;
private readonly pageMgrProxy: PageManagerImpl;
+ // Invoked by frontend/index.ts.
+ static initialize(args: AppInitArgs) {
+ AppContext.initialize(args).forPlugin(CORE_PLUGIN_ID);
+ }
+
// Gets access to the one instance that the core can use. Note that this is
// 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 AppContext(args).forPlugin(CORE_PLUGIN_ID);
- }
-
static get instance(): AppImpl {
- return assertExists(AppImpl._instance);
+ return AppContext.instance.forPlugin(CORE_PLUGIN_ID);
}
// Only called by AppContext.forPlugin().
@@ -173,6 +182,10 @@
});
}
+ forPlugin(pluginId: string): AppImpl {
+ return this.appCtx.forPlugin(pluginId);
+ }
+
get commands(): CommandManagerImpl {
return this.appCtx.commandMgr;
}
@@ -236,7 +249,6 @@
}
private async openTrace(src: TraceSource) {
- assertTrue(this.pluginId === CORE_PLUGIN_ID);
this.appCtx.closeCurrentTrace();
this.appCtx.isLoadingTrace = true;
try {
@@ -284,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/default_plugins.ts b/ui/src/core/default_plugins.ts
index d0ee370..4791e8e 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -56,6 +56,7 @@
'dev.perfetto.ProcessSummary',
'dev.perfetto.ProcessThreadGroups',
'dev.perfetto.QueryPage',
+ 'dev.perfetto.RecordTrace',
'dev.perfetto.RestorePinnedTrack',
'dev.perfetto.Sched',
'dev.perfetto.Screenshots',
diff --git a/ui/src/core/feature_flags.ts b/ui/src/core/feature_flags.ts
index 68ce511..4e60a61 100644
--- a/ui/src/core/feature_flags.ts
+++ b/ui/src/core/feature_flags.ts
@@ -202,10 +202,3 @@
export const FlagsForTesting = Flags;
export const featureFlags = new Flags(new LocalStorageStore());
-
-export const RECORDING_V2_FLAG = featureFlags.register({
- id: 'recordingv2',
- name: 'Recording V2',
- description: 'Record using V2 interface',
- defaultValue: false,
-});
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/sidebar_manager.ts b/ui/src/core/sidebar_manager.ts
index 11c12dd..9de9b90 100644
--- a/ui/src/core/sidebar_manager.ts
+++ b/ui/src/core/sidebar_manager.ts
@@ -27,9 +27,9 @@
readonly menuItems = new Registry<SidebarMenuItemInternal>((m) => m.id);
- constructor(args: {sidebarEnabled: boolean}) {
- this.enabled = args.sidebarEnabled;
- this._visible = args.sidebarEnabled;
+ constructor(args: {disabled?: boolean; hidden?: boolean}) {
+ this.enabled = !args.disabled;
+ this._visible = !args.hidden;
}
addMenuItem(item: SidebarMenuItem): Disposable {
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 b3ab662..abed7f5 100644
--- a/ui/src/core/trace_impl.ts
+++ b/ui/src/core/trace_impl.ts
@@ -48,6 +48,9 @@
import {PageManagerImpl} from './page_manager';
import {FeatureFlagManager, FlagSettings} from '../public/feature_flag';
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
@@ -423,6 +426,18 @@
this.appImpl.navigate(newHash);
}
+ openTraceFromFile(file: File): void {
+ this.appImpl.openTraceFromFile(file);
+ }
+
+ openTraceFromUrl(url: string, serializedAppState?: SerializedAppState) {
+ this.appImpl.openTraceFromUrl(url, serializedAppState);
+ }
+
+ openTraceFromBuffer(args: PostedTrace): void {
+ this.appImpl.openTraceFromBuffer(args);
+ }
+
addEventListener<T extends keyof EventListeners>(
event: T,
callback: EventListeners[T],
@@ -446,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/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index affbe70..99d4157 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -14,9 +14,7 @@
import m from 'mithril';
import {ErrorDetails} from '../base/logging';
-import {EXTENSION_URL} from '../common/recordingV2/recording_utils';
import {GcsUploader} from '../base/gcs_uploader';
-import {RECORDING_V2_FLAG} from '../core/feature_flags';
import {raf} from '../core/raf_scheduler';
import {VERSION} from '../gen/perfetto_version';
import {getCurrentModalKey, showModal} from '../widgets/modal';
@@ -47,22 +45,20 @@
return;
}
- if (!RECORDING_V2_FLAG.get()) {
- if (err.message.includes('Unable to claim interface')) {
- showWebUSBError();
- timeLastReport = now;
- return;
- }
+ if (err.message.includes('Unable to claim interface')) {
+ showWebUSBError();
+ timeLastReport = now;
+ return;
+ }
- if (
- err.message.includes('A transfer error has occurred') ||
- err.message.includes('The device was disconnected') ||
- err.message.includes('The transfer was cancelled')
- ) {
- showConnectionLostError();
- timeLastReport = now;
- return;
- }
+ if (
+ err.message.includes('A transfer error has occurred') ||
+ err.message.includes('The device was disconnected') ||
+ err.message.includes('The transfer was cancelled')
+ ) {
+ showConnectionLostError();
+ timeLastReport = now;
+ return;
}
if (err.message.includes('(ERR:fmt)')) {
@@ -356,135 +352,6 @@
});
}
-export function showWebUSBErrorV2() {
- showModal({
- title: 'A WebUSB error occurred',
- content: m(
- 'div',
- m(
- 'span',
- `Is adb already running on the host? Run this command and
- try again.`,
- ),
- m('br'),
- m('.modal-bash', '> adb kill-server'),
- m('br'),
- // The statement below covers the following edge case:
- // 1. 'adb server' is running on the device.
- // 2. The user selects the new Android target, so we try to fetch the
- // OS version and do QSS.
- // 3. The error modal is shown.
- // 4. The user runs 'adb kill-server'.
- // At this point we don't have a trigger to try fetching the OS version
- // + QSS again. Therefore, the user will need to refresh the page.
- m(
- 'span',
- "If after running 'adb kill-server', you don't see " +
- "a 'Start Recording' button on the page and you don't see " +
- "'Allow USB debugging' on the device, " +
- 'you will need to reload this page.',
- ),
- m('br'),
- m('br'),
- m('span', 'For details see '),
- m('a', {href: 'http://b/159048331', target: '_blank'}, 'b/159048331'),
- ),
- });
-}
-
-export function showConnectionLostError(): void {
- showModal({
- title: 'Connection with the ADB device lost',
- content: m(
- 'div',
- m('span', `Please connect the device again to restart the recording.`),
- m('br'),
- ),
- });
-}
-
-export function showAllowUSBDebugging(): void {
- showModal({
- title: 'Could not connect to the device',
- content: m(
- 'div',
- m('span', 'Please allow USB debugging on the device.'),
- m('br'),
- ),
- });
-}
-
-export function showNoDeviceSelected(): void {
- showModal({
- title: 'No device was selected for recording',
- content: m(
- 'div',
- m(
- 'span',
- `If you want to connect to an ADB device,
- please select it from the list.`,
- ),
- m('br'),
- ),
- });
-}
-
-export function showExtensionNotInstalled(): void {
- showModal({
- title: 'Perfetto Chrome extension not installed',
- content: m(
- 'div',
- m(
- '.note',
- `To trace Chrome from the Perfetto UI, you need to install our `,
- m('a', {href: EXTENSION_URL, target: '_blank'}, 'Chrome extension'),
- ' and then reload this page.',
- ),
- m('br'),
- ),
- });
-}
-
-export function showWebsocketConnectionIssue(message: string): void {
- showModal({
- title: 'Unable to connect to the device via websocket',
- content: m(
- 'div',
- m('div', 'trace_processor_shell --httpd is unreachable or crashed.'),
- m('pre', message),
- ),
- });
-}
-
-export function showIssueParsingTheTracedResponse(message: string): void {
- showModal({
- title:
- 'A problem was encountered while connecting to' +
- ' the Perfetto tracing service',
- content: m('div', m('span', message), m('br')),
- });
-}
-
-export function showFailedToPushBinary(message: string): void {
- showModal({
- title: 'Failed to push a binary to the device',
- content: m(
- 'div',
- m(
- 'span',
- 'This can happen if your Android device has an OS version lower ' +
- 'than Q. Perfetto tried to push the latest version of its ' +
- 'embedded binary but failed.',
- ),
- m('br'),
- m('br'),
- m('span', 'Error message:'),
- m('br'),
- m('span', message),
- ),
- });
-}
-
function showRpcSequencingError() {
showModal({
title: 'A TraceProcessor RPC error occurred',
@@ -534,3 +401,25 @@
],
});
}
+
+function showWebsocketConnectionIssue(message: string): void {
+ showModal({
+ title: 'Unable to connect to the device via websocket',
+ content: m(
+ 'div',
+ m('div', 'trace_processor_shell --httpd is unreachable or crashed.'),
+ m('pre', message),
+ ),
+ });
+}
+
+function showConnectionLostError(): void {
+ showModal({
+ title: 'Connection with the ADB device lost',
+ content: m(
+ 'div',
+ m('span', `Please connect the device again to restart the recording.`),
+ m('br'),
+ ),
+ });
+}
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 198f08e..4c87e4d 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -20,7 +20,7 @@
import m from 'mithril';
import {defer} from '../base/deferred';
import {addErrorHandler, reportError} from '../base/logging';
-import {RECORDING_V2_FLAG, featureFlags} from '../core/feature_flags';
+import {featureFlags} from '../core/feature_flags';
import {initLiveReload} from '../core/live_reload';
import {raf} from '../core/raf_scheduler';
import {initWasm} from '../trace_processor/wasm_engine_proxy';
@@ -33,8 +33,6 @@
import {globals} from './globals';
import {HomePage} from './home_page';
import {postMessageHandler} from './post_message_handler';
-import {RecordPage} from './record_page';
-import {RecordPageV2} from './record_page_v2';
import {Route, Router} from '../core/router';
import {CheckHttpRpcConnection} from './rpc_http_dialog';
import {maybeOpenTraceFromRoute} from './trace_url_handler';
@@ -65,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() {
@@ -224,18 +221,16 @@
const pages = AppImpl.instance.pages;
const traceless = true;
pages.registerPage({route: '/', traceless, page: HomePage});
- const recordPage = RECORDING_V2_FLAG.get() ? RecordPageV2 : RecordPage;
- pages.registerPage({route: '/record', traceless, page: recordPage});
pages.registerPage({route: '/viewer', page: ViewerPage});
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/sidebar.ts b/ui/src/frontend/sidebar.ts
index 9f71bcc..8ece2d3 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -14,7 +14,7 @@
import m from 'mithril';
import {getCurrentChannel} from '../core/channels';
-import {TRACE_SUFFIX} from '../common/constants';
+import {TRACE_SUFFIX} from '../public/trace';
import {
disableMetatracingAndGetTrace,
enableMetatracing,
@@ -554,13 +554,6 @@
// TODO(primiano): The Open file / Open with legacy entries are registered by
// the 'perfetto.CoreCommands' plugins. Make things consistent.
app.sidebar.addMenuItem({
- section: 'navigation',
- text: 'Record new trace',
- href: '#!/record',
- icon: 'fiber_smart_record',
- sortOrder: 2,
- });
- app.sidebar.addMenuItem({
section: 'support',
text: 'Keyboard shortcuts',
action: toggleHelp,
diff --git a/ui/src/frontend/sql_table_tab.ts b/ui/src/frontend/sql_table_tab.ts
index 40f2012..b803148 100644
--- a/ui/src/frontend/sql_table_tab.ts
+++ b/ui/src/frontend/sql_table_tab.ts
@@ -28,6 +28,12 @@
import {MenuItem, PopupMenu2} from '../widgets/menu';
import {addEphemeralTab} from '../common/add_ephemeral_tab';
import {Tab} from '../public/tab';
+import {addChartTab} from './widgets/charts/chart_tab';
+import {
+ ChartOption,
+ createChartConfigFromSqlTableState,
+} from './widgets/charts/chart';
+import {AddChartMenuItem} from './widgets/charts/add_chart_menu';
export interface AddSqlTableTabParams {
table: SqlTableDescription;
@@ -122,6 +128,16 @@
},
m(SqlTable, {
state: this.state,
+ addColumnMenuItems: (column, columnAlias) =>
+ m(AddChartMenuItem, {
+ chartConfig: createChartConfigFromSqlTableState(
+ column,
+ columnAlias,
+ this.state,
+ ),
+ chartOptions: [ChartOption.HISTOGRAM],
+ addChart: (chart) => addChartTab(chart),
+ }),
}),
);
}
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/charts/add_chart_menu.ts b/ui/src/frontend/widgets/charts/add_chart_menu.ts
index 384ed9a..cb3bb17 100644
--- a/ui/src/frontend/widgets/charts/add_chart_menu.ts
+++ b/ui/src/frontend/widgets/charts/add_chart_menu.ts
@@ -15,12 +15,12 @@
import m from 'mithril';
import {MenuItem} from '../../../widgets/menu';
import {Icons} from '../../../base/semantic_icons';
-import {ChartConfig, ChartOption, toTitleCase} from './chart';
+import {Chart, ChartConfig, ChartOption, toTitleCase} from './chart';
interface AddChartMenuItemAttrs {
readonly chartConfig: ChartConfig;
readonly chartOptions: Array<ChartOption>;
- readonly addChart: (option: ChartOption, config: ChartConfig) => void;
+ readonly addChart: (chart: Chart) => void;
}
export class AddChartMenuItem
@@ -29,12 +29,12 @@
private renderAddChartOptions(
config: ChartConfig,
chartOptions: Array<ChartOption>,
- addChart: (option: ChartOption, config: ChartConfig) => void,
+ addChart: (chart: Chart) => void,
): m.Children {
return chartOptions.map((option) => {
return m(MenuItem, {
label: toTitleCase(option),
- onclick: () => addChart(option, config),
+ onclick: () => addChart({option, config}),
});
});
}
diff --git a/ui/src/frontend/widgets/charts/chart.ts b/ui/src/frontend/widgets/charts/chart.ts
index ccbf566..124a52e 100644
--- a/ui/src/frontend/widgets/charts/chart.ts
+++ b/ui/src/frontend/widgets/charts/chart.ts
@@ -16,6 +16,8 @@
import {Engine} from '../../../trace_processor/engine';
import {Filter, TableColumn, TableColumnSet} from '../sql/table/column';
import {Histogram} from './histogram/histogram';
+import {SqlTableState} from '../sql/table/state';
+import {columnTitle} from '../sql/table/table';
export interface VegaLiteChartSpec {
$schema: string;
@@ -58,6 +60,11 @@
readonly aggregationType?: 'nominal' | 'quantitative'; // Aggregation type.
}
+export interface Chart {
+ readonly option: ChartOption;
+ readonly config: ChartConfig;
+}
+
export interface ChartData {
readonly rows: Row[];
readonly error?: string;
@@ -85,11 +92,29 @@
// renderChartComponent will take a chart option and config and map
// to the corresponding chart class component.
-export function renderChartComponent(option: ChartOption, config: ChartConfig) {
- switch (option) {
+export function renderChartComponent(chart: Chart) {
+ switch (chart.option) {
case ChartOption.HISTOGRAM:
- return m(Histogram, config);
+ return m(Histogram, chart.config);
default:
return;
}
}
+
+export function createChartConfigFromSqlTableState(
+ column: TableColumn,
+ columnAlias: string,
+ sqlTableState: SqlTableState,
+) {
+ return {
+ engine: sqlTableState.trace.engine,
+ columnTitle: columnTitle(column),
+ sqlColumn: [columnAlias],
+ filters: sqlTableState?.getFilters(),
+ tableDisplay: sqlTableState.config.displayName ?? sqlTableState.config.name,
+ query: sqlTableState.getSqlQuery(
+ Object.fromEntries([[columnAlias, column.primaryColumn()]]),
+ ),
+ aggregationType: column.aggregation?.().dataType,
+ };
+}
diff --git a/ui/src/frontend/widgets/charts/chart_tab.ts b/ui/src/frontend/widgets/charts/chart_tab.ts
index 0dcc6d9..6d802e6 100644
--- a/ui/src/frontend/widgets/charts/chart_tab.ts
+++ b/ui/src/frontend/widgets/charts/chart_tab.ts
@@ -17,25 +17,14 @@
import {filterTitle} from '../sql/table/column';
import {addEphemeralTab} from '../../../common/add_ephemeral_tab';
import {Tab} from '../../../public/tab';
-import {
- ChartConfig,
- ChartOption,
- renderChartComponent,
- toTitleCase,
-} from './chart';
+import {Chart, renderChartComponent, toTitleCase} from './chart';
-export function addChartTab(
- chartOption: ChartOption,
- chartConfig: ChartConfig,
-): void {
- addEphemeralTab('histogramTab', new ChartTab(chartOption, chartConfig));
+export function addChartTab(chart: Chart): void {
+ addEphemeralTab('histogramTab', new ChartTab(chart));
}
export class ChartTab implements Tab {
- constructor(
- private readonly chartOption: ChartOption,
- private readonly chartConfig: ChartConfig,
- ) {}
+ constructor(private readonly chart: Chart) {}
render() {
return m(
@@ -44,20 +33,20 @@
title: this.getTitle(),
description: this.getDescription(),
},
- renderChartComponent(this.chartOption, this.chartConfig),
+ renderChartComponent(this.chart),
);
}
getTitle(): string {
- return `${toTitleCase(this.chartConfig.columnTitle)} Histogram`;
+ return `${toTitleCase(this.chart.config.columnTitle)} Histogram`;
}
private getDescription(): string {
- let desc = `Count distribution for ${this.chartConfig.tableDisplay ?? ''} table`;
+ let desc = `Count distribution for ${this.chart.config.tableDisplay ?? ''} table`;
- if (this.chartConfig.filters && this.chartConfig.filters.length > 0) {
+ if (this.chart.config.filters && this.chart.config.filters.length > 0) {
desc += ' where ';
- desc += this.chartConfig.filters.map((f) => filterTitle(f)).join(', ');
+ desc += this.chart.config.filters.map((f) => filterTitle(f)).join(', ');
}
return desc;
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/frontend/widgets/sql/table/table.ts b/ui/src/frontend/widgets/sql/table/table.ts
index 28cc81c..761a32c 100644
--- a/ui/src/frontend/widgets/sql/table/table.ts
+++ b/ui/src/frontend/widgets/sql/table/table.ts
@@ -40,16 +40,20 @@
import {SqlTableState} from './state';
import {SqlTableDescription} from './table_description';
import {Intent} from '../../../../widgets/common';
-import {addChartTab} from '../../charts/chart_tab';
import {Form} from '../../../../widgets/form';
import {TextInput} from '../../../../widgets/text_input';
-import {AddChartMenuItem} from '../../charts/add_chart_menu';
-import {ChartConfig, ChartOption} from '../../charts/chart';
export interface SqlTableConfig {
readonly state: SqlTableState;
+ // For additional menu items to add to the column header menus
+ readonly addColumnMenuItems?: (
+ column: TableColumn,
+ columnAlias: string,
+ ) => m.Children;
}
+type AdditionalColumnMenuItems = Record<string, m.Children>;
+
function renderCell(
column: TableColumn,
row: Row,
@@ -276,7 +280,11 @@
);
}
- renderColumnHeader(column: TableColumn, index: number) {
+ renderColumnHeader(
+ column: TableColumn,
+ index: number,
+ additionalColumnHeaderMenuItems?: m.Children,
+ ) {
const sorted = this.state.isSortedBy(column);
const icon =
sorted === 'ASC'
@@ -285,22 +293,6 @@
? Icons.SortedDesc
: Icons.ContextMenu;
- const columnAlias =
- this.state.getCurrentRequest().columns[
- sqlColumnId(column.primaryColumn())
- ];
- const chartConfig: ChartConfig = {
- engine: this.state.trace.engine,
- columnTitle: columnTitle(column),
- sqlColumn: [columnAlias],
- filters: this.state.getFilters(),
- tableDisplay: this.table.displayName ?? this.table.name,
- query: this.state.getSqlQuery(
- Object.fromEntries([[columnAlias, column.primaryColumn()]]),
- ),
- aggregationType: column.aggregation?.().dataType,
- };
-
return m(
PopupMenu2,
{
@@ -345,11 +337,7 @@
{label: 'Add filter', icon: Icons.Filter},
this.renderColumnFilterOptions(column),
),
- m(AddChartMenuItem, {
- chartConfig,
- chartOptions: [ChartOption.HISTOGRAM],
- addChart: (option, config) => addChartTab(option, config),
- }),
+ additionalColumnHeaderMenuItems,
// Menu items before divider apply to selected column
m(MenuDivider),
// Menu items after divider apply to entire table
@@ -357,13 +345,49 @@
);
}
- view() {
+ getAdditionalColumnMenuItems(
+ addColumnMenuItems?: (
+ column: TableColumn,
+ columnAlias: string,
+ ) => m.Children,
+ ) {
+ if (addColumnMenuItems === undefined) return;
+
+ const additionalColumnMenuItems: AdditionalColumnMenuItems = {};
+ this.state.getSelectedColumns().forEach((column) => {
+ const columnAlias =
+ this.state.getCurrentRequest().columns[
+ sqlColumnId(column.primaryColumn())
+ ];
+
+ additionalColumnMenuItems[columnAlias] = addColumnMenuItems(
+ column,
+ columnAlias,
+ );
+ });
+
+ return additionalColumnMenuItems;
+ }
+
+ view({attrs}: m.Vnode<SqlTableConfig>) {
const rows = this.state.getDisplayedRows();
+ const additionalColumnMenuItems = this.getAdditionalColumnMenuItems(
+ attrs.addColumnMenuItems,
+ );
const columns = this.state.getSelectedColumns();
const columnDescriptors = columns.map((column, i) => {
return {
- title: this.renderColumnHeader(column, i),
+ title: this.renderColumnHeader(
+ column,
+ i,
+ additionalColumnMenuItems &&
+ additionalColumnMenuItems[
+ this.state.getCurrentRequest().columns[
+ sqlColumnId(column.primaryColumn())
+ ]
+ ],
+ ),
render: (row: Row) => renderCell(column, row, this.state),
};
});
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts b/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
index 5b1d0d4..1c60d4f 100644
--- a/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
@@ -33,8 +33,15 @@
import {Button} from '../../widgets/button';
import {Icons} from '../../base/semantic_icons';
import {DetailsShell} from '../../widgets/details_shell';
+import {
+ Chart,
+ ChartOption,
+ createChartConfigFromSqlTableState,
+ renderChartComponent,
+} from '../../frontend/widgets/charts/chart';
+import {AddChartMenuItem} from '../../frontend/widgets/charts/add_chart_menu';
-interface ExplorePageState {
+interface ExploreTableState {
sqlTableState?: SqlTableState;
selectedTable?: ExplorableTable;
}
@@ -46,13 +53,12 @@
}
export class ExplorePage implements m.ClassComponent<PageWithTraceAttrs> {
- private readonly state: ExplorePageState;
+ private readonly state: ExploreTableState;
+ private readonly charts: Chart[];
constructor() {
- this.state = {
- sqlTableState: undefined,
- selectedTable: undefined,
- };
+ this.charts = [];
+ this.state = {};
}
// Show menu with standard library tables
@@ -115,7 +121,7 @@
this.state.selectedTable = table;
- const sqlTableState = new SqlTableState(
+ this.state.sqlTableState = new SqlTableState(
trace,
{
name: table.name,
@@ -123,7 +129,6 @@
},
{imports: [table.module]},
);
- this.state.sqlTableState = sqlTableState;
},
});
});
@@ -162,6 +167,16 @@
},
m(SqlTable, {
state: sqlTableState,
+ addColumnMenuItems: (column, columnAlias) =>
+ m(AddChartMenuItem, {
+ chartConfig: createChartConfigFromSqlTableState(
+ column,
+ columnAlias,
+ sqlTableState,
+ ),
+ chartOptions: [ChartOption.HISTOGRAM],
+ addChart: (chart) => this.charts.push(chart),
+ }),
}),
);
}
@@ -170,6 +185,7 @@
return m(
'.explore-page',
m(Menu, this.renderSelectableTablesMenuItems(attrs.trace)),
+ this.charts.map((chart) => renderChartComponent(chart)),
this.state.selectedTable && this.renderSqlTable(),
);
}
diff --git a/ui/src/controller/adb.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb.ts
similarity index 98%
rename from ui/src/controller/adb.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb.ts
index e188ea7..5197d23 100644
--- a/ui/src/controller/adb.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb.ts
@@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {assertExists} from '../base/logging';
-import {isString} from '../base/object_utils';
-import {utf8Decode, utf8Encode} from '../base/string_utils';
+import {assertExists} from '../../base/logging';
+import {isString} from '../../base/object_utils';
+import {utf8Decode, utf8Encode} from '../../base/string_utils';
import {Adb, AdbMsg, AdbStream, CmdType} from './adb_interfaces';
export const VERSION_WITH_CHECKSUM = 0x01000000;
diff --git a/ui/src/controller/adb_base_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_base_controller.ts
similarity index 96%
rename from ui/src/controller/adb_base_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_base_controller.ts
index 2a72a33..c447df5 100644
--- a/ui/src/controller/adb_base_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_base_controller.ts
@@ -12,12 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {exists} from '../base/utils';
-import {RecordingState, RecordingTarget, isAdbTarget} from '../common/state';
+import {exists} from '../../base/utils';
+import {RecordingState, RecordingTarget, isAdbTarget} from './state';
import {
extractDurationFromTraceConfig,
extractTraceConfig,
-} from '../core/trace_config_utils';
+} from './trace_config_utils';
import {Adb} from './adb_interfaces';
import {ReadBuffersResponse} from './consumer_port_types';
import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
diff --git a/ui/src/controller/adb_interfaces.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_interfaces.ts
similarity index 100%
rename from ui/src/controller/adb_interfaces.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_interfaces.ts
diff --git a/ui/src/controller/adb_jsdomtest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_jsdomtest.ts
similarity index 97%
rename from ui/src/controller/adb_jsdomtest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_jsdomtest.ts
index 9f51a97..1d228a5 100644
--- a/ui/src/controller/adb_jsdomtest.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_jsdomtest.ts
@@ -19,7 +19,7 @@
DEFAULT_MAX_PAYLOAD_BYTES,
VERSION_WITH_CHECKSUM,
} from './adb';
-import {utf8Encode} from '../base/string_utils';
+import {utf8Encode} from '../../base/string_utils';
test('startAuthentication', async () => {
const adb = new AdbOverWebUsb();
diff --git a/ui/src/controller/adb_record_controller_jsdomtest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_record_controller_jsdomtest.ts
similarity index 95%
rename from ui/src/controller/adb_record_controller_jsdomtest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_record_controller_jsdomtest.ts
index e404397..6078a59 100644
--- a/ui/src/controller/adb_record_controller_jsdomtest.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_record_controller_jsdomtest.ts
@@ -13,12 +13,12 @@
// limitations under the License.
import {dingus} from 'dingusjs';
-import {utf8Encode} from '../base/string_utils';
-import {EnableTracingRequest, TraceConfig} from '../protos';
+import {utf8Encode} from '../../base/string_utils';
+import {EnableTracingRequest, TraceConfig} from '../../protos';
import {AdbStream, MockAdb, MockAdbStream} from './adb_interfaces';
import {AdbConsumerPort} from './adb_shell_controller';
import {Consumer} from './record_controller_interfaces';
-import {createEmptyState} from '../common/empty_state';
+import {createEmptyState} from './empty_state';
function generateMockConsumer(): Consumer {
return {
diff --git a/ui/src/controller/adb_shell_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_shell_controller.ts
similarity index 96%
rename from ui/src/controller/adb_shell_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_shell_controller.ts
index 5d1d156..623dc5d 100644
--- a/ui/src/controller/adb_shell_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_shell_controller.ts
@@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {base64Encode, utf8Decode} from '../base/string_utils';
-import {RecordingState} from '../common/state';
-import {extractTraceConfig} from '../core/trace_config_utils';
+import {base64Encode, utf8Decode} from '../../base/string_utils';
+import {RecordingState} from './state';
+import {extractTraceConfig} from './trace_config_utils';
import {AdbBaseConsumerPort, AdbConnectionState} from './adb_base_controller';
import {Adb, AdbStream} from './adb_interfaces';
import {ReadBuffersResponse} from './consumer_port_types';
diff --git a/ui/src/controller/adb_socket_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_socket_controller.ts
similarity index 98%
rename from ui/src/controller/adb_socket_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_socket_controller.ts
index 715be0d..a676747 100644
--- a/ui/src/controller/adb_socket_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_socket_controller.ts
@@ -20,14 +20,14 @@
GetTraceStatsResponse,
IPCFrame,
ReadBuffersResponse,
-} from '../protos';
+} from '../../protos';
import {AdbBaseConsumerPort, AdbConnectionState} from './adb_base_controller';
import {Adb, AdbStream} from './adb_interfaces';
import {isReadBuffersResponse} from './consumer_port_types';
import {Consumer} from './record_controller_interfaces';
-import {exists} from '../base/utils';
-import {assertTrue} from '../base/logging';
-import {RecordingState} from '../common/state';
+import {exists} from '../../base/utils';
+import {assertTrue} from '../../base/logging';
+import {RecordingState} from './state';
enum SocketState {
DISCONNECTED,
diff --git a/ui/src/frontend/recording/advanced_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/advanced_settings.ts
similarity index 97%
rename from ui/src/frontend/recording/advanced_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/advanced_settings.ts
index d762338..35e6fe2 100644
--- a/ui/src/frontend/recording/advanced_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/advanced_settings.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import m from 'mithril';
-import {Dropdown, Probe, Slider, Textarea, Toggle} from '../record_widgets';
+import {Dropdown, Probe, Slider, Textarea, Toggle} from './record_widgets';
import {RecordingSectionAttrs} from './recording_sections';
const FTRACE_CATEGORIES = new Map<string, string>();
diff --git a/ui/src/frontend/recording/android_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/android_settings.ts
similarity index 98%
rename from ui/src/frontend/recording/android_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/android_settings.ts
index 4154fa7..7c0d741 100644
--- a/ui/src/frontend/recording/android_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/android_settings.ts
@@ -14,9 +14,9 @@
import m from 'mithril';
import {AtomId, DataSourceDescriptor} from '../../protos';
-import {Dropdown, Probe, Slider, Textarea, Toggle} from '../record_widgets';
+import {Dropdown, Probe, Slider, Textarea, Toggle} from './record_widgets';
import {RecordingSectionAttrs} from './recording_sections';
-import {RecordConfig} from '../../controller/record_config_types';
+import {RecordConfig} from './record_config_types';
const PUSH_ATOM_IDS = new Map<string, string>();
const PULL_ATOM_IDS = new Map<string, string>();
diff --git a/ui/src/controller/chrome_proxy_record_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_proxy_record_controller.ts
similarity index 96%
rename from ui/src/controller/chrome_proxy_record_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/chrome_proxy_record_controller.ts
index d1e1b63..ef0b999 100644
--- a/ui/src/controller/chrome_proxy_record_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_proxy_record_controller.ts
@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {binaryDecode, binaryEncode} from '../base/string_utils';
-import {TRACE_SUFFIX} from '../common/constants';
+import {binaryDecode, binaryEncode} from '../../base/string_utils';
+import {TRACE_SUFFIX} from '../../public/trace';
import {
ConsumerPortResponse,
hasProperty,
diff --git a/ui/src/frontend/recording/chrome_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_settings.ts
similarity index 97%
rename from ui/src/frontend/recording/chrome_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/chrome_settings.ts
index 8ac14cd..fd09d82 100644
--- a/ui/src/frontend/recording/chrome_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_settings.ts
@@ -13,19 +13,19 @@
// limitations under the License.
import m from 'mithril';
-import {DataSource} from '../../common/recordingV2/recording_interfaces_v2';
+import {DataSource} from './recordingV2/recording_interfaces_v2';
import {
RecordingState,
getBuiltinChromeCategoryList,
isChromeTarget,
-} from '../../common/state';
+} from './state';
import {
MultiSelect,
MultiSelectDiff,
Option as MultiSelectOption,
} from '../../widgets/multiselect';
import {Section} from '../../widgets/section';
-import {CategoryGetter, CompactProbe, Toggle} from '../record_widgets';
+import {CategoryGetter, CompactProbe, Toggle} from './record_widgets';
import {RecordingSectionAttrs} from './recording_sections';
function extractChromeCategories(
diff --git a/ui/src/controller/consumer_port_types.ts b/ui/src/plugins/dev.perfetto.RecordTrace/consumer_port_types.ts
similarity index 98%
rename from ui/src/controller/consumer_port_types.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/consumer_port_types.ts
index 973205f..732e9e8 100644
--- a/ui/src/controller/consumer_port_types.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/consumer_port_types.ts
@@ -18,7 +18,7 @@
IFreeBuffersResponse,
IGetTraceStatsResponse,
IReadBuffersResponse,
-} from '../protos';
+} from '../../protos';
export interface Typed {
type: string;
diff --git a/ui/src/frontend/recording/cpu_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/cpu_settings.ts
similarity index 97%
rename from ui/src/frontend/recording/cpu_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/cpu_settings.ts
index ba12267..06b2713 100644
--- a/ui/src/frontend/recording/cpu_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/cpu_settings.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import m from 'mithril';
-import {Probe, Slider} from '../record_widgets';
+import {Probe, Slider} from './record_widgets';
import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
export class CpuSettings implements m.ClassComponent<RecordingSectionAttrs> {
diff --git a/ui/src/common/empty_state.ts b/ui/src/plugins/dev.perfetto.RecordTrace/empty_state.ts
similarity index 92%
rename from ui/src/common/empty_state.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/empty_state.ts
index c356b00..bfafe3f 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/empty_state.ts
@@ -12,10 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {
- autosaveConfigStore,
- recordTargetStore,
-} from '../frontend/record_config';
+import {autosaveConfigStore, recordTargetStore} from './record_config';
import {RecordingState} from './state';
export function createEmptyState(): RecordingState {
diff --git a/ui/src/frontend/recording/etw_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/etw_settings.ts
similarity index 96%
rename from ui/src/frontend/recording/etw_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/etw_settings.ts
index 70c1ab1..eefb8ac 100644
--- a/ui/src/frontend/recording/etw_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/etw_settings.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import m from 'mithril';
-import {Probe} from '../record_widgets';
+import {Probe} from './record_widgets';
import {RecordingSectionAttrs} from './recording_sections';
export class EtwSettings implements m.ClassComponent<RecordingSectionAttrs> {
diff --git a/ui/src/frontend/recording/gpu_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/gpu_settings.ts
similarity index 97%
rename from ui/src/frontend/recording/gpu_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/gpu_settings.ts
index 745af99..1040f75 100644
--- a/ui/src/frontend/recording/gpu_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/gpu_settings.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import m from 'mithril';
-import {Probe} from '../record_widgets';
+import {Probe} from './record_widgets';
import {RecordingSectionAttrs} from './recording_sections';
export class GpuSettings implements m.ClassComponent<RecordingSectionAttrs> {
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/index.ts b/ui/src/plugins/dev.perfetto.RecordTrace/index.ts
new file mode 100644
index 0000000..e0c5a1f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/index.ts
@@ -0,0 +1,56 @@
+// 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 {RecordPage} from './record_page';
+import {RecordPageV2} from './record_page_v2';
+import {App} from '../../public/app';
+import {PerfettoPlugin} from '../../public/plugin';
+import {RecordingPageController} from './recordingV2/recording_page_controller';
+import {RecordingManager} from './recording_manager';
+import {PageAttrs} from '../../public/page';
+import {bindMithrilAttrs} from '../../base/mithril_utils';
+
+export default class implements PerfettoPlugin {
+ static readonly id = 'dev.perfetto.RecordTrace';
+
+ static onActivate(app: App) {
+ app.sidebar.addMenuItem({
+ section: 'navigation',
+ text: 'Record new trace',
+ href: '#!/record',
+ icon: 'fiber_smart_record',
+ sortOrder: 2,
+ });
+
+ const RECORDING_V2_FLAG = app.featureFlags.register({
+ id: 'recordingv2',
+ name: 'Recording V2',
+ description: 'Record using V2 interface',
+ defaultValue: false,
+ });
+ const useRecordingV2 = RECORDING_V2_FLAG.get();
+
+ const recMgr = new RecordingManager(app, useRecordingV2);
+ let page: m.ClassComponent<PageAttrs>;
+ if (useRecordingV2) {
+ const recCtl = new RecordingPageController(app, recMgr);
+ recCtl.initFactories();
+ page = bindMithrilAttrs(RecordPageV2, {app, recCtl, recMgr});
+ } else {
+ page = bindMithrilAttrs(RecordPage, {app, recMgr});
+ }
+ app.pages.registerPage({route: '/record', traceless: true, page});
+ }
+}
diff --git a/ui/src/frontend/recording/linux_perf_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/linux_perf_settings.ts
similarity index 96%
rename from ui/src/frontend/recording/linux_perf_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/linux_perf_settings.ts
index 7f9c6d4..a0fcf9f 100644
--- a/ui/src/frontend/recording/linux_perf_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/linux_perf_settings.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import m from 'mithril';
-import {Probe, Slider, Textarea} from '../record_widgets';
+import {Probe, Slider, Textarea} from './record_widgets';
import {RecordingSectionAttrs} from './recording_sections';
const PLACEHOLDER_TEXT = `Filters for processes to profile, one per line e.g.:
diff --git a/ui/src/frontend/recording/memory_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/memory_settings.ts
similarity index 98%
rename from ui/src/frontend/recording/memory_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/memory_settings.ts
index ccfa9ef..231306f 100644
--- a/ui/src/frontend/recording/memory_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/memory_settings.ts
@@ -14,7 +14,7 @@
import m from 'mithril';
import {MeminfoCounters, VmstatCounters} from '../../protos';
-import {Dropdown, Probe, Slider, Textarea, Toggle} from '../record_widgets';
+import {Dropdown, Probe, Slider, Textarea, Toggle} from './record_widgets';
import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
class HeapSettings implements m.ClassComponent<RecordingSectionAttrs> {
diff --git a/ui/src/frontend/recording/power_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/power_settings.ts
similarity index 93%
rename from ui/src/frontend/recording/power_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/power_settings.ts
index be2f6ed..bf88217 100644
--- a/ui/src/frontend/recording/power_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/power_settings.ts
@@ -13,8 +13,8 @@
// limitations under the License.
import m from 'mithril';
-import {globals} from '../globals';
-import {Probe, Slider} from '../record_widgets';
+import {globals} from '../../frontend/globals';
+import {Probe, Slider} from './record_widgets';
import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
export class PowerSettings implements m.ClassComponent<RecordingSectionAttrs> {
@@ -34,6 +34,7 @@
m('span', ')'),
),
];
+ // TODO(primiano): figure out a better story for isInternalUser.
if (globals.isInternalUser) {
descr.push(
m(
diff --git a/ui/src/frontend/record_config.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_config.ts
similarity index 97%
rename from ui/src/frontend/record_config.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_config.ts
index f3bf3c5..ae41d9c 100644
--- a/ui/src/frontend/record_config.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_config.ts
@@ -12,15 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {exists} from '../base/utils';
-import {getDefaultRecordingTargets, RecordingTarget} from '../common/state';
+import {exists} from '../../base/utils';
+import {getDefaultRecordingTargets, RecordingTarget} from './state';
import {
createEmptyRecordConfig,
NamedRecordConfig,
NAMED_RECORD_CONFIG_SCHEMA,
RecordConfig,
RECORD_CONFIG_SCHEMA,
-} from '../controller/record_config_types';
+} from './record_config_types';
const LOCAL_STORAGE_RECORD_CONFIGS_KEY = 'recordConfigs';
const LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY = 'autosaveConfig';
diff --git a/ui/src/controller/record_config_types.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_config_types.ts
similarity index 100%
rename from ui/src/controller/record_config_types.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_config_types.ts
diff --git a/ui/src/controller/record_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
similarity index 95%
rename from ui/src/controller/record_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
index 1bfa074..8062650 100644
--- a/ui/src/controller/record_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
@@ -13,19 +13,19 @@
// limitations under the License.
import {Message, Method, rpc, RPCImplCallback} from 'protobufjs';
-import {isString} from '../base/object_utils';
-import {base64Encode} from '../base/string_utils';
-import {TRACE_SUFFIX} from '../common/constants';
-import {genTraceConfig} from '../common/recordingV2/recording_config_utils';
-import {TargetInfo} from '../common/recordingV2/recording_interfaces_v2';
+import {isString} from '../../base/object_utils';
+import {base64Encode} from '../../base/string_utils';
+import {TRACE_SUFFIX} from '../../public/trace';
+import {genTraceConfig} from './recordingV2/recording_config_utils';
+import {TargetInfo} from './recordingV2/recording_interfaces_v2';
import {
AdbRecordingTarget,
isAdbTarget,
isChromeTarget,
isWindowsTarget,
RecordingTarget,
-} from '../common/state';
-import {ConsumerPort, TraceConfig} from '../protos';
+} from './state';
+import {ConsumerPort, TraceConfig} from '../../protos';
import {AdbOverWebUsb} from './adb';
import {AdbConsumerPort} from './adb_shell_controller';
import {AdbSocketConsumerPort} from './adb_socket_controller';
@@ -41,9 +41,9 @@
} from './consumer_port_types';
import {RecordConfig} from './record_config_types';
import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
-import {AppImpl} from '../core/app_impl';
import {RecordingManager} from './recording_manager';
-import {raf} from '../core/raf_scheduler';
+import {scheduleFullRedraw} from '../../widgets/raf';
+import {App} from '../../public/app';
type RPCImplMethod = Method | rpc.ServiceMethod<Message<{}>, Message<{}>>;
@@ -189,6 +189,7 @@
}
export class RecordController implements Consumer {
+ private app: App;
private recMgr: RecordingManager;
private config: RecordConfig | null = null;
private readonly extensionPort: MessagePort;
@@ -206,7 +207,8 @@
// char, it is the 'targetOS'
private controllerPromises = new Map<string, Promise<RpcConsumerPort>>();
- constructor(recMgr: RecordingManager, extensionPort: MessagePort) {
+ constructor(app: App, recMgr: RecordingManager, extensionPort: MessagePort) {
+ this.app = app;
this.recMgr = recMgr;
this.consumerPort = ConsumerPort.create(this.rpcImpl.bind(this));
this.extensionPort = extensionPort;
@@ -219,7 +221,7 @@
refreshOnStateChange() {
// TODO(eseckler): Use ConsumerPort's QueryServiceState instead
// of posting a custom extension message to retrieve the category list.
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
if (this.state.fetchChromeCategories && !this.fetchedCategories) {
this.fetchedCategories = true;
if (this.state.extensionInstalled) {
@@ -320,7 +322,7 @@
return;
}
const trace = this.generateTrace();
- AppImpl.instance.openTraceFromBuffer({
+ this.app.openTraceFromBuffer({
title: 'Recorded trace',
buffer: trace.buffer,
fileName: `recorded_trace${this.recordedTraceSuffix}`,
diff --git a/ui/src/controller/record_controller_interfaces.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_interfaces.ts
similarity index 97%
rename from ui/src/controller/record_controller_interfaces.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_controller_interfaces.ts
index e9662fd..f29940a 100644
--- a/ui/src/controller/record_controller_interfaces.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_interfaces.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {TRACE_SUFFIX} from '../common/constants';
+import {TRACE_SUFFIX} from '../../public/trace';
import {ConsumerPortResponse} from './consumer_port_types';
export type ErrorCallback = (_: string) => void;
diff --git a/ui/src/controller/record_controller_jsdomtest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_jsdomtest.ts
similarity index 99%
rename from ui/src/controller/record_controller_jsdomtest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_controller_jsdomtest.ts
index 442e4b8..1035369 100644
--- a/ui/src/controller/record_controller_jsdomtest.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_jsdomtest.ts
@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {assertExists} from '../base/logging';
-import {TraceConfig} from '../protos';
+import {assertExists} from '../../base/logging';
+import {TraceConfig} from '../../protos';
import {createEmptyRecordConfig} from './record_config_types';
import {genConfigProto, toPbtxt} from './record_controller';
diff --git a/ui/src/frontend/record_page.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
similarity index 92%
rename from ui/src/frontend/record_page.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
index 6d7a579..021a5db 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
@@ -26,35 +26,32 @@
LoadedConfig,
MAX_TIME,
RecordingTarget,
-} from '../common/state';
-import {AdbOverWebUsb} from '../controller/adb';
-import {
- RECORD_CONFIG_SCHEMA,
- RecordConfig,
-} from '../controller/record_config_types';
-import {raf} from '../core/raf_scheduler';
-import {PageAttrs} from '../public/page';
+} from './state';
+import {AdbOverWebUsb} from './adb';
+import {RECORD_CONFIG_SCHEMA, RecordConfig} from './record_config_types';
+import {PageAttrs} from '../../public/page';
import {
autosaveConfigStore,
recordConfigStore,
recordTargetStore,
} from './record_config';
import {CodeSnippet} from './record_widgets';
-import {AdvancedSettings} from './recording/advanced_settings';
-import {AndroidSettings} from './recording/android_settings';
-import {ChromeSettings} from './recording/chrome_settings';
-import {CpuSettings} from './recording/cpu_settings';
-import {GpuSettings} from './recording/gpu_settings';
-import {LinuxPerfSettings} from './recording/linux_perf_settings';
-import {MemorySettings} from './recording/memory_settings';
-import {PowerSettings} from './recording/power_settings';
-import {RecordingSettings} from './recording/recording_settings';
-import {EtwSettings} from './recording/etw_settings';
-import {AppImpl} from '../core/app_impl';
-import {RecordingManager} from '../controller/recording_manager';
-import {BUCKET_NAME, GcsUploader, MIME_JSON} from '../base/gcs_uploader';
-import {showModal} from '../widgets/modal';
-import {CopyableLink} from '../widgets/copyable_link';
+import {AdvancedSettings} from './advanced_settings';
+import {AndroidSettings} from './android_settings';
+import {ChromeSettings} from './chrome_settings';
+import {CpuSettings} from './cpu_settings';
+import {GpuSettings} from './gpu_settings';
+import {LinuxPerfSettings} from './linux_perf_settings';
+import {MemorySettings} from './memory_settings';
+import {PowerSettings} from './power_settings';
+import {RecordingSettings} from './recording_settings';
+import {EtwSettings} from './etw_settings';
+import {RecordingManager} from './recording_manager';
+import {scheduleFullRedraw} from '../../widgets/raf';
+import {App} from '../../public/app';
+import {GcsUploader, BUCKET_NAME, MIME_JSON} from '../../base/gcs_uploader';
+import {showModal} from '../../widgets/modal';
+import {CopyableLink} from '../../widgets/copyable_link';
export const RECORDING_SECTIONS = [
'buffers',
@@ -155,7 +152,7 @@
recMgr.setRecordingTarget(recordingTarget);
recordTargetStore.save(target);
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
}
function Instructions(recMgr: RecordingManager, cssClass: string) {
@@ -198,7 +195,7 @@
disabled: loadedConfigEqual(configType, recMgr.state.lastLoadedConfig),
onclick: () => {
recMgr.setRecordConfig(config, configType);
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
},
},
m('i.material-icons', 'file_upload'),
@@ -244,7 +241,7 @@
type: 'NAMED',
name: item.title,
});
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
}
},
},
@@ -257,7 +254,7 @@
title: 'Remove configuration',
onclick: () => {
recordConfigStore.delete(item.key);
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
},
},
m('i.material-icons', 'delete'),
@@ -292,7 +289,7 @@
placeholder: 'Title for config',
oninput() {
ConfigTitleState.setTitle(this.value);
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
},
}),
m(
@@ -308,7 +305,7 @@
recMgr.state.recordConfig,
ConfigTitleState.getTitle(),
);
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
ConfigTitleState.clearTitle();
},
},
@@ -326,7 +323,7 @@
)
) {
recMgr.clearRecordConfig();
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
}
},
},
@@ -573,7 +570,7 @@
function onStartRecordingPressed(recMgr: RecordingManager) {
location.href = '#!/record/instructions';
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
autosaveConfigStore.save(recMgr.state.recordConfig);
const target = recMgr.state.recordingTarget;
@@ -582,7 +579,7 @@
isChromeTarget(target) ||
isWindowsTarget(target)
) {
- AppImpl.instance.analytics.logEvent(
+ recMgr.app.analytics.logEvent(
'Record Trace',
`Record trace (${target.os})`,
);
@@ -742,7 +739,7 @@
'.record-menu',
{
class: recInProgress ? 'disabled' : '',
- onclick: () => raf.scheduleFullRedraw(),
+ onclick: () => scheduleFullRedraw(),
},
m('header', 'Trace config'),
m(
@@ -789,20 +786,29 @@
return routePage === section ? '.active' : '';
}
-export class RecordPage implements m.ClassComponent<PageAttrs> {
- private readonly recMgr = RecordingManager.instance;
+export interface RecordPageAttrs extends PageAttrs {
+ app: App;
+ recMgr: RecordingManager;
+}
+
+export class RecordPage implements m.ClassComponent<RecordPageAttrs> {
+ private readonly recMgr: RecordingManager;
private lastSubpage: string | undefined = undefined;
- oninit({attrs}: m.CVnode<PageAttrs>) {
+ constructor({attrs}: m.CVnode<RecordPageAttrs>) {
+ this.recMgr = attrs.recMgr;
+ }
+
+ oninit({attrs}: m.CVnode<RecordPageAttrs>) {
this.lastSubpage = attrs.subpage;
if (attrs.subpage !== undefined && attrs.subpage.startsWith('/share/')) {
const hash = attrs.subpage.substring(7);
loadRecordConfig(this.recMgr, hash);
- AppImpl.instance.navigate('#!/record/instructions');
+ attrs.app.navigate('#!/record/instructions');
}
}
- view({attrs}: m.CVnode<PageAttrs>) {
+ view({attrs}: m.CVnode<RecordPageAttrs>) {
if (attrs.subpage !== this.lastSubpage) {
this.lastSubpage = attrs.subpage;
// TODO(primiano): this is a hack necesasry to retrigger the generation of
diff --git a/ui/src/frontend/record_page_v2.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts
similarity index 80%
rename from ui/src/frontend/record_page_v2.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts
index e0faccd..3559332 100644
--- a/ui/src/frontend/record_page_v2.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts
@@ -14,24 +14,20 @@
import m from 'mithril';
import {Attributes} from 'mithril';
-import {assertExists} from '../base/logging';
-import {RecordingConfigUtils} from '../common/recordingV2/recording_config_utils';
+import {assertExists} from '../../base/logging';
+import {RecordingConfigUtils} from './recordingV2/recording_config_utils';
import {
ChromeTargetInfo,
RecordingTargetV2,
TargetInfo,
-} from '../common/recordingV2/recording_interfaces_v2';
+} from './recordingV2/recording_interfaces_v2';
import {
RecordingPageController,
RecordingState,
-} from '../common/recordingV2/recording_page_controller';
-import {
- EXTENSION_NAME,
- EXTENSION_URL,
-} from '../common/recordingV2/recording_utils';
-import {targetFactoryRegistry} from '../common/recordingV2/target_factory_registry';
-import {raf} from '../core/raf_scheduler';
-import {PageAttrs} from '../public/page';
+} from './recordingV2/recording_page_controller';
+import {EXTENSION_NAME, EXTENSION_URL} from './recordingV2/recording_utils';
+import {targetFactoryRegistry} from './recordingV2/target_factory_registry';
+import {PageAttrs} from '../../public/page';
import {recordConfigStore} from './record_config';
import {
Configurations,
@@ -41,34 +37,29 @@
uploadRecordingConfig,
} from './record_page';
import {CodeSnippet} from './record_widgets';
-import {AdvancedSettings} from './recording/advanced_settings';
-import {AndroidSettings} from './recording/android_settings';
-import {ChromeSettings} from './recording/chrome_settings';
-import {CpuSettings} from './recording/cpu_settings';
-import {EtwSettings} from './recording/etw_settings';
-import {GpuSettings} from './recording/gpu_settings';
-import {LinuxPerfSettings} from './recording/linux_perf_settings';
-import {MemorySettings} from './recording/memory_settings';
-import {PowerSettings} from './recording/power_settings';
-import {RecordingSettings} from './recording/recording_settings';
-import {FORCE_RESET_MESSAGE} from './recording/recording_ui_utils';
-import {showAddNewTargetModal} from './recording/reset_target_modal';
-import {RecordingManager} from '../controller/recording_manager';
-import {RecordConfig} from '../controller/record_config_types';
-import {AppImpl} from '../core/app_impl';
+import {AdvancedSettings} from './advanced_settings';
+import {AndroidSettings} from './android_settings';
+import {ChromeSettings} from './chrome_settings';
+import {CpuSettings} from './cpu_settings';
+import {EtwSettings} from './etw_settings';
+import {GpuSettings} from './gpu_settings';
+import {LinuxPerfSettings} from './linux_perf_settings';
+import {MemorySettings} from './memory_settings';
+import {PowerSettings} from './power_settings';
+import {RecordingSettings} from './recording_settings';
+import {FORCE_RESET_MESSAGE} from './recording_ui_utils';
+import {showAddNewTargetModal} from './reset_target_modal';
+import {RecordingManager} from './recording_manager';
+import {RecordConfig} from './record_config_types';
+import {App} from '../../public/app';
+import {scheduleFullRedraw} from '../../widgets/raf';
const START_RECORDING_MESSAGE = 'Start Recording';
// TODO(primiano): this is needs to be rewritten, but then i'm going to rewrite
// the whole record_page_v2 so not worth cleaning up now.
-let _controller: RecordingPageController;
-function controller(): RecordingPageController {
- if (_controller === undefined) {
- _controller = new RecordingPageController(RecordingManager.instance);
- }
- return _controller;
-}
-const recordConfigUtils = new RecordingConfigUtils();
+let controller: RecordingPageController;
+let recordConfigUtils: RecordingConfigUtils;
// Options for displaying a target selection menu.
export interface TargetSelectionOptions {
@@ -107,13 +98,13 @@
function RecordingPlatformSelection() {
// Don't show the platform selector while we are recording a trace.
- if (controller().getState() >= RecordingState.RECORDING) return undefined;
+ if (controller.getState() >= RecordingState.RECORDING) return undefined;
return m(
'.target',
m(
'.chip',
- {onclick: () => showAddNewTargetModal(controller())},
+ {onclick: () => showAddNewTargetModal(controller)},
m('button', 'Add new recording target'),
m('i.material-icons', 'add'),
),
@@ -122,13 +113,13 @@
}
export function targetSelection(): m.Vnode | undefined {
- if (!controller().shouldShowTargetSelection()) {
+ if (!controller.shouldShowTargetSelection()) {
return undefined;
}
const targets: RecordingTargetV2[] = targetFactoryRegistry.listTargets();
const targetNames = [];
- const targetInfo = controller().getTargetInfo();
+ const targetInfo = controller.getTargetInfo();
if (!targetInfo) {
targetNames.push(m('option', 'PLEASE_SELECT_TARGET'));
}
@@ -150,7 +141,7 @@
{
selectedIndex,
onchange: (e: Event) => {
- controller().onTargetSelection((e.target as HTMLSelectElement).value);
+ controller.onTargetSelection((e.target as HTMLSelectElement).value);
},
onupdate: (select) => {
// Work around mithril bug
@@ -179,11 +170,11 @@
}
function Instructions(recCfg: RecordConfig, cssClass: string) {
- if (controller().getState() < RecordingState.TARGET_SELECTED) {
+ if (controller.getState() < RecordingState.TARGET_SELECTED) {
return undefined;
}
// We will have a valid target at this step because we checked the state.
- const targetInfo = assertExists(controller().getTargetInfo());
+ const targetInfo = assertExists(controller.getTargetInfo());
return m(
`.record-section.instructions${cssClass}`,
@@ -203,13 +194,13 @@
function BufferUsageProgressBar() {
// Show the Buffer Usage bar only after we start recording a trace.
- if (controller().getState() !== RecordingState.RECORDING) {
+ if (controller.getState() !== RecordingState.RECORDING) {
return undefined;
}
- controller().fetchBufferUsage();
+ controller.fetchBufferUsage();
- const bufferUsage = controller().getBufferUsagePercentage();
+ const bufferUsage = controller.getBufferUsagePercentage();
// Buffer usage is not available yet on Android.
if (bufferUsage === 0) return undefined;
@@ -221,11 +212,11 @@
}
function RecordingNotes(recCfg: RecordConfig) {
- if (controller().getState() !== RecordingState.TARGET_INFO_DISPLAYED) {
+ if (controller.getState() !== RecordingState.TARGET_INFO_DISPLAYED) {
return undefined;
}
// We will have a valid target at this step because we checked the state.
- const targetInfo = assertExists(controller().getTargetInfo());
+ const targetInfo = assertExists(controller.getTargetInfo());
const linuxUrl = 'https://perfetto.dev/docs/quickstart/linux-tracing';
const cmdlineUrl =
@@ -320,7 +311,7 @@
function RecordingSnippet(recCfg: RecordConfig, targetInfo: TargetInfo) {
// We don't need commands to start tracing on chrome
if (isChromeTargetInfo(targetInfo)) {
- if (controller().getState() > RecordingState.AUTH_P2) {
+ if (controller.getState() > RecordingState.AUTH_P2) {
// If the UI has started tracing, don't display a message guiding the user
// to start recording.
return undefined;
@@ -372,14 +363,14 @@
function RecordingButton(recCfg: RecordConfig) {
if (
- controller().getState() !== RecordingState.TARGET_INFO_DISPLAYED ||
- !controller().canCreateTracingSession()
+ controller.getState() !== RecordingState.TARGET_INFO_DISPLAYED ||
+ !controller.canCreateTracingSession()
) {
return undefined;
}
// We know we have a target because we checked the state.
- const targetInfo = assertExists(controller().getTargetInfo());
+ const targetInfo = assertExists(controller.getTargetInfo());
const hasDataSources = recordConfigUtils.fetchLatestRecordCommand(
recCfg,
targetInfo,
@@ -394,7 +385,7 @@
'button',
{
class: 'selected',
- onclick: () => controller().onStartRecordingPressed(),
+ onclick: () => controller.onStartRecordingPressed(),
},
START_RECORDING_MESSAGE,
),
@@ -403,21 +394,17 @@
function StopCancelButtons() {
// Show the Stop/Cancel buttons only while we are recording a trace.
- if (!controller().shouldShowStopCancelButtons()) {
+ if (!controller.shouldShowStopCancelButtons()) {
return undefined;
}
const stop = m(
`button.selected`,
- {onclick: () => controller().onStop()},
+ {onclick: () => controller.onStop()},
'Stop',
);
- const cancel = m(
- `button`,
- {onclick: () => controller().onCancel()},
- 'Cancel',
- );
+ const cancel = m(`button`, {onclick: () => controller.onCancel()}, 'Cancel');
return [stop, cancel];
}
@@ -507,7 +494,7 @@
// We only display the probes when we have a valid target, so it's not
// possible for the target to be undefined here.
- const targetType = assertExists(controller().getTargetInfo()).targetType;
+ const targetType = assertExists(controller.getTargetInfo()).targetType;
const probes = [];
if (targetType === 'LINUX') {
probes.push(cpuProbe, powerProbe, memoryProbe, chromeProbe, advancedProbe);
@@ -532,10 +519,10 @@
'.record-menu',
{
class:
- controller().getState() > RecordingState.TARGET_INFO_DISPLAYED
+ controller.getState() > RecordingState.TARGET_INFO_DISPLAYED
? 'disabled'
: '',
- onclick: () => raf.scheduleFullRedraw(),
+ onclick: () => scheduleFullRedraw(),
},
m('header', 'Trace config'),
m(
@@ -581,10 +568,10 @@
function getRecordContainer(recMgr: RecordingManager, subpage?: string) {
const recCfg = recMgr.state.recordConfig;
const components: m.Children[] = [RecordHeader(recMgr)];
- if (controller().getState() === RecordingState.NO_TARGET) {
+ if (controller.getState() === RecordingState.NO_TARGET) {
components.push(m('.full-centered', 'Please connect a valid target.'));
return m('.record-container', components);
- } else if (controller().getState() <= RecordingState.ASK_TO_FORCE_P1) {
+ } else if (controller.getState() <= RecordingState.ASK_TO_FORCE_P1) {
components.push(
m(
'.full-centered',
@@ -594,13 +581,13 @@
),
);
return m('.record-container', components);
- } else if (controller().getState() === RecordingState.AUTH_P1) {
+ } else if (controller.getState() === RecordingState.AUTH_P1) {
components.push(
m('.full-centered', 'Please allow USB debugging on the device.'),
);
return m('.record-container', components);
} else if (
- controller().getState() === RecordingState.WAITING_FOR_TRACE_DISPLAY
+ controller.getState() === RecordingState.WAITING_FOR_TRACE_DISPLAY
) {
components.push(
m('.full-centered', 'Waiting for the trace to be collected.'),
@@ -642,7 +629,7 @@
for (const [section, component] of settingsSections.entries()) {
pages.push(
m(component, {
- dataSources: controller().getTargetInfo()?.dataSources || [],
+ dataSources: controller.getTargetInfo()?.dataSources || [],
cssClass: maybeGetActiveCss(routePage, section),
recState: recMgr.state,
}),
@@ -653,34 +640,43 @@
return m('.record-container', components);
}
-export class RecordPageV2 implements m.ClassComponent<PageAttrs> {
- private readonly recMgr = RecordingManager.instance;
+export interface RecordPageV2Attrs extends PageAttrs {
+ app: App;
+ recCtl: RecordingPageController;
+ recMgr: RecordingManager;
+}
+
+export class RecordPageV2 implements m.ClassComponent<RecordPageV2Attrs> {
private lastSubpage: string | undefined = undefined;
- oninit({attrs}: m.CVnode<PageAttrs>) {
- controller().initFactories();
+ constructor({attrs}: m.CVnode<RecordPageV2Attrs>) {
+ controller ??= attrs.recCtl;
+ recordConfigUtils ??= new RecordingConfigUtils();
+ }
+
+ oninit({attrs}: m.CVnode<RecordPageV2Attrs>) {
this.lastSubpage = attrs.subpage;
if (attrs.subpage !== undefined && attrs.subpage.startsWith('/share/')) {
const hash = attrs.subpage.substring(7);
- loadRecordConfig(this.recMgr, hash);
- AppImpl.instance.navigate('#!/record/instructions');
+ loadRecordConfig(attrs.recMgr, hash);
+ attrs.app.navigate('#!/record/instructions');
}
}
- view({attrs}: m.CVnode<PageAttrs>) {
+ view({attrs}: m.CVnode<RecordPageV2Attrs>) {
if (attrs.subpage !== this.lastSubpage) {
this.lastSubpage = attrs.subpage;
// TODO(primiano): this is a hack necesasry to retrigger the generation of
// the record cmdline. Refactor this code once record v1 vs v2 is gone.
- this.recMgr.setRecordConfig(this.recMgr.state.recordConfig);
+ attrs.recMgr.setRecordConfig(attrs.recMgr.state.recordConfig);
}
return m(
'.record-page',
- controller().getState() > RecordingState.TARGET_INFO_DISPLAYED
+ controller.getState() > RecordingState.TARGET_INFO_DISPLAYED
? m('.hider')
: [],
- getRecordContainer(this.recMgr, attrs.subpage),
+ getRecordContainer(attrs.recMgr, attrs.subpage),
);
}
}
diff --git a/ui/src/frontend/record_widgets.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
similarity index 96%
rename from ui/src/frontend/record_widgets.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
index 90f3c3d..325237b 100644
--- a/ui/src/frontend/record_widgets.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
@@ -13,11 +13,11 @@
// limitations under the License.
import m from 'mithril';
-import {copyToClipboard} from '../base/clipboard';
-import {assertExists} from '../base/logging';
-import {RecordConfig} from '../controller/record_config_types';
-import {raf} from '../core/raf_scheduler';
-import {assetSrc} from '../base/assets';
+import {copyToClipboard} from '../../base/clipboard';
+import {assertExists} from '../../base/logging';
+import {RecordConfig} from './record_config_types';
+import {assetSrc} from '../../base/assets';
+import {scheduleFullRedraw} from '../../widgets/raf';
export declare type Setter<T> = (cfg: RecordConfig, val: T) => void;
export declare type Getter<T> = (cfg: RecordConfig) => T;
@@ -63,7 +63,7 @@
view({attrs, children}: m.CVnode<ProbeAttrs>) {
const onToggle = (enabled: boolean) => {
attrs.setEnabled(attrs.recCfg, enabled);
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
};
const enabled = attrs.isEnabled(attrs.recCfg);
@@ -130,7 +130,7 @@
view({attrs}: m.CVnode<ToggleAttrs>) {
const onToggle = (enabled: boolean) => {
attrs.setEnabled(attrs.recCfg, enabled);
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
};
const enabled = attrs.isEnabled(attrs.recCfg);
@@ -175,7 +175,7 @@
export class Slider implements m.ClassComponent<SliderAttrs> {
onValueChange(attrs: SliderAttrs, newVal: number) {
attrs.set(attrs.recCfg, newVal);
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
}
onTimeValueChange(attrs: SliderAttrs, hms: string) {
@@ -276,7 +276,7 @@
selKeys.push(item.value);
}
attrs.set(attrs.recCfg, selKeys);
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
}
view({attrs}: m.CVnode<DropdownAttrs>) {
@@ -326,7 +326,7 @@
export class Textarea implements m.ClassComponent<TextareaAttrs> {
onChange(attrs: TextareaAttrs, dom: HTMLTextAreaElement) {
attrs.set(attrs.recCfg, dom.value);
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
}
view({attrs}: m.CVnode<TextareaAttrs>) {
@@ -400,7 +400,7 @@
if (!enabled && index !== -1) {
values.splice(index, 1);
}
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
}
view({attrs}: m.CVnode<CategoriesCheckboxListParams>) {
diff --git a/ui/src/common/recordingV2/adb_connection_impl.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts
similarity index 94%
rename from ui/src/common/recordingV2/adb_connection_impl.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts
index 99ef224..33e0dc1 100644
--- a/ui/src/common/recordingV2/adb_connection_impl.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts
@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {defer} from '../../base/deferred';
-import {ArrayBufferBuilder} from '../../base/array_buffer_builder';
+import {defer} from '../../../base/deferred';
+import {ArrayBufferBuilder} from '../../../base/array_buffer_builder';
import {AdbFileHandler} from './adb_file_handler';
import {
AdbConnection,
@@ -21,7 +21,7 @@
OnDisconnectCallback,
OnMessageCallback,
} from './recording_interfaces_v2';
-import {utf8Decode} from '../../base/string_utils';
+import {utf8Decode} from '../../../base/string_utils';
export abstract class AdbConnectionImpl implements AdbConnection {
// onStatus and onDisconnect are set to callbacks passed from the caller.
diff --git a/ui/src/common/recordingV2/adb_connection_over_websocket.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts
similarity index 98%
rename from ui/src/common/recordingV2/adb_connection_over_websocket.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts
index 160b257..9c9d139 100644
--- a/ui/src/common/recordingV2/adb_connection_over_websocket.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts
@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {defer, Deferred} from '../../base/deferred';
-import {utf8Decode} from '../../base/string_utils';
+import {defer, Deferred} from '../../../base/deferred';
+import {utf8Decode} from '../../../base/string_utils';
import {AdbConnectionImpl} from './adb_connection_impl';
import {RecordingError} from './recording_error_handling';
import {
diff --git a/ui/src/common/recordingV2/adb_connection_over_webusb.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts
similarity index 98%
rename from ui/src/common/recordingV2/adb_connection_over_webusb.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts
index 713d8b3..715d366 100644
--- a/ui/src/common/recordingV2/adb_connection_over_webusb.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts
@@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {defer, Deferred} from '../../base/deferred';
-import {assertExists, assertFalse, assertTrue} from '../../base/logging';
-import {isString} from '../../base/object_utils';
-import {utf8Decode, utf8Encode} from '../../base/string_utils';
-import {CmdType} from '../../controller/adb_interfaces';
+import {defer, Deferred} from '../../../base/deferred';
+import {assertExists, assertFalse, assertTrue} from '../../../base/logging';
+import {isString} from '../../../base/object_utils';
+import {utf8Decode, utf8Encode} from '../../../base/string_utils';
+import {CmdType} from '../adb_interfaces';
import {AdbConnectionImpl} from './adb_connection_impl';
import {AdbKeyManager, maybeStoreKey} from './auth/adb_key_manager';
import {RecordingError, wrapRecordingError} from './recording_error_handling';
diff --git a/ui/src/common/recordingV2/adb_file_handler.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts
similarity index 94%
rename from ui/src/common/recordingV2/adb_file_handler.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts
index 1016fe7..078726f 100644
--- a/ui/src/common/recordingV2/adb_file_handler.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts
@@ -12,16 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {defer, Deferred} from '../../base/deferred';
-import {assertFalse} from '../../base/logging';
-import {ArrayBufferBuilder} from '../../base/array_buffer_builder';
+import {defer, Deferred} from '../../../base/deferred';
+import {assertFalse} from '../../../base/logging';
+import {ArrayBufferBuilder} from '../../../base/array_buffer_builder';
import {RecordingError} from './recording_error_handling';
import {ByteStream} from './recording_interfaces_v2';
import {
BINARY_PUSH_FAILURE,
BINARY_PUSH_UNKNOWN_RESPONSE,
} from './recording_utils';
-import {utf8Decode} from '../../base/string_utils';
+import {utf8Decode} from '../../../base/string_utils';
// https://cs.android.com/android/platform/superproject/+/main:packages/
// modules/adb/file_sync_protocol.h;l=144
diff --git a/ui/src/common/recordingV2/auth/adb_auth.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts
similarity index 97%
rename from ui/src/common/recordingV2/auth/adb_auth.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts
index aec8752..7ed275e 100644
--- a/ui/src/common/recordingV2/auth/adb_auth.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts
@@ -13,12 +13,12 @@
// limitations under the License.
import {BigInteger, RSAKey} from 'jsbn-rsa';
-import {assertExists, assertTrue} from '../../../base/logging';
+import {assertExists, assertTrue} from '../../../../base/logging';
import {
base64Decode,
base64Encode,
hexEncode,
-} from '../../../base/string_utils';
+} from '../../../../base/string_utils';
import {RecordingError} from '../recording_error_handling';
const WORD_SIZE = 4;
diff --git a/ui/src/common/recordingV2/auth/adb_key_manager.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts
similarity index 98%
rename from ui/src/common/recordingV2/auth/adb_key_manager.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts
index 53e233f..0ce297b 100644
--- a/ui/src/common/recordingV2/auth/adb_key_manager.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {assetSrc} from '../../../base/assets';
+import {assetSrc} from '../../../../base/assets';
import {AdbKey} from './adb_auth';
function isPasswordCredential(
diff --git a/ui/src/common/recordingV2/auth/credentials_interfaces.d.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/credentials_interfaces.d.ts
similarity index 100%
rename from ui/src/common/recordingV2/auth/credentials_interfaces.d.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/credentials_interfaces.d.ts
diff --git a/ui/src/common/recordingV2/chrome_traced_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts
similarity index 95%
rename from ui/src/common/recordingV2/chrome_traced_tracing_session.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts
index f8ecd03..9461190 100644
--- a/ui/src/common/recordingV2/chrome_traced_tracing_session.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts
@@ -12,28 +12,28 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {defer, Deferred} from '../../base/deferred';
-import {assertExists, assertTrue} from '../../base/logging';
-import {binaryDecode, binaryEncode} from '../../base/string_utils';
+import {defer, Deferred} from '../../../base/deferred';
+import {assertExists, assertTrue} from '../../../base/logging';
+import {binaryDecode, binaryEncode} from '../../../base/string_utils';
import {
ChromeExtensionMessage,
isChromeExtensionError,
isChromeExtensionStatus,
isGetCategoriesResponse,
-} from '../../controller/chrome_proxy_record_controller';
+} from '../chrome_proxy_record_controller';
import {
isDisableTracingResponse,
isEnableTracingResponse,
isFreeBuffersResponse,
isGetTraceStatsResponse,
isReadBuffersResponse,
-} from '../../controller/consumer_port_types';
+} from '../consumer_port_types';
import {
EnableTracingRequest,
IBufferStats,
ISlice,
TraceConfig,
-} from '../../protos';
+} from '../../../protos';
import {RecordingError} from './recording_error_handling';
import {
TracingSession,
diff --git a/ui/src/common/recordingV2/host_os_byte_stream.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts
similarity index 97%
rename from ui/src/common/recordingV2/host_os_byte_stream.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts
index 3c43630..a03b791 100644
--- a/ui/src/common/recordingV2/host_os_byte_stream.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {defer} from '../../base/deferred';
+import {defer} from '../../../base/deferred';
import {
ByteStream,
OnStreamCloseCallback,
diff --git a/ui/src/common/recordingV2/recording_config_utils.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils.ts
similarity index 98%
rename from ui/src/common/recordingV2/recording_config_utils.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils.ts
index bc3262e..e4eca50 100644
--- a/ui/src/common/recordingV2/recording_config_utils.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils.ts
@@ -12,10 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {isString} from '../../base/object_utils';
-import {base64Encode} from '../../base/string_utils';
-import {exists} from '../../base/utils';
-import {RecordConfig} from '../../controller/record_config_types';
+import {isString} from '../../../base/object_utils';
+import {base64Encode} from '../../../base/string_utils';
+import {exists} from '../../../base/utils';
+import {RecordConfig} from '../record_config_types';
import {
AndroidLogConfig,
AndroidLogId,
@@ -41,7 +41,7 @@
TraceConfig,
TrackEventConfig,
VmstatCounters,
-} from '../../protos';
+} from '../../../protos';
import {TargetInfo} from './recording_interfaces_v2';
import PerfClock = PerfEvents.PerfClock;
import Timebase = PerfEvents.Timebase;
@@ -463,7 +463,7 @@
if (uiCfg.androidStatsdPushedAtoms.length > 0) {
ds.config.statsdTracingConfig.pushAtomId =
- uiCfg.androidStatsdPushedAtoms.map((atom) => atom as any as AtomId);
+ uiCfg.androidStatsdPushedAtoms.map((atom) => atom as unknown as AtomId);
}
const needPulledAtomConfig =
@@ -471,7 +471,7 @@
uiCfg.androidStatsdPulledAtoms.length > 0;
if (needPulledAtomConfig) {
- let pullAtomConfig = new StatsdPullAtomConfig();
+ const pullAtomConfig = new StatsdPullAtomConfig();
if (uiCfg.androidStatsdRawPulledAtoms.length > 0) {
for (const line of uiCfg.androidStatsdRawPulledAtoms.split('\n')) {
if (line.trim().length > 0) {
@@ -479,8 +479,9 @@
}
}
}
- pullAtomConfig.pullAtomId =
- uiCfg.androidStatsdPulledAtoms.map((atom) => atom as any as AtomId);
+ pullAtomConfig.pullAtomId = uiCfg.androidStatsdPulledAtoms.map(
+ (atom) => atom as unknown as AtomId,
+ );
pullAtomConfig.pullFrequencyMs =
uiCfg.androidStatsdPulledAtomPullFrequencyMs;
if (uiCfg.androidStatsdPulledAtomPackages.length > 0) {
diff --git a/ui/src/common/recordingV2/recording_config_utils_unittest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils_unittest.ts
similarity index 96%
rename from ui/src/common/recordingV2/recording_config_utils_unittest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils_unittest.ts
index 67ac112..dd96a69 100644
--- a/ui/src/common/recordingV2/recording_config_utils_unittest.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils_unittest.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {createEmptyRecordConfig} from '../../controller/record_config_types';
+import {createEmptyRecordConfig} from '../record_config_types';
import {genTraceConfig} from './recording_config_utils';
import {AndroidTargetInfo} from './recording_interfaces_v2';
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_error_handling.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_error_handling.ts
new file mode 100644
index 0000000..ba86e65
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_error_handling.ts
@@ -0,0 +1,263 @@
+// 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 {getErrorMessage} from '../../../base/errors';
+import {showModal} from '../../../widgets/modal';
+import {OnMessageCallback} from './recording_interfaces_v2';
+import {
+ ALLOW_USB_DEBUGGING,
+ BINARY_PUSH_FAILURE,
+ BINARY_PUSH_UNKNOWN_RESPONSE,
+ EXTENSION_NOT_INSTALLED,
+ EXTENSION_URL,
+ NO_DEVICE_SELECTED,
+ PARSING_UNABLE_TO_DECODE_METHOD,
+ PARSING_UNKNWON_REQUEST_ID,
+ PARSING_UNRECOGNIZED_MESSAGE,
+ PARSING_UNRECOGNIZED_PORT,
+ WEBSOCKET_UNABLE_TO_CONNECT,
+} from './recording_utils';
+
+// The pattern for handling recording error can have the following nesting in
+// case of errors:
+// A. wrapRecordingError -> wraps a promise
+// B. onFailure -> has user defined logic and calls showRecordingModal
+// C. showRecordingModal -> shows UX for a given error; this is not called
+// directly by wrapRecordingError, because we want the caller (such as the
+// UI) to dictate the UX
+
+// This method takes a promise and a callback to be execute in case the promise
+// fails. It then awaits the promise and executes the callback in case of
+// failure. In the recording code it is used to wrap:
+// 1. Acessing the WebUSB API.
+// 2. Methods returning promises which can be rejected. For instance:
+// a) When the user clicks 'Add a new device' but then doesn't select a valid
+// device.
+// b) When the user starts a tracing session, but cancels it before they
+// authorize the session on the device.
+export async function wrapRecordingError<T>(
+ promise: Promise<T>,
+ onFailure: OnMessageCallback,
+): Promise<T | undefined> {
+ try {
+ return await promise;
+ } catch (e) {
+ // Sometimes the message is wrapped in an Error object, sometimes not, so
+ // we make sure we transform it into a string.
+ const errorMessage = getErrorMessage(e);
+ onFailure(errorMessage);
+ return undefined;
+ }
+}
+
+// Shows a modal for every known type of error which can arise during recording.
+// In this way, errors occuring at different levels of the recording process
+// can be handled in a central location.
+export function showRecordingModal(message: string): void {
+ if (
+ [
+ 'Unable to claim interface.',
+ 'The specified endpoint is not part of a claimed and selected ' +
+ 'alternate interface.',
+ // thrown when calling the 'reset' method on a WebUSB device.
+ 'Unable to reset the device.',
+ ].some((partOfMessage) => message.includes(partOfMessage))
+ ) {
+ showWebUSBErrorV2();
+ } else if (
+ [
+ 'A transfer error has occurred.',
+ 'The device was disconnected.',
+ 'The transfer was cancelled.',
+ ].some((partOfMessage) => message.includes(partOfMessage)) ||
+ isDeviceDisconnectedError(message)
+ ) {
+ showConnectionLostError();
+ } else if (message === ALLOW_USB_DEBUGGING) {
+ showAllowUSBDebugging();
+ } else if (
+ isMessageComposedOf(message, [
+ BINARY_PUSH_FAILURE,
+ BINARY_PUSH_UNKNOWN_RESPONSE,
+ ])
+ ) {
+ showFailedToPushBinary(message.substring(message.indexOf(':') + 1));
+ } else if (message === NO_DEVICE_SELECTED) {
+ showNoDeviceSelected();
+ } else if (WEBSOCKET_UNABLE_TO_CONNECT === message) {
+ showWebsocketConnectionIssue(message);
+ } else if (message === EXTENSION_NOT_INSTALLED) {
+ showExtensionNotInstalled();
+ } else if (
+ isMessageComposedOf(message, [
+ PARSING_UNKNWON_REQUEST_ID,
+ PARSING_UNABLE_TO_DECODE_METHOD,
+ PARSING_UNRECOGNIZED_PORT,
+ PARSING_UNRECOGNIZED_MESSAGE,
+ ])
+ ) {
+ showIssueParsingTheTracedResponse(message);
+ } else {
+ throw new Error(`${message}`);
+ }
+}
+
+function isDeviceDisconnectedError(message: string) {
+ return (
+ message.includes('Device with serial') &&
+ message.includes('was disconnected.')
+ );
+}
+
+function isMessageComposedOf(message: string, issues: string[]) {
+ for (const issue of issues) {
+ if (message.includes(issue)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// Exception thrown by the Recording logic.
+export class RecordingError extends Error {}
+
+function showWebUSBErrorV2() {
+ showModal({
+ title: 'A WebUSB error occurred',
+ content: m(
+ 'div',
+ m(
+ 'span',
+ `Is adb already running on the host? Run this command and
+ try again.`,
+ ),
+ m('br'),
+ m('.modal-bash', '> adb kill-server'),
+ m('br'),
+ // The statement below covers the following edge case:
+ // 1. 'adb server' is running on the device.
+ // 2. The user selects the new Android target, so we try to fetch the
+ // OS version and do QSS.
+ // 3. The error modal is shown.
+ // 4. The user runs 'adb kill-server'.
+ // At this point we don't have a trigger to try fetching the OS version
+ // + QSS again. Therefore, the user will need to refresh the page.
+ m(
+ 'span',
+ "If after running 'adb kill-server', you don't see " +
+ "a 'Start Recording' button on the page and you don't see " +
+ "'Allow USB debugging' on the device, " +
+ 'you will need to reload this page.',
+ ),
+ m('br'),
+ m('br'),
+ m('span', 'For details see '),
+ m('a', {href: 'http://b/159048331', target: '_blank'}, 'b/159048331'),
+ ),
+ });
+}
+
+function showConnectionLostError(): void {
+ showModal({
+ title: 'Connection with the ADB device lost',
+ content: m(
+ 'div',
+ m('span', `Please connect the device again to restart the recording.`),
+ m('br'),
+ ),
+ });
+}
+
+function showAllowUSBDebugging(): void {
+ showModal({
+ title: 'Could not connect to the device',
+ content: m(
+ 'div',
+ m('span', 'Please allow USB debugging on the device.'),
+ m('br'),
+ ),
+ });
+}
+
+function showNoDeviceSelected(): void {
+ showModal({
+ title: 'No device was selected for recording',
+ content: m(
+ 'div',
+ m(
+ 'span',
+ `If you want to connect to an ADB device,
+ please select it from the list.`,
+ ),
+ m('br'),
+ ),
+ });
+}
+
+function showExtensionNotInstalled(): void {
+ showModal({
+ title: 'Perfetto Chrome extension not installed',
+ content: m(
+ 'div',
+ m(
+ '.note',
+ `To trace Chrome from the Perfetto UI, you need to install our `,
+ m('a', {href: EXTENSION_URL, target: '_blank'}, 'Chrome extension'),
+ ' and then reload this page.',
+ ),
+ m('br'),
+ ),
+ });
+}
+
+function showIssueParsingTheTracedResponse(message: string): void {
+ showModal({
+ title:
+ 'A problem was encountered while connecting to' +
+ ' the Perfetto tracing service',
+ content: m('div', m('span', message), m('br')),
+ });
+}
+
+function showFailedToPushBinary(message: string): void {
+ showModal({
+ title: 'Failed to push a binary to the device',
+ content: m(
+ 'div',
+ m(
+ 'span',
+ 'This can happen if your Android device has an OS version lower ' +
+ 'than Q. Perfetto tried to push the latest version of its ' +
+ 'embedded binary but failed.',
+ ),
+ m('br'),
+ m('br'),
+ m('span', 'Error message:'),
+ m('br'),
+ m('span', message),
+ ),
+ });
+}
+
+function showWebsocketConnectionIssue(message: string): void {
+ showModal({
+ title: 'Unable to connect to the device via websocket',
+ content: m(
+ 'div',
+ m('div', 'trace_processor_shell --httpd is unreachable or crashed.'),
+ m('pre', message),
+ ),
+ });
+}
diff --git a/ui/src/common/recordingV2/recording_interfaces_v2.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_interfaces_v2.ts
similarity index 99%
rename from ui/src/common/recordingV2/recording_interfaces_v2.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_interfaces_v2.ts
index 954a145..c8a030e 100644
--- a/ui/src/common/recordingV2/recording_interfaces_v2.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_interfaces_v2.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {TraceConfig} from '../../protos';
+import {TraceConfig} from '../../../protos';
// TargetFactory connects, disconnects and keeps track of targets.
// There is one factory for AndroidWebusb, AndroidWebsocket, Chrome etc.
diff --git a/ui/src/common/recordingV2/recording_page_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
similarity index 94%
rename from ui/src/common/recordingV2/recording_page_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
index de568aa..76617d5 100644
--- a/ui/src/common/recordingV2/recording_page_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
@@ -12,19 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {assertExists, assertTrue} from '../../base/logging';
-import {currentDateHourAndMinute} from '../../base/time';
-import {RecordingManager} from '../../controller/recording_manager';
-import {AppImpl} from '../../core/app_impl';
-import {raf} from '../../core/raf_scheduler';
-import {autosaveConfigStore} from '../../frontend/record_config';
+import {assertExists, assertTrue} from '../../../base/logging';
+import {currentDateHourAndMinute} from '../../../base/time';
+import {RecordingManager} from '../recording_manager';
+import {autosaveConfigStore} from '../record_config';
import {
DEFAULT_ADB_WEBSOCKET_URL,
DEFAULT_TRACED_WEBSOCKET_URL,
-} from '../../frontend/recording/recording_ui_utils';
-import {couldNotClaimInterface} from '../../frontend/recording/reset_interface_modal';
-import {TraceConfig} from '../../protos';
-import {TRACE_SUFFIX} from '../constants';
+} from '../recording_ui_utils';
+import {couldNotClaimInterface} from '../reset_interface_modal';
+import {TraceConfig} from '../../../protos';
+import {TRACE_SUFFIX} from '../../../public/trace';
import {genTraceConfig} from './recording_config_utils';
import {RecordingError, showRecordingModal} from './recording_error_handling';
import {
@@ -47,6 +45,8 @@
HostOsTargetFactory,
} from './target_factories/host_os_target_factory';
import {targetFactoryRegistry} from './target_factory_registry';
+import {scheduleFullRedraw} from '../../../widgets/raf';
+import {App} from '../../../public/app';
// The recording page can be in any of these states. It can transition between
// states:
@@ -250,6 +250,7 @@
// Keeps track of the state the Ui is in. Has methods which are executed on
// user actions such as starting/stopping/cancelling a tracing session.
export class RecordingPageController {
+ private app: App;
private recMgr: RecordingManager;
// State of the recording page. This is set by user actions and/or automatic
@@ -267,7 +268,8 @@
// transitions don't override one another in async functions.
private stateGeneration = 0;
- constructor(recMgr: RecordingManager) {
+ constructor(app: App, recMgr: RecordingManager) {
+ this.app = app;
this.recMgr = recMgr;
}
@@ -296,7 +298,7 @@
}
this.setState(state);
this.recMgr.setRecordingStatus(undefined);
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
}
maybeClearRecordingState(tracingSessionWrapper: TracingSessionWrapper): void {
@@ -312,7 +314,7 @@
if (this.tracingSessionWrapper !== tracingSessionWrapper) {
return;
}
- AppImpl.instance.openTraceFromBuffer({
+ this.app.openTraceFromBuffer({
title: 'Recorded trace',
buffer: trace.buffer,
fileName: `trace_${currentDateHourAndMinute()}${TRACE_SUFFIX}`,
@@ -390,11 +392,11 @@
if (!this.target) {
this.setState(RecordingState.NO_TARGET);
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
return;
}
this.setState(RecordingState.TARGET_SELECTED);
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
this.tracingSessionWrapper = this.createTracingSessionWrapper(this.target);
this.tracingSessionWrapper.fetchTargetInfo();
@@ -431,7 +433,7 @@
const target = this.getTarget();
const targetInfo = target.getInfo();
- AppImpl.instance.analytics.logEvent(
+ this.app.analytics.logEvent(
'Record Trace',
`Record trace (${targetInfo.targetType})`,
);
@@ -484,7 +486,7 @@
// We redraw if:
// 1. We received a correct buffer usage value.
// 2. We receive a RecordingError.
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
}
initFactories() {
@@ -531,7 +533,7 @@
// If the change happens for an existing target, the controller keeps the
// currently selected target in focus.
if (this.target && allTargets.includes(this.target)) {
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
return;
}
// If the change happens to a new target or the controller does not have a
@@ -552,7 +554,7 @@
this.recMgr.setRecordingStatus(undefined);
// Redrawing because this method has changed the RecordingState, which will
// affect the display of the record_page.
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
}
private setState(state: RecordingState) {
diff --git a/ui/src/common/recordingV2/recording_utils.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_utils.ts
similarity index 100%
rename from ui/src/common/recordingV2/recording_utils.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_utils.ts
diff --git a/ui/src/common/recordingV2/target_factories/android_websocket_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts
similarity index 96%
rename from ui/src/common/recordingV2/target_factories/android_websocket_target_factory.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts
index 21097eb..03cda1f 100644
--- a/ui/src/common/recordingV2/target_factories/android_websocket_target_factory.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {RECORDING_V2_FLAG} from '../../../core/feature_flags';
import {
OnTargetChangeCallback,
RecordingTargetV2,
@@ -22,7 +21,6 @@
buildAbdWebsocketCommand,
WEBSOCKET_CLOSED_ABNORMALLY_CODE,
} from '../recording_utils';
-import {targetFactoryRegistry} from '../target_factory_registry';
import {AndroidWebsocketTarget} from '../targets/android_websocket_target';
export const ANDROID_WEBSOCKET_TARGET_FACTORY = 'AndroidWebsocketTargetFactory';
@@ -268,8 +266,3 @@
this.onTargetChange = onTargetChange;
}
}
-
-// We only want to instantiate this class if Recording V2 is enabled.
-if (RECORDING_V2_FLAG.get()) {
- targetFactoryRegistry.register(new AndroidWebsocketTargetFactory());
-}
diff --git a/ui/src/common/recordingV2/target_factories/android_websocket_target_factory_unittest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory_unittest.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/android_websocket_target_factory_unittest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory_unittest.ts
diff --git a/ui/src/common/recordingV2/target_factories/android_webusb_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts
similarity index 89%
rename from ui/src/common/recordingV2/target_factories/android_webusb_target_factory.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts
index d27ab07..a969c31 100644
--- a/ui/src/common/recordingV2/target_factories/android_webusb_target_factory.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts
@@ -12,9 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {getErrorMessage} from '../../../base/errors';
-import {assertExists} from '../../../base/logging';
-import {RECORDING_V2_FLAG} from '../../../core/feature_flags';
+import {getErrorMessage} from '../../../../base/errors';
+import {assertExists} from '../../../../base/logging';
import {AdbKeyManager} from '../auth/adb_key_manager';
import {RecordingError} from '../recording_error_handling';
import {
@@ -23,7 +22,6 @@
TargetFactory,
} from '../recording_interfaces_v2';
import {ADB_DEVICE_FILTER, findInterfaceAndEndpoint} from '../recording_utils';
-import {targetFactoryRegistry} from '../target_factory_registry';
import {AndroidWebusbTarget} from '../targets/android_webusb_target';
export const ANDROID_WEBUSB_TARGET_FACTORY = 'AndroidWebusbTargetFactory';
@@ -155,11 +153,3 @@
return deviceValidity;
}
}
-
-// We only want to instantiate this class if:
-// 1. The browser implements the USB functionality.
-// 2. Recording V2 is enabled.
-// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-if (navigator.usb && RECORDING_V2_FLAG.get()) {
- targetFactoryRegistry.register(new AndroidWebusbTargetFactory(navigator.usb));
-}
diff --git a/ui/src/common/recordingV2/target_factories/chrome_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/chrome_target_factory.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory.ts
diff --git a/ui/src/common/recordingV2/target_factories/chrome_target_factory_unittest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory_unittest.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/chrome_target_factory_unittest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory_unittest.ts
diff --git a/ui/src/common/recordingV2/target_factories/host_os_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/host_os_target_factory.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/host_os_target_factory.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/host_os_target_factory.ts
diff --git a/ui/src/common/recordingV2/target_factories/index.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/index.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/index.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/index.ts
diff --git a/ui/src/common/recordingV2/target_factories/virtual_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/virtual_target_factory.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/virtual_target_factory.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/virtual_target_factory.ts
diff --git a/ui/src/common/recordingV2/target_factory_registry.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factory_registry.ts
similarity index 96%
rename from ui/src/common/recordingV2/target_factory_registry.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factory_registry.ts
index e8de655..b34070d 100644
--- a/ui/src/common/recordingV2/target_factory_registry.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factory_registry.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {Registry} from '../../base/registry';
+import {Registry} from '../../../base/registry';
import {RecordingTargetV2, TargetFactory} from './recording_interfaces_v2';
export class TargetFactoryRegistry extends Registry<TargetFactory> {
diff --git a/ui/src/common/recordingV2/targets/android_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts
similarity index 96%
rename from ui/src/common/recordingV2/targets/android_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts
index 926846d..0bac1e4 100644
--- a/ui/src/common/recordingV2/targets/android_target.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts
@@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {fetchWithTimeout} from '../../../base/http_utils';
-import {exists} from '../../../base/utils';
-import {VERSION} from '../../../gen/perfetto_version';
+import {fetchWithTimeout} from '../../../../base/http_utils';
+import {exists} from '../../../../base/utils';
+import {VERSION} from '../../../../gen/perfetto_version';
import {AdbConnectionImpl} from '../adb_connection_impl';
import {
DataSource,
diff --git a/ui/src/common/recordingV2/targets/android_virtual_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_virtual_target.ts
similarity index 100%
rename from ui/src/common/recordingV2/targets/android_virtual_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_virtual_target.ts
diff --git a/ui/src/common/recordingV2/targets/android_websocket_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_websocket_target.ts
similarity index 100%
rename from ui/src/common/recordingV2/targets/android_websocket_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_websocket_target.ts
diff --git a/ui/src/common/recordingV2/targets/android_webusb_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts
similarity index 96%
rename from ui/src/common/recordingV2/targets/android_webusb_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts
index e70a19a..dc6e64d 100644
--- a/ui/src/common/recordingV2/targets/android_webusb_target.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {assertExists} from '../../../base/logging';
+import {assertExists} from '../../../../base/logging';
import {AdbConnectionOverWebusb} from '../adb_connection_over_webusb';
import {AdbKeyManager} from '../auth/adb_key_manager';
import {OnTargetChangeCallback, TargetInfo} from '../recording_interfaces_v2';
diff --git a/ui/src/common/recordingV2/targets/chrome_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/chrome_target.ts
similarity index 100%
rename from ui/src/common/recordingV2/targets/chrome_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/chrome_target.ts
diff --git a/ui/src/common/recordingV2/targets/host_os_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/host_os_target.ts
similarity index 100%
rename from ui/src/common/recordingV2/targets/host_os_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/host_os_target.ts
diff --git a/ui/src/common/recordingV2/traced_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts
similarity index 98%
rename from ui/src/common/recordingV2/traced_tracing_session.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts
index c0ba444..8687432 100644
--- a/ui/src/common/recordingV2/traced_tracing_session.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts
@@ -13,8 +13,8 @@
// limitations under the License.
import protobuf from 'protobufjs/minimal';
-import {defer, Deferred} from '../../base/deferred';
-import {assertExists, assertFalse, assertTrue} from '../../base/logging';
+import {defer, Deferred} from '../../../base/deferred';
+import {assertExists, assertFalse, assertTrue} from '../../../base/logging';
import {
DisableTracingRequest,
DisableTracingResponse,
@@ -33,7 +33,7 @@
ReadBuffersRequest,
ReadBuffersResponse,
TraceConfig,
-} from '../../protos';
+} from '../../../protos';
import {RecordingError} from './recording_error_handling';
import {
ByteStream,
@@ -50,7 +50,7 @@
PARSING_UNRECOGNIZED_PORT,
RECORDING_IN_PROGRESS,
} from './recording_utils';
-import {exists} from '../../base/utils';
+import {exists} from '../../../base/utils';
// See wire_protocol.proto for more details.
const WIRE_PROTOCOL_HEADER_SIZE = 4;
diff --git a/ui/src/common/recordingV2/websocket_menu_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts
similarity index 97%
rename from ui/src/common/recordingV2/websocket_menu_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts
index 8b800a7..2da8f5b 100644
--- a/ui/src/common/recordingV2/websocket_menu_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts
@@ -16,7 +16,7 @@
ADB_ENDPOINT,
DEFAULT_WEBSOCKET_URL,
TRACED_ENDPOINT,
-} from '../../frontend/recording/recording_ui_utils';
+} from '../recording_ui_utils';
import {TargetFactory} from './recording_interfaces_v2';
import {
ANDROID_WEBSOCKET_TARGET_FACTORY,
diff --git a/ui/src/controller/recording_manager.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
similarity index 87%
rename from ui/src/controller/recording_manager.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
index 5d96017..be29691 100644
--- a/ui/src/controller/recording_manager.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {createEmptyState} from '../common/empty_state';
+import {createEmptyState} from './empty_state';
import {
AdbRecordingTarget,
LoadedConfig,
@@ -20,39 +20,41 @@
RecordingTarget,
getDefaultRecordingTargets,
isAdbTarget,
-} from '../common/state';
-import {RECORDING_V2_FLAG} from '../core/feature_flags';
-import {raf} from '../core/raf_scheduler';
+} from './state';
import {AdbOverWebUsb} from './adb';
import {isGetCategoriesResponse} from './chrome_proxy_record_controller';
import {RecordConfig, createEmptyRecordConfig} from './record_config_types';
import {RecordController} from './record_controller';
+import {scheduleFullRedraw} from '../../widgets/raf';
+import {App} from '../../public/app';
+import {targetFactoryRegistry} from './recordingV2/target_factory_registry';
+import {AndroidWebsocketTargetFactory} from './recordingV2/target_factories/android_websocket_target_factory';
+import {AndroidWebusbTargetFactory} from './recordingV2/target_factories/android_webusb_target_factory';
+import {exists} from '../../base/utils';
const EXTENSION_ID = 'lfmkphfpdbjijhpomgecfikhfohaoine';
// TODO(primiano): this class and RecordController should be merged. I'm keeping
// them separate for now to reduce scope of refactorings.
export class RecordingManager {
+ readonly app: App;
private _state: RecordingState = createEmptyState();
private recCtl: RecordController;
- // TODO(primiano): this singleton is temporary. RecordingManager shoudl be
- // injected in all the recording pages and the instance should be created and
- // owned by the recording plugin. But for now we don't have a plugin.
- private static _instance: RecordingManager | undefined = undefined;
- static get instance() {
- if (this._instance === undefined) {
- this._instance = new RecordingManager();
- }
- return this._instance;
- }
-
- constructor() {
+ constructor(app: App, useRecordingV2: boolean) {
+ this.app = app;
const extensionLocalChannel = new MessageChannel();
- this.recCtl = new RecordController(this, extensionLocalChannel.port1);
+ this.recCtl = new RecordController(app, this, extensionLocalChannel.port1);
this.setupExtentionPort(extensionLocalChannel);
- if (!RECORDING_V2_FLAG.get()) {
+ if (useRecordingV2) {
+ targetFactoryRegistry.register(new AndroidWebsocketTargetFactory());
+ if (exists(navigator.usb)) {
+ targetFactoryRegistry.register(
+ new AndroidWebusbTargetFactory(navigator.usb),
+ );
+ }
+ } else {
this.updateAvailableAdbDevices();
try {
navigator.usb.addEventListener('connect', () =>
@@ -156,7 +158,7 @@
(message: object, _port: chrome.runtime.Port) => {
if (isGetCategoriesResponse(message)) {
this._state.chromeCategories = message.categories;
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
return;
}
extensionLocalChannel.port2.postMessage(message);
@@ -191,7 +193,7 @@
this.setAvailableAdbDevices(availableAdbDevices);
this.selectAndroidDeviceIfAvailable(availableAdbDevices, recordingTarget);
- raf.scheduleFullRedraw();
+ scheduleFullRedraw();
return availableAdbDevices;
}
diff --git a/ui/src/frontend/recording/recording_multiple_choice.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts
similarity index 93%
rename from ui/src/frontend/recording/recording_multiple_choice.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts
index 27a83fc..0e34f5c 100644
--- a/ui/src/frontend/recording/recording_multiple_choice.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts
@@ -16,9 +16,9 @@
import {
RecordingTargetV2,
TargetFactory,
-} from '../../common/recordingV2/recording_interfaces_v2';
-import {RecordingPageController} from '../../common/recordingV2/recording_page_controller';
-import {RECORDING_MODAL_DIALOG_KEY} from '../../common/recordingV2/recording_utils';
+} from './recordingV2/recording_interfaces_v2';
+import {RecordingPageController} from './recordingV2/recording_page_controller';
+import {RECORDING_MODAL_DIALOG_KEY} from './recordingV2/recording_utils';
import {closeModal} from '../../widgets/modal';
interface RecordingMultipleChoiceAttrs {
diff --git a/ui/src/frontend/recording/recording_sections.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_sections.ts
similarity index 86%
rename from ui/src/frontend/recording/recording_sections.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recording_sections.ts
index 03ebff8..c83b9e0 100644
--- a/ui/src/frontend/recording/recording_sections.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_sections.ts
@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {DataSource} from '../../common/recordingV2/recording_interfaces_v2';
-import {RecordingState} from '../../common/state';
+import {DataSource} from './recordingV2/recording_interfaces_v2';
+import {RecordingState} from './state';
export interface RecordingSectionAttrs {
recState: RecordingState;
diff --git a/ui/src/frontend/recording/recording_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_settings.ts
similarity index 96%
rename from ui/src/frontend/recording/recording_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recording_settings.ts
index c61b49d..e3058be 100644
--- a/ui/src/frontend/recording/recording_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_settings.ts
@@ -13,8 +13,8 @@
// limitations under the License.
import m from 'mithril';
-import {RecordMode} from '../../common/state';
-import {Slider} from '../record_widgets';
+import {RecordMode} from './state';
+import {Slider} from './record_widgets';
import {RecordingSectionAttrs} from './recording_sections';
import {assetSrc} from '../../base/assets';
diff --git a/ui/src/frontend/recording/recording_ui_utils.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_ui_utils.ts
similarity index 100%
rename from ui/src/frontend/recording/recording_ui_utils.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recording_ui_utils.ts
diff --git a/ui/src/frontend/recording/reset_interface_modal.ts b/ui/src/plugins/dev.perfetto.RecordTrace/reset_interface_modal.ts
similarity index 100%
rename from ui/src/frontend/recording/reset_interface_modal.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/reset_interface_modal.ts
diff --git a/ui/src/frontend/recording/reset_target_modal.ts b/ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts
similarity index 91%
rename from ui/src/frontend/recording/reset_target_modal.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts
index 4d3feb3..4d3d048 100644
--- a/ui/src/frontend/recording/reset_target_modal.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts
@@ -13,19 +13,19 @@
// limitations under the License.
import m from 'mithril';
-import {RecordingPageController} from '../../common/recordingV2/recording_page_controller';
+import {RecordingPageController} from './recordingV2/recording_page_controller';
import {
EXTENSION_URL,
RECORDING_MODAL_DIALOG_KEY,
-} from '../../common/recordingV2/recording_utils';
+} from './recordingV2/recording_utils';
import {
CHROME_TARGET_FACTORY,
ChromeTargetFactory,
-} from '../../common/recordingV2/target_factories/chrome_target_factory';
-import {targetFactoryRegistry} from '../../common/recordingV2/target_factory_registry';
-import {WebsocketMenuController} from '../../common/recordingV2/websocket_menu_controller';
+} from './recordingV2/target_factories/chrome_target_factory';
+import {targetFactoryRegistry} from './recordingV2/target_factory_registry';
+import {WebsocketMenuController} from './recordingV2/websocket_menu_controller';
import {closeModal, showModal} from '../../widgets/modal';
-import {CodeSnippet} from '../record_widgets';
+import {CodeSnippet} from './record_widgets';
import {RecordingMultipleChoice} from './recording_multiple_choice';
const RUN_WEBSOCKET_CMD =
diff --git a/ui/src/common/state.ts b/ui/src/plugins/dev.perfetto.RecordTrace/state.ts
similarity index 98%
rename from ui/src/common/state.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/state.ts
index 9c361f6..b94074b 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/state.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {RecordConfig} from '../controller/record_config_types';
+import {RecordConfig} from './record_config_types';
export const MAX_TIME = 180;
diff --git a/ui/src/core/trace_config_utils.ts b/ui/src/plugins/dev.perfetto.RecordTrace/trace_config_utils.ts
similarity index 96%
rename from ui/src/core/trace_config_utils.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/trace_config_utils.ts
index e05f711..c7697dd 100644
--- a/ui/src/core/trace_config_utils.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/trace_config_utils.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {EnableTracingRequest, TraceConfig} from '../protos';
+import {EnableTracingRequest, TraceConfig} from '../../protos';
// In this file are contained a few functions to simplify the proto parsing.
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/public/app.ts b/ui/src/public/app.ts
index 5dcd368..50def57 100644
--- a/ui/src/public/app.ts
+++ b/ui/src/public/app.ts
@@ -60,4 +60,12 @@
* Navigate to a new page.
*/
navigate(newHash: string): void;
+
+ openTraceFromFile(file: File): void;
+ openTraceFromUrl(url: string): void;
+ openTraceFromBuffer(args: {
+ buffer: ArrayBuffer;
+ title: string;
+ fileName: string;
+ }): void;
}
diff --git a/ui/src/public/trace.ts b/ui/src/public/trace.ts
index 6e60c79..95db546 100644
--- a/ui/src/public/trace.ts
+++ b/ui/src/public/trace.ts
@@ -96,3 +96,5 @@
export interface TraceAttrs {
trace: Trace;
}
+
+export const TRACE_SUFFIX = '.perfetto-trace';
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,
+ ),
);
}