Snap for 12901447 from e324242074e2e64a65e90a2933afd3ca4413554f to simpleperf-release
Change-Id: I038cae4aab50b05ea13108bfc17f216b0e5b973f
diff --git a/Android.bp b/Android.bp
index 77dc23c..84c9377 100644
--- a/Android.bp
+++ b/Android.bp
@@ -844,7 +844,6 @@
":perfetto_src_tracing_ipc_default_socket",
":perfetto_src_tracing_ipc_producer_producer",
":perfetto_src_tracing_ipc_service_service",
- ":perfetto_src_tracing_platform_impl",
":perfetto_src_tracing_service_service",
":perfetto_src_tracing_system_backend",
],
@@ -1019,7 +1018,6 @@
":perfetto_src_tracing_ipc_default_socket",
":perfetto_src_tracing_ipc_producer_producer",
":perfetto_src_tracing_ipc_service_service",
- ":perfetto_src_tracing_platform_impl",
":perfetto_src_tracing_service_service",
":perfetto_src_tracing_system_backend",
],
@@ -2531,6 +2529,7 @@
":perfetto_src_trace_processor_sqlite_sqlite",
":perfetto_src_trace_processor_storage_minimal",
":perfetto_src_trace_processor_storage_storage",
+ ":perfetto_src_trace_processor_tables_macros_internal",
":perfetto_src_trace_processor_tables_tables",
":perfetto_src_trace_processor_types_types",
":perfetto_src_trace_processor_util_build_id",
@@ -2588,7 +2587,6 @@
":perfetto_src_tracing_ipc_producer_producer",
":perfetto_src_tracing_ipc_producer_relay",
":perfetto_src_tracing_ipc_service_service",
- ":perfetto_src_tracing_platform_impl",
":perfetto_src_tracing_service_service",
":perfetto_src_tracing_system_backend",
":perfetto_src_tracing_test_api_test_support",
@@ -2737,7 +2735,6 @@
"perfetto_src_base_version_gen_h",
"perfetto_src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
- "perfetto_src_trace_processor_importers_proto_gen_cc_config_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_trace_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
@@ -2775,7 +2772,9 @@
"protos/perfetto/metrics/android/ad_services_metric.proto",
"protos/perfetto/metrics/android/android_anomaly_metric.proto",
"protos/perfetto/metrics/android/android_blocking_call.proto",
+ "protos/perfetto/metrics/android/android_blocking_call_per_frame.proto",
"protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+ "protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto",
"protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
"protos/perfetto/metrics/android/android_boot.proto",
"protos/perfetto/metrics/android/android_boot_unagg.proto",
@@ -5421,7 +5420,9 @@
"protos/perfetto/metrics/android/ad_services_metric.proto",
"protos/perfetto/metrics/android/android_anomaly_metric.proto",
"protos/perfetto/metrics/android/android_blocking_call.proto",
+ "protos/perfetto/metrics/android/android_blocking_call_per_frame.proto",
"protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+ "protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto",
"protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
"protos/perfetto/metrics/android/android_boot.proto",
"protos/perfetto/metrics/android/android_boot_unagg.proto",
@@ -5519,7 +5520,9 @@
"protos/perfetto/metrics/android/ad_services_metric.proto",
"protos/perfetto/metrics/android/android_anomaly_metric.proto",
"protos/perfetto/metrics/android/android_blocking_call.proto",
+ "protos/perfetto/metrics/android/android_blocking_call_per_frame.proto",
"protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+ "protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto",
"protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
"protos/perfetto/metrics/android/android_boot.proto",
"protos/perfetto/metrics/android/android_boot_unagg.proto",
@@ -5599,7 +5602,9 @@
"protos/perfetto/metrics/android/ad_services_metric.proto",
"protos/perfetto/metrics/android/android_anomaly_metric.proto",
"protos/perfetto/metrics/android/android_blocking_call.proto",
+ "protos/perfetto/metrics/android/android_blocking_call_per_frame.proto",
"protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+ "protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto",
"protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
"protos/perfetto/metrics/android/android_boot.proto",
"protos/perfetto/metrics/android/android_boot_unagg.proto",
@@ -5722,6 +5727,7 @@
"protos/perfetto/trace/android/android_game_intervention_list.proto",
"protos/perfetto/trace/android/android_log.proto",
"protos/perfetto/trace/android/android_system_property.proto",
+ "protos/perfetto/trace/android/bluetooth_trace.proto",
"protos/perfetto/trace/android/camera_event.proto",
"protos/perfetto/trace/android/frame_timeline_event.proto",
"protos/perfetto/trace/android/gpu_mem_event.proto",
@@ -5751,6 +5757,7 @@
"external/perfetto/protos/perfetto/trace/android/android_game_intervention_list.gen.cc",
"external/perfetto/protos/perfetto/trace/android/android_log.gen.cc",
"external/perfetto/protos/perfetto/trace/android/android_system_property.gen.cc",
+ "external/perfetto/protos/perfetto/trace/android/bluetooth_trace.gen.cc",
"external/perfetto/protos/perfetto/trace/android/camera_event.gen.cc",
"external/perfetto/protos/perfetto/trace/android/frame_timeline_event.gen.cc",
"external/perfetto/protos/perfetto/trace/android/gpu_mem_event.gen.cc",
@@ -5780,6 +5787,7 @@
"external/perfetto/protos/perfetto/trace/android/android_game_intervention_list.gen.h",
"external/perfetto/protos/perfetto/trace/android/android_log.gen.h",
"external/perfetto/protos/perfetto/trace/android/android_system_property.gen.h",
+ "external/perfetto/protos/perfetto/trace/android/bluetooth_trace.gen.h",
"external/perfetto/protos/perfetto/trace/android/camera_event.gen.h",
"external/perfetto/protos/perfetto/trace/android/frame_timeline_event.gen.h",
"external/perfetto/protos/perfetto/trace/android/gpu_mem_event.gen.h",
@@ -5802,6 +5810,7 @@
"protos/perfetto/trace/android/android_game_intervention_list.proto",
"protos/perfetto/trace/android/android_log.proto",
"protos/perfetto/trace/android/android_system_property.proto",
+ "protos/perfetto/trace/android/bluetooth_trace.proto",
"protos/perfetto/trace/android/camera_event.proto",
"protos/perfetto/trace/android/frame_timeline_event.proto",
"protos/perfetto/trace/android/gpu_mem_event.proto",
@@ -5830,6 +5839,7 @@
"external/perfetto/protos/perfetto/trace/android/android_game_intervention_list.pb.cc",
"external/perfetto/protos/perfetto/trace/android/android_log.pb.cc",
"external/perfetto/protos/perfetto/trace/android/android_system_property.pb.cc",
+ "external/perfetto/protos/perfetto/trace/android/bluetooth_trace.pb.cc",
"external/perfetto/protos/perfetto/trace/android/camera_event.pb.cc",
"external/perfetto/protos/perfetto/trace/android/frame_timeline_event.pb.cc",
"external/perfetto/protos/perfetto/trace/android/gpu_mem_event.pb.cc",
@@ -5858,6 +5868,7 @@
"external/perfetto/protos/perfetto/trace/android/android_game_intervention_list.pb.h",
"external/perfetto/protos/perfetto/trace/android/android_log.pb.h",
"external/perfetto/protos/perfetto/trace/android/android_system_property.pb.h",
+ "external/perfetto/protos/perfetto/trace/android/bluetooth_trace.pb.h",
"external/perfetto/protos/perfetto/trace/android/camera_event.pb.h",
"external/perfetto/protos/perfetto/trace/android/frame_timeline_event.pb.h",
"external/perfetto/protos/perfetto/trace/android/gpu_mem_event.pb.h",
@@ -6452,6 +6463,7 @@
"protos/perfetto/trace/android/android_game_intervention_list.proto",
"protos/perfetto/trace/android/android_log.proto",
"protos/perfetto/trace/android/android_system_property.proto",
+ "protos/perfetto/trace/android/bluetooth_trace.proto",
"protos/perfetto/trace/android/camera_event.proto",
"protos/perfetto/trace/android/frame_timeline_event.proto",
"protos/perfetto/trace/android/gpu_mem_event.proto",
@@ -6481,6 +6493,7 @@
"external/perfetto/protos/perfetto/trace/android/android_game_intervention_list.pbzero.cc",
"external/perfetto/protos/perfetto/trace/android/android_log.pbzero.cc",
"external/perfetto/protos/perfetto/trace/android/android_system_property.pbzero.cc",
+ "external/perfetto/protos/perfetto/trace/android/bluetooth_trace.pbzero.cc",
"external/perfetto/protos/perfetto/trace/android/camera_event.pbzero.cc",
"external/perfetto/protos/perfetto/trace/android/frame_timeline_event.pbzero.cc",
"external/perfetto/protos/perfetto/trace/android/gpu_mem_event.pbzero.cc",
@@ -6510,6 +6523,7 @@
"external/perfetto/protos/perfetto/trace/android/android_game_intervention_list.pbzero.h",
"external/perfetto/protos/perfetto/trace/android/android_log.pbzero.h",
"external/perfetto/protos/perfetto/trace/android/android_system_property.pbzero.h",
+ "external/perfetto/protos/perfetto/trace/android/bluetooth_trace.pbzero.h",
"external/perfetto/protos/perfetto/trace/android/camera_event.pbzero.h",
"external/perfetto/protos/perfetto/trace/android/frame_timeline_event.pbzero.h",
"external/perfetto/protos/perfetto/trace/android/gpu_mem_event.pbzero.h",
@@ -6753,6 +6767,7 @@
"protos/perfetto/trace/android/android_game_intervention_list.proto",
"protos/perfetto/trace/android/android_log.proto",
"protos/perfetto/trace/android/android_system_property.proto",
+ "protos/perfetto/trace/android/bluetooth_trace.proto",
"protos/perfetto/trace/android/camera_event.proto",
"protos/perfetto/trace/android/frame_timeline_event.proto",
"protos/perfetto/trace/android/gpu_mem_event.proto",
@@ -12807,10 +12822,12 @@
"src/trace_processor/importers/proto/graphics_frame_event_parser.cc",
"src/trace_processor/importers/proto/heap_graph_module.cc",
"src/trace_processor/importers/proto/heap_graph_tracker.cc",
+ "src/trace_processor/importers/proto/jit_tracker.cc",
"src/trace_processor/importers/proto/metadata_module.cc",
"src/trace_processor/importers/proto/pigweed_detokenizer.cc",
"src/trace_processor/importers/proto/pixel_modem_module.cc",
"src/trace_processor/importers/proto/pixel_modem_parser.cc",
+ "src/trace_processor/importers/proto/profile_module.cc",
"src/trace_processor/importers/proto/statsd_module.cc",
"src/trace_processor/importers/proto/string_encoding_utils.cc",
"src/trace_processor/importers/proto/system_probes_module.cc",
@@ -12853,21 +12870,6 @@
],
}
-// GN: //src/trace_processor/importers/proto:gen_cc_config_descriptor
-genrule {
- name: "perfetto_src_trace_processor_importers_proto_gen_cc_config_descriptor",
- srcs: [
- ":perfetto_protos_perfetto_config_descriptor",
- ],
- cmd: "$(location tools/gen_cc_proto_descriptor.py) --gen_dir=$(genDir) --cpp_out=$(out) $(in)",
- out: [
- "src/trace_processor/importers/proto/config.descriptor.h",
- ],
- tool_files: [
- "tools/gen_cc_proto_descriptor.py",
- ],
-}
-
// GN: //src/trace_processor/importers/proto:gen_cc_statsd_atoms_descriptor
genrule {
name: "perfetto_src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
@@ -12923,7 +12925,6 @@
"src/trace_processor/importers/proto/chrome_system_probes_module.cc",
"src/trace_processor/importers/proto/chrome_system_probes_parser.cc",
"src/trace_processor/importers/proto/default_modules.cc",
- "src/trace_processor/importers/proto/jit_tracker.cc",
"src/trace_processor/importers/proto/memory_tracker_snapshot_module.cc",
"src/trace_processor/importers/proto/memory_tracker_snapshot_parser.cc",
"src/trace_processor/importers/proto/metadata_minimal_module.cc",
@@ -12932,7 +12933,6 @@
"src/trace_processor/importers/proto/packet_analyzer.cc",
"src/trace_processor/importers/proto/packet_sequence_state_generation.cc",
"src/trace_processor/importers/proto/perf_sample_tracker.cc",
- "src/trace_processor/importers/proto/profile_module.cc",
"src/trace_processor/importers/proto/profile_packet_sequence_state.cc",
"src/trace_processor/importers/proto/profile_packet_utils.cc",
"src/trace_processor/importers/proto/proto_trace_parser_impl.cc",
@@ -13139,6 +13139,7 @@
"src/trace_processor/metrics/sql/android/android_batt.sql",
"src/trace_processor/metrics/sql/android/android_binder.sql",
"src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql",
+ "src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_per_frame_metric.sql",
"src/trace_processor/metrics/sql/android/android_blocking_calls_unagg.sql",
"src/trace_processor/metrics/sql/android/android_boot.sql",
"src/trace_processor/metrics/sql/android/android_boot_unagg.sql",
@@ -13471,10 +13472,8 @@
"src/trace_processor/perfetto_sql/intrinsics/table_functions/descendant.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_annotated_stack.cc",
- "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flamegraph.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.cc",
- "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_slice_layout.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/flamegraph_construction_algorithms.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.cc",
@@ -13532,7 +13531,6 @@
"src/trace_processor/perfetto_sql/intrinsics/table_functions/ancestor_unittest.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/connected_flow_unittest.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/descendant_unittest.cc",
- "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur_unittest.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice_unittest.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_slice_layout_unittest.cc",
],
@@ -13654,6 +13652,7 @@
"src/trace_processor/perfetto_sql/stdlib/android/winscope/inputmethod.sql",
"src/trace_processor/perfetto_sql/stdlib/android/winscope/viewcapture.sql",
"src/trace_processor/perfetto_sql/stdlib/android/winscope/windowmanager.sql",
+ "src/trace_processor/perfetto_sql/stdlib/appleos/instruments/samples.sql",
"src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql",
"src/trace_processor/perfetto_sql/stdlib/chrome/**/*.sql",
"src/trace_processor/perfetto_sql/stdlib/counters/global_tracks.sql",
@@ -13719,11 +13718,12 @@
"src/trace_processor/perfetto_sql/stdlib/viz/summary/threads.sql",
"src/trace_processor/perfetto_sql/stdlib/viz/summary/threads_w_processes.sql",
"src/trace_processor/perfetto_sql/stdlib/viz/summary/trace.sql",
- "src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql",
+ "src/trace_processor/perfetto_sql/stdlib/viz/summary/track_event.sql",
"src/trace_processor/perfetto_sql/stdlib/viz/threads.sql",
"src/trace_processor/perfetto_sql/stdlib/wattson/arm_dsu.sql",
"src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq.sql",
"src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq_idle.sql",
+ "src/trace_processor/perfetto_sql/stdlib/wattson/cpu_hotplug.sql",
"src/trace_processor/perfetto_sql/stdlib/wattson/cpu_idle.sql",
"src/trace_processor/perfetto_sql/stdlib/wattson/cpu_split.sql",
"src/trace_processor/perfetto_sql/stdlib/wattson/curves/device.sql",
@@ -13869,6 +13869,14 @@
],
}
+// GN: //src/trace_processor/tables:macros_internal
+filegroup {
+ name: "perfetto_src_trace_processor_tables_macros_internal",
+ srcs: [
+ "src/trace_processor/tables/macros_internal.cc",
+ ],
+}
+
// GN: //src/trace_processor/tables:py_tables_unittest
genrule {
name: "perfetto_src_trace_processor_tables_py_tables_unittest",
@@ -13901,7 +13909,6 @@
filegroup {
name: "perfetto_src_trace_processor_tables_tables",
srcs: [
- "src/trace_processor/tables/macros_internal.cc",
"src/trace_processor/tables/table_destructors.cc",
],
}
@@ -14836,6 +14843,8 @@
"src/tracing/internal/track_event_internal.cc",
"src/tracing/internal/track_event_interned_fields.cc",
"src/tracing/platform.cc",
+ "src/tracing/platform_posix.cc",
+ "src/tracing/platform_windows.cc",
"src/tracing/traced_value.cc",
"src/tracing/tracing.cc",
"src/tracing/tracing_policy.cc",
@@ -14969,15 +14978,6 @@
],
}
-// GN: //src/tracing:platform_impl
-filegroup {
- name: "perfetto_src_tracing_platform_impl",
- srcs: [
- "src/tracing/platform_posix.cc",
- "src/tracing/platform_windows.cc",
- ],
-}
-
// GN: //src/tracing/service:service
filegroup {
name: "perfetto_src_tracing_service_service",
@@ -15164,6 +15164,7 @@
"protos/perfetto/trace/android/android_game_intervention_list.proto",
"protos/perfetto/trace/android/android_log.proto",
"protos/perfetto/trace/android/android_system_property.proto",
+ "protos/perfetto/trace/android/bluetooth_trace.proto",
"protos/perfetto/trace/android/camera_event.proto",
"protos/perfetto/trace/android/frame_timeline_event.proto",
"protos/perfetto/trace/android/gpu_mem_event.proto",
@@ -15783,6 +15784,7 @@
":perfetto_src_trace_processor_sqlite_unittests",
":perfetto_src_trace_processor_storage_minimal",
":perfetto_src_trace_processor_storage_storage",
+ ":perfetto_src_trace_processor_tables_macros_internal",
":perfetto_src_trace_processor_tables_tables",
":perfetto_src_trace_processor_tables_unittests",
":perfetto_src_trace_processor_top_level_unittests",
@@ -15866,7 +15868,6 @@
":perfetto_src_tracing_ipc_producer_producer",
":perfetto_src_tracing_ipc_producer_relay",
":perfetto_src_tracing_ipc_unittests",
- ":perfetto_src_tracing_platform_impl",
":perfetto_src_tracing_service_service",
":perfetto_src_tracing_service_unittests",
":perfetto_src_tracing_service_zlib_compressor",
@@ -16025,7 +16026,6 @@
"perfetto_src_trace_processor_gen_cc_test_messages_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
- "perfetto_src_trace_processor_importers_proto_gen_cc_config_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_trace_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
@@ -16470,6 +16470,7 @@
"protos/perfetto/trace/android/android_system_property.proto",
"protos/perfetto/trace/android/app/statusbarmanager.proto",
"protos/perfetto/trace/android/app/window_configuration.proto",
+ "protos/perfetto/trace/android/bluetooth_trace.proto",
"protos/perfetto/trace/android/camera_event.proto",
"protos/perfetto/trace/android/content/activityinfo.proto",
"protos/perfetto/trace/android/content/configuration.proto",
@@ -16843,6 +16844,7 @@
":perfetto_src_trace_processor_sqlite_sqlite",
":perfetto_src_trace_processor_storage_minimal",
":perfetto_src_trace_processor_storage_storage",
+ ":perfetto_src_trace_processor_tables_macros_internal",
":perfetto_src_trace_processor_tables_tables",
":perfetto_src_trace_processor_types_types",
":perfetto_src_trace_processor_util_build_id",
@@ -16914,7 +16916,6 @@
"perfetto_src_base_version_gen_h",
"perfetto_src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
- "perfetto_src_trace_processor_importers_proto_gen_cc_config_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_trace_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
@@ -17057,6 +17058,7 @@
":perfetto_src_trace_processor_sorter_sorter",
":perfetto_src_trace_processor_storage_minimal",
":perfetto_src_trace_processor_storage_storage",
+ ":perfetto_src_trace_processor_tables_macros_internal",
":perfetto_src_trace_processor_tables_tables",
":perfetto_src_trace_processor_types_types",
":perfetto_src_trace_processor_util_build_id",
@@ -17270,6 +17272,7 @@
":perfetto_src_trace_processor_sqlite_sqlite",
":perfetto_src_trace_processor_storage_minimal",
":perfetto_src_trace_processor_storage_storage",
+ ":perfetto_src_trace_processor_tables_macros_internal",
":perfetto_src_trace_processor_tables_tables",
":perfetto_src_trace_processor_types_types",
":perfetto_src_trace_processor_util_build_id",
@@ -17347,7 +17350,6 @@
"perfetto_src_base_version_gen_h",
"perfetto_src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
- "perfetto_src_trace_processor_importers_proto_gen_cc_config_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_trace_descriptor",
"perfetto_src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
diff --git a/BUILD b/BUILD
index 0af9520..e9e13ab 100644
--- a/BUILD
+++ b/BUILD
@@ -173,7 +173,6 @@
":src_tracing_ipc_default_socket",
":src_tracing_ipc_producer_producer",
":src_tracing_ipc_service_service",
- ":src_tracing_platform_impl",
":src_tracing_service_service",
":src_tracing_system_backend",
],
@@ -392,6 +391,7 @@
":src_trace_processor_sqlite_sqlite",
":src_trace_processor_storage_minimal",
":src_trace_processor_storage_storage",
+ ":src_trace_processor_tables_macros_internal",
":src_trace_processor_tables_tables",
":src_trace_processor_tables_tables_python",
":src_trace_processor_types_types",
@@ -493,7 +493,6 @@
":src_trace_processor_containers_containers",
":src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
":src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
- ":src_trace_processor_importers_proto_gen_cc_config_descriptor",
":src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
":src_trace_processor_importers_proto_gen_cc_trace_descriptor",
":src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
@@ -1888,6 +1887,7 @@
"src/trace_processor/importers/etm/mapping_version.cc",
"src/trace_processor/importers/etm/mapping_version.h",
"src/trace_processor/importers/etm/opencsd.h",
+ "src/trace_processor/importers/etm/sql_values.h",
"src/trace_processor/importers/etm/storage_handle.cc",
"src/trace_processor/importers/etm/storage_handle.h",
"src/trace_processor/importers/etm/target_memory.cc",
@@ -2272,6 +2272,8 @@
"src/trace_processor/importers/proto/heap_graph_module.h",
"src/trace_processor/importers/proto/heap_graph_tracker.cc",
"src/trace_processor/importers/proto/heap_graph_tracker.h",
+ "src/trace_processor/importers/proto/jit_tracker.cc",
+ "src/trace_processor/importers/proto/jit_tracker.h",
"src/trace_processor/importers/proto/metadata_module.cc",
"src/trace_processor/importers/proto/metadata_module.h",
"src/trace_processor/importers/proto/pigweed_detokenizer.cc",
@@ -2280,6 +2282,8 @@
"src/trace_processor/importers/proto/pixel_modem_module.h",
"src/trace_processor/importers/proto/pixel_modem_parser.cc",
"src/trace_processor/importers/proto/pixel_modem_parser.h",
+ "src/trace_processor/importers/proto/profile_module.cc",
+ "src/trace_processor/importers/proto/profile_module.h",
"src/trace_processor/importers/proto/statsd_module.cc",
"src/trace_processor/importers/proto/statsd_module.h",
"src/trace_processor/importers/proto/string_encoding_utils.cc",
@@ -2323,17 +2327,6 @@
],
)
-# GN target: //src/trace_processor/importers/proto:gen_cc_config_descriptor
-perfetto_cc_proto_descriptor(
- name = "src_trace_processor_importers_proto_gen_cc_config_descriptor",
- deps = [
- ":protos_perfetto_config_descriptor",
- ],
- outs = [
- "src/trace_processor/importers/proto/config.descriptor.h",
- ],
-)
-
# GN target: //src/trace_processor/importers/proto:gen_cc_statsd_atoms_descriptor
perfetto_cc_proto_descriptor(
name = "src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
@@ -2383,8 +2376,6 @@
"src/trace_processor/importers/proto/chrome_system_probes_parser.h",
"src/trace_processor/importers/proto/default_modules.cc",
"src/trace_processor/importers/proto/default_modules.h",
- "src/trace_processor/importers/proto/jit_tracker.cc",
- "src/trace_processor/importers/proto/jit_tracker.h",
"src/trace_processor/importers/proto/memory_tracker_snapshot_module.cc",
"src/trace_processor/importers/proto/memory_tracker_snapshot_module.h",
"src/trace_processor/importers/proto/memory_tracker_snapshot_parser.cc",
@@ -2401,8 +2392,6 @@
"src/trace_processor/importers/proto/packet_sequence_state_generation.cc",
"src/trace_processor/importers/proto/perf_sample_tracker.cc",
"src/trace_processor/importers/proto/perf_sample_tracker.h",
- "src/trace_processor/importers/proto/profile_module.cc",
- "src/trace_processor/importers/proto/profile_module.h",
"src/trace_processor/importers/proto/profile_packet_sequence_state.cc",
"src/trace_processor/importers/proto/profile_packet_sequence_state.h",
"src/trace_processor/importers/proto/profile_packet_utils.cc",
@@ -2495,6 +2484,7 @@
"src/trace_processor/metrics/sql/android/android_batt.sql",
"src/trace_processor/metrics/sql/android/android_binder.sql",
"src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql",
+ "src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_per_frame_metric.sql",
"src/trace_processor/metrics/sql/android/android_blocking_calls_unagg.sql",
"src/trace_processor/metrics/sql/android/android_boot.sql",
"src/trace_processor/metrics/sql/android/android_boot_unagg.sql",
@@ -2870,6 +2860,7 @@
name = "src_trace_processor_perfetto_sql_intrinsics_operators_etm_hdr",
srcs = [
"src/trace_processor/perfetto_sql/intrinsics/operators/etm_decode_trace_vtable.h",
+ "src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.h",
],
)
@@ -2878,6 +2869,7 @@
name = "src_trace_processor_perfetto_sql_intrinsics_operators_etm_impl",
srcs = [
"src/trace_processor/perfetto_sql/intrinsics/operators/etm_decode_trace_vtable.cc",
+ "src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.cc",
],
)
@@ -2919,14 +2911,10 @@
"src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.h",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_annotated_stack.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_annotated_stack.h",
- "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.cc",
- "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.h",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flamegraph.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flamegraph.h",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.h",
- "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.cc",
- "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.h",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_slice_layout.cc",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_slice_layout.h",
"src/trace_processor/perfetto_sql/intrinsics/table_functions/flamegraph_construction_algorithms.cc",
@@ -3134,6 +3122,19 @@
],
)
+# GN target: //src/trace_processor/perfetto_sql/stdlib/appleos/instruments:instruments
+perfetto_filegroup(
+ name = "src_trace_processor_perfetto_sql_stdlib_appleos_instruments_instruments",
+ srcs = [
+ "src/trace_processor/perfetto_sql/stdlib/appleos/instruments/samples.sql",
+ ],
+)
+
+# GN target: //src/trace_processor/perfetto_sql/stdlib/appleos:appleos
+perfetto_filegroup(
+ name = "src_trace_processor_perfetto_sql_stdlib_appleos_appleos",
+)
+
# GN target: //src/trace_processor/perfetto_sql/stdlib/callstacks:callstacks
perfetto_filegroup(
name = "src_trace_processor_perfetto_sql_stdlib_callstacks_callstacks",
@@ -3351,7 +3352,7 @@
"src/trace_processor/perfetto_sql/stdlib/viz/summary/threads.sql",
"src/trace_processor/perfetto_sql/stdlib/viz/summary/threads_w_processes.sql",
"src/trace_processor/perfetto_sql/stdlib/viz/summary/trace.sql",
- "src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql",
+ "src/trace_processor/perfetto_sql/stdlib/viz/summary/track_event.sql",
],
)
@@ -3372,6 +3373,7 @@
"src/trace_processor/perfetto_sql/stdlib/wattson/arm_dsu.sql",
"src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq.sql",
"src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq_idle.sql",
+ "src/trace_processor/perfetto_sql/stdlib/wattson/cpu_hotplug.sql",
"src/trace_processor/perfetto_sql/stdlib/wattson/cpu_idle.sql",
"src/trace_processor/perfetto_sql/stdlib/wattson/cpu_split.sql",
"src/trace_processor/perfetto_sql/stdlib/wattson/curves/device.sql",
@@ -3400,6 +3402,8 @@
":src_trace_processor_perfetto_sql_stdlib_android_memory_memory",
":src_trace_processor_perfetto_sql_stdlib_android_startup_startup",
":src_trace_processor_perfetto_sql_stdlib_android_winscope_winscope",
+ ":src_trace_processor_perfetto_sql_stdlib_appleos_appleos",
+ ":src_trace_processor_perfetto_sql_stdlib_appleos_instruments_instruments",
":src_trace_processor_perfetto_sql_stdlib_callstacks_callstacks",
":src_trace_processor_perfetto_sql_stdlib_chrome_chrome_sql",
":src_trace_processor_perfetto_sql_stdlib_counters_counters",
@@ -3538,12 +3542,19 @@
],
)
+# GN target: //src/trace_processor/tables:macros_internal
+perfetto_filegroup(
+ name = "src_trace_processor_tables_macros_internal",
+ srcs = [
+ "src/trace_processor/tables/macros_internal.cc",
+ "src/trace_processor/tables/macros_internal.h",
+ ],
+)
+
# GN target: //src/trace_processor/tables:tables
perfetto_filegroup(
name = "src_trace_processor_tables_tables",
srcs = [
- "src/trace_processor/tables/macros_internal.cc",
- "src/trace_processor/tables/macros_internal.h",
"src/trace_processor/tables/table_destructors.cc",
],
)
@@ -4293,6 +4304,8 @@
"src/tracing/internal/track_event_internal.cc",
"src/tracing/internal/track_event_interned_fields.cc",
"src/tracing/platform.cc",
+ "src/tracing/platform_posix.cc",
+ "src/tracing/platform_windows.cc",
"src/tracing/traced_value.cc",
"src/tracing/tracing.cc",
"src/tracing/tracing_policy.cc",
@@ -4320,15 +4333,6 @@
],
)
-# GN target: //src/tracing:platform_impl
-perfetto_filegroup(
- name = "src_tracing_platform_impl",
- srcs = [
- "src/tracing/platform_posix.cc",
- "src/tracing/platform_windows.cc",
- ],
-)
-
# GN target: //src/tracing:system_backend
perfetto_filegroup(
name = "src_tracing_system_backend",
@@ -5276,7 +5280,9 @@
"protos/perfetto/metrics/android/ad_services_metric.proto",
"protos/perfetto/metrics/android/android_anomaly_metric.proto",
"protos/perfetto/metrics/android/android_blocking_call.proto",
+ "protos/perfetto/metrics/android/android_blocking_call_per_frame.proto",
"protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+ "protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto",
"protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
"protos/perfetto/metrics/android/android_boot.proto",
"protos/perfetto/metrics/android/android_boot_unagg.proto",
@@ -5500,6 +5506,7 @@
"protos/perfetto/trace/android/android_game_intervention_list.proto",
"protos/perfetto/trace/android/android_log.proto",
"protos/perfetto/trace/android/android_system_property.proto",
+ "protos/perfetto/trace/android/bluetooth_trace.proto",
"protos/perfetto/trace/android/camera_event.proto",
"protos/perfetto/trace/android/frame_timeline_event.proto",
"protos/perfetto/trace/android/gpu_mem_event.proto",
@@ -6524,7 +6531,6 @@
":src_tracing_ipc_default_socket",
":src_tracing_ipc_producer_producer",
":src_tracing_ipc_service_service",
- ":src_tracing_platform_impl",
":src_tracing_service_service",
":src_tracing_system_backend",
],
@@ -6775,6 +6781,7 @@
":src_trace_processor_sqlite_sqlite",
":src_trace_processor_storage_minimal",
":src_trace_processor_storage_storage",
+ ":src_trace_processor_tables_macros_internal",
":src_trace_processor_tables_tables",
":src_trace_processor_tables_tables_python",
":src_trace_processor_types_types",
@@ -6876,7 +6883,6 @@
":src_trace_processor_containers_containers",
":src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
":src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
- ":src_trace_processor_importers_proto_gen_cc_config_descriptor",
":src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
":src_trace_processor_importers_proto_gen_cc_trace_descriptor",
":src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
@@ -6994,6 +7000,7 @@
":src_trace_processor_sqlite_sqlite",
":src_trace_processor_storage_minimal",
":src_trace_processor_storage_storage",
+ ":src_trace_processor_tables_macros_internal",
":src_trace_processor_tables_tables",
":src_trace_processor_tables_tables_python",
":src_trace_processor_types_types",
@@ -7080,7 +7087,6 @@
":src_trace_processor_containers_containers",
":src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
":src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
- ":src_trace_processor_importers_proto_gen_cc_config_descriptor",
":src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
":src_trace_processor_importers_proto_gen_cc_trace_descriptor",
":src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
@@ -7196,6 +7202,7 @@
":src_trace_processor_sqlite_sqlite",
":src_trace_processor_storage_minimal",
":src_trace_processor_storage_storage",
+ ":src_trace_processor_tables_macros_internal",
":src_trace_processor_tables_tables",
":src_trace_processor_tables_tables_python",
":src_trace_processor_types_types",
@@ -7284,7 +7291,6 @@
":src_trace_processor_containers_containers",
":src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
":src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
- ":src_trace_processor_importers_proto_gen_cc_config_descriptor",
":src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
":src_trace_processor_importers_proto_gen_cc_trace_descriptor",
":src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
diff --git a/BUILD.gn b/BUILD.gn
index b495c20..41d8014 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -283,7 +283,6 @@
public_deps = [
"gn:default_deps",
"src/tracing:client_api",
- "src/tracing:platform_impl",
]
sources = [ "include/perfetto/tracing.h" ]
assert_no_deps = [ "gn:protobuf_lite" ]
@@ -300,7 +299,6 @@
deps = [
"src/trace_processor/importers/memory_tracker:graph_processor",
"src/tracing:client_api",
- "src/tracing:platform_impl",
"src/tracing/core",
]
configs -= [ "//build/config/compiler:chromium_code" ] # nogncheck
diff --git a/CHANGELOG b/CHANGELOG
index 567fc95..d61826e 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,18 @@
Unreleased:
Tracing service and probes:
+ *
+ SQL Standard library:
+ *
+ Trace Processor:
+ *
+ UI:
+ *
+ SDK:
+ *
+
+
+v49.0 - 2025-01-06:
+ Tracing service and probes:
* Add `--clone-by-name` to the perfetto command line. This allows cloning a
tracing session by its unique_session_name.
* Fixed a bug that would delay the trace start acknowledgement, resulting
@@ -46,11 +59,22 @@
TIMESTAMP and DURATION refers to time columns in nanoseconds. ID column
is a primary key for the column and JOINID is referencing ID
column of other table.
- Trace Processor:
- *
+ * Removed the `experimental_sched_upid` table. Prefer the joining `sched`
+ and `thread` tables directly instead.
+ * Removed the experimental_counter_dur table. Prefer using the
+ `counter_leading_intervals` macro from the `counters.intervals` standard
+ library module.
UI:
* Introduced `Open table:` command which would open any Perfetto Standard
Library table in a new tab.
+ * Fixed behaviour of sorting and nesting of track event tracks for counter
+ tracks, thread tracks and process tracks.
+ * Various improvements to timeline rendering performance.
+ * Added workspace switcher and menu to move tracks between workspaces.
+ * Completely overhauled recording page.
+ * Improved area selection UX.
+ * Improved fuzzy search.
+ * Hide 'Open with legacy UI' by default.
SDK:
* Added `NamedTrack`, it allows creating arbitrarily named tracks.
diff --git a/OWNERS b/OWNERS
index 1303764..4dca898 100644
--- a/OWNERS
+++ b/OWNERS
@@ -38,3 +38,4 @@
eseckler@chromium.org
nuskos@chromium.org
skyostil@chromium.org
+include platform/system/core:main:/janitors/OWNERS #{LAST_RESORT_SUGGESTION}
diff --git a/docs/instrumentation/tracing-sdk.md b/docs/instrumentation/tracing-sdk.md
index a3b7c85a..f9164e0 100644
--- a/docs/instrumentation/tracing-sdk.md
+++ b/docs/instrumentation/tracing-sdk.md
@@ -29,7 +29,7 @@
To start using the Client API, first check out the latest SDK release:
```bash
-git clone https://android.googlesource.com/platform/external/perfetto -b v48.1
+git clone https://android.googlesource.com/platform/external/perfetto -b v49.0
```
The SDK consists of two files, `sdk/perfetto.h` and `sdk/perfetto.cc`. These are
diff --git a/examples/sdk/README.md b/examples/sdk/README.md
index 042b7b5..3e7a88a 100644
--- a/examples/sdk/README.md
+++ b/examples/sdk/README.md
@@ -15,7 +15,7 @@
First, check out the latest Perfetto release:
```bash
-git clone https://android.googlesource.com/platform/external/perfetto -b v48.1
+git clone https://android.googlesource.com/platform/external/perfetto -b v49.0
```
Then, build using CMake:
diff --git a/gn/standalone/wasm_typescript_declaration.d.ts b/gn/standalone/wasm_typescript_declaration.d.ts
index 3f8547d..6e27ebb 100644
--- a/gn/standalone/wasm_typescript_declaration.d.ts
+++ b/gn/standalone/wasm_typescript_declaration.d.ts
@@ -64,5 +64,6 @@
printErr(s: string): void;
onRuntimeInitialized(): void;
onAbort?(): void;
+ wasmBinary ?: ArrayBuffer;
}
}
diff --git a/include/perfetto/ext/trace_processor/export_json.h b/include/perfetto/ext/trace_processor/export_json.h
index 23119bf..f115a2f 100644
--- a/include/perfetto/ext/trace_processor/export_json.h
+++ b/include/perfetto/ext/trace_processor/export_json.h
@@ -44,12 +44,12 @@
OutputWriter();
virtual ~OutputWriter();
- virtual util::Status AppendString(const std::string&) = 0;
+ virtual base::Status AppendString(const std::string&) = 0;
};
// Public for Chrome. Exports the trace loaded in TraceProcessorStorage to json,
// applying argument, metadata and label filtering using the callbacks.
-util::Status PERFETTO_EXPORT_COMPONENT
+base::Status PERFETTO_EXPORT_COMPONENT
ExportJson(TraceProcessorStorage*,
OutputWriter*,
ArgumentFilterPredicate = nullptr,
diff --git a/include/perfetto/trace_processor/basic_types.h b/include/perfetto/trace_processor/basic_types.h
index 716e73a..d572459 100644
--- a/include/perfetto/trace_processor/basic_types.h
+++ b/include/perfetto/trace_processor/basic_types.h
@@ -148,13 +148,17 @@
SortingMode sorting_mode = SortingMode::kDefaultHeuristics;
// When set to false, this option makes the trace processor not include ftrace
- // events in the raw table; this makes converting events back to the systrace
- // text format impossible. On the other hand, it also saves ~50% of memory
- // usage of trace processor. For reference, Studio intends to use this option.
+ // events in the ftrace_event table; this makes converting events back to the
+ // systrace text format impossible. On the other hand, it also saves ~50% of
+ // memory usage of trace processor. For reference, Studio intends to use this
+ // option.
//
- // Note: "generic" ftrace events will be parsed into the raw table even if
- // this flag is false and all other events which parse into the raw table are
- // unaffected by this flag.
+ // Note: "generic" ftrace events will be parsed into the ftrace_event table
+ // even if this flag is false.
+ //
+ // Note: this option should really be named
+ // `ingest_ftrace_in_ftrace_event_table` as the use of the `raw` table is
+ // deprecated.
bool ingest_ftrace_in_raw_table = true;
// Indicates the event which should be used as a marker to drop ftrace data in
diff --git a/include/perfetto/trace_processor/iterator.h b/include/perfetto/trace_processor/iterator.h
index 6ab2442..ec41458 100644
--- a/include/perfetto/trace_processor/iterator.h
+++ b/include/perfetto/trace_processor/iterator.h
@@ -88,7 +88,7 @@
std::string LastStatementSql();
// Returns the status of the iterator.
- util::Status Status();
+ base::Status Status();
private:
friend class QueryResultSerializer;
diff --git a/include/perfetto/trace_processor/read_trace.h b/include/perfetto/trace_processor/read_trace.h
index 1b8ab12..f60254c 100644
--- a/include/perfetto/trace_processor/read_trace.h
+++ b/include/perfetto/trace_processor/read_trace.h
@@ -29,13 +29,13 @@
class TraceProcessor;
-util::Status PERFETTO_EXPORT_COMPONENT ReadTrace(
+base::Status PERFETTO_EXPORT_COMPONENT ReadTrace(
TraceProcessor* tp,
const char* filename,
const std::function<void(uint64_t parsed_size)>& progress_callback =
[](uint64_t) {});
-util::Status PERFETTO_EXPORT_COMPONENT
+base::Status PERFETTO_EXPORT_COMPONENT
DecompressTrace(const uint8_t* data, size_t size, std::vector<uint8_t>* output);
} // namespace trace_processor
diff --git a/include/perfetto/trace_processor/trace_processor.h b/include/perfetto/trace_processor/trace_processor.h
index 1886ee2f..07c0a12 100644
--- a/include/perfetto/trace_processor/trace_processor.h
+++ b/include/perfetto/trace_processor/trace_processor.h
@@ -17,22 +17,21 @@
#ifndef INCLUDE_PERFETTO_TRACE_PROCESSOR_TRACE_PROCESSOR_H_
#define INCLUDE_PERFETTO_TRACE_PROCESSOR_TRACE_PROCESSOR_H_
+#include <cstddef>
+#include <cstdint>
#include <memory>
#include <string>
#include <vector>
-#include "perfetto/base/build_config.h"
#include "perfetto/base/export.h"
#include "perfetto/base/status.h"
#include "perfetto/trace_processor/basic_types.h"
#include "perfetto/trace_processor/iterator.h"
#include "perfetto/trace_processor/metatrace_config.h"
-#include "perfetto/trace_processor/status.h"
#include "perfetto/trace_processor/trace_blob_view.h"
#include "perfetto/trace_processor/trace_processor_storage.h"
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
// Extends TraceProcessorStorage to support execution of SQL queries on loaded
// traces. See TraceProcessorStorage for parsing of trace files.
@@ -47,6 +46,10 @@
~TraceProcessor() override;
+ // =================================================================
+ // | PerfettoSQL related functionality starts here |
+ // =================================================================
+
// Executes the SQL on the loaded portion of the trace.
//
// More than one SQL statement can be passed to this function; all but the
@@ -68,6 +71,69 @@
// name.
virtual base::Status RegisterSqlPackage(SqlPackage) = 0;
+ // Deprecated. Use |RegisterSqlPackage()| instead, which is identical in
+ // functionality to |RegisterSqlModule()| and the only difference is in
+ // the argument, which is directly translatable to |SqlPackage|.
+ virtual base::Status RegisterSqlModule(SqlModule) = 0;
+
+ // =================================================================
+ // | Metatracing related functionality starts here |
+ // =================================================================
+
+ // Enables "meta-tracing" of trace processor.
+ // Metatracing involves tracing trace processor itself to root-cause
+ // performace issues in trace processor. See |DisableAndReadMetatrace| for
+ // more information on the format of the metatrace.
+ using MetatraceConfig = metatrace::MetatraceConfig;
+ using MetatraceCategories = metatrace::MetatraceCategories;
+ virtual void EnableMetatrace(MetatraceConfig config = {}) = 0;
+
+ // Disables "meta-tracing" of trace processor and writes the trace as a
+ // sequence of |TracePackets| into |trace_proto| returning the status of this
+ // read.
+ virtual base::Status DisableAndReadMetatrace(
+ std::vector<uint8_t>* trace_proto) = 0;
+
+ // =================================================================
+ // | Advanced functionality starts here |
+ // =================================================================
+
+ // Sets/returns the name of the currently loaded trace or an empty string if
+ // no trace is fully loaded yet. This has no effect on the Trace Processor
+ // functionality and is used for UI purposes only.
+ // The returned name is NOT a path and will contain extra text w.r.t. the
+ // argument originally passed to SetCurrentTraceName(), e.g., "file (42 MB)".
+ virtual std::string GetCurrentTraceName() = 0;
+ virtual void SetCurrentTraceName(const std::string&) = 0;
+
+ // Registers the contents of a file.
+ // This method can be used to pass out of band data to the trace processor
+ // which can be used by importers to do some advanced processing. For example
+ // if you pass binaries these are used to decode ETM traces.
+ // Registering the same file twice will return an error.
+ virtual base::Status RegisterFileContent(const std::string& path,
+ TraceBlobView content) = 0;
+
+ // Interrupts the current query. Typically used by Ctrl-C handler.
+ virtual void InterruptQuery() = 0;
+
+ // Restores Trace Processor to its pristine state. It preserves the built-in
+ // tables/views/functions created by the ingestion process. Returns the number
+ // of objects created in runtime that has been deleted.
+ // NOTE: No Iterators can active when called.
+ virtual size_t RestoreInitialTables() = 0;
+
+ // =================================================================
+ // | Trace-based metrics (v1) related functionality starts here |
+ // =================================================================
+ //
+ // WARNING: The metrics v1 system is "soft" deprecated: no new metrics are
+ // allowed but we still fully support any existing metrics written using this
+ // system.
+ //
+ // If possible, prefer using the metrics v2 methods above for any new
+ // usecases.
+
// Registers a metric at the given path which will run the specified SQL.
virtual base::Status RegisterMetric(const std::string& path,
const std::string& sql) = 0;
@@ -105,58 +171,13 @@
MetricResultFormat format,
std::string* metrics_string) = 0;
- // Interrupts the current query. Typically used by Ctrl-C handler.
- virtual void InterruptQuery() = 0;
-
- // Restores Trace Processor to its pristine state. It preserves the built-in
- // tables/views/functions created by the ingestion process. Returns the number
- // of objects created in runtime that has been deleted.
- // NOTE: No Iterators can active when called.
- virtual size_t RestoreInitialTables() = 0;
-
- // Sets/returns the name of the currently loaded trace or an empty string if
- // no trace is fully loaded yet. This has no effect on the Trace Processor
- // functionality and is used for UI purposes only.
- // The returned name is NOT a path and will contain extra text w.r.t. the
- // argument originally passed to SetCurrentTraceName(), e.g., "file (42 MB)".
- virtual std::string GetCurrentTraceName() = 0;
- virtual void SetCurrentTraceName(const std::string&) = 0;
-
- // Enables "meta-tracing" of trace processor.
- // Metatracing involves tracing trace processor itself to root-cause
- // performace issues in trace processor. See |DisableAndReadMetatrace| for
- // more information on the format of the metatrace.
- using MetatraceConfig = metatrace::MetatraceConfig;
- using MetatraceCategories = metatrace::MetatraceCategories;
- virtual void EnableMetatrace(MetatraceConfig config = {}) = 0;
-
- // Disables "meta-tracing" of trace processor and writes the trace as a
- // sequence of |TracePackets| into |trace_proto| returning the status of this
- // read.
- virtual base::Status DisableAndReadMetatrace(
- std::vector<uint8_t>* trace_proto) = 0;
-
// Gets all the currently loaded proto descriptors used in metric computation.
// This includes all compiled-in binary descriptors, and all proto descriptors
// loaded by trace processor shell at runtime. The message is encoded as
// DescriptorSet, defined in perfetto/trace_processor/trace_processor.proto.
virtual std::vector<uint8_t> GetMetricDescriptors() = 0;
-
- // Deprecated. Use |RegisterSqlPackage()| instead, which is identical in
- // functionality to |RegisterSqlModule()| and the only difference is in
- // the argument, which is directly translatable to |SqlPackage|.
- virtual base::Status RegisterSqlModule(SqlModule) = 0;
-
- // Registers the contents of a file.
- // This method can be used to pass out of band data to the trace processor
- // which can be used by importers to do some advanced processing. For example
- // if you pass binaries these are used to decode ETM traces.
- // Registering the same file twice will return an error.
- virtual base::Status RegisterFileContent(const std::string& path,
- TraceBlobView content) = 0;
};
-} // namespace trace_processor
-} // namespace perfetto
+} // namespace perfetto::trace_processor
#endif // INCLUDE_PERFETTO_TRACE_PROCESSOR_TRACE_PROCESSOR_H_
diff --git a/include/perfetto/trace_processor/trace_processor_storage.h b/include/perfetto/trace_processor/trace_processor_storage.h
index 417ac72..a286873 100644
--- a/include/perfetto/trace_processor/trace_processor_storage.h
+++ b/include/perfetto/trace_processor/trace_processor_storage.h
@@ -44,11 +44,11 @@
// status if some unrecoverable error happened. If this happens, the
// TraceProcessor will ignore the following Parse() requests, drop data on the
// floor and return errors forever.
- virtual util::Status Parse(TraceBlobView) = 0;
+ virtual base::Status Parse(TraceBlobView) = 0;
// Shorthand for Parse(TraceBlobView(TraceBlob(TakeOwnership(buf, size))).
// For compatibility with older API clients.
- util::Status Parse(std::unique_ptr<uint8_t[]> buf, size_t size);
+ base::Status Parse(std::unique_ptr<uint8_t[]> buf, size_t size);
// Forces all data in the trace to be pushed to tables without buffering data
// in sorting queues. This is useful if queries need to be performed to
diff --git a/include/perfetto/tracing/core/data_source_config.h b/include/perfetto/tracing/core/data_source_config.h
index 765a642..31f2d54 100644
--- a/include/perfetto/tracing/core/data_source_config.h
+++ b/include/perfetto/tracing/core/data_source_config.h
@@ -21,8 +21,8 @@
// using ::perfetto::Foo = ::perfetto::protos::gen::Foo.
// See comments in forward_decls.h for the historical reasons of this
// indirection layer.
-#include "perfetto/tracing/core/forward_decls.h"
+#include "perfetto/tracing/core/forward_decls.h" // IWYU pragma: export
-#include "protos/perfetto/config/data_source_config.gen.h"
+#include "protos/perfetto/config/data_source_config.gen.h" // IWYU pragma: export
#endif // INCLUDE_PERFETTO_TRACING_CORE_DATA_SOURCE_CONFIG_H_
diff --git a/include/perfetto/tracing/internal/data_source_internal.h b/include/perfetto/tracing/internal/data_source_internal.h
index 493098a..c81ff0d 100644
--- a/include/perfetto/tracing/internal/data_source_internal.h
+++ b/include/perfetto/tracing/internal/data_source_internal.h
@@ -22,12 +22,10 @@
#include <array>
#include <atomic>
-#include <functional>
#include <memory>
#include <mutex>
// No perfetto headers (other than tracing/api and protozero) should be here.
-#include "perfetto/tracing/buffer_exhausted_policy.h"
#include "perfetto/tracing/core/data_source_config.h"
#include "perfetto/tracing/internal/basic_types.h"
#include "perfetto/tracing/trace_writer_base.h"
diff --git a/include/perfetto/tracing/internal/tracing_muxer.h b/include/perfetto/tracing/internal/tracing_muxer.h
index e7f4956..eb61a07 100644
--- a/include/perfetto/tracing/internal/tracing_muxer.h
+++ b/include/perfetto/tracing/internal/tracing_muxer.h
@@ -21,11 +21,13 @@
#include <memory>
#include "perfetto/base/export.h"
+#include "perfetto/tracing/buffer_exhausted_policy.h"
#include "perfetto/tracing/core/forward_decls.h"
#include "perfetto/tracing/interceptor.h"
#include "perfetto/tracing/internal/basic_types.h"
#include "perfetto/tracing/internal/tracing_tls.h"
#include "perfetto/tracing/platform.h"
+
namespace perfetto {
class DataSourceBase;
diff --git a/include/perfetto/tracing/internal/track_event_internal.h b/include/perfetto/tracing/internal/track_event_internal.h
index 446c9e8..2afb9c0 100644
--- a/include/perfetto/tracing/internal/track_event_internal.h
+++ b/include/perfetto/tracing/internal/track_event_internal.h
@@ -271,8 +271,15 @@
auto it_and_inserted = incr_state->seen_tracks.insert(uuid);
if (PERFETTO_LIKELY(!it_and_inserted.second))
return;
- uuid = WriteTrackDescriptor(Track(uuid, Track()), trace_writer,
- incr_state, tls_state, timestamp);
+ std::optional<TrackRegistry::TrackInfo> track_info =
+ TrackRegistry::Get()->FindTrackInfo(uuid);
+ if (!track_info) {
+ return;
+ }
+ TrackRegistry::WriteTrackDescriptor(
+ std::move(track_info->desc),
+ NewTracePacket(trace_writer, incr_state, tls_state, timestamp));
+ uuid = track_info->parent_uuid;
}
}
diff --git a/include/perfetto/tracing/track.h b/include/perfetto/tracing/track.h
index 946d7d0..2a85729 100644
--- a/include/perfetto/tracing/track.h
+++ b/include/perfetto/tracing/track.h
@@ -39,6 +39,7 @@
#include <stdint.h>
#include <map>
#include <mutex>
+#include <optional>
namespace perfetto {
namespace internal {
@@ -438,6 +439,10 @@
class PERFETTO_EXPORT_COMPONENT TrackRegistry {
public:
using SerializedTrackDescriptor = std::string;
+ struct TrackInfo {
+ SerializedTrackDescriptor desc;
+ uint64_t parent_uuid = 0;
+ };
TrackRegistry();
~TrackRegistry();
@@ -464,26 +469,31 @@
// If the track has extra metadata (recorded with UpdateTrack), it will be
// found in the registry. To minimize the time the lock is held, make a copy
// of the data held in the registry and write it outside the lock.
- std::string desc_copy;
- uint64_t parent_uuid = 0;
- {
- std::lock_guard<std::mutex> lock(mutex_);
- const auto& it = tracks_.find(track.uuid);
- if (it != tracks_.end()) {
- desc_copy = it->second.desc;
- parent_uuid = it->second.parent_uuid;
- PERFETTO_DCHECK(!desc_copy.empty());
- }
- }
- if (!desc_copy.empty()) {
- WriteTrackDescriptor(std::move(desc_copy), std::move(packet));
+ auto track_info = FindTrackInfo(track.uuid);
+ if (track_info) {
+ WriteTrackDescriptor(std::move(track_info->desc), std::move(packet));
+ return track_info->parent_uuid;
} else {
// Otherwise we just write the basic descriptor for this type of track
// (e.g., just uuid, no name).
track.Serialize(packet->set_track_descriptor());
- parent_uuid = track.parent_uuid;
+ return track.parent_uuid;
}
- return parent_uuid;
+ }
+
+ // If saved in the registry, returns the serialize track descriptor and parent
+ // uuid for `uuid`.
+ std::optional<TrackInfo> FindTrackInfo(uint64_t uuid) {
+ std::optional<TrackInfo> track_info;
+ {
+ std::lock_guard<std::mutex> lock(mutex_);
+ const auto it = tracks_.find(uuid);
+ if (it != tracks_.end()) {
+ track_info = it->second;
+ PERFETTO_DCHECK(!track_info->desc.empty());
+ }
+ }
+ return track_info;
}
static void WriteTrackDescriptor(
@@ -491,10 +501,6 @@
protozero::MessageHandle<protos::pbzero::TracePacket> packet);
private:
- struct TrackInfo {
- SerializedTrackDescriptor desc;
- uint64_t parent_uuid = 0;
- };
std::mutex mutex_;
std::map<uint64_t /* uuid */, TrackInfo> tracks_;
diff --git a/infra/ci/config.py b/infra/ci/config.py
index 8994865..3546a9c 100755
--- a/infra/ci/config.py
+++ b/infra/ci/config.py
@@ -116,7 +116,7 @@
'linux-clang-x86_64-bazel': {
'PERFETTO_TEST_GN_ARGS': '',
'PERFETTO_TEST_SCRIPT': 'test/ci/bazel_tests.sh',
- 'PERFETTO_INSTALL_BUILD_DEPS_ARGS': '',
+ 'PERFETTO_INSTALL_BUILD_DEPS_ARGS': '--bazel',
},
'ui-clang-x86_64-release': {
'PERFETTO_TEST_GN_ARGS': 'is_debug=false',
diff --git a/infra/ci/sandbox/Dockerfile b/infra/ci/sandbox/Dockerfile
index 8968b31..90e9b9d 100644
--- a/infra/ci/sandbox/Dockerfile
+++ b/infra/ci/sandbox/Dockerfile
@@ -26,26 +26,21 @@
apt-get -y install python3 python3-pip git curl sudo lz4 tar ccache tini \
libpulse0 libgl1 libxml2 libc6-dev-i386 libtinfo5 \
gnupg2 pkg-config zip g++ zlib1g-dev unzip \
- python3-distutils gcc-8 g++-8; \
+ python3-distutils gcc-8 g++-8 \
+ openjdk-11-jdk; \
apt-get -y install libc++-8-dev libc++abi-8-dev clang-8; \
update-alternatives --install /usr/bin/python python /usr/bin/python3.7 1; \
gcc-8 --version; \
g++-8 --version; \
clang-8 --version; \
clang++-8 --version; \
+ java --version; \
pip3 install protobuf pandas grpcio; \
groupadd -g 1337 perfetto; \
useradd -d /ci/ramdisk -u 1337 -g perfetto perfetto; \
apt-get -y autoremove; \
rm -rf /var/lib/apt/lists/* /usr/share/man/* /usr/share/doc/*;
-RUN set -ex; \
- curl -LO https://github.com/bazelbuild/bazel/releases/download/7.0.2/bazel-7.0.2-installer-linux-x86_64.sh; \
- chmod +x bazel-*-installer-linux-x86_64.sh; \
- ./bazel-*-installer-linux-x86_64.sh; \
- rm bazel-*-installer-linux-x86_64.sh; \
- bazel version;
-
# Chrome/puppeteer deps.
RUN set -ex; \
export DEBIAN_FRONTEND=noninteractive; \
diff --git a/perfetto.rc b/perfetto.rc
index 5c6d853..8169d8f 100644
--- a/perfetto.rc
+++ b/perfetto.rc
@@ -34,6 +34,7 @@
onrestart exec_background - nobody shell -- /system/bin/traced_probes --cleanup-after-crash
file /dev/kmsg w
capabilities DAC_READ_SEARCH
+ shared_kallsyms
on property:persist.device_config.global_settings.sys_traced=1
setprop persist.traced.enable 1
diff --git a/protos/perfetto/config/chrome/scenario_config.proto b/protos/perfetto/config/chrome/scenario_config.proto
index eca7165..26bae25 100644
--- a/protos/perfetto/config/chrome/scenario_config.proto
+++ b/protos/perfetto/config/chrome/scenario_config.proto
@@ -116,6 +116,10 @@
optional TraceConfig trace_config = 6;
repeated NestedScenarioConfig nested_scenarios = 7;
+
+ // When set to true, this scenario initiates a tracing session using the
+ // system backend instead of the default in-browser custom backend.
+ optional bool use_system_backend = 8;
}
message ChromeFieldTracingConfig {
diff --git a/protos/perfetto/metrics/android/BUILD.gn b/protos/perfetto/metrics/android/BUILD.gn
index cddfe68..3b08662 100644
--- a/protos/perfetto/metrics/android/BUILD.gn
+++ b/protos/perfetto/metrics/android/BUILD.gn
@@ -20,7 +20,9 @@
"ad_services_metric.proto",
"android_anomaly_metric.proto",
"android_blocking_call.proto",
+ "android_blocking_call_per_frame.proto",
"android_blocking_calls_cuj_metric.proto",
+ "android_blocking_calls_cuj_per_frame_metric.proto",
"android_blocking_calls_unagg.proto",
"android_boot.proto",
"android_boot_unagg.proto",
diff --git a/protos/perfetto/metrics/android/android_blocking_call_per_frame.proto b/protos/perfetto/metrics/android/android_blocking_call_per_frame.proto
new file mode 100644
index 0000000..cf7d59c
--- /dev/null
+++ b/protos/perfetto/metrics/android/android_blocking_call_per_frame.proto
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+// Blocking call per frame on the main thread.
+message AndroidBlockingCallPerFrame {
+ // Name of the blocking call
+ optional string name = 1;
+ // Maximal duration within a frame.
+ optional int64 max_dur_per_frame_ms = 2;
+ // Maximal duration within a frame in nanoseconds
+ optional int64 max_dur_per_frame_ns = 3;
+ // Mean duration within the CUJ
+ optional int64 mean_dur_per_frame_ms = 4;
+ // Mean duration within the CUJ in nanoseconds
+ optional int64 mean_dur_per_frame_ns = 5;
+ // Max count in a frame
+ optional int64 max_cnt_per_frame = 6;
+ // Mean count in a frame
+ optional double mean_cnt_per_frame = 7;
+}
diff --git a/protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto b/protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto
new file mode 100644
index 0000000..abcca47
--- /dev/null
+++ b/protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+import "protos/perfetto/metrics/android/android_blocking_call_per_frame.proto";
+import "protos/perfetto/metrics/android/process_metadata.proto";
+
+// Blocking calls per frame inside Android jank CUJs. Shows count and duration for each.
+message AndroidCujBlockingCallsPerFrameMetric {
+ repeated Cuj cuj = 1;
+
+ message Cuj {
+
+ // Name of the CUJ, extracted from the CUJ jank trace marker.
+ // For example SHADE_EXPAND_COLLAPSE from J<SHADE_EXPAND_COLLAPSE>.
+ optional string name = 1;
+
+ optional AndroidProcessMetadata process = 2;
+
+ // List of blocking calls on the process UI thread.
+ // Aggregation is done by CUJ name.
+ repeated AndroidBlockingCallPerFrame blocking_calls = 3;
+ }
+}
diff --git a/protos/perfetto/metrics/metrics.proto b/protos/perfetto/metrics/metrics.proto
index bbbca99..4c904ea 100644
--- a/protos/perfetto/metrics/metrics.proto
+++ b/protos/perfetto/metrics/metrics.proto
@@ -31,6 +31,7 @@
import "protos/perfetto/metrics/android/batt_metric.proto";
import "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto";
import "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto";
+import "protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto";
import "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto";
import "protos/perfetto/metrics/android/codec_metrics.proto";
import "protos/perfetto/metrics/android/cpu_metric.proto";
@@ -80,7 +81,7 @@
import "protos/perfetto/metrics/common/clone_duration.proto";
// Trace processor metadata
-// Next id: 17
+// Next id: 18
message TraceMetadata {
reserved 1;
optional int64 trace_duration_ns = 2;
@@ -90,6 +91,7 @@
optional int64 statsd_triggering_subscription_id = 5;
optional int64 trace_size_bytes = 6;
repeated string trace_trigger = 7;
+ optional string trace_causal_trigger = 17;
optional string unique_session_name = 8;
optional string trace_config_pbtxt = 9;
optional int64 sched_duration_ns = 10;
@@ -353,6 +355,9 @@
optional CloneDuration clone_duration = 77;
+ // Per-frame blocking calls (e.g. binder calls) during CUJs (important UI transitions).
+ optional AndroidCujBlockingCallsPerFrameMetric android_blocking_calls_cuj_per_frame_metric = 78;
+
// Android
// Demo extensions.
extensions 450 to 499;
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index e73404b..78ebb81 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -176,6 +176,50 @@
// End of protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto
+// Begin of protos/perfetto/metrics/android/android_blocking_call_per_frame.proto
+
+// Blocking call per frame on the main thread.
+message AndroidBlockingCallPerFrame {
+ // Name of the blocking call
+ optional string name = 1;
+ // Maximal duration within a frame.
+ optional int64 max_dur_per_frame_ms = 2;
+ // Maximal duration within a frame in nanoseconds
+ optional int64 max_dur_per_frame_ns = 3;
+ // Mean duration within the CUJ
+ optional int64 mean_dur_per_frame_ms = 4;
+ // Mean duration within the CUJ in nanoseconds
+ optional int64 mean_dur_per_frame_ns = 5;
+ // Max count in a frame
+ optional int64 max_cnt_per_frame = 6;
+ // Mean count in a frame
+ optional double mean_cnt_per_frame = 7;
+}
+
+// End of protos/perfetto/metrics/android/android_blocking_call_per_frame.proto
+
+// Begin of protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto
+
+// Blocking calls per frame inside Android jank CUJs. Shows count and duration for each.
+message AndroidCujBlockingCallsPerFrameMetric {
+ repeated Cuj cuj = 1;
+
+ message Cuj {
+
+ // Name of the CUJ, extracted from the CUJ jank trace marker.
+ // For example SHADE_EXPAND_COLLAPSE from J<SHADE_EXPAND_COLLAPSE>.
+ optional string name = 1;
+
+ optional AndroidProcessMetadata process = 2;
+
+ // List of blocking calls on the process UI thread.
+ // Aggregation is done by CUJ name.
+ repeated AndroidBlockingCallPerFrame blocking_calls = 3;
+ }
+}
+
+// End of protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto
+
// Begin of protos/perfetto/metrics/android/android_blocking_calls_unagg.proto
// All blocking calls for a trace. Shows count and total duration for each.
@@ -3091,7 +3135,7 @@
// Begin of protos/perfetto/metrics/metrics.proto
// Trace processor metadata
-// Next id: 17
+// Next id: 18
message TraceMetadata {
reserved 1;
optional int64 trace_duration_ns = 2;
@@ -3101,6 +3145,7 @@
optional int64 statsd_triggering_subscription_id = 5;
optional int64 trace_size_bytes = 6;
repeated string trace_trigger = 7;
+ optional string trace_causal_trigger = 17;
optional string unique_session_name = 8;
optional string trace_config_pbtxt = 9;
optional int64 sched_duration_ns = 10;
@@ -3364,6 +3409,9 @@
optional CloneDuration clone_duration = 77;
+ // Per-frame blocking calls (e.g. binder calls) during CUJs (important UI transitions).
+ optional AndroidCujBlockingCallsPerFrameMetric android_blocking_calls_cuj_per_frame_metric = 78;
+
// Android
// Demo extensions.
extensions 450 to 499;
diff --git a/protos/perfetto/trace/android/BUILD.gn b/protos/perfetto/trace/android/BUILD.gn
index 429bbeb..320ce08 100644
--- a/protos/perfetto/trace/android/BUILD.gn
+++ b/protos/perfetto/trace/android/BUILD.gn
@@ -24,6 +24,7 @@
"android_game_intervention_list.proto",
"android_log.proto",
"android_system_property.proto",
+ "bluetooth_trace.proto",
"camera_event.proto",
"frame_timeline_event.proto",
"gpu_mem_event.proto",
diff --git a/protos/perfetto/trace/android/bluetooth_trace.proto b/protos/perfetto/trace/android/bluetooth_trace.proto
new file mode 100644
index 0000000..cbe8315
--- /dev/null
+++ b/protos/perfetto/trace/android/bluetooth_trace.proto
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+// Describes the packet type and direction. CMD and EVT are unidirectional, so
+// no need to differentiate the direction.
+enum BluetoothTracePacketType {
+ HCI_CMD = 1;
+ HCI_EVT = 2;
+ HCI_ACL_RX = 3;
+ HCI_ACL_TX = 4;
+ HCI_SCO_RX = 5;
+ HCI_SCO_TX = 6;
+ HCI_ISO_RX = 7;
+ HCI_ISO_TX = 8;
+}
+
+// Trace event for bluetooth
+message BluetoothTraceEvent {
+
+ // Packet type and direction
+ optional BluetoothTracePacketType packet_type = 1;
+
+ // Total count of the packets collected during the collection interval
+ optional uint32 count = 2;
+
+ // Total cumulative length of the packets collected during the collection
+ // interval
+ optional uint32 length = 3;
+
+ // The collection interval in ms. This is the duration between the first and
+ // last packets collected.
+ optional uint32 duration = 4;
+
+ // In case of CMD type, further breakdown of the type of command
+ optional uint32 op_code = 5;
+
+ // In the case of EVT type, further breakdown of the type of event
+ optional uint32 event_code = 6;
+
+ // When applicable for EVT type, further breakdown of event type into specific
+ // subevent
+ optional uint32 subevent_code = 7;
+
+ // Associated handle for the bluetooth packet
+ optional uint32 connection_handle = 8;
+}
\ No newline at end of file
diff --git a/protos/perfetto/trace/chrome/v8.proto b/protos/perfetto/trace/chrome/v8.proto
index fdd551e..3a4c5c1 100644
--- a/protos/perfetto/trace/chrome/v8.proto
+++ b/protos/perfetto/trace/chrome/v8.proto
@@ -144,6 +144,8 @@
// Where in the script source this function is defined. This is counted in
// bytes not characters.
optional uint32 byte_offset = 6;
+ optional uint32 line = 7;
+ optional uint32 column = 8;
}
// A V8 Isolate instance. A V8 Isolate represents an isolated instance of the V8
@@ -268,4 +270,4 @@
message V8CodeDefaults {
optional uint32 tid = 1;
-}
\ No newline at end of file
+}
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index e3b8268..4969574 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -4810,6 +4810,53 @@
// End of protos/perfetto/trace/android/android_system_property.proto
+// Begin of protos/perfetto/trace/android/bluetooth_trace.proto
+
+// Describes the packet type and direction. CMD and EVT are unidirectional, so
+// no need to differentiate the direction.
+enum BluetoothTracePacketType {
+ HCI_CMD = 1;
+ HCI_EVT = 2;
+ HCI_ACL_RX = 3;
+ HCI_ACL_TX = 4;
+ HCI_SCO_RX = 5;
+ HCI_SCO_TX = 6;
+ HCI_ISO_RX = 7;
+ HCI_ISO_TX = 8;
+}
+
+// Trace event for bluetooth
+message BluetoothTraceEvent {
+
+ // Packet type and direction
+ optional BluetoothTracePacketType packet_type = 1;
+
+ // Total count of the packets collected during the collection interval
+ optional uint32 count = 2;
+
+ // Total cumulative length of the packets collected during the collection
+ // interval
+ optional uint32 length = 3;
+
+ // The collection interval in ms. This is the duration between the first and
+ // last packets collected.
+ optional uint32 duration = 4;
+
+ // In case of CMD type, further breakdown of the type of command
+ optional uint32 op_code = 5;
+
+ // In the case of EVT type, further breakdown of the type of event
+ optional uint32 event_code = 6;
+
+ // When applicable for EVT type, further breakdown of event type into specific
+ // subevent
+ optional uint32 subevent_code = 7;
+
+ // Associated handle for the bluetooth packet
+ optional uint32 connection_handle = 8;
+}
+// End of protos/perfetto/trace/android/bluetooth_trace.proto
+
// Begin of protos/perfetto/trace/android/camera_event.proto
// A profiling event corresponding to a single camera frame. This message
@@ -6491,6 +6538,8 @@
// Where in the script source this function is defined. This is counted in
// bytes not characters.
optional uint32 byte_offset = 6;
+ optional uint32 line = 7;
+ optional uint32 column = 8;
}
// A V8 Isolate instance. A V8 Isolate represents an isolated instance of the V8
@@ -6616,6 +6665,7 @@
message V8CodeDefaults {
optional uint32 tid = 1;
}
+
// End of protos/perfetto/trace/chrome/v8.proto
// Begin of protos/perfetto/trace/clock_snapshot.proto
@@ -15688,7 +15738,7 @@
// See the [Buffers and Dataflow](/docs/concepts/buffers.md) doc for details.
//
// Next reserved id: 14 (up to 15).
-// Next id: 114.
+// Next id: 115.
message TracePacket {
// The timestamp of the TracePacket.
// By default this timestamps refers to the trace clock (CLOCK_BOOTTIME on
@@ -15830,6 +15880,8 @@
Trigger clone_snapshot_trigger = 113;
+ BluetoothTraceEvent bluetooth_trace_event = 114;
+
// This field is only used for testing.
// In previous versions of this proto this field had the id 268435455
// This caused many problems:
diff --git a/protos/perfetto/trace/trace_packet.proto b/protos/perfetto/trace/trace_packet.proto
index 6e84dc9..cbb8724 100644
--- a/protos/perfetto/trace/trace_packet.proto
+++ b/protos/perfetto/trace/trace_packet.proto
@@ -22,6 +22,7 @@
import "protos/perfetto/trace/android/android_game_intervention_list.proto";
import "protos/perfetto/trace/android/android_log.proto";
import "protos/perfetto/trace/android/android_system_property.proto";
+import "protos/perfetto/trace/android/bluetooth_trace.proto";
import "protos/perfetto/trace/android/camera_event.proto";
import "protos/perfetto/trace/android/frame_timeline_event.proto";
import "protos/perfetto/trace/android/gpu_mem_event.proto";
@@ -104,7 +105,7 @@
// See the [Buffers and Dataflow](/docs/concepts/buffers.md) doc for details.
//
// Next reserved id: 14 (up to 15).
-// Next id: 114.
+// Next id: 115.
message TracePacket {
// The timestamp of the TracePacket.
// By default this timestamps refers to the trace clock (CLOCK_BOOTTIME on
@@ -246,6 +247,8 @@
Trigger clone_snapshot_trigger = 113;
+ BluetoothTraceEvent bluetooth_trace_event = 114;
+
// This field is only used for testing.
// In previous versions of this proto this field had the id 268435455
// This caused many problems:
diff --git a/python/generators/sql_processing/docs_parse.py b/python/generators/sql_processing/docs_parse.py
index 5b75b55..90c227b 100644
--- a/python/generators/sql_processing/docs_parse.py
+++ b/python/generators/sql_processing/docs_parse.py
@@ -74,7 +74,6 @@
type: str
long_type: str
description: str
- joinid_column: Optional[str]
class AbstractDocParser(ABC):
@@ -166,9 +165,17 @@
m = re.match(r'JOINID\(([_A-Za-z\.]*)\)', type)
if m:
- result[name] = Arg('JOINID', type, comment, m.groups()[0])
- else:
- result[name] = Arg(type, type, comment, None)
+ result[name] = Arg('JOINID', type, comment)
+ remaining_args = groups[-1].lstrip().lstrip(',').lstrip()
+ continue
+
+ m = re.match(r'ID\(([_A-Za-z\.]*)\)', type)
+ if m:
+ result[name] = Arg('ID', type, comment)
+ remaining_args = groups[-1].lstrip().lstrip(',').lstrip()
+ continue
+
+ result[name] = Arg(type, type, comment)
# Strip whitespace and comma and parse the next arg.
remaining_args = groups[-1].lstrip().lstrip(',').lstrip()
@@ -185,16 +192,12 @@
type: str
desc: str
cols: Dict[str, Arg]
- id_columns: List[str]
- joinid_cols: Dict[str, Arg]
- def __init__(self, name, type, desc, cols, id_columns, joinid_columns):
+ def __init__(self, name, type, desc, cols):
self.name = name
self.type = type
self.desc = desc
self.cols = cols
- self.id_columns = id_columns
- self.joinid_cols = joinid_columns
class TableViewDocParser(AbstractDocParser):
@@ -236,22 +239,13 @@
return
cols = self._parse_columns(schema, ObjKind.table_view)
- id_columns = []
- joinid_cols = {}
- for col_name, arg in cols.items():
- if arg.type == "ID":
- id_columns.append(col_name)
- elif arg.type == "JOINID":
- joinid_cols[col_name] = arg
return TableOrView(
name=self._parse_name(),
type=type,
desc=self._parse_desc_not_empty(doc.description),
- cols=self._parse_columns(schema, ObjKind.table_view),
- id_columns=id_columns,
- joinid_columns=joinid_cols)
+ cols=self._parse_columns(schema, ObjKind.table_view))
class Function:
@@ -443,7 +437,6 @@
table_functions: List[TableFunction] = []
macros: List[Macro] = []
includes: List[Include]
- id_columns: Dict[str, List[str]]
def __init__(self, package_name: str, module_as_list: List[str],
errors: List[str], table_views: List[TableOrView],
@@ -458,7 +451,6 @@
self.table_functions = table_functions
self.macros = macros
self.includes = includes
- self.id_columns = {o.name: o.id_columns for o in table_views}
def parse_file(path: str, sql: str) -> Optional[ParsedModule]:
diff --git a/python/generators/trace_processor_table/serialize.py b/python/generators/trace_processor_table/serialize.py
index dedfe51..de6a94f 100644
--- a/python/generators/trace_processor_table/serialize.py
+++ b/python/generators/trace_processor_table/serialize.py
@@ -41,7 +41,6 @@
self.data_layer_type = data_layer_type(table.table, self.parsed_col)
self.is_implicit_id = self.parsed_col.is_implicit_id
- self.is_implicit_type = self.parsed_col.is_implicit_type
self.is_ancestor = self.parsed_col.is_ancestor
self.is_string = parsed_type.cpp_type == 'StringPool::Id'
self.is_optional = parsed_type.is_optional
@@ -53,26 +52,26 @@
return f' using {self.name} = {self.typed_column_type};'
def row_field(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
if self.is_ancestor:
return None
return f' {self.cpp_type} {self.name};'
def row_param(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
return f'{self.cpp_type} in_{self.name} = {{}}'
def parent_row_initializer(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
if not self.is_ancestor:
return None
return f'in_{self.name}'
def row_initializer(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
if self.is_ancestor:
return None
@@ -84,7 +83,7 @@
}}'''
def row_ref_getter(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
return f'''void set_{self.name}(
ColumnType::{self.name}::non_optional_type v) {{
@@ -92,7 +91,7 @@
}}'''
def flag(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
if self.is_ancestor:
return None
@@ -106,7 +105,7 @@
'''
def storage_init(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
if self.is_ancestor:
return None
@@ -116,7 +115,7 @@
return f'''{self.name}_({storage}::Create<{dense}>())'''
def column_init(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
if self.is_ancestor:
return None
@@ -133,7 +132,7 @@
return f' {self.name}_.ShrinkToFit();'
def append(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
if self.is_ancestor:
return None
@@ -148,7 +147,7 @@
'''
def mutable_accessor(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
return f'''
{self.typed_column_type}* mutable_{self.name}() {{
@@ -158,7 +157,7 @@
'''
def storage(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
if self.is_ancestor:
return None
@@ -176,7 +175,7 @@
'''
def static_schema(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
return f'''
schema.columns.emplace_back(Table::Schema::Column{{
@@ -187,26 +186,26 @@
'''
def row_eq(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
return f'ColumnType::{self.name}::Equals({self.name}, other.{self.name})'
def extend_parent_param(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
if self.is_ancestor:
return None
return f'ColumnStorage<ColumnType::{self.name}::stored_type> {self.name}'
def extend_parent_param_arg(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
if self.is_ancestor:
return None
return f'std::move({self.name})'
def static_assert_flags(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
if self.is_ancestor:
return None
@@ -218,7 +217,7 @@
'''
def extend_nullable_vector(self) -> Optional[str]:
- if self.is_implicit_id or self.is_implicit_type:
+ if self.is_implicit_id:
return None
if self.is_ancestor:
return None
diff --git a/python/generators/trace_processor_table/util.py b/python/generators/trace_processor_table/util.py
index 6896d94..e301e17 100644
--- a/python/generators/trace_processor_table/util.py
+++ b/python/generators/trace_processor_table/util.py
@@ -78,10 +78,6 @@
# parsing the tables rather than by the user.
is_implicit_id: bool = False
- # Whether this column is the implicit "type" column which is added by while
- # parsing the tables rather than by the user.
- is_implicit_type: bool = False
-
# Whether this column comes from copying a column from the ancestor. If this
# is set to false, the user explicitly specified it for this table.
is_ancestor: bool = False
diff --git a/python/perfetto/prebuilts/manifests/trace_processor_shell.py b/python/perfetto/prebuilts/manifests/trace_processor_shell.py
index 1bebad7..40e68df 100755
--- a/python/perfetto/prebuilts/manifests/trace_processor_shell.py
+++ b/python/perfetto/prebuilts/manifests/trace_processor_shell.py
@@ -1,15 +1,15 @@
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
TRACE_PROCESSOR_SHELL_MANIFEST = [{
'arch':
'mac-amd64',
'file_name':
'trace_processor_shell',
'file_size':
- 9949656,
+ 10524008,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/trace_processor_shell',
'sha256':
- 'e9dcf95aaa02f8c00a724f0ff34ba3a454c717beb9900cf9fd97ab142b362452',
+ '867c70800cfe81c2640f2aae8bb58eca68fa1389a3258a25c285ee5510edbbe3',
'platform':
'darwin',
'machine': ['x86_64']
@@ -19,11 +19,11 @@
'file_name':
'trace_processor_shell',
'file_size':
- 9223224,
+ 9767976,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/trace_processor_shell',
'sha256':
- '9a0541a0f52f95bfcb8dc88d94bc4494c660d95eefc40fc946ab43d995051ff7',
+ '9c325030078bc4de8693083c9e4e2b72c83ca694c3a4ef8cc1bd9c29fb421815',
'platform':
'darwin',
'machine': ['arm64']
@@ -33,11 +33,11 @@
'file_name':
'trace_processor_shell',
'file_size':
- 10142800,
+ 10935344,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/trace_processor_shell',
'sha256':
- '18c8730b52f8ee1d9e202031527435b6b2e3149fbd9b1046b2e77d18f06aa337',
+ '6af6f87e6521eec186e74c68c0c6eeeeb557556e368d0e4f563be5ce5d9d936b',
'platform':
'linux',
'machine': ['x86_64']
@@ -47,11 +47,11 @@
'file_name':
'trace_processor_shell',
'file_size':
- 7329432,
+ 8010576,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/trace_processor_shell',
'sha256':
- '0558040998666576e1063d6d626b8aa9e354f18d73d225240f043b3c9236befa',
+ 'da0c361d4a2c8d8b2d1ffd45cd388d964cc58b09e8e41f48aa045ed357510755',
'platform':
'linux',
'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
'file_name':
'trace_processor_shell',
'file_size':
- 9703384,
+ 10448648,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/trace_processor_shell',
'sha256':
- 'eeb95cc54358df08375ffae4862c043a6737902179ce8e0408984004c32cf93c',
+ 'ecb6a1a073eb4bbfe36af56ab4406671e8febe02fb4c6dcef73fb1fe5d817fad',
'platform':
'linux',
'machine': ['aarch64']
@@ -75,55 +75,55 @@
'file_name':
'trace_processor_shell',
'file_size':
- 7367412,
+ 7905948,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/trace_processor_shell',
'sha256':
- 'd29b1e6aee52ceff24c072f56c7be7795d0fa29f3596e2633fafa60782384718'
+ 'efeed53819e11f5f82d1ec9c3edce00590b3db1e3e4f0e64acddafba2c35a52e'
}, {
'arch':
'android-arm64',
'file_name':
'trace_processor_shell',
'file_size':
- 9598784,
+ 10154712,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/trace_processor_shell',
'sha256':
- '06e80c562c0043cca9225ade3c961a081bcc7435660117d5a6db26b815d0b9ca'
+ 'c6b0bb85228e4a3785030bbaf718bb428580f7d31de127e73042a30b9a67128a'
}, {
'arch':
'android-x86',
'file_name':
'trace_processor_shell',
'file_size':
- 10625488,
+ 11244904,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/trace_processor_shell',
'sha256':
- '2a576fb397da14d0dabcfa97f5eeec15b4dc55df009308f75a5fdf9de8a9b0dd'
+ 'a113d0f68b89c63da4faefebc094a5be15620afdbe862a23127d55d7ff44ed43'
}, {
'arch':
'android-x64',
'file_name':
'trace_processor_shell',
'file_size':
- 9915664,
+ 10506544,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/trace_processor_shell',
'sha256':
- 'a30be9f09b53110394e87af4d6b41ae24cd74d9a3f97ac1cc4d6ae2057ac6977'
+ 'f062e38fd28ab94b0232df8f4eeac70506bb7d304b82100e288d5acb603f84c1'
}, {
'arch':
'windows-amd64',
'file_name':
'trace_processor_shell.exe',
'file_size':
- 9922560,
+ 10479616,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/trace_processor_shell.exe',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/windows-amd64/trace_processor_shell.exe',
'sha256':
- 'd41639844a6c36dbaa195d91e9c356f2172d924c70a1bfed5432c407f857f009',
+ 'a881f3e2d4c6131493e85bfd1f36d1efe58e1478e2991825418d5d21614c1e48',
'platform':
'win32',
'machine': ['amd64']
diff --git a/python/perfetto/prebuilts/manifests/tracebox.py b/python/perfetto/prebuilts/manifests/tracebox.py
index 0745b63..31a0afe 100755
--- a/python/perfetto/prebuilts/manifests/tracebox.py
+++ b/python/perfetto/prebuilts/manifests/tracebox.py
@@ -1,15 +1,15 @@
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
TRACEBOX_MANIFEST = [{
'arch':
'mac-amd64',
'file_name':
'tracebox',
'file_size':
- 1613864,
+ 1646808,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/tracebox',
'sha256':
- 'dfb1a3affe905d2e7d1f82bc4dda46b1fda6db054d60ae87c3215dd529b77fee',
+ '85b3060ed4d49e2c8d69dbb4d6ff26ab662f9b28c0032791674c90683dd33d39',
'platform':
'darwin',
'machine': ['x86_64']
@@ -19,11 +19,11 @@
'file_name':
'tracebox',
'file_size':
- 1492184,
+ 1508856,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/tracebox',
'sha256':
- '4a492a629dd1f13f3146c4b8267c0b163afba8cef1d49e0c00c48bb727496066',
+ 'ea2cce845daf0eba469ff356b3bcefc8e9a384084569271a470b58a9dcbf8def',
'platform':
'darwin',
'machine': ['arm64']
@@ -33,11 +33,11 @@
'file_name':
'tracebox',
'file_size':
- 2380040,
+ 2415168,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/tracebox',
'sha256':
- 'd70b284e8c28858fd539ae61ca59764d7f9fd6232073c304926e892fe75e692a',
+ '5361676fb3c2490ae2136ab7a37dcd9e4ee5a2a6c0ba722facf3215a23a8c633',
'platform':
'linux',
'machine': ['x86_64']
@@ -47,11 +47,11 @@
'file_name':
'tracebox',
'file_size':
- 1450708,
+ 1478024,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/tracebox',
'sha256':
- '178fa6a1a9bc80f72d81938d40fe201c25c595ffaff7e030d59c2af09dfcc06c',
+ '18db321576be555d8c9281df9fc03aa6b3b4358ae2424ffbd65fc120cd650b8b',
'platform':
'linux',
'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
'file_name':
'tracebox',
'file_size':
- 2269816,
+ 2304384,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/tracebox',
'sha256':
- '42c64f9807756aaa08a2bfa13e9e4828c193a6b90ba1329408873c3ebf5adf3f',
+ '70c9e2b63eb92a82db65916c346b09867bfedc0c4593974c019102f485c0dc9d',
'platform':
'linux',
'machine': ['aarch64']
@@ -75,42 +75,42 @@
'file_name':
'tracebox',
'file_size':
- 1333336,
+ 1354916,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/tracebox',
'sha256':
- '93a78d2c42e3c00f117e2f155326383f69c891281ed693a39d87b8cb54ca4e19'
+ '724a1cb4774bdf8a64beb37194f7394df5a052c36369ea52f64fe519fcb40117'
}, {
'arch':
'android-arm64',
'file_name':
'tracebox',
'file_size':
- 2115984,
+ 2142008,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/tracebox',
'sha256':
- '508248a9e47ab605fd742efb700391d7267b68b586199a93e13e6ca14b72fe3d'
+ '7616bfc3be1269c3ac1eec5a1f868fb65c2830ed001b5fbcc3800c909c676848'
}, {
'arch':
'android-x86',
'file_name':
'tracebox',
'file_size':
- 2302960,
+ 2341884,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/tracebox',
'sha256':
- '63d20a69c4e0c291329d7917e640fa0d4f146c344e79988e87393b1431d594b1'
+ '29124bee9bf4e2e296b0c96071b8c9706b57d963cbf0359d6afd95a9049b2b82'
}, {
'arch':
'android-x64',
'file_name':
'tracebox',
'file_size':
- 2147880,
+ 2178416,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/tracebox',
'sha256':
- 'c0ea1d5fd6d020e4c2b45d4d45cdd0c44ae63cd755d69260a3e5d2bacd3cbd6a'
+ '826fffce1e138c1d5ac107492ee696c09ad83f9ae9aa647c810d71084f797509'
}]
diff --git a/python/perfetto/prebuilts/manifests/traceconv.py b/python/perfetto/prebuilts/manifests/traceconv.py
index abe58a0..f492f09 100755
--- a/python/perfetto/prebuilts/manifests/traceconv.py
+++ b/python/perfetto/prebuilts/manifests/traceconv.py
@@ -1,15 +1,15 @@
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
TRACECONV_MANIFEST = [{
'arch':
'mac-amd64',
'file_name':
'traceconv',
'file_size':
- 9041560,
+ 9599720,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/traceconv',
'sha256':
- 'cec2da5cb771a4812d0b2d15604d5023954d28e0af12e87313da2ab70d26b970',
+ '5e583da4ee716b077a649f366049fbe1eed8ff8f469db92d841307eb817e06c7',
'platform':
'darwin',
'machine': ['x86_64']
@@ -19,11 +19,11 @@
'file_name':
'traceconv',
'file_size':
- 8375512,
+ 8920424,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/traceconv',
'sha256':
- '64e200a58ea9c9f366e1071dd274d0023d1fd14043f75dbba3fe0cc138ff5fc7',
+ '794f45213cb81511c6e2594c47d917ce407650d81c16e2ff1442685e5da3a533',
'platform':
'darwin',
'machine': ['arm64']
@@ -33,11 +33,11 @@
'file_name':
'traceconv',
'file_size':
- 9134136,
+ 9920848,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/traceconv',
'sha256':
- '87b87e1778367c1e3b99fc77439a28b4911125d2751f9909fd1b51f6bd60b6f4',
+ '5f0b86cfb8d75fd574aaabc36c97d229d5234511d3bf77ddcc2a180b96cbd014',
'platform':
'linux',
'machine': ['x86_64']
@@ -47,11 +47,11 @@
'file_name':
'traceconv',
'file_size':
- 6753020,
+ 7430084,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/traceconv',
'sha256':
- '804c4e13aca5798731056952d9cb0c6ee58795c03477c69514ccd39703060812',
+ '1561f9bbbd2b192b834132bd8c515cfde6f6afe2117bf68ac0aeb3caedfeb3fd',
'platform':
'linux',
'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
'file_name':
'traceconv',
'file_size':
- 8740064,
+ 9479552,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/traceconv',
'sha256':
- '0d781886531d11e1d573a1ec5e06376ef139bb479eec38c16c8735821c35b895',
+ '4cb56805a5d1baf5756f459d5fa4a05c982faffc8fc96d9760ca3e86c6ced279',
'platform':
'linux',
'machine': ['aarch64']
@@ -75,55 +75,55 @@
'file_name':
'traceconv',
'file_size':
- 6792280,
+ 7329320,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/traceconv',
'sha256':
- '7d91e4133184a3722a25488edd3692c5a195148eba56621014311d3f85d3fc15'
+ '719ac44e87c45a58d1ba3a6264518ed7384f738cdc293703b7e9a29ebcde6788'
}, {
'arch':
'android-arm64',
'file_name':
'traceconv',
'file_size':
- 8677992,
+ 9232824,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/traceconv',
'sha256':
- 'c03c4a901ed23f1e20a12c98ce4556353a62bddcd260fb4d797cd29ff6c49a05'
+ 'a76f954e8b6bba1e302ee136745ae5a478ba4737bf97bde1f8eeeec1b5238de2'
}, {
'arch':
'android-x86',
'file_name':
'traceconv',
'file_size':
- 9503704,
+ 10121840,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/traceconv',
'sha256':
- '704e58a7249de56aadec64d4c0d83bab0821d2c4fd77114a9b71705ff4224539'
+ '7dcbe7ce3962155a156cb3e85e7fe17389973f93bf22b14ce13e45173f263ea4'
}, {
'arch':
'android-x64',
'file_name':
'traceconv',
'file_size':
- 8964488,
+ 9554408,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/traceconv',
'sha256':
- 'e4f07836fc2a5fb7cd997a9acc4183af7a06997d1e73aac71021af5114b921bc'
+ 'e44bc63def32674c99c67e5525ea36b66b6c1714e8fffe7606957813aaf212fa'
}, {
'arch':
'windows-amd64',
'file_name':
'traceconv.exe',
'file_size':
- 8763904,
+ 9316864,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/traceconv.exe',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/windows-amd64/traceconv.exe',
'sha256':
- '084670ac28ed59a9642782a30e051735c1b7474b8cd569b9bc94c305af68290e',
+ '937df755c7a54484c1a1aa1bbbaf392d978c300d6ca631bd9d9a20fe2b974deb',
'platform':
'win32',
'machine': ['amd64']
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 6041254..eb3f9b0 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/python/test/stdlib_unittest.py b/python/test/stdlib_unittest.py
index 6f4c98d..f0f3023 100644
--- a/python/test/stdlib_unittest.py
+++ b/python/test/stdlib_unittest.py
@@ -42,7 +42,7 @@
self.assertEqual(fn.name, 'cr_table')
self.assertEqual(fn.desc, 'Comment')
self.assertEqual(fn.cols, {
- 'x': Arg('LONG', 'LONG', 'Column.', None),
+ 'x': Arg('LONG', 'LONG', 'Column.'),
})
# Checks that when custom prefixes (cr for chrome/util) are present,
@@ -63,7 +63,7 @@
self.assertEqual(fn.name, 'chrome_table')
self.assertEqual(fn.desc, 'Comment')
self.assertEqual(fn.cols, {
- 'x': Arg('LONG', 'LONG', 'Column.', None),
+ 'x': Arg('LONG', 'LONG', 'Column.'),
})
# Checks that when custom prefixes (cr for chrome/util) are present,
@@ -166,7 +166,7 @@
self.assertEqual(table.desc, 'Table comment.')
self.assertEqual(table.type, 'TABLE')
self.assertEqual(table.cols, {
- 'id': Arg('LONG', 'LONG', 'Id of slice.', None),
+ 'id': Arg('LONG', 'LONG', 'Id of slice.'),
})
def test_perfetto_view_with_schema(self):
@@ -189,8 +189,8 @@
self.assertEqual(table.type, 'VIEW')
self.assertEqual(
table.cols, {
- 'foo': Arg('LONG', 'LONG', 'Foo.', None),
- 'bar': Arg('STRING', 'STRING', 'Bar.', None),
+ 'foo': Arg('LONG', 'LONG', 'Foo.'),
+ 'bar': Arg('STRING', 'STRING', 'Bar.'),
})
def test_function_with_new_style_docs(self):
@@ -214,8 +214,8 @@
self.assertEqual(fn.desc, 'Function foo.')
self.assertEqual(
fn.args, {
- 'utid': Arg('LONG', 'LONG', 'Utid of thread.', None),
- 'name': Arg('STRING', 'STRING', 'String name.', None),
+ 'utid': Arg('LONG', 'LONG', 'Utid of thread.'),
+ 'name': Arg('STRING', 'STRING', 'String name.'),
})
self.assertEqual(fn.return_type, 'BOOL')
self.assertEqual(fn.return_desc, 'Exists.')
@@ -241,10 +241,10 @@
self.assertEqual(fn.name, 'foo_fn')
self.assertEqual(fn.desc, 'Function foo.')
self.assertEqual(fn.args, {
- 'utid': Arg('LONG', 'LONG', 'Utid of thread.', None),
+ 'utid': Arg('LONG', 'LONG', 'Utid of thread.'),
})
self.assertEqual(fn.cols, {
- 'count': Arg('LONG', 'LONG', 'Count.', None),
+ 'count': Arg('LONG', 'LONG', 'Count.'),
})
def test_function_with_new_style_docs_multiline_comment(self):
@@ -268,7 +268,7 @@
self.assertEqual(fn.name, 'foo_fn')
self.assertEqual(fn.desc, 'Function foo.')
self.assertEqual(fn.args, {
- 'arg': Arg('LONG', 'LONG', 'Multi line comment.', None),
+ 'arg': Arg('LONG', 'LONG', 'Multi line comment.'),
})
self.assertEqual(fn.return_type, 'BOOL')
self.assertEqual(fn.return_desc, 'Exists.')
@@ -294,7 +294,7 @@
self.assertEqual(fn.name, 'foo_fn')
self.assertEqual(fn.desc, 'Function foo.')
self.assertEqual(fn.args, {
- 'arg': Arg('LONG', 'LONG', 'Arg', None),
+ 'arg': Arg('LONG', 'LONG', 'Arg'),
})
self.assertEqual(fn.return_type, 'BOOL')
self.assertEqual(fn.return_desc, 'Multi line return comment.')
@@ -360,7 +360,7 @@
self.assertEqual(fn.name, 'foo_fn')
self.assertEqual(fn.desc, 'Function foo.')
self.assertEqual(fn.args, {
- 'utid': Arg('LONG', 'LONG', 'Utid of thread (important).', None),
+ 'utid': Arg('LONG', 'LONG', 'Utid of thread (important).'),
})
self.assertEqual(fn.return_type, 'BOOL')
self.assertEqual(fn.return_desc, 'Exists.')
@@ -396,7 +396,7 @@
self.assertEqual(macro.name, 'foo_macro')
self.assertEqual(macro.desc, 'Macro')
self.assertEqual(macro.args, {
- 'x': Arg('TableOrSubquery', 'TableOrSubquery', 'x Arg.', None),
+ 'x': Arg('TableOrSubquery', 'TableOrSubquery', 'x Arg.'),
})
self.assertEqual(macro.return_type, 'TableOrSubquery')
self.assertEqual(macro.return_desc, 'Exists.')
diff --git a/python/tools/check_imports.py b/python/tools/check_imports.py
index 67a5990..1364094 100755
--- a/python/tools/check_imports.py
+++ b/python/tools/check_imports.py
@@ -91,7 +91,10 @@
['/core/*', '/frontend/*', '/common/actions'],
),
- # Miscl legitimate deps.
+ # The record plugin needs access to the wasm .d.ts for trace_config_utils.
+ ('/plugins/dev.perfetto.RecordTrace*', '/gen/trace_config_utils'),
+
+ # Misc legitimate deps.
('/frontend/index', ['/gen/*']),
('/traceconv/index', '/gen/traceconv'),
('/engine/wasm_bridge', '/gen/trace_processor'),
@@ -195,7 +198,7 @@
return
if len(all_deps) > 1:
raise Exception('Ambiguous plugin deps in %s: %s' % (path, all_deps))
- declared_deps = re.sub('\s*', '', all_deps[0]).split(',')
+ declared_deps = [x for x in re.sub('\s*', '', all_deps[0]).split(',') if x]
for imported_as in declared_deps:
resolved_dep = import_map.get(imported_as)
if resolved_dep is None:
diff --git a/src/kallsyms/kernel_symbol_map.cc b/src/kallsyms/kernel_symbol_map.cc
index 4ff73d1..be71fff 100644
--- a/src/kallsyms/kernel_symbol_map.cc
+++ b/src/kallsyms/kernel_symbol_map.cc
@@ -27,6 +27,7 @@
#include "perfetto/protozero/proto_utils.h"
#include <stdio.h>
+#include <unistd.h>
#include <algorithm>
#include <cinttypes>
@@ -51,17 +52,11 @@
constexpr size_t kSymNameMaxLen = 128;
constexpr size_t kSymMaxSizeBytes = 1024 * 1024;
-// Reads a kallsyms file in blocks of 4 pages each and decode its lines using
-// a simple FSM. Calls the passed lambda for each valid symbol.
-// It skips undefined symbols and other useless stuff.
+// Reads a kallsyms file and decodes its lines using a simple FSM. Calls the
+// passed lambda for each valid symbol. It skips undefined symbols and other
+// useless stuff.
template <typename Lambda /* void(uint64_t, char, base::StringView) */>
-void ForEachSym(const std::string& kallsyms_path, Lambda fn) {
- base::ScopedFile fd = base::OpenFile(kallsyms_path.c_str(), O_RDONLY);
- if (!fd) {
- PERFETTO_PLOG("Cannot open %s", kallsyms_path.c_str());
- return;
- }
-
+void ForEachSym(int fd, Lambda fn) {
// /proc/kallsyms looks as follows:
// 0000000000026a80 A bpf_trace_sds
//
@@ -75,15 +70,20 @@
static constexpr size_t kBufSize = 16 * 1024;
base::PagedMemory buffer = base::PagedMemory::Allocate(kBufSize);
enum { kSymAddr, kSymType, kSymName, kEatRestOfLine } state = kSymAddr;
+ off_t rd_offset = 0;
uint64_t sym_addr = 0;
char sym_type = '\0';
char sym_name[kSymNameMaxLen + 1];
size_t sym_name_len = 0;
for (;;) {
char* buf = static_cast<char*>(buffer.Get());
- auto rsize = base::Read(*fd, buf, kBufSize);
+ // Use pread because on android we might be sharing an open file across
+ // processes. Even if they should be mutually excluded, not relying on a
+ // seek position is simpler to reason about.
+ ssize_t rsize = PERFETTO_EINTR(pread(fd, buf, kBufSize, rd_offset));
+ rd_offset += rsize;
if (rsize < 0) {
- PERFETTO_PLOG("read(%s) failed", kallsyms_path.c_str());
+ PERFETTO_PLOG("pread(kallsyms) failed");
return;
}
if (rsize == 0)
@@ -234,7 +234,7 @@
return base::StringView();
}
-size_t KernelSymbolMap::Parse(const std::string& kallsyms_path) {
+size_t KernelSymbolMap::Parse(int fd) {
PERFETTO_METATRACE_SCOPED(TAG_PRODUCER, KALLSYMS_PARSE);
using SymAddr = uint64_t;
@@ -263,8 +263,12 @@
// Based on `cat /proc/kallsyms | egrep "\b[tT]\b" | wc -l`.
symbol_tokens.reserve(128 * 1024);
- ForEachSym(kallsyms_path, [&](SymAddr addr, char type,
- base::StringView name) {
+ if (fd < 0) {
+ PERFETTO_ELOG("Invalid kallsyms fd");
+ return 0;
+ }
+
+ ForEachSym(fd, [&](SymAddr addr, char type, base::StringView name) {
// Special cases:
//
// Skip arm mapping symbols such as $x, $x.123, $d, $d.123. They exist to
diff --git a/src/kallsyms/kernel_symbol_map.h b/src/kallsyms/kernel_symbol_map.h
index c69d86a..bf26eb4 100644
--- a/src/kallsyms/kernel_symbol_map.h
+++ b/src/kallsyms/kernel_symbol_map.h
@@ -24,6 +24,8 @@
#include <string>
#include <vector>
+#include "perfetto/ext/base/scoped_file.h"
+
namespace perfetto {
namespace base {
@@ -123,7 +125,8 @@
static size_t kTokenIndexSampling;
// Parses a kallsyms file. Returns the number of valid symbols decoded.
- size_t Parse(const std::string& kallsyms_path);
+ // Does not take ownership of the fd.
+ size_t Parse(int fd);
// Looks up the closest symbol (i.e. the one with the highest address <=
// |addr|) from its absolute 64-bit address.
diff --git a/src/kallsyms/kernel_symbol_map_benchmark.cc b/src/kallsyms/kernel_symbol_map_benchmark.cc
index 70bd8d4..08ddf30 100644
--- a/src/kallsyms/kernel_symbol_map_benchmark.cc
+++ b/src/kallsyms/kernel_symbol_map_benchmark.cc
@@ -101,7 +101,9 @@
// which slows down significantly the CI.
const bool skip = IsBenchmarkFunctionalOnly();
if (!skip) {
- kallsyms.Parse(perfetto::base::GetTestDataPath("test/data/kallsyms.txt"));
+ auto fd = perfetto::base::OpenFile(
+ perfetto::base::GetTestDataPath("test/data/kallsyms.txt"), O_RDONLY);
+ kallsyms.Parse(*fd);
}
for (auto _ : state) {
@@ -137,7 +139,8 @@
for (auto _ : state) {
perfetto::KernelSymbolMap kallsyms;
if (!skip) {
- kallsyms.Parse(kallsyms_path);
+ auto fd = perfetto::base::OpenFile(kallsyms_path, O_RDONLY);
+ kallsyms.Parse(*fd);
}
}
}
diff --git a/src/kallsyms/kernel_symbol_map_unittest.cc b/src/kallsyms/kernel_symbol_map_unittest.cc
index 7a00bc0..bab6c70 100644
--- a/src/kallsyms/kernel_symbol_map_unittest.cc
+++ b/src/kallsyms/kernel_symbol_map_unittest.cc
@@ -100,7 +100,7 @@
KernelSymbolMap kallsyms;
EXPECT_EQ(kallsyms.Lookup(0x42), "");
- kallsyms.Parse(tmp.path().c_str());
+ kallsyms.Parse(*base::OpenFile(tmp.path().c_str(), O_RDONLY));
EXPECT_EQ(kallsyms.num_syms(), 10u);
// Test first exact lookups.
@@ -157,7 +157,7 @@
base::FlushFile(tmp.fd());
KernelSymbolMap kallsyms;
- kallsyms.Parse(tmp.path().c_str());
+ kallsyms.Parse(*base::OpenFile(tmp.path().c_str(), O_RDONLY));
ASSERT_EQ(kallsyms.num_syms(), symbols.size());
for (const auto& kv : symbols) {
ASSERT_EQ(kallsyms.Lookup(kv.first), kv.second);
diff --git a/src/kallsyms/lazy_kernel_symbolizer.cc b/src/kallsyms/lazy_kernel_symbolizer.cc
index 8ed0af8..474ab45 100644
--- a/src/kallsyms/lazy_kernel_symbolizer.cc
+++ b/src/kallsyms/lazy_kernel_symbolizer.cc
@@ -18,49 +18,78 @@
#include <string>
+#include <sys/file.h>
#include <unistd.h>
#include "perfetto/base/build_config.h"
#include "perfetto/base/compiler.h"
#include "perfetto/ext/base/file_utils.h"
#include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/base/string_utils.h"
#include "perfetto/ext/base/utils.h"
#include "src/kallsyms/kernel_symbol_map.h"
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#include <sys/system_properties.h>
-#endif
-
namespace perfetto {
-
namespace {
const char kKallsymsPath[] = "/proc/kallsyms";
const char kPtrRestrictPath[] = "/proc/sys/kernel/kptr_restrict";
-const char kLowerPtrRestrictAndroidProp[] = "security.lower_kptr_restrict";
+const char kEnvName[] = "ANDROID_FILE__proc_kallsyms";
-// This class takes care of temporarily lowering kptr_restrict and putting it
-// back to the original value if necessary. It solves the following problem:
-// When reading /proc/kallsyms on Linux/Android, the symbol addresses can be
-// masked out (i.e. they are all 00000000) through the kptr_restrict file.
-// On Android kptr_restrict defaults to 2. On Linux, it depends on the
-// distribution. On Android we cannot simply write() kptr_restrict ourselves.
-// Doing so requires the union of:
-// - filesystem ACLs: kptr_restrict is rw-r--r--// and owned by root.
-// - Selinux rules: kptr_restrict is labelled as proc_security and restricted.
-// - CAP_SYS_ADMIN: when writing to kptr_restrict, the kernel enforces that the
-// caller has the SYS_ADMIN capability at write() time.
-// The latter would be problematic, we don't want traced_probes to have that,
-// CAP_SYS_ADMIN is too broad.
-// Instead, we opt for the following model: traced_probes sets an Android
-// property introduced in S (security.lower_kptr_restrict); init (which
-// satisfies all the requirements above) in turn sets kptr_restrict.
-// On Linux and standalone builds, instead, we don't have many options. Either:
-// - The system administrator takes care of lowering kptr_restrict before
-// tracing.
-// - The system administrator runs traced_probes as root / CAP_SYS_ADMIN and we
-// temporarily lower and restore kptr_restrict ourselves.
-// This class deals with all these cases.
+size_t ParseInheritedAndroidKallsyms(KernelSymbolMap* symbol_map) {
+ const char* fd_str = getenv(kEnvName);
+ auto inherited_fd = base::CStringToInt32(fd_str ? fd_str : "");
+ // Note: this is also the early exit for non-platform builds.
+ if (!inherited_fd.has_value()) {
+ PERFETTO_DLOG("Failed to parse %s (%s)", kEnvName, fd_str ? fd_str : "N/A");
+ return 0;
+ }
+
+ // We've inherited a special fd for kallsyms from init, but we might be
+ // sharing the underlying open file description with a concurrent process.
+ // Even if we use pread() for reading at absolute offsets, the underlying
+ // kernel seqfile is stateful and remembers where the last read stopped. In
+ // the worst case, two concurrent readers will cause a quadratic slowdown
+ // since the kernel reconstructs the seqfile from the beginning whenever two
+ // reads are not consequent.
+ // The chosen approach is to use provisional file locks to coordinate access.
+ // However we cannot use the special fd for locking, since the locks are based
+ // on the underlying open file description (in other words, both sharers will
+ // think they own the same lock). Therefore we open /proc/kallsyms again
+ // purely for locking purposes.
+ base::ScopedFile fd_for_lock = base::OpenFile(kKallsymsPath, O_RDONLY);
+ if (!fd_for_lock) {
+ PERFETTO_PLOG("Failed to open kallsyms for locking.");
+ return 0;
+ }
+
+ // Blocking lock since the only possible contention is
+ // traced_probes<->traced_perf, which will both lock only for the duration of
+ // the parse. Worst case, the task watchdog will restart the process.
+ //
+ // Lock goes away when |fd_for_lock| gets closed at end of scope.
+ if (flock(*fd_for_lock, LOCK_EX) != 0) {
+ PERFETTO_PLOG("Unexpected error in flock(kallsyms).");
+ return 0;
+ }
+
+ return symbol_map->Parse(*inherited_fd);
+}
+
+// This class takes care of temporarily lowering the kptr_restrict sysctl.
+// Otherwise the symbol addresses in /proc/kallsyms will be zeroed out on most
+// Linux configurations.
+//
+// On Android platform builds, this is solved by inheriting a kallsyms fd from
+// init, with symbols being visible as that is evaluated at the time of the
+// initial open().
+//
+// On Linux and standalone builds, we rely on this class in combination with
+// either:
+// - the sysctls (kptr_restrict, perf_event_paranoid) or this process'
+// capabilitied to be sufficient for addresses to be visible.
+// - this process to be running as root / CAP_SYS_ADMIN, in which case this
+// class will attempt to temporarily override kptr_restrict ourselves.
class ScopedKptrUnrestrict {
public:
ScopedKptrUnrestrict(); // Lowers kptr_restrict if necessary.
@@ -69,46 +98,15 @@
private:
static void WriteKptrRestrict(const std::string&);
- static const bool kUseAndroidProperty;
std::string initial_value_;
- bool restore_on_dtor_ = true;
};
-#if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
-// This is true only on Android in-tree builds (not on standalone).
-const bool ScopedKptrUnrestrict::kUseAndroidProperty = true;
-#else
-const bool ScopedKptrUnrestrict::kUseAndroidProperty = false;
-#endif
-
ScopedKptrUnrestrict::ScopedKptrUnrestrict() {
if (LazyKernelSymbolizer::CanReadKernelSymbolAddresses()) {
- // Everything seems to work (e.g., we are running as root and kptr_restrict
- // is < 2). Don't touching anything.
- restore_on_dtor_ = false;
+ // Symbols already visible, don't touch anything.
return;
}
- if (kUseAndroidProperty) {
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
- __system_property_set(kLowerPtrRestrictAndroidProp, "1");
-#endif
- // Init takes some time to react to the property change.
- // Unfortunately, we cannot read kptr_restrict because of SELinux. Instead,
- // we detect this by reading the initial lines of kallsyms and checking
- // that they are non-zero. This loop waits for at most 250ms (50 * 5ms).
- for (int attempt = 1; attempt <= 50; ++attempt) {
- usleep(5000);
- if (LazyKernelSymbolizer::CanReadKernelSymbolAddresses())
- return;
- }
- PERFETTO_ELOG("kallsyms addresses are still masked after setting %s",
- kLowerPtrRestrictAndroidProp);
- return;
- } // if (kUseAndroidProperty)
-
- // On Linux and Android standalone, read the kptr_restrict value and lower it
- // if needed.
bool read_res = base::ReadFile(kPtrRestrictPath, &initial_value_);
if (!read_res) {
PERFETTO_PLOG("Failed to read %s", kPtrRestrictPath);
@@ -124,15 +122,9 @@
}
ScopedKptrUnrestrict::~ScopedKptrUnrestrict() {
- if (!restore_on_dtor_)
+ if (initial_value_.empty())
return;
- if (kUseAndroidProperty) {
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
- __system_property_set(kLowerPtrRestrictAndroidProp, "0");
-#endif
- } else if (!initial_value_.empty()) {
- WriteKptrRestrict(initial_value_);
- }
+ WriteKptrRestrict(initial_value_);
}
void ScopedKptrUnrestrict::WriteKptrRestrict(const std::string& value) {
@@ -140,8 +132,9 @@
PERFETTO_DCHECK(!value.empty());
base::ScopedFile fd = base::OpenFile(kPtrRestrictPath, O_WRONLY);
auto wsize = write(*fd, value.c_str(), value.size());
- if (wsize <= 0)
+ if (wsize <= 0) {
PERFETTO_PLOG("Failed to set %s to %s", kPtrRestrictPath, value.c_str());
+ }
}
} // namespace
@@ -154,12 +147,19 @@
if (symbol_map_)
return symbol_map_.get();
- symbol_map_.reset(new KernelSymbolMap());
+ symbol_map_ = std::make_unique<KernelSymbolMap>();
- // If kptr_restrict is set, try temporarily lifting it (it works only if
- // traced_probes is run as a privileged user).
+ // Android platform builds: we have an fd from init.
+ size_t num_syms = ParseInheritedAndroidKallsyms(symbol_map_.get());
+ if (num_syms) {
+ return symbol_map_.get();
+ }
+
+ // Otherwise, try reading the file directly, temporarily lowering
+ // kptr_restrict if we're running with sufficient privileges.
ScopedKptrUnrestrict kptr_unrestrict;
- symbol_map_->Parse(kKallsymsPath);
+ auto fd = base::OpenFile(kKallsymsPath, O_RDONLY);
+ symbol_map_->Parse(*fd);
return symbol_map_.get();
}
diff --git a/src/profiling/common/producer_support.cc b/src/profiling/common/producer_support.cc
index 5303658..e9e193d 100644
--- a/src/profiling/common/producer_support.cc
+++ b/src/profiling/common/producer_support.cc
@@ -16,6 +16,7 @@
#include "src/profiling/common/producer_support.h"
+#include <algorithm>
#include <optional>
#include "perfetto/ext/base/android_utils.h"
diff --git a/src/profiling/symbolizer/local_symbolizer.cc b/src/profiling/symbolizer/local_symbolizer.cc
index 5271e82..4121687 100644
--- a/src/profiling/symbolizer/local_symbolizer.cc
+++ b/src/profiling/symbolizer/local_symbolizer.cc
@@ -347,12 +347,8 @@
return;
}
ssize_t rd = base::Read(*fd, &magic, sizeof(magic));
- if (rd != sizeof(magic)) {
- PERFETTO_PLOG("Failed to read %s", fname);
- return;
- }
- if (!IsElf(magic, static_cast<size_t>(rd)) &&
- !IsMachO64(magic, static_cast<size_t>(rd))) {
+ if (rd != sizeof(magic) || (!IsElf(magic, static_cast<size_t>(rd)) &&
+ !IsMachO64(magic, static_cast<size_t>(rd)))) {
PERFETTO_DLOG("%s not an ELF or Mach-O 64.", fname);
return;
}
diff --git a/src/protozero/scattered_stream_writer.cc b/src/protozero/scattered_stream_writer.cc
index 2743a68..ce138cc 100644
--- a/src/protozero/scattered_stream_writer.cc
+++ b/src/protozero/scattered_stream_writer.cc
@@ -66,11 +66,13 @@
// TODO(primiano): perf optimization: I suspect that at the end this will always
// be called with |size| == 4, in which case we might just hardcode it.
uint8_t* ScatteredStreamWriter::ReserveBytes(size_t size) {
- if (write_ptr_ + size > cur_range_.end) {
+ PERFETTO_DCHECK(write_ptr_ <= cur_range_.end);
+ if (size > static_cast<size_t>(cur_range_.end - write_ptr_)) {
// Assume the reservations are always < Delegate::GetNewBuffer().size(),
// so that one single call to Extend() will definitely give enough headroom.
Extend();
- PERFETTO_DCHECK(write_ptr_ + size <= cur_range_.end);
+ PERFETTO_DCHECK(write_ptr_ <= cur_range_.end);
+ PERFETTO_DCHECK(size <= static_cast<size_t>(cur_range_.end - write_ptr_));
}
uint8_t* begin = write_ptr_;
write_ptr_ += size;
diff --git a/src/shared_lib/BUILD.gn b/src/shared_lib/BUILD.gn
index 0e3115b..ee0aa49 100644
--- a/src/shared_lib/BUILD.gn
+++ b/src/shared_lib/BUILD.gn
@@ -34,7 +34,6 @@
"../../include/perfetto/public",
"../base",
"../tracing:client_api",
- "../tracing:platform_impl",
]
sources = [
"data_source.cc",
diff --git a/src/shared_lib/data_source.cc b/src/shared_lib/data_source.cc
index 32b57f8..64ad670 100644
--- a/src/shared_lib/data_source.cc
+++ b/src/shared_lib/data_source.cc
@@ -78,6 +78,25 @@
}
};
+struct PerfettoDsOnStopArgs {
+ struct PerfettoDsAsyncStopper* stopper = nullptr;
+};
+
+struct PerfettoDsAsyncStopper {
+ PerfettoDsImpl* ds_impl;
+ uint32_t instance_idx;
+ std::function<void()> async_stop_closure;
+
+ void FinishStop() {
+ std::lock_guard<std::mutex> lock(ds_impl->mu);
+ ds_impl->enabled_instances.reset(instance_idx);
+ if (ds_impl->enabled_instances.none()) {
+ ds_impl->enabled.store(false, std::memory_order_release);
+ }
+ async_stop_closure();
+ }
+};
+
namespace perfetto {
namespace shlib {
@@ -125,17 +144,24 @@
}
void OnStop(const StopArgs& args) override {
+ PerfettoDsOnStopArgs c_args;
+ c_args.stopper = new PerfettoDsAsyncStopper();
+ // Capturing ds_impl is ok, because data sources cannot be unregistered.
+ c_args.stopper->ds_impl = &type_;
+ c_args.stopper->async_stop_closure = args.HandleStopAsynchronously();
+ c_args.stopper->instance_idx = args.internal_instance_index;
+
if (type_.on_stop_cb) {
- type_.on_stop_cb(
- &type_, args.internal_instance_index, type_.cb_user_arg, inst_ctx_,
- const_cast<PerfettoDsOnStopArgs*>(
- reinterpret_cast<const PerfettoDsOnStopArgs*>(&args)));
+ type_.on_stop_cb(&type_, args.internal_instance_index, type_.cb_user_arg,
+ inst_ctx_, &c_args);
}
- std::lock_guard<std::mutex> lock(type_.mu);
- type_.enabled_instances.reset(args.internal_instance_index);
- if (type_.enabled_instances.none()) {
- type_.enabled.store(false, std::memory_order_release);
+ // If c_args.stopper is nullptr, the user must have called
+ // PerfettoDsOnStopArgsPostpone() in the callback above: the user will
+ // invoke PerfettoDsStopDone later. If c_args.stopper is not nullptr, we
+ // need to invoke it.
+ if (c_args.stopper) {
+ PerfettoDsStopDone(c_args.stopper);
}
}
@@ -353,16 +379,14 @@
PerfettoDsAsyncStopper* PerfettoDsOnStopArgsPostpone(
PerfettoDsOnStopArgs* args) {
- auto* cb = new std::function<void()>();
- *cb = reinterpret_cast<const ShlibDataSource::StopArgs*>(args)
- ->HandleStopAsynchronously();
- return reinterpret_cast<PerfettoDsAsyncStopper*>(cb);
+ PerfettoDsAsyncStopper* stopper = args->stopper;
+ args->stopper = nullptr;
+ return stopper;
}
void PerfettoDsStopDone(PerfettoDsAsyncStopper* stopper) {
- auto* cb = reinterpret_cast<std::function<void()>*>(stopper);
- (*cb)();
- delete cb;
+ stopper->FinishStop();
+ delete stopper;
}
PerfettoDsAsyncFlusher* PerfettoDsOnFlushArgsPostpone(
diff --git a/src/shared_lib/test/api_integrationtest.cc b/src/shared_lib/test/api_integrationtest.cc
index d5c7d45..5092ada 100644
--- a/src/shared_lib/test/api_integrationtest.cc
+++ b/src/shared_lib/test/api_integrationtest.cc
@@ -882,9 +882,50 @@
std::thread t([&]() { tracing_session.StopBlocking(); });
stop_called.WaitForNotification();
+
+ PERFETTO_DS_TRACE(data_source_2, ctx) {
+ struct PerfettoDsRootTracePacket trace_packet;
+ PerfettoDsTracerPacketBegin(&ctx, &trace_packet);
+
+ {
+ struct perfetto_protos_TestEvent for_testing;
+ perfetto_protos_TracePacket_begin_for_testing(&trace_packet.msg,
+ &for_testing);
+ {
+ struct perfetto_protos_TestEvent_TestPayload payload;
+ perfetto_protos_TestEvent_begin_payload(&for_testing, &payload);
+ perfetto_protos_TestEvent_TestPayload_set_cstr_str(&payload,
+ "After stop");
+ perfetto_protos_TestEvent_end_payload(&for_testing, &payload);
+ }
+ perfetto_protos_TracePacket_end_for_testing(&trace_packet.msg,
+ &for_testing);
+ }
+ PerfettoDsTracerPacketEnd(&ctx, &trace_packet);
+ }
+
PerfettoDsStopDone(stopper);
t.join();
+
+ PERFETTO_DS_TRACE(data_source_2, ctx) {
+ // After the postponed stop has been acknowledged, this should not be
+ // executed.
+ ADD_FAILURE();
+ }
+
+ std::vector<uint8_t> data = tracing_session.ReadBlocking();
+ EXPECT_THAT(
+ FieldView(data),
+ Contains(PbField(
+ perfetto_protos_Trace_packet_field_number,
+ MsgField(Contains(PbField(
+ perfetto_protos_TracePacket_for_testing_field_number,
+ MsgField(Contains(PbField(
+ perfetto_protos_TestEvent_payload_field_number,
+ MsgField(ElementsAre(PbField(
+ perfetto_protos_TestEvent_TestPayload_str_field_number,
+ StringField("After stop")))))))))))));
}
TEST_F(SharedLibDataSourceTest, FlushDone) {
diff --git a/src/trace_config_utils/BUILD.gn b/src/trace_config_utils/BUILD.gn
index 586b2de..090aa91 100644
--- a/src/trace_config_utils/BUILD.gn
+++ b/src/trace_config_utils/BUILD.gn
@@ -73,9 +73,12 @@
if (enable_perfetto_ui) {
wasm_lib("trace_config_utils_wasm") {
name = "trace_config_utils"
+ sources = [ "wasm.cc" ]
deps = [
- ":main",
+ ":pb_to_txt",
+ ":txt_to_pb",
"../../gn:default_deps",
+ "../../include/perfetto/ext/base:base",
]
}
}
diff --git a/src/trace_config_utils/wasm.cc b/src/trace_config_utils/wasm.cc
new file mode 100644
index 0000000..b7d3853
--- /dev/null
+++ b/src/trace_config_utils/wasm.cc
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <emscripten/emscripten.h>
+
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <string>
+
+#include "perfetto/ext/base/string_utils.h"
+#include "src/trace_config_utils/pb_to_txt.h"
+#include "src/trace_config_utils/txt_to_pb.h"
+
+namespace {
+// The buffer used to exchange input and output arguments. We assume 16MB
+// is enough to handle trace configs.
+char wasm_buf[16 * 1024 * 1024];
+} // namespace
+
+extern "C" {
+
+// Returns the pointer to the buffer.
+void* EMSCRIPTEN_KEEPALIVE trace_config_utils_buf();
+void* trace_config_utils_buf() {
+ return &wasm_buf;
+}
+
+// Returns the size of the buffer, so trace_config_utils_wasm.ts doesn't have
+// to hardcode the 16MB.
+uint32_t EMSCRIPTEN_KEEPALIVE trace_config_utils_buf_size();
+uint32_t trace_config_utils_buf_size() {
+ return static_cast<uint32_t>(sizeof(wasm_buf));
+}
+
+// Converts a proto-encoded protos.TraceConfig to text.
+// The caller must memcpy the bytes into the wasm_buf and pass the size of the
+// copied data into `size`. The returned pbtxt will be written in wasm_buf and
+// its size returned here.
+uint32_t EMSCRIPTEN_KEEPALIVE trace_config_pb_to_txt(uint32_t size);
+
+uint32_t trace_config_pb_to_txt(uint32_t size) {
+ std::string txt = perfetto::TraceConfigPbToTxt(wasm_buf, size);
+ size_t wsize = perfetto::base::SprintfTrunc(wasm_buf, sizeof(wasm_buf), "%s",
+ txt.c_str());
+ return static_cast<uint32_t>(wsize);
+}
+
+// Like the above, but converts a pbtxt into proto-encoded bytes.
+// Because this can fail (the C++ function returns a StatusOr) we write
+// a success/failure in the first byte to tell the diffrence.
+uint32_t EMSCRIPTEN_KEEPALIVE trace_config_txt_to_pb(uint32_t size);
+
+uint32_t trace_config_txt_to_pb(uint32_t size) {
+ auto res = perfetto::TraceConfigTxtToPb(std::string(wasm_buf, size));
+ if (!res.ok()) {
+ wasm_buf[0] = 0;
+ size_t wsize = perfetto::base::SprintfTrunc(
+ &wasm_buf[1], sizeof(wasm_buf) - 1, "%s", res.status().c_message());
+ return static_cast<uint32_t>(wsize);
+ }
+ const size_t resp_size = std::min(res->size(), sizeof(wasm_buf) - 1);
+ wasm_buf[0] = 1;
+ memcpy(&wasm_buf[1], res->data(), resp_size);
+ return static_cast<uint32_t>(resp_size);
+}
+} // extern "C"
+
+// This is unused but is needed to keep emscripten happy.
+int main(int, char**) {
+ return 0;
+}
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index 7df41eb..0b86a1b 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -127,9 +127,6 @@
"importers/ftrace:minimal",
"importers/fuchsia:fuchsia_record",
"importers/memory_tracker:graph_processor",
- "importers/proto:gen_cc_android_track_event_descriptor",
- "importers/proto:gen_cc_chrome_track_event_descriptor",
- "importers/proto:gen_cc_track_event_descriptor",
"importers/proto:minimal",
"importers/systrace:systrace_line",
"sorter",
diff --git a/src/trace_processor/db/query_executor.cc b/src/trace_processor/db/query_executor.cc
index 840564a..258123d 100644
--- a/src/trace_processor/db/query_executor.cc
+++ b/src/trace_processor/db/query_executor.cc
@@ -75,10 +75,15 @@
uint32_t rm_last = rm->Get(rm_size - 1);
uint32_t range_size = rm_last - rm_first;
+ // Always prefer linear search if on a range *except* when the range is small
+ // but the last element of the range is large: this will cause a big bitvector
+ // to be created which negates the benefits of using linear search over
+ // index search.
+ bool disallows_index_search = rm->IsRange() && rm_last < range_size * 100;
+
// If the number of elements in the rowmap is small or the number of
// elements is less than 1/10th of the range, use indexed filtering.
// TODO(b/283763282): use Overlay estimations.
- bool disallows_index_search = rm->IsRange();
bool prefers_index_search =
rm->IsIndexVector() || rm_size < 1024 || rm_size * 10 < range_size;
diff --git a/src/trace_processor/db/query_executor_benchmark.cc b/src/trace_processor/db/query_executor_benchmark.cc
index 6969793..3213417 100644
--- a/src/trace_processor/db/query_executor_benchmark.cc
+++ b/src/trace_processor/db/query_executor_benchmark.cc
@@ -42,8 +42,6 @@
namespace {
using SliceTable = tables::SliceTable;
-using ExpectedFrameTimelineSliceTable = tables::ExpectedFrameTimelineSliceTable;
-using RawTable = tables::RawTable;
using FtraceEventTable = tables::FtraceEventTable;
using HeapGraphObjectTable = tables::HeapGraphObjectTable;
@@ -51,10 +49,6 @@
constexpr std::string_view kSliceTable =
"test/data/slice_table_for_benchmarks.csv";
-// `SELECT * FROM SLICE` on android_monitor_contention_trace.at
-constexpr std::string_view kExpectedFrameTimelineTable =
- "test/data/expected_frame_timeline_for_benchmarks.csv";
-
// `SELECT id, cpu FROM raw` on chrome_android_systrace.pftrace.
constexpr std::string_view kRawTable = "test/data/raw_cpu_for_benchmarks.csv";
@@ -151,47 +145,6 @@
SliceTable table_;
};
-struct ExpectedFrameTimelineTableForBenchmark {
- explicit ExpectedFrameTimelineTableForBenchmark(benchmark::State& state)
- : table_{&pool_, &parent_} {
- std::vector<std::string> table_rows_as_string =
- ReadCSV(state, kExpectedFrameTimelineTable);
- std::vector<std::string> parent_rows_as_string =
- ReadCSV(state, kSliceTable);
-
- uint32_t cur_idx = 0;
- for (size_t i = 1; i < table_rows_as_string.size(); ++i, ++cur_idx) {
- std::vector<std::string> row_vec = SplitCSVLine(table_rows_as_string[i]);
-
- uint32_t idx = *base::StringToUInt32(row_vec[0]);
- while (cur_idx < idx) {
- parent_.Insert(
- GetSliceTableRow(parent_rows_as_string[cur_idx + 1], pool_));
- cur_idx++;
- }
-
- ExpectedFrameTimelineSliceTable::Row row;
- row.ts = *base::StringToInt64(row_vec[2]);
- row.dur = *base::StringToInt64(row_vec[3]);
- row.track_id = tables::TrackTable::Id(*base::StringToUInt32(row_vec[4]));
- row.depth = *base::StringToUInt32(row_vec[7]);
- row.stack_id = *base::StringToInt32(row_vec[8]);
- row.parent_stack_id = *base::StringToInt32(row_vec[9]);
- row.parent_id = base::StringToUInt32(row_vec[11]).has_value()
- ? std::make_optional<SliceTable::Id>(
- *base::StringToUInt32(row_vec[11]))
- : std::nullopt;
- row.arg_set_id = *base::StringToUInt32(row_vec[11]);
- row.thread_ts = base::StringToInt64(row_vec[12]);
- row.thread_dur = base::StringToInt64(row_vec[13]);
- table_.Insert(row);
- }
- }
- StringPool pool_;
- SliceTable parent_{&pool_};
- ExpectedFrameTimelineSliceTable table_;
-};
-
struct FtraceEventTableForBenchmark {
explicit FtraceEventTableForBenchmark(benchmark::State& state) {
std::vector<std::string> raw_rows = ReadCSV(state, kRawTable);
@@ -201,14 +154,6 @@
uint32_t cur_idx = 0;
for (size_t i = 1; i < ftrace_event_rows.size(); ++i, cur_idx++) {
std::vector<std::string> row_vec = SplitCSVLine(ftrace_event_rows[i]);
- uint32_t idx = *base::StringToUInt32(row_vec[0]);
- while (cur_idx < idx) {
- std::vector<std::string> raw_row = SplitCSVLine(raw_rows[cur_idx + 1]);
- RawTable::Row r;
- r.ucpu = tables::CpuTable::Id(*base::StringToUInt32(raw_row[1]));
- raw_.Insert(r);
- cur_idx++;
- }
FtraceEventTable::Row row;
row.ucpu = tables::CpuTable::Id(*base::StringToUInt32(row_vec[1]));
table_.Insert(row);
@@ -216,8 +161,7 @@
}
StringPool pool_;
- RawTable raw_{&pool_};
- tables::FtraceEventTable table_{&pool_, &raw_};
+ tables::FtraceEventTable table_{&pool_};
};
struct HeapGraphObjectTableForBenchmark {
@@ -268,23 +212,6 @@
benchmark::Counter::kInvert);
}
-void BenchmarkExpectedFrameTableQuery(
- benchmark::State& state,
- ExpectedFrameTimelineTableForBenchmark& table,
- Query q) {
- for (auto _ : state) {
- benchmark::DoNotOptimize(table.table_.FilterToIterator(q));
- }
- state.counters["s/row"] =
- benchmark::Counter(static_cast<double>(table.table_.row_count()),
- benchmark::Counter::kIsIterationInvariantRate |
- benchmark::Counter::kInvert);
- state.counters["s/out"] =
- benchmark::Counter(CountRows(table.table_.FilterToIterator(q)),
- benchmark::Counter::kIsIterationInvariantRate |
- benchmark::Counter::kInvert);
-}
-
void BenchmarkFtraceEventTableQuery(benchmark::State& state,
FtraceEventTableForBenchmark& table,
Query q) {
@@ -370,14 +297,6 @@
}
BENCHMARK(BM_QESliceTableSorted);
-void BM_QEFilterWithSparseSelector(benchmark::State& state) {
- ExpectedFrameTimelineTableForBenchmark table(state);
- Query q;
- q.constraints = {table.table_.track_id().eq(1445)};
- BenchmarkExpectedFrameTableQuery(state, table, q);
-}
-BENCHMARK(BM_QEFilterWithSparseSelector);
-
void BM_QEFilterWithDenseSelector(benchmark::State& state) {
FtraceEventTableForBenchmark table(state);
Query q;
@@ -585,15 +504,6 @@
}
BENCHMARK(BM_QEFtraceEventSortSelectorNumericDesc);
-void BM_QEDistinctWithSparseSelector(benchmark::State& state) {
- ExpectedFrameTimelineTableForBenchmark table(state);
- Query q;
- q.order_type = Query::OrderType::kDistinct;
- q.orders = {table.table_.track_id().descending()};
- BenchmarkExpectedFrameTableQuery(state, table, q);
-}
-BENCHMARK(BM_QEDistinctWithSparseSelector);
-
void BM_QEDistinctWithDenseSelector(benchmark::State& state) {
FtraceEventTableForBenchmark table(state);
Query q;
@@ -603,15 +513,6 @@
}
BENCHMARK(BM_QEDistinctWithDenseSelector);
-void BM_QEDistinctSortedWithSparseSelector(benchmark::State& state) {
- ExpectedFrameTimelineTableForBenchmark table(state);
- Query q;
- q.order_type = Query::OrderType::kDistinctAndSort;
- q.orders = {table.table_.track_id().descending()};
- BenchmarkExpectedFrameTableQuery(state, table, q);
-}
-BENCHMARK(BM_QEDistinctSortedWithSparseSelector);
-
void BM_QEDistinctSortedWithDenseSelector(benchmark::State& state) {
FtraceEventTableForBenchmark table(state);
Query q;
diff --git a/src/trace_processor/export_json.cc b/src/trace_processor/export_json.cc
index 760f058..845298b 100644
--- a/src/trace_processor/export_json.cc
+++ b/src/trace_processor/export_json.cc
@@ -37,6 +37,7 @@
#include "perfetto/base/build_config.h"
#include "perfetto/base/logging.h"
#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
#include "perfetto/ext/base/string_splitter.h"
#include "perfetto/ext/base/string_utils.h"
#include "perfetto/ext/base/string_view.h"
@@ -503,28 +504,24 @@
inf_value_(Json::StaticString("Infinity")),
neg_inf_value_(Json::StaticString("-Infinity")) {
const auto& arg_table = storage_->arg_table();
- uint32_t count = arg_table.row_count();
- if (count == 0) {
- args_sets_.resize(1, empty_value_);
- return;
- }
- args_sets_.resize(arg_table[count - 1].arg_set_id() + 1, empty_value_);
-
+ Json::Value* cur_args_ptr = nullptr;
+ uint32_t cur_args_set_id = std::numeric_limits<uint32_t>::max();
for (auto it = arg_table.IterateRows(); it; ++it) {
ArgSetId set_id = it.arg_set_id();
+ if (set_id != cur_args_set_id) {
+ cur_args_ptr =
+ args_sets_.Insert(set_id, Json::Value(Json::objectValue)).first;
+ cur_args_set_id = set_id;
+ }
const char* key = storage->GetString(it.key()).c_str();
Variadic value = storage_->GetArgValue(it.row_number().row_number());
- AppendArg(set_id, key, VariadicToJson(value));
+ AppendArg(cur_args_ptr, key, VariadicToJson(value));
}
PostprocessArgs();
}
- const Json::Value& GetArgs(ArgSetId set_id) const {
- // If |set_id| was empty and added to the storage last, it may not be in
- // args_sets_.
- if (set_id > args_sets_.size())
- return empty_value_;
- return args_sets_[set_id];
+ const Json::Value& GetArgs(std::optional<ArgSetId> set_id) const {
+ return set_id ? *args_sets_.Find(*set_id) : empty_value_;
}
private:
@@ -566,15 +563,13 @@
PERFETTO_FATAL("Not reached"); // For gcc.
}
- void AppendArg(ArgSetId set_id,
- const std::string& key,
- const Json::Value& value) {
- Json::Value* target = &args_sets_[set_id];
+ static void AppendArg(Json::Value* target,
+ const std::string& key,
+ const Json::Value& value) {
for (base::StringSplitter parts(key, '.'); parts.Next();) {
if (PERFETTO_UNLIKELY(!target->isNull() && !target->isObject())) {
PERFETTO_DLOG("Malformed arguments. Can't append %s to %s.",
- key.c_str(),
- args_sets_[set_id].toStyledString().c_str());
+ key.c_str(), target->toStyledString().c_str());
return;
}
std::string key_part = parts.cur_token();
@@ -592,8 +587,7 @@
bracketpos - 1);
if (PERFETTO_UNLIKELY(!target->isNull() && !target->isArray())) {
PERFETTO_DLOG("Malformed arguments. Can't append %s to %s.",
- key.c_str(),
- args_sets_[set_id].toStyledString().c_str());
+ key.c_str(), target->toStyledString().c_str());
return;
}
std::optional<uint32_t> index = base::StringToUInt32(s);
@@ -611,7 +605,8 @@
}
void PostprocessArgs() {
- for (Json::Value& args : args_sets_) {
+ for (auto it = args_sets_.GetIterator(); it; ++it) {
+ auto& args = it.value();
// Move all fields from "debug" key to upper level.
if (args.isMember("debug")) {
Json::Value debug = args["debug"];
@@ -648,7 +643,7 @@
}
const TraceStorage* storage_;
- std::vector<Json::Value> args_sets_;
+ base::FlatHashMap<ArgSetId, Json::Value> args_sets_;
const Json::Value empty_value_;
const Json::Value nan_value_;
const Json::Value inf_value_;
@@ -1073,12 +1068,12 @@
for (auto it = flow_table.IterateRows(); it; ++it) {
SliceId slice_out = it.slice_out();
SliceId slice_in = it.slice_in();
- uint32_t arg_set_id = it.arg_set_id();
+ std::optional<uint32_t> arg_set_id = it.arg_set_id();
std::string cat;
std::string name;
auto args = args_builder_.GetArgs(arg_set_id);
- if (arg_set_id != kInvalidArgSetId) {
+ if (arg_set_id != std::nullopt) {
cat = args["cat"].asString();
name = args["name"].asString();
// Don't export these args since they are only used for this export and
@@ -1107,7 +1102,7 @@
}
Json::Value ConvertLegacyRawEventToJson(
- const tables::RawTable::ConstIterator& it) {
+ const tables::ChromeRawTable::ConstIterator& it) {
Json::Value event;
event["ts"] = Json::Int64(it.ts() / 1000);
@@ -1192,7 +1187,7 @@
std::optional<StringId> raw_chrome_metadata_event_id =
storage_->string_pool().GetId("chrome_event.metadata");
- const auto& events = storage_->raw_table();
+ const auto& events = storage_->chrome_raw_table();
for (auto it = events.IterateRows(); it; ++it) {
if (raw_legacy_event_key_id && it.name() == *raw_legacy_event_key_id) {
Json::Value event = ConvertLegacyRawEventToJson(it);
diff --git a/src/trace_processor/export_json.h b/src/trace_processor/export_json.h
index 47726bf..34f0973 100644
--- a/src/trace_processor/export_json.h
+++ b/src/trace_processor/export_json.h
@@ -28,10 +28,10 @@
namespace json {
// Export trace to a file stream in json format.
-util::Status ExportJson(const TraceStorage*, FILE* output);
+base::Status ExportJson(const TraceStorage*, FILE* output);
// For testing.
-util::Status ExportJson(const TraceStorage* storage,
+base::Status ExportJson(const TraceStorage* storage,
OutputWriter*,
ArgumentFilterPredicate = nullptr,
MetadataFilterPredicate = nullptr,
diff --git a/src/trace_processor/export_json_unittest.cc b/src/trace_processor/export_json_unittest.cc
index aa46048..dd009d1 100644
--- a/src/trace_processor/export_json_unittest.cc
+++ b/src/trace_processor/export_json_unittest.cc
@@ -155,8 +155,8 @@
StringId name_id = context_.storage->InternString(base::StringView(kName));
// The thread_slice table is a sub table of slice.
context_.storage->mutable_slice_table()->Insert(
- {kTimestamp, kDuration, track, cat_id, name_id, 0, 0, 0, SliceId(0u), 0,
- kThreadTimestamp, kThreadDuration, kThreadInstructionCount,
+ {kTimestamp, kDuration, track, cat_id, name_id, 0, 0, 0, SliceId(0u),
+ std::nullopt, kThreadTimestamp, kThreadDuration, kThreadInstructionCount,
kThreadInstructionDelta});
base::TempFile temp_file = base::TempFile::Create();
@@ -180,7 +180,7 @@
EXPECT_EQ(event["cat"].asString(), kCategory);
EXPECT_EQ(event["name"].asString(), kName);
EXPECT_TRUE(event["args"].isObject());
- EXPECT_EQ(event["args"].size(), 0u);
+ EXPECT_EQ(event["args"].size(), 0u) << event["args"].toStyledString();
}
TEST_F(ExportJsonTest, StorageWithOneUnfinishedSlice) {
@@ -200,8 +200,8 @@
StringId cat_id = context_.storage->InternString(base::StringView(kCategory));
StringId name_id = context_.storage->InternString(base::StringView(kName));
context_.storage->mutable_slice_table()->Insert(
- {kTimestamp, kDuration, track, cat_id, name_id, 0, 0, 0, SliceId(0u), 0,
- kThreadTimestamp, kThreadDuration, kThreadInstructionCount,
+ {kTimestamp, kDuration, track, cat_id, name_id, 0, 0, 0, SliceId(0u),
+ std::nullopt, kThreadTimestamp, kThreadDuration, kThreadInstructionCount,
kThreadInstructionDelta});
base::TempFile temp_file = base::TempFile::Create();
@@ -414,11 +414,10 @@
TraceStorage* storage = context_.storage.get();
- auto ucpu = context_.cpu_tracker->GetOrCreateCpu(0);
- RawId id = storage->mutable_raw_table()
- ->Insert({0, storage->InternString("chrome_event.metadata"), 0,
- 0, 0, ucpu})
- .id;
+ tables::ChromeRawTable::Id id =
+ storage->mutable_chrome_raw_table()
+ ->Insert({0, storage->InternString("chrome_event.metadata"), 0, 0})
+ .id;
StringId name1_id = storage->InternString(base::StringView(kName1));
StringId name2_id = storage->InternString(base::StringView(kName2));
@@ -780,7 +779,6 @@
context_.storage->InternString("source"),
Variadic::String(context_.storage->InternString("chrome")));
});
- context_.args_tracker->Flush(); // Flush track args.
StringId cat_id = context_.storage->InternString(base::StringView(kCategory));
StringId name_id = context_.storage->InternString(base::StringView(kName));
context_.storage->mutable_slice_table()->Insert(
@@ -788,8 +786,8 @@
// Global track.
TrackEventTracker track_event_tracker(&context_);
- TrackId track2 = track_event_tracker.GetOrCreateDefaultDescriptorTrack();
- context_.args_tracker->Flush(); // Flush track args.
+ TrackId track2 = *track_event_tracker.GetDescriptorTrack(
+ TrackEventTracker::kDefaultDescriptorTrackUuid);
context_.storage->mutable_slice_table()->Insert(
{kTimestamp2, 0, track2, cat_id, name_id, 0, 0, 0});
@@ -798,7 +796,6 @@
reservation.parent_uuid = 0;
track_event_tracker.ReserveDescriptorTrack(1234, reservation);
TrackId track3 = *track_event_tracker.GetDescriptorTrack(1234);
- context_.args_tracker->Flush(); // Flush track args.
context_.storage->mutable_slice_table()->Insert(
{kTimestamp3, 0, track3, cat_id, name_id, 0, 0, 0});
@@ -1428,10 +1425,8 @@
auto& tt = *context_.storage->mutable_thread_table();
tt[utid].set_upid(upid);
- auto ucpu = context_.cpu_tracker->GetOrCreateCpu(0);
- auto id_and_row = storage->mutable_raw_table()->Insert(
- {kTimestamp, storage->InternString("track_event.legacy_event"), utid, 0,
- 0, ucpu});
+ auto id_and_row = storage->mutable_chrome_raw_table()->Insert(
+ {kTimestamp, storage->InternString("track_event.legacy_event"), utid, 0});
auto inserter = context_.args_tracker->AddArgsTo(id_and_row.id);
auto add_arg = [&](const char* key, Variadic value) {
@@ -1500,7 +1495,7 @@
const char* kLegacyJsonData2 = "er\": 1},{\"user\": 2}";
TraceStorage* storage = context_.storage.get();
- auto* raw = storage->mutable_raw_table();
+ auto* raw = storage->mutable_chrome_raw_table();
auto id_and_row = raw->Insert(
{0, storage->InternString("chrome_event.legacy_system_trace"), 0, 0});
@@ -1726,8 +1721,8 @@
TraceStorage* storage = context_.storage.get();
- auto* raw = storage->mutable_raw_table();
- RawId id =
+ auto* raw = storage->mutable_chrome_raw_table();
+ tables::ChromeRawTable::Id id =
raw->Insert({0, storage->InternString("chrome_event.metadata"), 0, 0}).id;
StringId name1_id = storage->InternString(base::StringView(kName1));
diff --git a/src/trace_processor/importers/android_bugreport/android_battery_stats_reader.cc b/src/trace_processor/importers/android_bugreport/android_battery_stats_reader.cc
index a719a15..c8c9816 100644
--- a/src/trace_processor/importers/android_bugreport/android_battery_stats_reader.cc
+++ b/src/trace_processor/importers/android_bugreport/android_battery_stats_reader.cc
@@ -58,7 +58,7 @@
AndroidBatteryStatsReader::~AndroidBatteryStatsReader() = default;
-util::Status AndroidBatteryStatsReader::ParseLine(base::StringView line) {
+base::Status AndroidBatteryStatsReader::ParseLine(base::StringView line) {
base::StringViewSplitter splitter(line, ',');
// consume the legacy version number which we expect to be at the start of
@@ -131,7 +131,7 @@
return base::OkStatus();
}
-util::Status AndroidBatteryStatsReader::ProcessBatteryStatsHistoryEvent(
+base::Status AndroidBatteryStatsReader::ProcessBatteryStatsHistoryEvent(
base::StringView raw_event) {
AndroidDumpstateEvent event{
AndroidDumpstateEvent::EventType::kBatteryStatsHistoryEvent,
@@ -139,7 +139,7 @@
return SendToSorter(std::chrono::milliseconds(current_timestamp_ms_), event);
}
-util::Status AndroidBatteryStatsReader::SendToSorter(
+base::Status AndroidBatteryStatsReader::SendToSorter(
std::chrono::nanoseconds event_ts,
AndroidDumpstateEvent event) {
ASSIGN_OR_RETURN(
diff --git a/src/trace_processor/importers/android_bugreport/android_bugreport_reader.cc b/src/trace_processor/importers/android_bugreport/android_bugreport_reader.cc
index 07c8434..2e28fb7 100644
--- a/src/trace_processor/importers/android_bugreport/android_bugreport_reader.cc
+++ b/src/trace_processor/importers/android_bugreport/android_bugreport_reader.cc
@@ -95,7 +95,7 @@
}
// static
-util::Status AndroidBugreportReader::Parse(TraceProcessorContext* context,
+base::Status AndroidBugreportReader::Parse(TraceProcessorContext* context,
std::vector<util::ZipFile> files) {
auto res = FindBugReportFile(files);
if (!res.has_value()) {
@@ -136,7 +136,7 @@
AndroidBugreportReader::~AndroidBugreportReader() = default;
-util::Status AndroidBugreportReader::ParseImpl() {
+base::Status AndroidBugreportReader::ParseImpl() {
// All logs in Android bugreports use wall time (which creates problems
// in case of early boot events before NTP kicks in, which get emitted as
// 1970), but that is the state of affairs.
diff --git a/src/trace_processor/importers/android_bugreport/android_bugreport_reader.h b/src/trace_processor/importers/android_bugreport/android_bugreport_reader.h
index b36c45c..5269a19 100644
--- a/src/trace_processor/importers/android_bugreport/android_bugreport_reader.h
+++ b/src/trace_processor/importers/android_bugreport/android_bugreport_reader.h
@@ -42,7 +42,7 @@
public:
static bool IsAndroidBugReport(
const std::vector<util::ZipFile>& zip_file_entries);
- static util::Status Parse(TraceProcessorContext* context,
+ static base::Status Parse(TraceProcessorContext* context,
std::vector<util::ZipFile> zip_file_entries);
private:
@@ -70,7 +70,7 @@
BugReportFile bug_report,
std::set<LogFile> ordered_log_files);
~AndroidBugreportReader();
- util::Status ParseImpl();
+ base::Status ParseImpl();
base::StatusOr<std::vector<TimestampedAndroidLogEvent>>
ParsePersistentLogcat();
diff --git a/src/trace_processor/importers/android_bugreport/android_log_reader.cc b/src/trace_processor/importers/android_bugreport/android_log_reader.cc
index 740538d..ea0fbe1 100644
--- a/src/trace_processor/importers/android_bugreport/android_log_reader.cc
+++ b/src/trace_processor/importers/android_bugreport/android_log_reader.cc
@@ -128,7 +128,7 @@
AndroidLogReader::~AndroidLogReader() = default;
-util::Status AndroidLogReader::ParseLine(base::StringView line) {
+base::Status AndroidLogReader::ParseLine(base::StringView line) {
if (line.size() < 30 ||
(line.at(0) == '-' && line.at(1) == '-' && line.at(2) == '-')) {
// These are markers like "--------- switch to radio" which we ignore.
@@ -221,12 +221,12 @@
return ProcessEvent(event_ts, std::move(event));
}
-util::Status AndroidLogReader::ProcessEvent(std::chrono::nanoseconds event_ts,
+base::Status AndroidLogReader::ProcessEvent(std::chrono::nanoseconds event_ts,
AndroidLogEvent event) {
return SendToSorter(event_ts, std::move(event));
}
-util::Status AndroidLogReader::SendToSorter(std::chrono::nanoseconds event_ts,
+base::Status AndroidLogReader::SendToSorter(std::chrono::nanoseconds event_ts,
AndroidLogEvent event) {
event_ts -= timezone_offset_;
ASSIGN_OR_RETURN(
diff --git a/src/trace_processor/importers/archive/tar_trace_reader.cc b/src/trace_processor/importers/archive/tar_trace_reader.cc
index d0cf11f4..2ea8955 100644
--- a/src/trace_processor/importers/archive/tar_trace_reader.cc
+++ b/src/trace_processor/importers/archive/tar_trace_reader.cc
@@ -126,7 +126,7 @@
TarTraceReader::~TarTraceReader() = default;
-util::Status TarTraceReader::Parse(TraceBlobView blob) {
+base::Status TarTraceReader::Parse(TraceBlobView blob) {
ParseResult result = ParseResult::kOk;
buffer_.PushBack(std::move(blob));
while (!buffer_.empty() && result == ParseResult::kOk) {
diff --git a/src/trace_processor/importers/common/args_tracker.h b/src/trace_processor/importers/common/args_tracker.h
index bacd67b..dd1b645 100644
--- a/src/trace_processor/importers/common/args_tracker.h
+++ b/src/trace_processor/importers/common/args_tracker.h
@@ -21,6 +21,7 @@
#include "perfetto/ext/base/small_vector.h"
#include "src/trace_processor/importers/common/global_args_tracker.h"
#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
#include "src/trace_processor/types/trace_processor_context.h"
#include "src/trace_processor/types/variadic.h"
@@ -104,8 +105,12 @@
virtual ~ArgsTracker();
- BoundInserter AddArgsTo(RawId id) {
- return AddArgsTo(context_->storage->mutable_raw_table(), id);
+ BoundInserter AddArgsTo(tables::ChromeRawTable::Id id) {
+ return AddArgsTo(context_->storage->mutable_chrome_raw_table(), id);
+ }
+
+ BoundInserter AddArgsTo(tables::FtraceEventTable::Id id) {
+ return AddArgsTo(context_->storage->mutable_ftrace_event_table(), id);
}
BoundInserter AddArgsTo(CounterId id) {
diff --git a/src/trace_processor/importers/common/flow_tracker.cc b/src/trace_processor/importers/common/flow_tracker.cc
index 4d6cc66..6990e2a 100644
--- a/src/trace_processor/importers/common/flow_tracker.cc
+++ b/src/trace_processor/importers/common/flow_tracker.cc
@@ -142,8 +142,7 @@
void FlowTracker::InsertFlow(FlowId flow_id,
SliceId slice_out_id,
SliceId slice_in_id) {
- tables::FlowTable::Row row(slice_out_id, slice_in_id, flow_id,
- kInvalidArgSetId);
+ tables::FlowTable::Row row(slice_out_id, slice_in_id, flow_id, std::nullopt);
auto id = context_->storage->mutable_flow_table()->Insert(row).id;
auto* it = flow_id_to_v1_flow_id_map_.Find(flow_id);
@@ -158,7 +157,7 @@
void FlowTracker::InsertFlow(SliceId slice_out_id, SliceId slice_in_id) {
tables::FlowTable::Row row(slice_out_id, slice_in_id, std::nullopt,
- kInvalidArgSetId);
+ std::nullopt);
context_->storage->mutable_flow_table()->Insert(row);
}
diff --git a/src/trace_processor/importers/common/global_args_tracker.h b/src/trace_processor/importers/common/global_args_tracker.h
index 2fd9f36..916ed2b 100644
--- a/src/trace_processor/importers/common/global_args_tracker.h
+++ b/src/trace_processor/importers/common/global_args_tracker.h
@@ -145,21 +145,18 @@
auto& arg_table = *storage_->mutable_arg_table();
+ uint32_t arg_set_id = arg_table.row_count();
ArgSetHash digest = hash.digest();
- auto it_and_inserted =
- arg_row_for_hash_.Insert(digest, arg_table.row_count());
- if (!it_and_inserted.second) {
+ auto [it, inserted] = arg_row_for_hash_.Insert(digest, arg_set_id);
+ if (!inserted) {
// Already inserted.
- return arg_table[*it_and_inserted.first].arg_set_id();
+ return *it;
}
- // Taking size() after the Insert() ensures that nothing has an id == 0
- // (0 == kInvalidArgSetId).
- auto id = static_cast<uint32_t>(arg_row_for_hash_.size());
for (const CompactArg* ptr : valid) {
const auto& arg = *ptr;
tables::ArgTable::Row row;
- row.arg_set_id = id;
+ row.arg_set_id = arg_set_id;
row.flat_key = arg.flat_key;
row.key = arg.key;
switch (arg.value.type) {
@@ -190,7 +187,7 @@
row.value_type = storage_->GetIdForVariadicType(arg.value.type);
arg_table.Insert(row);
}
- return id;
+ return arg_set_id;
}
base::FlatHashMap<ArgSetHash, uint32_t, base::AlreadyHashed<ArgSetHash>>
diff --git a/src/trace_processor/importers/common/jit_cache.cc b/src/trace_processor/importers/common/jit_cache.cc
index 8380177..452aaf2 100644
--- a/src/trace_processor/importers/common/jit_cache.cc
+++ b/src/trace_processor/importers/common/jit_cache.cc
@@ -108,6 +108,30 @@
return jit_code_id;
}
+tables::JitCodeTable::Id JitCache::MoveCode(int64_t timestamp,
+ UniqueTid,
+ uint64_t from_code_start,
+ uint64_t to_code_start) {
+ auto* jit_code_table = context_->storage->mutable_jit_code_table();
+
+ auto it = functions_.Find(from_code_start);
+ AddressRange old_code_range = it->first;
+ JittedFunction func = std::move(it->second);
+ functions_.erase(it);
+
+ auto code_id = func.jit_code_id();
+ AddressRange new_code_range(to_code_start, old_code_range.size());
+
+ functions_.DeleteOverlapsAndEmplace(
+ [&](std::pair<const AddressRange, JittedFunction>& entry) {
+ jit_code_table->FindById(entry.second.jit_code_id())
+ ->set_estimated_delete_ts(timestamp);
+ },
+ new_code_range, std::move(func));
+
+ return code_id;
+}
+
std::pair<FrameId, bool> JitCache::InternFrame(VirtualMemoryMapping* mapping,
uint64_t rel_pc,
base::StringView function_name) {
diff --git a/src/trace_processor/importers/common/jit_cache.h b/src/trace_processor/importers/common/jit_cache.h
index 19205b6..4a7eaf2 100644
--- a/src/trace_processor/importers/common/jit_cache.h
+++ b/src/trace_processor/importers/common/jit_cache.h
@@ -68,6 +68,10 @@
StringId function_name,
std::optional<SourceLocation> source_location,
TraceBlobView native_code);
+ tables::JitCodeTable::Id MoveCode(int64_t timestamp,
+ UniqueTid utid,
+ uint64_t from_code_start,
+ uint64_t to_code_start);
// Forward frame interning request.
// MappingTracker allows other trackers to register ranges of memory for
diff --git a/src/trace_processor/importers/common/parser_types.h b/src/trace_processor/importers/common/parser_types.h
index 917479a..07eb8df 100644
--- a/src/trace_processor/importers/common/parser_types.h
+++ b/src/trace_processor/importers/common/parser_types.h
@@ -23,6 +23,7 @@
#include <optional>
#include <string>
#include <utility>
+#include <variant>
#include "perfetto/trace_processor/ref_counted.h"
#include "perfetto/trace_processor/trace_blob_view.h"
@@ -56,16 +57,19 @@
static_assert(sizeof(InlineSchedWaking) == 16);
struct alignas(8) JsonEvent {
+ struct Begin {};
+ struct End {};
+ struct Scoped {
+ int64_t dur;
+ };
+ struct Other {};
+ using Type = std::variant<Begin, End, Scoped, Other>;
+
std::string value;
+ Type type;
};
static_assert(sizeof(JsonEvent) % 8 == 0);
-struct alignas(8) JsonWithDurEvent {
- int64_t dur;
- std::string value;
-};
-static_assert(sizeof(JsonWithDurEvent) % 8 == 0);
-
struct alignas(8) TracePacketData {
TraceBlobView packet;
RefPtr<PacketSequenceStateGeneration> sequence_state;
diff --git a/src/trace_processor/importers/common/slice_tracker_unittest.cc b/src/trace_processor/importers/common/slice_tracker_unittest.cc
index b999ca1..ba73cd7 100644
--- a/src/trace_processor/importers/common/slice_tracker_unittest.cc
+++ b/src/trace_processor/importers/common/slice_tracker_unittest.cc
@@ -91,7 +91,7 @@
EXPECT_EQ(slices[0].category().value_or(kNullStringId).raw_id(), 0u);
EXPECT_EQ(slices[0].name().value_or(kNullStringId).raw_id(), 1u);
EXPECT_EQ(slices[0].depth(), 0u);
- EXPECT_EQ(slices[0].arg_set_id(), kInvalidArgSetId);
+ EXPECT_EQ(slices[0].arg_set_id(), std::nullopt);
}
TEST_F(SliceTrackerTest, OneSliceDetailedWithTranslatedName) {
@@ -115,7 +115,7 @@
EXPECT_EQ(slices[0].name().value_or(kNullStringId).raw_id(),
mapped_name.raw_id());
EXPECT_EQ(slices[0].depth(), 0u);
- EXPECT_EQ(slices[0].arg_set_id(), kInvalidArgSetId);
+ EXPECT_EQ(slices[0].arg_set_id(), std::nullopt);
}
TEST_F(SliceTrackerTest, NegativeTimestamps) {
@@ -137,7 +137,7 @@
EXPECT_EQ(rr.category().value_or(kNullStringId).raw_id(), 0u);
EXPECT_EQ(rr.name().value_or(kNullStringId).raw_id(), 1u);
EXPECT_EQ(rr.depth(), 0u);
- EXPECT_EQ(rr.arg_set_id(), kInvalidArgSetId);
+ EXPECT_EQ(rr.arg_set_id(), std::nullopt);
}
TEST_F(SliceTrackerTest, OneSliceWithArgs) {
diff --git a/src/trace_processor/importers/common/tracks.h b/src/trace_processor/importers/common/tracks.h
index 22f0297..aeda1bb 100644
--- a/src/trace_processor/importers/common/tracks.h
+++ b/src/trace_processor/importers/common/tracks.h
@@ -158,7 +158,7 @@
}
// Indicates that the name of the track was provided in the blueprint.
-constexpr nullptr_t BlueprintName() {
+constexpr std::nullptr_t BlueprintName() {
return nullptr;
}
@@ -169,7 +169,7 @@
}
// Indicates that the unit of the track was provided in the blueprint.
-constexpr nullptr_t BlueprintUnit() {
+constexpr std::nullptr_t BlueprintUnit() {
return nullptr;
}
diff --git a/src/trace_processor/importers/common/tracks_internal.h b/src/trace_processor/importers/common/tracks_internal.h
index 5cc0fa1..680cdfb 100644
--- a/src/trace_processor/importers/common/tracks_internal.h
+++ b/src/trace_processor/importers/common/tracks_internal.h
@@ -42,17 +42,17 @@
struct NameBlueprintT {
struct Auto {
- using name_t = nullptr_t;
+ using name_t = std::nullptr_t;
};
struct Static {
- using name_t = nullptr_t;
+ using name_t = std::nullptr_t;
const char* name;
};
struct Dynamic {
using name_t = StringPool::Id;
};
struct FnBase {
- using name_t = nullptr_t;
+ using name_t = std::nullptr_t;
};
template <typename F>
struct Fn : FnBase {
@@ -84,7 +84,7 @@
struct UnitBlueprintT {
struct Unknown {
- using unit_t = nullptr_t;
+ using unit_t = std::nullptr_t;
};
struct Static {
using unit_t = const char*;
diff --git a/src/trace_processor/importers/etm/BUILD.gn b/src/trace_processor/importers/etm/BUILD.gn
index 7b03fcf..4f15044 100644
--- a/src/trace_processor/importers/etm/BUILD.gn
+++ b/src/trace_processor/importers/etm/BUILD.gn
@@ -63,6 +63,7 @@
"mapping_version.cc",
"mapping_version.h",
"opencsd.h",
+ "sql_values.h",
"storage_handle.cc",
"storage_handle.h",
"target_memory.cc",
diff --git a/src/trace_processor/importers/etm/element_cursor.cc b/src/trace_processor/importers/etm/element_cursor.cc
index 9366643..cc27040 100644
--- a/src/trace_processor/importers/etm/element_cursor.cc
+++ b/src/trace_processor/importers/etm/element_cursor.cc
@@ -23,6 +23,8 @@
#include "perfetto/base/logging.h"
#include "perfetto/base/status.h"
#include "src/trace_processor/importers/etm/etm_v4_decoder.h"
+#include "src/trace_processor/importers/etm/mapping_version.h"
+#include "src/trace_processor/importers/etm/sql_values.h"
#include "src/trace_processor/importers/etm/storage_handle.h"
#include "src/trace_processor/importers/etm/target_memory.h"
#include "src/trace_processor/importers/etm/target_memory_reader.h"
@@ -131,4 +133,24 @@
return OCSD_RESP_WAIT;
}
+std::unique_ptr<InstructionRangeSqlValue> ElementCursor::GetInstructionRange()
+ const {
+ auto r = std::make_unique<InstructionRangeSqlValue>();
+ AddressRange range(element_->st_addr, element_->en_addr);
+ r->config_id = *config_id_;
+ r->isa = element_->isa;
+ r->st_addr = range.start();
+ // How did we get a range if there is no mapping.
+ PERFETTO_CHECK(mapping_);
+
+ if (!mapping_->Contains(range) || !mapping_->data()) {
+ r->start = nullptr;
+ r->end = nullptr;
+ } else {
+ r->start = mapping_->data() + (range.start() - mapping_->start());
+ r->end = r->start + range.size();
+ }
+ return r;
+}
+
} // namespace perfetto::trace_processor::etm
diff --git a/src/trace_processor/importers/etm/element_cursor.h b/src/trace_processor/importers/etm/element_cursor.h
index 9dabf9a..aa5e559 100644
--- a/src/trace_processor/importers/etm/element_cursor.h
+++ b/src/trace_processor/importers/etm/element_cursor.h
@@ -33,6 +33,7 @@
class MappingVersion;
class TargetMemory;
class TargetMemoryReader;
+struct InstructionRangeSqlValue;
class ElementTypeMask {
public:
@@ -96,6 +97,12 @@
const MappingVersion* mapping() const { return mapping_; }
+ bool has_instruction_range() const {
+ return element_->getType() == OCSD_GEN_TRC_ELEM_INSTR_RANGE;
+ }
+
+ std::unique_ptr<InstructionRangeSqlValue> GetInstructionRange() const;
+
private:
void SetAtEof();
base::Status ResetDecoder(tables::EtmV4ConfigurationTable::Id config_id);
diff --git a/src/trace_processor/importers/etm/etm_v4_stream_demultiplexer.cc b/src/trace_processor/importers/etm/etm_v4_stream_demultiplexer.cc
index acff104..f24dfc4 100644
--- a/src/trace_processor/importers/etm/etm_v4_stream_demultiplexer.cc
+++ b/src/trace_processor/importers/etm/etm_v4_stream_demultiplexer.cc
@@ -225,13 +225,27 @@
// extract the various streams. AUX data is passed by the perf importer to the
// CPU specific `AuxDataStream`, but as we just said we need to first decode
// this data to extract the real per CPU streams, so the `EtmV4Stream` classes
-// (`AuxDataStream` subclasses) forward such data tho this class, that will
+// (`AuxDataStream` subclasses) forward such data to this class, that will
// decode the streams and finally forward them back to the CPU specific
// `EtmV4Stream` where it can now be handled.
//
// For the TRBE the data that arrives in the AUX record is unformatted and is
// the data for that given CPU so it can be directly processed by the
// `EtmV4Stream` class without needing to decode it first.
+//
+// Data flow for framed data (ETR):
+// 1. `PerfDataTokenizer` parses `AuxData` for cpu x and forwards it to the
+// `AuxDataStream` bound to that cpu
+// 2. `EtmV4Stream` bound to cpu x determines AuxData is framed and forwards
+// it to the `FrameDecoder` owned by `EtmV4StreamDemultiplexer`.
+// 3. De-multiplexed ETM data is sent to its corresponding `EtmV4Stream` where
+// it is stored in `TraceStorage`.
+//
+// Data flow for raw data (TRBE):
+// 1. `PerfDataTokenizer` parses `AuxData` for cpu x and forwards it to the
+// `AuxDataStream` bound to that cpu
+// 2. `EtmV4Stream` bound to cpu x determines AuxData is raw and can directly
+// store it in `TraceStorage`.
class EtmV4StreamDemultiplexer : public perf_importer::AuxDataTokenizer {
public:
explicit EtmV4StreamDemultiplexer(TraceProcessorContext* context)
diff --git a/src/trace_processor/importers/etm/mapping_version.h b/src/trace_processor/importers/etm/mapping_version.h
index 971daef..5333845 100644
--- a/src/trace_processor/importers/etm/mapping_version.h
+++ b/src/trace_processor/importers/etm/mapping_version.h
@@ -49,8 +49,6 @@
return content_.data();
}
- const uint8_t* FindData(AddressRange range) const;
-
MappingVersion SplitFront(uint64_t mid);
private:
diff --git a/src/trace_processor/importers/etm/sql_values.h b/src/trace_processor/importers/etm/sql_values.h
new file mode 100644
index 0000000..1ed71d8
--- /dev/null
+++ b/src/trace_processor/importers/etm/sql_values.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_ETM_SQL_VALUES_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ETM_SQL_VALUES_H_
+
+#include <cstdint>
+
+#include "src/trace_processor/importers/etm/opencsd.h"
+#include "src/trace_processor/tables/etm_tables_py.h"
+
+namespace perfetto::trace_processor::etm {
+
+struct InstructionRangeSqlValue {
+ static constexpr const char kPtrType[] = "etm::InstructionRangeSqlValue";
+ tables::EtmV4ConfigurationTable::Id config_id;
+ ocsd_isa isa;
+ uint64_t st_addr;
+ const uint8_t* start;
+ const uint8_t* end;
+};
+
+} // namespace perfetto::trace_processor::etm
+
+#endif // SRC_TRACE_PROCESSOR_IMPORTERS_ETM_SQL_VALUES_H_
diff --git a/src/trace_processor/importers/etm/virtual_address_space.cc b/src/trace_processor/importers/etm/virtual_address_space.cc
index 6e24c7b..9e20214 100644
--- a/src/trace_processor/importers/etm/virtual_address_space.cc
+++ b/src/trace_processor/importers/etm/virtual_address_space.cc
@@ -81,7 +81,7 @@
if (node.value().end() == *end) {
slabs.push_back(std::move(node.value()));
}
- // The mapping ends at this vertex, no need to split it.
+ // Split needed
else {
slabs.push_back(node.value().SplitFront(*end));
mappings_.insert(std::move(node));
diff --git a/src/trace_processor/importers/etw/etw_module_impl.h b/src/trace_processor/importers/etw/etw_module_impl.h
index 4a27846..0ec15e1 100644
--- a/src/trace_processor/importers/etw/etw_module_impl.h
+++ b/src/trace_processor/importers/etw/etw_module_impl.h
@@ -45,7 +45,7 @@
void ParseEtwEventData(uint32_t cpu,
int64_t ts,
const TracePacketData& data) override {
- util::Status res = parser_.ParseEtwEvent(cpu, ts, data);
+ base::Status res = parser_.ParseEtwEvent(cpu, ts, data);
if (!res.ok()) {
PERFETTO_ELOG("%s", res.message().c_str());
}
diff --git a/src/trace_processor/importers/etw/etw_parser.h b/src/trace_processor/importers/etw/etw_parser.h
index 380c6e8..96306de 100644
--- a/src/trace_processor/importers/etw/etw_parser.h
+++ b/src/trace_processor/importers/etw/etw_parser.h
@@ -30,7 +30,7 @@
public:
explicit EtwParser(TraceProcessorContext* context);
- util::Status ParseEtwEvent(uint32_t cpu,
+ base::Status ParseEtwEvent(uint32_t cpu,
int64_t ts,
const TracePacketData& data);
diff --git a/src/trace_processor/importers/ftrace/ftrace_module_impl.h b/src/trace_processor/importers/ftrace/ftrace_module_impl.h
index d6df0f9..64f8fbc 100644
--- a/src/trace_processor/importers/ftrace/ftrace_module_impl.h
+++ b/src/trace_processor/importers/ftrace/ftrace_module_impl.h
@@ -44,7 +44,7 @@
void ParseFtraceEventData(uint32_t cpu,
int64_t ts,
const TracePacketData& data) override {
- util::Status res = parser_.ParseFtraceEvent(cpu, ts, data);
+ base::Status res = parser_.ParseFtraceEvent(cpu, ts, data);
if (!res.ok()) {
PERFETTO_ELOG("%s", res.message().c_str());
}
@@ -53,7 +53,7 @@
void ParseInlineSchedSwitch(uint32_t cpu,
int64_t ts,
const InlineSchedSwitch& data) override {
- util::Status res = parser_.ParseInlineSchedSwitch(cpu, ts, data);
+ base::Status res = parser_.ParseInlineSchedSwitch(cpu, ts, data);
if (!res.ok()) {
PERFETTO_ELOG("%s", res.message().c_str());
}
@@ -62,7 +62,7 @@
void ParseInlineSchedWaking(uint32_t cpu,
int64_t ts,
const InlineSchedWaking& data) override {
- util::Status res = parser_.ParseInlineSchedWaking(cpu, ts, data);
+ base::Status res = parser_.ParseInlineSchedWaking(cpu, ts, data);
if (!res.ok()) {
PERFETTO_ELOG("%s", res.message().c_str());
}
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index a62a374..d5a0528 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -65,6 +65,7 @@
#include "src/trace_processor/storage/metadata.h"
#include "src/trace_processor/storage/stats.h"
#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
#include "src/trace_processor/types/softirq_action.h"
#include "src/trace_processor/types/tcp_state.h"
#include "src/trace_processor/types/variadic.h"
@@ -452,7 +453,9 @@
context->storage->InternString("event_type")),
device_name_id_(context->storage->InternString("device_name")),
block_io_id_(context->storage->InternString("block_io")),
- block_io_arg_sector_id_(context->storage->InternString("sector")) {
+ block_io_arg_sector_id_(context->storage->InternString("sector")),
+ cpuhp_action_cpu_id_(context->storage->InternString("action_cpu")),
+ cpuhp_idx_id_(context->storage->InternString("cpuhp_idx")) {
// Build the lookup table for the strings inside ftrace events (e.g. the
// name of ftrace event fields and the names of their args).
for (size_t i = 0; i < GetDescriptorsSize(); i++) {
@@ -1341,6 +1344,17 @@
ParseBlockIoDone(ts, fld_bytes);
break;
}
+ // Intentional fallthrough for Cpuhp multienter/enter, since they both
+ // have same fields and require identical processing.
+ case FtraceEvent::kCpuhpMultiEnterFieldNumber:
+ case FtraceEvent::kCpuhpEnterFieldNumber: {
+ ParseCpuhpEnter(fld.id(), ts, cpu, fld_bytes);
+ break;
+ }
+ case FtraceEvent::kCpuhpExitFieldNumber: {
+ ParseCpuhpExit(ts, fld_bytes);
+ break;
+ }
default:
break;
}
@@ -1472,9 +1486,10 @@
StringId event_id = context_->storage->InternString(evt.event_name());
UniqueTid utid = context_->process_tracker->GetOrCreateThread(tid);
auto ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
- RawId id = context_->storage->mutable_ftrace_event_table()
- ->Insert({ts, event_id, utid, {}, {}, ucpu})
- .id;
+ tables::FtraceEventTable::Id id =
+ context_->storage->mutable_ftrace_event_table()
+ ->Insert({ts, event_id, utid, {}, {}, ucpu})
+ .id;
auto inserter = context_->args_tracker->AddArgsTo(id);
for (auto it = evt.field(); it; ++it) {
@@ -1514,7 +1529,7 @@
const auto& message_strings = ftrace_message_strings_[ftrace_id];
UniqueTid utid = context_->process_tracker->GetOrCreateThread(tid);
auto ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
- RawId id =
+ tables::FtraceEventTable::Id id =
context_->storage->mutable_ftrace_event_table()
->Insert(
{timestamp, message_strings.message_name_id, utid, {}, {}, ucpu})
@@ -1798,7 +1813,12 @@
return;
}
- uint32_t tgid = static_cast<uint32_t>(evt.pid());
+ // dpu_tracing_mark_write can be buggy and can indicate that the swapper
+ // thread is parented to some random process (probably because of IRQs). We
+ // should ignore this as it causes bad cascading effects down the line.
+ //
+ // See b/388130600 for context.
+ uint32_t tgid = pid == 0 ? 0 : static_cast<uint32_t>(evt.pid());
SystraceParser::GetOrCreate(context_)->ParseKernelTracingMarkWrite(
timestamp, pid, static_cast<char>(evt.type()), false /*trace_begin*/,
evt.name(), tgid, evt.value());
@@ -2054,7 +2074,8 @@
protos::pbzero::DmaHeapStatFtraceEvent::Decoder dma_heap(data);
static constexpr auto kBlueprint = tracks::CounterBlueprint(
- "dma_heap", tracks::UnknownUnitBlueprint(), tracks::DimensionBlueprints(),
+ "android_dma_heap", tracks::UnknownUnitBlueprint(),
+ tracks::DimensionBlueprints(),
tracks::StaticNameBlueprint("mem.dma_heap"));
// Push the global counter.
@@ -2063,7 +2084,7 @@
timestamp, static_cast<double>(dma_heap.total_allocated()), track);
static constexpr auto kChangeBlueprint = tracks::CounterBlueprint(
- "dma_heap_change", tracks::UnknownUnitBlueprint(),
+ "android_dma_heap_change", tracks::UnknownUnitBlueprint(),
tracks::Dimensions(tracks::kThreadDimensionBlueprint),
tracks::StaticNameBlueprint("mem.dma_heap_change"));
@@ -3636,7 +3657,12 @@
namespace {
-constexpr auto kFuncgraphBlueprint = tracks::SliceBlueprint(
+constexpr auto kThreadFuncgraphBlueprint = tracks::SliceBlueprint(
+ "thread_funcgraph",
+ tracks::DimensionBlueprints(tracks::kThreadDimensionBlueprint),
+ tracks::StaticNameBlueprint("Funcgraph"));
+
+constexpr auto kCpuFuncgraphBlueprint = tracks::SliceBlueprint(
"cpu_funcgraph",
tracks::DimensionBlueprints(tracks::kCpuDimensionBlueprint),
tracks::FnNameBlueprint([](uint32_t cpu) {
@@ -3658,13 +3684,14 @@
if (pid != 0) {
// common case: normal thread
UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
- track = context_->track_tracker->InternThreadTrack(utid);
+ track = context_->track_tracker->InternTrack(kThreadFuncgraphBlueprint,
+ tracks::Dimensions(utid));
} else {
// Idle threads (swapper) are implicit, and all share the same thread id
// 0. Therefore we cannot use a thread-scoped track because many instances
// of swapper might be running concurrently. Fall back onto global tracks
// (one per cpu).
- track = context_->track_tracker->InternTrack(kFuncgraphBlueprint,
+ track = context_->track_tracker->InternTrack(kCpuFuncgraphBlueprint,
tracks::Dimensions(cpu));
}
context_->slice_tracker->Begin(timestamp, track, kNullStringId, name_id);
@@ -3683,10 +3710,11 @@
if (pid != 0) {
// common case: normal thread
UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
- track = context_->track_tracker->InternThreadTrack(utid);
+ track = context_->track_tracker->InternTrack(kThreadFuncgraphBlueprint,
+ tracks::Dimensions(utid));
} else {
// special case: see |ParseFuncgraphEntry|
- track = context_->track_tracker->InternTrack(kFuncgraphBlueprint,
+ track = context_->track_tracker->InternTrack(kCpuFuncgraphBlueprint,
tracks::Dimensions(cpu));
}
context_->slice_tracker->End(timestamp, track, kNullStringId, name_id);
@@ -3941,4 +3969,60 @@
});
}
+namespace {
+constexpr auto kCpuHpBlueprint = tracks::SliceBlueprint(
+ "cpu_hotplug",
+ tracks::DimensionBlueprints(tracks::kCpuDimensionBlueprint),
+ tracks::FnNameBlueprint([](uint32_t cpu) {
+ return base::StackString<255>("CPU Hotplug %u", cpu);
+ }));
+} // namespace
+
+void FtraceParser::ParseCpuhpEnter(uint32_t fld_id,
+ int64_t ts,
+ uint32_t action_cpu,
+ protozero::ConstBytes blob) {
+ uint32_t hp_cpu = UINT32_MAX;
+ int32_t idx = INT32_MAX;
+ switch (fld_id) {
+ case protos::pbzero::FtraceEvent::kCpuhpEnterFieldNumber: {
+ protos::pbzero::CpuhpEnterFtraceEvent::Decoder cpuhp_event(blob);
+ hp_cpu = cpuhp_event.cpu();
+ idx = cpuhp_event.idx();
+ break;
+ }
+ case protos::pbzero::FtraceEvent::kCpuhpMultiEnterFieldNumber: {
+ protos::pbzero::CpuhpMultiEnterFtraceEvent::Decoder cpuhp_event(blob);
+ hp_cpu = cpuhp_event.cpu();
+ idx = cpuhp_event.idx();
+ break;
+ }
+ default:
+ // Only support hotplug_enter and hotplug_multi_enter
+ return;
+ }
+
+ // hp_cpu, the CPU being hotplugged, is stored in track dimension. action_cpu
+ // is the CPU assisting hp_cpu in the hotplug operation. action_cpu could be
+ // the hp_cpu itself or a different CPU, but the distinction is important
+ // since it helps indicate when exactly the hp_cpu is powered off.
+ StringId slice_name_id = context_->storage->InternString(
+ base::StackString<32>("cpuhp(%d)", idx).string_view());
+ TrackId track_id = context_->track_tracker->InternTrack(
+ kCpuHpBlueprint, tracks::Dimensions(hp_cpu));
+ context_->slice_tracker->Begin(
+ ts, track_id, cpu_id_, slice_name_id,
+ [&](ArgsTracker::BoundInserter* inserter) {
+ inserter->AddArg(cpuhp_action_cpu_id_,
+ Variadic::UnsignedInteger(action_cpu));
+ inserter->AddArg(cpuhp_idx_id_, Variadic::Integer(idx));
+ });
+}
+
+void FtraceParser::ParseCpuhpExit(int64_t ts, protozero::ConstBytes blob) {
+ protos::pbzero::CpuhpExitFtraceEvent::Decoder cpuhp_event(blob);
+ TrackId track_id = context_->track_tracker->InternTrack(
+ kCpuHpBlueprint, tracks::Dimensions(cpuhp_event.cpu()));
+ context_->slice_tracker->End(ts, track_id);
+}
} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.h b/src/trace_processor/importers/ftrace/ftrace_parser.h
index 0e05016..08162cc 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.h
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.h
@@ -315,6 +315,11 @@
void ParseParamSetValueCpm(protozero::ConstBytes blob);
void ParseBlockIoStart(int64_t ts, protozero::ConstBytes blob);
void ParseBlockIoDone(int64_t ts, protozero::ConstBytes blob);
+ void ParseCpuhpEnter(uint32_t fld_id,
+ int64_t ts,
+ uint32_t cpu,
+ protozero::ConstBytes blob);
+ void ParseCpuhpExit(int64_t ts, protozero::ConstBytes blob);
TraceProcessorContext* context_;
RssStatTracker rss_stat_tracker_;
@@ -400,6 +405,8 @@
const StringId device_name_id_;
const StringId block_io_id_;
const StringId block_io_arg_sector_id_;
+ const StringId cpuhp_action_cpu_id_;
+ const StringId cpuhp_idx_id_;
std::vector<StringId> syscall_arg_name_ids_;
diff --git a/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc b/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc
index 833d25a..cb6f033 100644
--- a/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc
@@ -235,7 +235,8 @@
row.ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
// Add an entry to the raw table.
- RawId id = context_->storage->mutable_ftrace_event_table()->Insert(row).id;
+ tables::FtraceEventTable::Id id =
+ context_->storage->mutable_ftrace_event_table()->Insert(row).id;
using SW = protos::pbzero::SchedWakingFtraceEvent;
auto inserter = context_->args_tracker->AddArgsTo(id);
@@ -272,9 +273,10 @@
// Push the raw event - this is done as the raw ftrace event codepath does
// not insert sched_switch.
auto ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
- RawId id = context_->storage->mutable_ftrace_event_table()
- ->Insert({ts, sched_switch_id_, prev_utid, {}, {}, ucpu})
- .id;
+ tables::FtraceEventTable::Id id =
+ context_->storage->mutable_ftrace_event_table()
+ ->Insert({ts, sched_switch_id_, prev_utid, {}, {}, ucpu})
+ .id;
// Note: this ordering is important. The events should be pushed in the same
// order as the order of fields in the proto; this is used by the raw table
diff --git a/src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc b/src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc
index 187fb6a..f96bf2b 100644
--- a/src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc
+++ b/src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc
@@ -24,7 +24,6 @@
#include "protos/perfetto/trace/ftrace/power.pbzero.h"
#include "src/trace_processor/importers/common/event_tracker.h"
#include "src/trace_processor/importers/common/slice_tracker.h"
-#include "src/trace_processor/importers/common/track_compressor.h"
#include "src/trace_processor/importers/common/track_tracker.h"
#include "src/trace_processor/importers/common/tracks.h"
#include "src/trace_processor/importers/common/tracks_common.h"
@@ -64,10 +63,13 @@
row.track_id = track_id;
row.category = kNullStringId;
row.name = entry_name_id;
- row.thread_ts = timestamp;
- row.thread_dur = active_duration;
- context_->slice_tracker->ScopedTyped(context_->storage->mutable_slice_table(),
- row);
+ auto slice_id = context_->slice_tracker->Scoped(
+ timestamp, track_id, kNullStringId, entry_name_id, duration);
+ if (slice_id) {
+ auto rr = context_->storage->mutable_slice_table()->FindById(*slice_id);
+ rr->set_thread_ts(timestamp);
+ rr->set_thread_dur(active_duration);
+ }
}
} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/fuchsia/BUILD.gn b/src/trace_processor/importers/fuchsia/BUILD.gn
index d048f86..fc0ee38 100644
--- a/src/trace_processor/importers/fuchsia/BUILD.gn
+++ b/src/trace_processor/importers/fuchsia/BUILD.gn
@@ -85,6 +85,8 @@
"../../../protozero",
"../../sorter",
"../../storage",
+ "../../tables",
+ "../../types",
"../../util:descriptors",
"../common",
"../ftrace:full",
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc b/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc
index ec29509..5c8e2a9 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc
+++ b/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc
@@ -14,15 +14,18 @@
* limitations under the License.
*/
-#include "src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h"
+#include "perfetto/base/status.h"
+#include <cstdint>
+#include <cstring>
#include <memory>
+#include <optional>
+#include <utility>
+#include <vector>
-#include "perfetto/base/logging.h"
#include "perfetto/ext/base/string_view.h"
#include "perfetto/protozero/scattered_heap_buffer.h"
#include "perfetto/trace_processor/trace_blob.h"
-
#include "src/trace_processor/importers/common/args_tracker.h"
#include "src/trace_processor/importers/common/args_translation_table.h"
#include "src/trace_processor/importers/common/chunked_trace_reader.h"
@@ -30,58 +33,37 @@
#include "src/trace_processor/importers/common/cpu_tracker.h"
#include "src/trace_processor/importers/common/event_tracker.h"
#include "src/trace_processor/importers/common/flow_tracker.h"
+#include "src/trace_processor/importers/common/global_args_tracker.h"
#include "src/trace_processor/importers/common/machine_tracker.h"
#include "src/trace_processor/importers/common/metadata_tracker.h"
#include "src/trace_processor/importers/common/process_track_translation_table.h"
#include "src/trace_processor/importers/common/process_tracker.h"
#include "src/trace_processor/importers/common/slice_tracker.h"
+#include "src/trace_processor/importers/common/slice_translation_table.h"
#include "src/trace_processor/importers/common/stack_profile_tracker.h"
#include "src/trace_processor/importers/common/trace_parser.h"
#include "src/trace_processor/importers/common/track_tracker.h"
#include "src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.h"
+#include "src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h"
#include "src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h"
#include "src/trace_processor/importers/proto/additional_modules.h"
#include "src/trace_processor/importers/proto/default_modules.h"
#include "src/trace_processor/importers/proto/proto_trace_parser_impl.h"
#include "src/trace_processor/sorter/trace_sorter.h"
-#include "src/trace_processor/storage/metadata.h"
+#include "src/trace_processor/storage/stats.h"
#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
+#include "src/trace_processor/types/variadic.h"
#include "src/trace_processor/util/descriptors.h"
#include "test/gtest_and_gmock.h"
-#include "protos/perfetto/common/builtin_clock.pbzero.h"
-#include "protos/perfetto/common/sys_stats_counters.pbzero.h"
-#include "protos/perfetto/config/trace_config.pbzero.h"
-#include "protos/perfetto/trace/android/packages_list.pbzero.h"
-#include "protos/perfetto/trace/chrome/chrome_benchmark_metadata.pbzero.h"
-#include "protos/perfetto/trace/chrome/chrome_trace_event.pbzero.h"
-#include "protos/perfetto/trace/clock_snapshot.pbzero.h"
-#include "protos/perfetto/trace/ftrace/ftrace.pbzero.h"
-#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
-#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
-#include "protos/perfetto/trace/ftrace/generic.pbzero.h"
-#include "protos/perfetto/trace/ftrace/power.pbzero.h"
-#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
-#include "protos/perfetto/trace/ftrace/task.pbzero.h"
-#include "protos/perfetto/trace/interned_data/interned_data.pbzero.h"
-#include "protos/perfetto/trace/profiling/profile_packet.pbzero.h"
-#include "protos/perfetto/trace/ps/process_tree.pbzero.h"
-#include "protos/perfetto/trace/sys_stats/sys_stats.pbzero.h"
#include "protos/perfetto/trace/trace.pbzero.h"
#include "protos/perfetto/trace/trace_packet.pbzero.h"
-#include "protos/perfetto/trace/track_event/chrome_thread_descriptor.pbzero.h"
-#include "protos/perfetto/trace/track_event/counter_descriptor.pbzero.h"
-#include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
-#include "protos/perfetto/trace/track_event/log_message.pbzero.h"
-#include "protos/perfetto/trace/track_event/process_descriptor.pbzero.h"
-#include "protos/perfetto/trace/track_event/source_location.pbzero.h"
-#include "protos/perfetto/trace/track_event/task_execution.pbzero.h"
#include "protos/perfetto/trace/track_event/thread_descriptor.pbzero.h"
#include "protos/perfetto/trace/track_event/track_descriptor.pbzero.h"
#include "protos/perfetto/trace/track_event/track_event.pbzero.h"
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
namespace {
using ::testing::_;
using ::testing::Args;
@@ -197,60 +179,21 @@
(override));
};
-class MockSliceTracker : public SliceTracker {
- public:
- explicit MockSliceTracker(TraceProcessorContext* context)
- : SliceTracker(context) {}
-
- MOCK_METHOD(std::optional<SliceId>,
- Begin,
- (int64_t timestamp,
- TrackId track_id,
- StringId cat,
- StringId name,
- SetArgsCallback args_callback),
- (override));
- MOCK_METHOD(std::optional<SliceId>,
- End,
- (int64_t timestamp,
- TrackId track_id,
- StringId cat,
- StringId name,
- SetArgsCallback args_callback),
- (override));
- MOCK_METHOD(std::optional<SliceId>,
- Scoped,
- (int64_t timestamp,
- TrackId track_id,
- StringId cat,
- StringId name,
- int64_t duration,
- SetArgsCallback args_callback),
- (override));
- MOCK_METHOD(std::optional<SliceId>,
- StartSlice,
- (int64_t timestamp,
- TrackId track_id,
- SetArgsCallback args_callback,
- std::function<SliceId()> inserter),
- (override));
-};
-
class FuchsiaTraceParserTest : public ::testing::Test {
public:
FuchsiaTraceParserTest() {
- context_.storage.reset(new TraceStorage());
+ context_.storage = std::make_shared<TraceStorage>();
storage_ = context_.storage.get();
- context_.track_tracker.reset(new TrackTracker(&context_));
- context_.global_args_tracker.reset(
- new GlobalArgsTracker(context_.storage.get()));
+ context_.track_tracker = std::make_unique<TrackTracker>(&context_);
+ context_.global_args_tracker =
+ std::make_shared<GlobalArgsTracker>(context_.storage.get());
context_.stack_profile_tracker.reset(new StackProfileTracker(&context_));
- context_.args_tracker.reset(new ArgsTracker(&context_));
+ context_.args_tracker = std::make_unique<ArgsTracker>(&context_);
context_.args_translation_table.reset(new ArgsTranslationTable(storage_));
- context_.metadata_tracker.reset(
- new MetadataTracker(context_.storage.get()));
- context_.machine_tracker.reset(new MachineTracker(&context_, 0));
- context_.cpu_tracker.reset(new CpuTracker(&context_));
+ context_.metadata_tracker =
+ std::make_unique<MetadataTracker>(context_.storage.get());
+ context_.machine_tracker = std::make_unique<MachineTracker>(&context_, 0);
+ context_.cpu_tracker = std::make_unique<CpuTracker>(&context_);
event_ = new MockEventTracker(&context_);
context_.event_tracker.reset(event_);
sched_ = new MockSchedEventTracker(&context_);
@@ -259,17 +202,19 @@
context_.process_tracker.reset(process_);
context_.process_track_translation_table.reset(
new ProcessTrackTranslationTable(storage_));
- slice_ = new NiceMock<MockSliceTracker>(&context_);
- context_.slice_tracker.reset(slice_);
- context_.slice_translation_table.reset(new SliceTranslationTable(storage_));
- context_.clock_tracker.reset(new ClockTracker(&context_));
+ context_.slice_tracker = std::make_unique<SliceTracker>(&context_);
+ context_.slice_translation_table =
+ std::make_unique<SliceTranslationTable>(storage_);
+ context_.clock_tracker = std::make_unique<ClockTracker>(&context_);
clock_ = context_.clock_tracker.get();
- context_.flow_tracker.reset(new FlowTracker(&context_));
- context_.fuchsia_record_parser.reset(new FuchsiaTraceParser(&context_));
- context_.proto_trace_parser.reset(new ProtoTraceParserImpl(&context_));
- context_.sorter.reset(
- new TraceSorter(&context_, TraceSorter::SortingMode::kFullSort));
- context_.descriptor_pool_.reset(new DescriptorPool());
+ context_.flow_tracker = std::make_unique<FlowTracker>(&context_);
+ context_.fuchsia_record_parser =
+ std::make_unique<FuchsiaTraceParser>(&context_);
+ context_.proto_trace_parser =
+ std::make_unique<ProtoTraceParserImpl>(&context_);
+ context_.sorter = std::make_shared<TraceSorter>(
+ &context_, TraceSorter::SortingMode::kFullSort);
+ context_.descriptor_pool_ = std::make_unique<DescriptorPool>();
RegisterDefaultModules(&context_);
RegisterAdditionalModules(&context_);
@@ -285,7 +230,7 @@
void SetUp() override { ResetTraceBuffers(); }
- util::Status Tokenize() {
+ base::Status Tokenize() {
const size_t num_bytes = trace_bytes_.size() * sizeof(uint64_t);
std::unique_ptr<uint8_t[]> raw_trace(new uint8_t[num_bytes]);
memcpy(raw_trace.get(), trace_bytes_.data(), num_bytes);
@@ -305,7 +250,6 @@
MockEventTracker* event_;
MockSchedEventTracker* sched_;
MockProcessTracker* process_;
- MockSliceTracker* slice_;
ClockTracker* clock_;
TraceStorage* storage_;
};
@@ -475,22 +419,27 @@
// Only the begin thread time can be imported into the counter table.
EXPECT_CALL(*event_, PushCounter(1005000, testing::DoubleEq(2003000),
thread_time_track));
- EXPECT_CALL(*slice_, StartSlice(1005000, track, _, _))
- .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
- InvokeArgument<2>(&inserter), Return(SliceId(0u))));
EXPECT_CALL(*event_, PushCounter(1010000, testing::DoubleEq(2005000),
thread_time_track));
- EXPECT_CALL(*slice_, StartSlice(1010000, track, _, _))
- .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
- InvokeArgument<2>(&inserter), Return(SliceId(1u))));
EXPECT_CALL(*event_, PushCounter(1020000, testing::DoubleEq(2010000),
thread_time_track));
- EXPECT_CALL(*slice_, End(1020000, track, unknown_cat, kNullStringId, _))
- .WillOnce(DoAll(InvokeArgument<4>(&inserter), Return(SliceId(1u))));
auto status = Tokenize();
EXPECT_TRUE(status.ok());
context_.sorter->ExtractEventsForced();
+
+ EXPECT_EQ(storage_->slice_table().row_count(), 2u);
+ auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+ EXPECT_TRUE(rr_0);
+ EXPECT_EQ(rr_0->ts(), 1005000);
+ EXPECT_EQ(rr_0->track_id(), track);
+
+ auto rr_1 = storage_->slice_table().FindById(SliceId(1u));
+ EXPECT_TRUE(rr_1);
+ EXPECT_EQ(rr_1->ts(), 1010000);
+ EXPECT_EQ(rr_1->track_id(), track);
+ EXPECT_EQ(rr_1->dur(), 10000);
+ EXPECT_EQ(rr_1->category(), unknown_cat);
}
TEST_F(FuchsiaTraceParserTest, SchedulerEvents) {
@@ -620,5 +569,4 @@
}
} // namespace
-} // namespace trace_processor
-} // namespace perfetto
+} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc
index ea6197d..8632648 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc
+++ b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc
@@ -83,7 +83,7 @@
FuchsiaTraceTokenizer::~FuchsiaTraceTokenizer() = default;
-util::Status FuchsiaTraceTokenizer::Parse(TraceBlobView blob) {
+base::Status FuchsiaTraceTokenizer::Parse(TraceBlobView blob) {
size_t size = blob.size();
// The relevant internal state is |leftover_bytes_|. Each call to Parse should
@@ -111,7 +111,7 @@
// record, so just add the new bytes to |leftover_bytes_| and return.
leftover_bytes_.insert(leftover_bytes_.end(), blob.data() + byte_offset,
blob.data() + size);
- return util::OkStatus();
+ return base::OkStatus();
}
if (!leftover_bytes_.empty()) {
// There is a record starting from leftover bytes.
@@ -155,7 +155,7 @@
// have to leftover_bytes_ and wait for more.
leftover_bytes_.insert(leftover_bytes_.end(), blob.data() + byte_offset,
blob.data() + byte_offset + size);
- return util::OkStatus();
+ return base::OkStatus();
}
}
@@ -172,7 +172,7 @@
fuchsia_trace_utils::ReadField<uint32_t>(header, 4, 15) *
sizeof(uint64_t);
if (record_len_bytes == 0)
- return util::ErrStatus("Unexpected record of size 0");
+ return base::ErrStatus("Unexpected record of size 0");
if (record_offset + record_len_bytes > size)
break;
diff --git a/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc
index 06038b7..409644f 100644
--- a/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc
+++ b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc
@@ -280,6 +280,8 @@
std::string key(attrs[i]);
if (key == "addr") {
new_frame.ptr->addr = strtoll(attrs[i + 1], nullptr, 16);
+ } else if (key == "name") {
+ new_frame.ptr->name = std::string(attrs[i + 1]);
}
}
current_new_frame_ = new_frame.id;
diff --git a/src/trace_processor/importers/instruments/row.h b/src/trace_processor/importers/instruments/row.h
index 531a36d..55a021c 100644
--- a/src/trace_processor/importers/instruments/row.h
+++ b/src/trace_processor/importers/instruments/row.h
@@ -41,6 +41,7 @@
struct Frame {
long long addr = 0;
+ std::string name;
BinaryId binary = kNullId;
};
diff --git a/src/trace_processor/importers/instruments/row_parser.cc b/src/trace_processor/importers/instruments/row_parser.cc
index 39d7a92..1cfa13e 100644
--- a/src/trace_processor/importers/instruments/row_parser.cc
+++ b/src/trace_processor/importers/instruments/row_parser.cc
@@ -64,47 +64,47 @@
Frame* frame = data_.GetFrame(*it);
Binary* binary = data_.GetBinary(frame->binary);
- uint64_t rel_pc = static_cast<uint64_t>(frame->addr);
+ uint64_t pc = static_cast<uint64_t>(frame->addr);
if (frame->binary) {
- rel_pc -= static_cast<uint64_t>(binary->load_addr);
+ pc -= static_cast<uint64_t>(binary->load_addr);
}
// For non-leaf functions, the pc will be after the end of the call. Adjust
// it to be within the call instruction.
- if (rel_pc != 0 && it != leaf) {
- --rel_pc;
+ if (pc != 0 && it != leaf) {
+ --pc;
}
- auto frame_inserted = frame_to_frame_id_.Insert(*it, FrameId{0});
- if (frame_inserted.second) {
- auto mapping_inserted = binary_to_mapping_.Insert(frame->binary, nullptr);
- if (mapping_inserted.second) {
- if (binary == nullptr) {
- *mapping_inserted.first = GetDummyMapping(upid);
- } else {
+ VirtualMemoryMapping* mapping = nullptr;
+ mapping = context_->mapping_tracker->FindUserMappingForAddress(upid, pc);
+ if (!mapping) {
+ if (binary == nullptr) {
+ mapping = GetDummyMapping(upid);
+ } else {
+ auto mapping_inserted =
+ binary_to_mapping_.Insert(frame->binary, nullptr);
+ if (mapping_inserted.second) {
BuildId build_id = binary->uuid;
*mapping_inserted.first =
&context_->mapping_tracker->CreateUserMemoryMapping(
upid, {AddressRange(static_cast<uint64_t>(binary->load_addr),
static_cast<uint64_t>(binary->max_addr)),
- 0, 0, 0, binary->path, build_id});
+ static_cast<uint64_t>(binary->load_addr), 0, 0,
+ binary->path, build_id});
}
+ mapping = *mapping_inserted.first;
}
- VirtualMemoryMapping* mapping = *mapping_inserted.first;
-
- // Intern the frame with no function name -- the symbolizer will annotate
- // frames later.
- *frame_inserted.first =
- mapping->InternFrame(rel_pc, base::StringView(""));
}
- FrameId frame_id = *frame_inserted.first;
+
+ FrameId frame_id = mapping->InternFrame(mapping->ToRelativePc(pc),
+ base::StringView(frame->name));
parent = stack_profile_tracker.InternCallsite(parent, frame_id, depth);
depth++;
}
context_->storage->mutable_instruments_sample_table()->Insert(
- {ts, utid, row.core_id, parent});
+ {ts, utid, parent, row.core_id});
}
DummyMemoryMapping* RowParser::GetDummyMapping(UniquePid upid) {
diff --git a/src/trace_processor/importers/instruments/row_parser.h b/src/trace_processor/importers/instruments/row_parser.h
index ead208c..6c15520 100644
--- a/src/trace_processor/importers/instruments/row_parser.h
+++ b/src/trace_processor/importers/instruments/row_parser.h
@@ -41,10 +41,8 @@
TraceProcessorContext* context_;
RowDataTracker& data_;
- // Cache FrameId and binary mappings by instruments frame and binary
- // pointers, respectively. These are already de-duplicated in the
- // instruments XML parsing.
- base::FlatHashMap<BacktraceFrameId, FrameId> frame_to_frame_id_;
+ // Cache binary mappings by instruments binary pointers. These are already
+ // de-duplicated in the instruments XML parsing.
base::FlatHashMap<BinaryId, VirtualMemoryMapping*> binary_to_mapping_;
base::FlatHashMap<UniquePid, DummyMemoryMapping*> dummy_mappings_;
};
diff --git a/src/trace_processor/importers/json/json_trace_parser_impl.cc b/src/trace_processor/importers/json/json_trace_parser_impl.cc
index 77953af..7b30623 100644
--- a/src/trace_processor/importers/json/json_trace_parser_impl.cc
+++ b/src/trace_processor/importers/json/json_trace_parser_impl.cc
@@ -151,27 +151,20 @@
// Only used for 'B', 'E', and 'X' events so wrap in lambda so it gets
// ignored in other cases. This lambda is only safe to call within the
// scope of this function due to the capture by reference.
- auto make_slice_row = [&](TrackId track_id) {
- tables::SliceTable::Row row;
- row.ts = timestamp;
- row.track_id = track_id;
- row.category = cat_id;
- row.name =
- name_id == kNullStringId ? storage->InternString("[No name]") : name_id;
- row.thread_ts = json::CoerceToTs(value["tts"]);
- // tdur will only exist on 'X' events.
- row.thread_dur = json::CoerceToTs(value["tdur"]);
- // JSON traces don't report these counters as part of slices.
- row.thread_instruction_count = std::nullopt;
- row.thread_instruction_delta = std::nullopt;
- return row;
- };
-
+ StringId slice_name_id =
+ name_id == kNullStringId ? storage->InternString("[No name]") : name_id;
switch (phase) {
case 'B': { // TRACE_EVENT_BEGIN.
TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
- slice_tracker->BeginTyped(storage->mutable_slice_table(),
- make_slice_row(track_id), args_inserter);
+ auto slice_id = slice_tracker->Begin(timestamp, track_id, cat_id,
+ slice_name_id, args_inserter);
+ if (slice_id) {
+ if (auto thread_ts = json::CoerceToTs(value["tts"]); thread_ts) {
+ auto rr =
+ context_->storage->mutable_slice_table()->FindById(*slice_id);
+ rr->set_thread_ts(*thread_ts);
+ }
+ }
MaybeAddFlow(track_id, value);
break;
}
@@ -220,8 +213,8 @@
}
if (phase == 'b') {
- slice_tracker->BeginTyped(storage->mutable_slice_table(),
- make_slice_row(track_id), args_inserter);
+ slice_tracker->Begin(timestamp, track_id, cat_id, slice_name_id,
+ args_inserter);
MaybeAddFlow(track_id, value);
} else if (phase == 'e') {
slice_tracker->End(timestamp, track_id, cat_id, name_id, args_inserter);
@@ -239,10 +232,17 @@
if (!opt_dur.has_value())
return;
TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
- auto row = make_slice_row(track_id);
- row.dur = opt_dur.value();
- slice_tracker->ScopedTyped(storage->mutable_slice_table(), std::move(row),
- args_inserter);
+ auto slice_id = slice_tracker->Scoped(
+ timestamp, track_id, cat_id, slice_name_id, *opt_dur, args_inserter);
+ if (slice_id) {
+ auto rr = context_->storage->mutable_slice_table()->FindById(*slice_id);
+ if (auto thread_ts = json::CoerceToTs(value["tts"]); thread_ts) {
+ rr->set_thread_ts(*thread_ts);
+ }
+ if (auto thread_dur = json::CoerceToTs(value["tdur"]); thread_dur) {
+ rr->set_thread_dur(*thread_dur);
+ }
+ }
MaybeAddFlow(track_id, value);
break;
}
@@ -318,14 +318,15 @@
break;
}
track_id = context_->track_tracker->InternThreadTrack(utid);
- auto row = make_slice_row(track_id);
- row.dur = 0;
- if (row.thread_ts) {
- // Only set thread_dur to zero if we have a thread_ts.
- row.thread_dur = 0;
+ auto slice_id = slice_tracker->Scoped(timestamp, track_id, cat_id,
+ slice_name_id, 0, args_inserter);
+ if (slice_id) {
+ if (auto thread_ts = json::CoerceToTs(value["tts"]); thread_ts) {
+ auto rr =
+ context_->storage->mutable_slice_table()->FindById(*slice_id);
+ rr->set_thread_ts(*thread_ts);
+ }
}
- slice_tracker->ScopedTyped(storage->mutable_slice_table(),
- std::move(row), args_inserter);
break;
} else {
context_->storage->IncrementStats(stats::json_parser_failure);
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer.cc b/src/trace_processor/importers/json/json_trace_tokenizer.cc
index 8ec36af..f4f5e47 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer.cc
+++ b/src/trace_processor/importers/json/json_trace_tokenizer.cc
@@ -30,6 +30,7 @@
#include "perfetto/public/compiler.h"
#include "perfetto/trace_processor/trace_blob_view.h"
#include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
+#include "src/trace_processor/importers/common/parser_types.h"
#include "src/trace_processor/importers/json/json_utils.h"
#include "src/trace_processor/sorter/trace_sorter.h" // IWYU pragma: keep
#include "src/trace_processor/storage/stats.h"
@@ -576,7 +577,27 @@
continue;
}
}
- context_->sorter->PushJsonValue(ts, unparsed.ToStdString(), opt_dur);
+ JsonEvent::Type type;
+ if (opt_raw_ph && opt_raw_ph->size() == 1) {
+ switch ((*opt_raw_ph)[0]) {
+ case 'B':
+ type = JsonEvent::Begin();
+ break;
+ case 'E':
+ type = JsonEvent::End();
+ break;
+ case 'X':
+ if (opt_dur) {
+ type = JsonEvent::Scoped{*opt_dur};
+ } else {
+ type = JsonEvent::Other();
+ }
+ break;
+ }
+ } else {
+ type = JsonEvent::Other();
+ }
+ context_->sorter->PushJsonValue(ts, unparsed.ToStdString(), type);
}
return SetOutAndReturn(next, out);
}
diff --git a/src/trace_processor/importers/perf/features.cc b/src/trace_processor/importers/perf/features.cc
index 877f2fd..be65da57 100644
--- a/src/trace_processor/importers/perf/features.cc
+++ b/src/trace_processor/importers/perf/features.cc
@@ -107,31 +107,31 @@
return true;
}
-util::Status ParseEventTypeInfo(std::string value, SimpleperfMetaInfo& out) {
+base::Status ParseEventTypeInfo(std::string value, SimpleperfMetaInfo& out) {
for (const auto& line : base::SplitString(value, "\n")) {
auto tokens = base::SplitString(line, ",");
if (tokens.size() != 3) {
- return util::ErrStatus("Invalid event_type_info: '%s'", line.c_str());
+ return base::ErrStatus("Invalid event_type_info: '%s'", line.c_str());
}
auto type = base::StringToUInt32(tokens[1]);
if (!type) {
- return util::ErrStatus("Could not parse type in event_type_info: '%s'",
+ return base::ErrStatus("Could not parse type in event_type_info: '%s'",
tokens[1].c_str());
}
auto config = base::StringToUInt64(tokens[2]);
if (!config) {
- return util::ErrStatus("Could not parse config in event_type_info: '%s'",
+ return base::ErrStatus("Could not parse config in event_type_info: '%s'",
tokens[2].c_str());
}
out.event_type_info.Insert({*type, *config}, std::move(tokens[0]));
}
- return util::OkStatus();
+ return base::OkStatus();
}
-util::Status ParseSimpleperfMetaInfoEntry(
+base::Status ParseSimpleperfMetaInfoEntry(
std::pair<std::string, std::string> entry,
SimpleperfMetaInfo& out) {
static constexpr char kEventTypeInfoKey[] = "event_type_info";
@@ -142,14 +142,14 @@
PERFETTO_CHECK(
out.entries.Insert(std::move(entry.first), std::move(entry.second))
.second);
- return util::OkStatus();
+ return base::OkStatus();
}
} // namespace
// static
-util::Status BuildId::Parse(TraceBlobView bytes,
- std::function<util::Status(BuildId)> cb) {
+base::Status BuildId::Parse(TraceBlobView bytes,
+ std::function<base::Status(BuildId)> cb) {
Reader reader(std::move(bytes));
while (reader.size_left() != 0) {
perf_event_header header;
@@ -175,27 +175,27 @@
RETURN_IF_ERROR(cb(std::move(build_id)));
}
- return util::OkStatus();
+ return base::OkStatus();
}
// static
-util::Status SimpleperfMetaInfo::Parse(const TraceBlobView& bytes,
+base::Status SimpleperfMetaInfo::Parse(const TraceBlobView& bytes,
SimpleperfMetaInfo& out) {
auto* it_end = reinterpret_cast<const char*>(bytes.data() + bytes.size());
for (auto* it = reinterpret_cast<const char*>(bytes.data()); it != it_end;) {
auto end = std::find(it, it_end, '\0');
if (end == it_end) {
- return util::ErrStatus("Failed to read key from Simpleperf MetaInfo");
+ return base::ErrStatus("Failed to read key from Simpleperf MetaInfo");
}
std::string key(it, end);
it = end;
++it;
if (it == it_end) {
- return util::ErrStatus("Missing value in Simpleperf MetaInfo");
+ return base::ErrStatus("Missing value in Simpleperf MetaInfo");
}
end = std::find(it, it_end, '\0');
if (end == it_end) {
- return util::ErrStatus("Failed to read value from Simpleperf MetaInfo");
+ return base::ErrStatus("Failed to read value from Simpleperf MetaInfo");
}
std::string value(it, end);
it = end;
@@ -204,18 +204,18 @@
RETURN_IF_ERROR(ParseSimpleperfMetaInfoEntry(
std::make_pair(std::move(key), std::move(value)), out));
}
- return util::OkStatus();
+ return base::OkStatus();
}
// static
-util::Status EventDescription::Parse(
+base::Status EventDescription::Parse(
TraceBlobView bytes,
- std::function<util::Status(EventDescription)> cb) {
+ std::function<base::Status(EventDescription)> cb) {
Reader reader(std::move(bytes));
uint32_t nr;
uint32_t attr_size;
if (!reader.Read(nr) || !reader.Read(attr_size)) {
- return util::ErrStatus("Failed to parse header for PERF_EVENT_DESC");
+ return base::ErrStatus("Failed to parse header for PERF_EVENT_DESC");
}
for (; nr != 0; --nr) {
@@ -223,21 +223,21 @@
uint32_t nr_ids;
if (!reader.ReadPerfEventAttr(desc.attr, attr_size) ||
!reader.Read(nr_ids) || !ParseString(reader, desc.event_string)) {
- return util::ErrStatus("Failed to parse record for PERF_EVENT_DESC");
+ return base::ErrStatus("Failed to parse record for PERF_EVENT_DESC");
}
desc.ids.resize(nr_ids);
for (uint64_t& id : desc.ids) {
if (!reader.Read(id)) {
- return util::ErrStatus("Failed to parse ids for PERF_EVENT_DESC");
+ return base::ErrStatus("Failed to parse ids for PERF_EVENT_DESC");
}
}
RETURN_IF_ERROR(cb(std::move(desc)));
}
- return util::OkStatus();
+ return base::OkStatus();
}
-util::Status ParseSimpleperfFile2(TraceBlobView bytes,
+base::Status ParseSimpleperfFile2(TraceBlobView bytes,
std::function<void(TraceBlobView)> cb) {
Reader reader(std::move(bytes));
while (reader.size_left() != 0) {
@@ -252,15 +252,15 @@
}
cb(std::move(payload));
}
- return util::OkStatus();
+ return base::OkStatus();
}
// static
-util::Status HeaderGroupDesc::Parse(TraceBlobView bytes, HeaderGroupDesc& out) {
+base::Status HeaderGroupDesc::Parse(TraceBlobView bytes, HeaderGroupDesc& out) {
Reader reader(std::move(bytes));
uint32_t nr;
if (!reader.Read(nr)) {
- return util::ErrStatus("Failed to parse header for HEADER_GROUP_DESC");
+ return base::ErrStatus("Failed to parse header for HEADER_GROUP_DESC");
}
HeaderGroupDesc group_desc;
@@ -268,7 +268,7 @@
for (auto& e : group_desc.entries) {
if (!ParseString(reader, e.string) || !reader.Read(e.leader_idx) ||
!reader.Read(e.nr_members)) {
- return util::ErrStatus("Failed to parse HEADER_GROUP_DESC entry");
+ return base::ErrStatus("Failed to parse HEADER_GROUP_DESC entry");
}
}
out = std::move(group_desc);
@@ -279,7 +279,7 @@
Reader reader(std::move(bytes));
uint32_t nr;
if (!reader.Read(nr)) {
- return util::ErrStatus("Failed to parse nr for CMDLINE");
+ return base::ErrStatus("Failed to parse nr for CMDLINE");
}
std::vector<std::string> args;
diff --git a/src/trace_processor/importers/perf/features.h b/src/trace_processor/importers/perf/features.h
index e028957..12aeaea 100644
--- a/src/trace_processor/importers/perf/features.h
+++ b/src/trace_processor/importers/perf/features.h
@@ -74,15 +74,15 @@
};
struct BuildId {
- static util::Status Parse(TraceBlobView,
- std::function<util::Status(BuildId)> cb);
+ static base::Status Parse(TraceBlobView,
+ std::function<base::Status(BuildId)> cb);
int32_t pid;
std::string build_id;
std::string filename;
};
struct HeaderGroupDesc {
- static util::Status Parse(TraceBlobView, HeaderGroupDesc& out);
+ static base::Status Parse(TraceBlobView, HeaderGroupDesc& out);
struct Entry {
std::string string;
uint32_t leader_idx;
@@ -92,15 +92,15 @@
};
struct EventDescription {
- static util::Status Parse(TraceBlobView,
- std::function<util::Status(EventDescription)> cb);
+ static base::Status Parse(TraceBlobView,
+ std::function<base::Status(EventDescription)> cb);
perf_event_attr attr;
std::string event_string;
std::vector<uint64_t> ids;
};
struct SimpleperfMetaInfo {
- static util::Status Parse(const TraceBlobView&, SimpleperfMetaInfo& out);
+ static base::Status Parse(const TraceBlobView&, SimpleperfMetaInfo& out);
base::FlatHashMap<std::string, std::string> entries;
struct EventTypeAndConfig {
uint32_t type;
@@ -122,7 +122,7 @@
event_type_info;
};
-util::Status ParseSimpleperfFile2(TraceBlobView,
+base::Status ParseSimpleperfFile2(TraceBlobView,
std::function<void(TraceBlobView)> cb);
base::StatusOr<std::vector<std::string>> ParseCmdline(TraceBlobView blob);
diff --git a/src/trace_processor/importers/proto/BUILD.gn b/src/trace_processor/importers/proto/BUILD.gn
index aa61056..a5b9181 100644
--- a/src/trace_processor/importers/proto/BUILD.gn
+++ b/src/trace_processor/importers/proto/BUILD.gn
@@ -28,8 +28,6 @@
"chrome_system_probes_parser.h",
"default_modules.cc",
"default_modules.h",
- "jit_tracker.cc",
- "jit_tracker.h",
"memory_tracker_snapshot_module.cc",
"memory_tracker_snapshot_module.h",
"memory_tracker_snapshot_parser.cc",
@@ -46,8 +44,6 @@
"packet_sequence_state_generation.cc",
"perf_sample_tracker.cc",
"perf_sample_tracker.h",
- "profile_module.cc",
- "profile_module.h",
"profile_packet_sequence_state.cc",
"profile_packet_sequence_state.h",
"profile_packet_utils.cc",
@@ -72,6 +68,9 @@
]
public_deps = [ ":proto_importer_module" ]
deps = [
+ ":gen_cc_android_track_event_descriptor",
+ ":gen_cc_chrome_track_event_descriptor",
+ ":gen_cc_track_event_descriptor",
"../../../../gn:default_deps",
"../../../../include/perfetto/trace_processor:trace_processor",
"../../../../protos/perfetto/common:zero",
@@ -136,6 +135,8 @@
"heap_graph_module.h",
"heap_graph_tracker.cc",
"heap_graph_tracker.h",
+ "jit_tracker.cc",
+ "jit_tracker.h",
"metadata_module.cc",
"metadata_module.h",
"pigweed_detokenizer.cc",
@@ -144,6 +145,8 @@
"pixel_modem_module.h",
"pixel_modem_parser.cc",
"pixel_modem_parser.h",
+ "profile_module.cc",
+ "profile_module.h",
"statsd_module.cc",
"statsd_module.h",
"string_encoding_utils.cc",
@@ -164,7 +167,6 @@
"vulkan_memory_tracker.h",
]
deps = [
- ":gen_cc_config_descriptor",
":gen_cc_statsd_atoms_descriptor",
":gen_cc_trace_descriptor",
":minimal",
@@ -193,6 +195,7 @@
"../../storage",
"../../tables",
"../../types",
+ "../../util:build_id",
"../../util:descriptors",
"../../util:profiler_util",
"../../util:proto_profiler",
@@ -263,11 +266,6 @@
"../../../../protos/perfetto/trace/android:android_track_event_descriptor"
}
-perfetto_cc_proto_descriptor("gen_cc_config_descriptor") {
- descriptor_name = "config.descriptor"
- descriptor_target = "../../../../protos/perfetto/config:descriptor"
-}
-
source_set("unittests") {
testonly = true
sources = [
@@ -284,6 +282,7 @@
]
deps = [
":full",
+ ":gen_cc_trace_descriptor",
":minimal",
":packet_sequence_state_generation_hdr",
"../../../../gn:default_deps",
diff --git a/src/trace_processor/importers/proto/additional_modules.cc b/src/trace_processor/importers/proto/additional_modules.cc
index 0805476..296de58 100644
--- a/src/trace_processor/importers/proto/additional_modules.cc
+++ b/src/trace_processor/importers/proto/additional_modules.cc
@@ -15,26 +15,35 @@
*/
#include "src/trace_processor/importers/proto/additional_modules.h"
+
+#include <memory>
+
#include "src/trace_processor/importers/etw/etw_module_impl.h"
#include "src/trace_processor/importers/ftrace/ftrace_module_impl.h"
#include "src/trace_processor/importers/proto/android_camera_event_module.h"
#include "src/trace_processor/importers/proto/android_probes_module.h"
+#include "src/trace_processor/importers/proto/content_analyzer.h"
#include "src/trace_processor/importers/proto/graphics_event_module.h"
#include "src/trace_processor/importers/proto/heap_graph_module.h"
#include "src/trace_processor/importers/proto/metadata_module.h"
#include "src/trace_processor/importers/proto/multi_machine_trace_manager.h"
#include "src/trace_processor/importers/proto/network_trace_module.h"
#include "src/trace_processor/importers/proto/pixel_modem_module.h"
+#include "src/trace_processor/importers/proto/profile_module.h"
#include "src/trace_processor/importers/proto/statsd_module.h"
#include "src/trace_processor/importers/proto/system_probes_module.h"
+#include "src/trace_processor/importers/proto/trace.descriptor.h"
#include "src/trace_processor/importers/proto/translation_table_module.h"
#include "src/trace_processor/importers/proto/v8_module.h"
#include "src/trace_processor/importers/proto/winscope/winscope_module.h"
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
void RegisterAdditionalModules(TraceProcessorContext* context) {
+ // Content analyzer and metadata module both depend on this.
+ context->descriptor_pool_->AddFromFileDescriptorSet(kTraceDescriptor.data(),
+ kTraceDescriptor.size());
+
context->modules.emplace_back(new AndroidProbesModule(context));
context->modules.emplace_back(new NetworkTraceModule(context));
context->modules.emplace_back(new GraphicsEventModule(context));
@@ -47,6 +56,7 @@
context->modules.emplace_back(new V8Module(context));
context->modules.emplace_back(new WinscopeModule(context));
context->modules.emplace_back(new PixelModemModule(context));
+ context->modules.emplace_back(new ProfileModule(context));
// Ftrace/Etw modules are special, because it has one extra method for parsing
// ftrace/etw packets. So we need to store a pointer to it separately.
@@ -60,7 +70,10 @@
context->multi_machine_trace_manager->EnableAdditionalModules(
RegisterAdditionalModules);
}
+
+ if (context->config.analyze_trace_proto_content) {
+ context->content_analyzer = std::make_unique<ProtoContentAnalyzer>(context);
+ }
}
-} // namespace trace_processor
-} // namespace perfetto
+} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/android_probes_module.cc b/src/trace_processor/importers/proto/android_probes_module.cc
index 9976e70..7163ab5 100644
--- a/src/trace_processor/importers/proto/android_probes_module.cc
+++ b/src/trace_processor/importers/proto/android_probes_module.cc
@@ -51,6 +51,7 @@
#include "protos/perfetto/trace/power/android_entity_state_residency.pbzero.h"
#include "protos/perfetto/trace/power/power_rails.pbzero.h"
#include "protos/perfetto/trace/trace_packet.pbzero.h"
+#include "protos/perfetto/trace/android/bluetooth_trace.pbzero.h"
namespace perfetto::trace_processor {
namespace {
@@ -115,6 +116,7 @@
context);
RegisterForField(TracePacket::kInitialDisplayStateFieldNumber, context);
RegisterForField(TracePacket::kAndroidSystemPropertyFieldNumber, context);
+ RegisterForField(TracePacket::kBluetoothTraceEventFieldNumber, context);
}
ModuleResult AndroidProbesModule::TokenizePacket(
@@ -260,6 +262,9 @@
case TracePacket::kAndroidSystemPropertyFieldNumber:
parser_.ParseAndroidSystemProperty(ts, decoder.android_system_property());
return;
+ case TracePacket::kBluetoothTraceEventFieldNumber:
+ parser_.ParseBtTraceEvent(ts, decoder.bluetooth_trace_event());
+ return;
}
}
diff --git a/src/trace_processor/importers/proto/android_probes_parser.cc b/src/trace_processor/importers/proto/android_probes_parser.cc
index 7075be5..c6bbb3c 100644
--- a/src/trace_processor/importers/proto/android_probes_parser.cc
+++ b/src/trace_processor/importers/proto/android_probes_parser.cc
@@ -56,6 +56,7 @@
#include "protos/perfetto/trace/power/android_entity_state_residency.pbzero.h"
#include "protos/perfetto/trace/power/battery_counters.pbzero.h"
#include "protos/perfetto/trace/power/power_rails.pbzero.h"
+#include "protos/perfetto/trace/android/bluetooth_trace.pbzero.h"
namespace perfetto::trace_processor {
@@ -67,7 +68,16 @@
energy_consumer_id_(
context_->storage->InternString("energy_consumer_id")),
consumer_type_id_(context_->storage->InternString("consumer_type")),
- ordinal_id_(context_->storage->InternString("ordinal")) {}
+ ordinal_id_(context_->storage->InternString("ordinal")),
+ bt_trace_event_id_(context_->storage->InternString("BluetoothTraceEvent")),
+ bt_packet_type_id_(context_->storage->InternString("TracePacketType")),
+ bt_count_id_(context_->storage->InternString("Count")),
+ bt_length_id_(context_->storage->InternString("Length")),
+ bt_duration_id_(context_->storage->InternString("Duration")),
+ bt_op_code_id_(context_->storage->InternString("Op Code")),
+ bt_event_code_id_(context_->storage->InternString("Event Code")),
+ bt_subevent_code_id_(context_->storage->InternString("Subevent Code")),
+ bt_handle_id_(context_->storage->InternString("Handle")) {}
void AndroidProbesParser::ParseBatteryCounters(int64_t ts, ConstBytes blob) {
protos::pbzero::BatteryCounters::Decoder evt(blob);
@@ -491,4 +501,65 @@
}
}
+void AndroidProbesParser::ParseBtTraceEvent(int64_t ts,
+ ConstBytes blob) {
+ protos::pbzero::BluetoothTraceEvent::Decoder evt(blob);
+
+ static constexpr auto kBluetoothTraceEventBlueprint =
+ tracks::SliceBlueprint("bluetooth_trace_event",
+ tracks::DimensionBlueprints(),
+ tracks::StaticNameBlueprint("BluetoothTraceEvent"));
+
+ TrackId track_id = context_->track_tracker->InternTrack(
+ kBluetoothTraceEventBlueprint);
+
+ context_->slice_tracker->Scoped(ts, track_id, kNullStringId,
+ bt_trace_event_id_, 0, [&evt, this](ArgsTracker::BoundInserter* inserter) {
+ if (evt.has_packet_type()) {
+ StringId packet_type_str = context_->storage->InternString(
+ protos::pbzero::BluetoothTracePacketType_Name(
+ static_cast<::perfetto::protos::pbzero::BluetoothTracePacketType>(
+ evt.packet_type())));
+ inserter->AddArg(
+ bt_packet_type_id_,
+ Variadic::String(packet_type_str));
+ }
+ if (evt.has_count()) {
+ inserter->AddArg(
+ bt_count_id_,
+ Variadic::UnsignedInteger(evt.count()));
+ }
+ if (evt.has_length()) {
+ inserter->AddArg(
+ bt_length_id_,
+ Variadic::UnsignedInteger(evt.length()));
+ }
+ if (evt.has_duration()) {
+ inserter->AddArg(
+ bt_duration_id_,
+ Variadic::UnsignedInteger(evt.duration()));
+ }
+ if (evt.has_op_code()) {
+ inserter->AddArg(
+ bt_op_code_id_,
+ Variadic::UnsignedInteger(evt.op_code()));
+ }
+ if (evt.has_event_code()) {
+ inserter->AddArg(
+ bt_event_code_id_,
+ Variadic::UnsignedInteger(evt.event_code()));
+ }
+ if (evt.has_subevent_code()) {
+ inserter->AddArg(
+ bt_subevent_code_id_,
+ Variadic::UnsignedInteger(evt.subevent_code()));
+ }
+ if (evt.has_connection_handle()) {
+ inserter->AddArg(
+ bt_handle_id_,
+ Variadic::UnsignedInteger(evt.connection_handle()));
+ }
+ });
+}
+
} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/android_probes_parser.h b/src/trace_processor/importers/proto/android_probes_parser.h
index 8a8a04a..613ac77 100644
--- a/src/trace_processor/importers/proto/android_probes_parser.h
+++ b/src/trace_processor/importers/proto/android_probes_parser.h
@@ -43,6 +43,7 @@
void ParseInitialDisplayState(int64_t ts, ConstBytes);
void ParseAndroidSystemProperty(int64_t ts, ConstBytes);
void ParseAndroidGameIntervention(ConstBytes);
+ void ParseBtTraceEvent(int64_t ts, ConstBytes);
private:
TraceProcessorContext* const context_;
@@ -53,6 +54,15 @@
const StringId energy_consumer_id_;
const StringId consumer_type_id_;
const StringId ordinal_id_;
+ const StringId bt_trace_event_id_;
+ const StringId bt_packet_type_id_;
+ const StringId bt_count_id_;
+ const StringId bt_length_id_;
+ const StringId bt_duration_id_;
+ const StringId bt_op_code_id_;
+ const StringId bt_event_code_id_;
+ const StringId bt_subevent_code_id_;
+ const StringId bt_handle_id_;
};
} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/args_parser.cc b/src/trace_processor/importers/proto/args_parser.cc
index 2aeb785..9692d2c 100644
--- a/src/trace_processor/importers/proto/args_parser.cc
+++ b/src/trace_processor/importers/proto/args_parser.cc
@@ -67,10 +67,10 @@
Variadic::Real(value));
}
-void ArgsParser::AddPointer(const Key& key, const void* value) {
+void ArgsParser::AddPointer(const Key& key, uint64_t value) {
inserter_.AddArg(storage_.InternString(base::StringView(key.flat_key)),
storage_.InternString(base::StringView(key.key)),
- Variadic::Pointer(reinterpret_cast<uintptr_t>(value)));
+ Variadic::Pointer(reinterpret_cast<uint64_t>(value)));
}
void ArgsParser::AddBoolean(const Key& key, bool value) {
diff --git a/src/trace_processor/importers/proto/args_parser.h b/src/trace_processor/importers/proto/args_parser.h
index 7cd1578..c557891 100644
--- a/src/trace_processor/importers/proto/args_parser.h
+++ b/src/trace_processor/importers/proto/args_parser.h
@@ -41,7 +41,7 @@
void AddString(const Key&, const protozero::ConstChars&) override;
void AddString(const Key&, const std::string&) override;
void AddDouble(const Key&, double) override;
- void AddPointer(const Key&, const void*) override;
+ void AddPointer(const Key&, uint64_t) override;
void AddBoolean(const Key&, bool) override;
void AddBytes(const Key&, const protozero::ConstBytes&) override;
bool AddJson(const Key&, const protozero::ConstChars&) override;
diff --git a/src/trace_processor/importers/proto/content_analyzer.cc b/src/trace_processor/importers/proto/content_analyzer.cc
index 0123c44..025f138 100644
--- a/src/trace_processor/importers/proto/content_analyzer.cc
+++ b/src/trace_processor/importers/proto/content_analyzer.cc
@@ -16,29 +16,26 @@
#include "src/trace_processor/importers/proto/content_analyzer.h"
-#include "perfetto/ext/base/string_utils.h"
+#include <cstdint>
+#include <optional>
+#include <string>
+
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
#include "src/trace_processor/importers/common/args_tracker.h"
-#include "src/trace_processor/importers/proto/content_analyzer.h"
#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/trace_proto_tables_py.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/types/variadic.h"
+#include "src/trace_processor/util/proto_profiler.h"
-#include "src/trace_processor/importers/proto/trace.descriptor.h"
-
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
ProtoContentAnalyzer::ProtoContentAnalyzer(TraceProcessorContext* context)
: context_(context),
- pool_([]() {
- DescriptorPool pool;
- base::Status status = pool.AddFromFileDescriptorSet(
- kTraceDescriptor.data(), kTraceDescriptor.size());
- if (!status.ok()) {
- PERFETTO_ELOG("Could not add TracePacket proto descriptor %s",
- status.c_message());
- }
- return pool;
- }()),
- computer_(&pool_, ".perfetto.protos.TracePacket") {}
+ computer_(context_->descriptor_pool_.get(),
+ ".perfetto.protos.TracePacket") {}
ProtoContentAnalyzer::~ProtoContentAnalyzer() = default;
@@ -135,5 +132,4 @@
aggregated_samples_.Clear();
}
-} // namespace trace_processor
-} // namespace perfetto
+} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/content_analyzer.h b/src/trace_processor/importers/proto/content_analyzer.h
index 84fd674..eabb858 100644
--- a/src/trace_processor/importers/proto/content_analyzer.h
+++ b/src/trace_processor/importers/proto/content_analyzer.h
@@ -17,18 +17,17 @@
#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_CONTENT_ANALYZER_H_
#define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_CONTENT_ANALYZER_H_
+#include <cstddef>
#include <utility>
-#include <vector>
#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/hash.h"
#include "perfetto/trace_processor/trace_blob_view.h"
#include "src/trace_processor/importers/proto/packet_analyzer.h"
-#include "src/trace_processor/storage/trace_storage.h"
#include "src/trace_processor/types/trace_processor_context.h"
#include "src/trace_processor/util/proto_profiler.h"
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
// Interface for a module that processes track event information.
class ProtoContentAnalyzer : public PacketAnalyzer {
@@ -59,22 +58,19 @@
using AnnotatedSamplesMap = base::
FlatHashMap<SampleAnnotation, PathToSamplesMap, SampleAnnotationHasher>;
- ProtoContentAnalyzer(TraceProcessorContext* context);
+ explicit ProtoContentAnalyzer(TraceProcessorContext* context);
~ProtoContentAnalyzer() override;
- void ProcessPacket(const TraceBlobView& packet,
- const SampleAnnotation& annotation) override;
+ void ProcessPacket(const TraceBlobView&, const SampleAnnotation&) override;
void NotifyEndOfFile() override;
private:
TraceProcessorContext* context_;
- DescriptorPool pool_;
util::SizeProfileComputer computer_;
AnnotatedSamplesMap aggregated_samples_;
};
-} // namespace trace_processor
-} // namespace perfetto
+} // namespace perfetto::trace_processor
#endif // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_CONTENT_ANALYZER_H_
diff --git a/src/trace_processor/importers/proto/default_modules.cc b/src/trace_processor/importers/proto/default_modules.cc
index f7611d7..2e5d397 100644
--- a/src/trace_processor/importers/proto/default_modules.cc
+++ b/src/trace_processor/importers/proto/default_modules.cc
@@ -20,7 +20,6 @@
#include "src/trace_processor/importers/proto/chrome_system_probes_module.h"
#include "src/trace_processor/importers/proto/memory_tracker_snapshot_module.h"
#include "src/trace_processor/importers/proto/metadata_minimal_module.h"
-#include "src/trace_processor/importers/proto/profile_module.h"
#include "src/trace_processor/importers/proto/proto_importer_module.h"
#include "src/trace_processor/importers/proto/track_event_module.h"
@@ -43,7 +42,6 @@
context->modules.emplace_back(new MemoryTrackerSnapshotModule(context));
context->modules.emplace_back(new ChromeSystemProbesModule(context));
- context->modules.emplace_back(new ProfileModule(context));
context->modules.emplace_back(new MetadataMinimalModule(context));
}
diff --git a/src/trace_processor/importers/proto/frame_timeline_event_parser.cc b/src/trace_processor/importers/proto/frame_timeline_event_parser.cc
index 91d137a..d3f4c45 100644
--- a/src/trace_processor/importers/proto/frame_timeline_event_parser.cc
+++ b/src/trace_processor/importers/proto/frame_timeline_event_parser.cc
@@ -33,7 +33,6 @@
#include "src/trace_processor/importers/common/process_tracker.h"
#include "src/trace_processor/importers/common/slice_tracker.h"
#include "src/trace_processor/importers/common/track_compressor.h"
-#include "src/trace_processor/importers/common/track_tracker.h"
#include "src/trace_processor/importers/common/tracks.h"
#include "src/trace_processor/importers/common/tracks_common.h"
#include "src/trace_processor/storage/stats.h"
@@ -195,6 +194,7 @@
context->storage->InternString("Jank severity type")),
layer_name_id_(context->storage->InternString("Layer name")),
prediction_type_id_(context->storage->InternString("Prediction type")),
+ jank_tag_id_(context->storage->InternString("Jank tag")),
is_buffer_id_(context->storage->InternString("Is Buffer?")),
jank_tag_none_id_(context->storage->InternString("No Jank")),
jank_tag_self_id_(context->storage->InternString("Self Jank")),
@@ -223,18 +223,11 @@
static_cast<uint32_t>(event.pid()));
cookie_map_[cookie] = std::make_pair(upid, TrackType::kExpected);
- tables::ExpectedFrameTimelineSliceTable::Row expected_row;
- expected_row.ts = timestamp;
- expected_row.track_id = context_->track_compressor->InternBegin(
+ TrackId track_id = context_->track_compressor->InternBegin(
kExpectedBlueprint, tracks::Dimensions(upid), cookie);
- expected_row.name = name_id;
-
- expected_row.display_frame_token = token;
- expected_row.upid = upid;
-
- context_->slice_tracker->BeginTyped(
- context_->storage->mutable_expected_frame_timeline_slice_table(),
- expected_row, [this, token](ArgsTracker::BoundInserter* inserter) {
+ context_->slice_tracker->Begin(
+ timestamp, track_id, kNullStringId, name_id,
+ [this, token](ArgsTracker::BoundInserter* inserter) {
inserter->AddArg(display_frame_token_id_, Variadic::Integer(token));
});
}
@@ -257,15 +250,8 @@
static_cast<uint32_t>(event.pid()));
cookie_map_[cookie] = std::make_pair(upid, TrackType::kActual);
- tables::ActualFrameTimelineSliceTable::Row actual_row;
- actual_row.ts = timestamp;
- actual_row.track_id = context_->track_compressor->InternBegin(
+ TrackId track_id = context_->track_compressor->InternBegin(
kActualBlueprint, tracks::Dimensions(upid), cookie);
- actual_row.name = name_id;
- actual_row.display_frame_token = token;
- actual_row.upid = upid;
- actual_row.on_time_finish = event.on_time_finish();
- actual_row.gpu_composition = event.gpu_composition();
// parse present type
StringId present_type = present_type_ids_[0];
@@ -273,25 +259,22 @@
ValidatePresentType(context_, event.present_type())) {
present_type = present_type_ids_[static_cast<size_t>(event.present_type())];
}
- actual_row.present_type = present_type;
// parse jank type
StringId jank_type = JankTypeBitmaskToStringId(context_, event.jank_type());
- actual_row.jank_type = jank_type;
// parse jank severity type
+ StringId jank_severity_type;
if (event.has_jank_severity_type()) {
- actual_row.jank_severity_type = jank_severity_type_ids_[static_cast<size_t>(
+ jank_severity_type = jank_severity_type_ids_[static_cast<size_t>(
event.jank_severity_type())];
} else {
// NOTE: Older traces don't have this field. If JANK_NONE use
// |severity_type| "None", and is not present, use "Unknown".
- actual_row.jank_severity_type =
- (event.jank_type() == FrameTimelineEvent::JANK_NONE)
- ? jank_severity_type_ids_[1] /* None */
- : jank_severity_type_ids_[0]; /* Unknown */
+ jank_severity_type = (event.jank_type() == FrameTimelineEvent::JANK_NONE)
+ ? jank_severity_type_ids_[1] /* None */
+ : jank_severity_type_ids_[0]; /* Unknown */
}
- StringId jank_severity_type = actual_row.jank_severity_type;
// parse prediction type
StringId prediction_type = prediction_type_ids_[0];
@@ -300,23 +283,21 @@
prediction_type =
prediction_type_ids_[static_cast<size_t>(event.prediction_type())];
}
- actual_row.prediction_type = prediction_type;
+ StringId jank_tag;
if (DisplayFrameJanky(event.jank_type())) {
- actual_row.jank_tag = jank_tag_self_id_;
+ jank_tag = jank_tag_self_id_;
} else if (event.jank_type() == FrameTimelineEvent::JANK_SF_STUFFING) {
- actual_row.jank_tag = jank_tag_sf_stuffing_id_;
+ jank_tag = jank_tag_sf_stuffing_id_;
} else if (event.jank_type() == FrameTimelineEvent::JANK_DROPPED) {
- actual_row.jank_tag = jank_tag_dropped_id_;
+ jank_tag = jank_tag_dropped_id_;
} else {
- actual_row.jank_tag = jank_tag_none_id_;
+ jank_tag = jank_tag_none_id_;
}
- std::optional<SliceId> opt_slice_id = context_->slice_tracker->BeginTyped(
- context_->storage->mutable_actual_frame_timeline_slice_table(),
- actual_row,
- [this, token, jank_type, jank_severity_type, present_type,
- prediction_type, &event](ArgsTracker::BoundInserter* inserter) {
+ std::optional<SliceId> opt_slice_id = context_->slice_tracker->Begin(
+ timestamp, track_id, kNullStringId, name_id,
+ [&](ArgsTracker::BoundInserter* inserter) {
inserter->AddArg(display_frame_token_id_, Variadic::Integer(token));
inserter->AddArg(present_type_id_, Variadic::String(present_type));
inserter->AddArg(on_time_finish_id_,
@@ -328,6 +309,7 @@
Variadic::String(jank_severity_type));
inserter->AddArg(prediction_type_id_,
Variadic::String(prediction_type));
+ inserter->AddArg(jank_tag_id_, Variadic::String(jank_tag));
});
// SurfaceFrames will always be parsed before the matching DisplayFrame
@@ -385,21 +367,14 @@
StringId name_id =
context_->storage->InternString(base::StringView(std::to_string(token)));
- tables::ExpectedFrameTimelineSliceTable::Row expected_row;
- expected_row.ts = timestamp;
- expected_row.track_id = context_->track_compressor->InternBegin(
+ TrackId track_id = context_->track_compressor->InternBegin(
kExpectedBlueprint, tracks::Dimensions(upid), cookie);
- expected_row.name = name_id;
-
- expected_row.surface_frame_token = token;
- expected_row.display_frame_token = display_frame_token;
- expected_row.upid = upid;
- expected_row.layer_name = layer_name_id;
- context_->slice_tracker->BeginTyped(
- context_->storage->mutable_expected_frame_timeline_slice_table(),
- expected_row,
- [this, token, layer_name_id](ArgsTracker::BoundInserter* inserter) {
- inserter->AddArg(display_frame_token_id_, Variadic::Integer(token));
+ context_->slice_tracker->Begin(
+ timestamp, track_id, kNullStringId, name_id,
+ [&](ArgsTracker::BoundInserter* inserter) {
+ inserter->AddArg(surface_frame_token_id_, Variadic::Integer(token));
+ inserter->AddArg(display_frame_token_id_,
+ Variadic::Integer(display_frame_token));
inserter->AddArg(layer_name_id_, Variadic::String(layer_name_id));
});
}
@@ -429,17 +404,8 @@
StringId name_id =
context_->storage->InternString(base::StringView(std::to_string(token)));
- tables::ActualFrameTimelineSliceTable::Row actual_row;
- actual_row.ts = timestamp;
- actual_row.track_id = context_->track_compressor->InternBegin(
+ TrackId track_id = context_->track_compressor->InternBegin(
kActualBlueprint, tracks::Dimensions(upid), cookie);
- actual_row.name = name_id;
- actual_row.surface_frame_token = token;
- actual_row.display_frame_token = display_frame_token;
- actual_row.upid = upid;
- actual_row.layer_name = layer_name_id;
- actual_row.on_time_finish = event.on_time_finish();
- actual_row.gpu_composition = event.gpu_composition();
// parse present type
StringId present_type = present_type_ids_[0];
@@ -449,25 +415,22 @@
present_type_validated = true;
present_type = present_type_ids_[static_cast<size_t>(event.present_type())];
}
- actual_row.present_type = present_type;
// parse jank type
StringId jank_type = JankTypeBitmaskToStringId(context_, event.jank_type());
- actual_row.jank_type = jank_type;
// parse jank severity type
+ StringId jank_severity_type;
if (event.has_jank_severity_type()) {
- actual_row.jank_severity_type = jank_severity_type_ids_[static_cast<size_t>(
+ jank_severity_type = jank_severity_type_ids_[static_cast<size_t>(
event.jank_severity_type())];
} else {
// NOTE: Older traces don't have this field. If JANK_NONE use
// |severity_type| "None", and is not present, use "Unknown".
- actual_row.jank_severity_type =
- (event.jank_type() == FrameTimelineEvent::JANK_NONE)
- ? jank_severity_type_ids_[1] /* None */
- : jank_severity_type_ids_[0]; /* Unknown */
+ jank_severity_type = (event.jank_type() == FrameTimelineEvent::JANK_NONE)
+ ? jank_severity_type_ids_[1] /* None */
+ : jank_severity_type_ids_[0]; /* Unknown */
}
- StringId jank_severity_type = actual_row.jank_severity_type;
// parse prediction type
StringId prediction_type = prediction_type_ids_[0];
@@ -476,34 +439,32 @@
prediction_type =
prediction_type_ids_[static_cast<size_t>(event.prediction_type())];
}
- actual_row.prediction_type = prediction_type;
+ StringId jank_tag;
if (SurfaceFrameJanky(event.jank_type())) {
- actual_row.jank_tag = jank_tag_self_id_;
+ jank_tag = jank_tag_self_id_;
} else if (DisplayFrameJanky(event.jank_type())) {
- actual_row.jank_tag = jank_tag_other_id_;
+ jank_tag = jank_tag_other_id_;
} else if (event.jank_type() == FrameTimelineEvent::JANK_BUFFER_STUFFING) {
- actual_row.jank_tag = jank_tag_buffer_stuffing_id_;
+ jank_tag = jank_tag_buffer_stuffing_id_;
} else if (present_type_validated &&
event.present_type() == FrameTimelineEvent::PRESENT_DROPPED) {
- actual_row.jank_tag = jank_tag_dropped_id_;
+ jank_tag = jank_tag_dropped_id_;
} else {
- actual_row.jank_tag = jank_tag_none_id_;
+ jank_tag = jank_tag_none_id_;
}
StringId is_buffer = context_->storage->InternString("Unspecified");
if (event.has_is_buffer()) {
- if (event.is_buffer())
+ if (event.is_buffer()) {
is_buffer = context_->storage->InternString("Yes");
- else
+ } else {
is_buffer = context_->storage->InternString("No");
+ }
}
- std::optional<SliceId> opt_slice_id = context_->slice_tracker->BeginTyped(
- context_->storage->mutable_actual_frame_timeline_slice_table(),
- actual_row,
- [this, jank_type, jank_severity_type, present_type, token, layer_name_id,
- display_frame_token, prediction_type, is_buffer,
- &event](ArgsTracker::BoundInserter* inserter) {
+ std::optional<SliceId> opt_slice_id = context_->slice_tracker->Begin(
+ timestamp, track_id, kNullStringId, name_id,
+ [&](ArgsTracker::BoundInserter* inserter) {
inserter->AddArg(surface_frame_token_id_, Variadic::Integer(token));
inserter->AddArg(display_frame_token_id_,
Variadic::Integer(display_frame_token));
@@ -518,6 +479,7 @@
Variadic::String(jank_severity_type));
inserter->AddArg(prediction_type_id_,
Variadic::String(prediction_type));
+ inserter->AddArg(jank_tag_id_, Variadic::String(jank_tag));
inserter->AddArg(is_buffer_id_, Variadic::String(is_buffer));
});
diff --git a/src/trace_processor/importers/proto/frame_timeline_event_parser.h b/src/trace_processor/importers/proto/frame_timeline_event_parser.h
index 30ab79b..f52508d 100644
--- a/src/trace_processor/importers/proto/frame_timeline_event_parser.h
+++ b/src/trace_processor/importers/proto/frame_timeline_event_parser.h
@@ -74,23 +74,24 @@
std::array<StringId, 4> prediction_type_ids_;
std::array<StringId, 4> jank_severity_type_ids_;
- StringId surface_frame_token_id_;
- StringId display_frame_token_id_;
- StringId present_type_id_;
- StringId on_time_finish_id_;
- StringId gpu_composition_id_;
- StringId jank_type_id_;
- StringId jank_severity_type_id_;
- StringId layer_name_id_;
- StringId prediction_type_id_;
- StringId is_buffer_id_;
+ const StringId surface_frame_token_id_;
+ const StringId display_frame_token_id_;
+ const StringId present_type_id_;
+ const StringId on_time_finish_id_;
+ const StringId gpu_composition_id_;
+ const StringId jank_type_id_;
+ const StringId jank_severity_type_id_;
+ const StringId layer_name_id_;
+ const StringId prediction_type_id_;
+ const StringId jank_tag_id_;
+ const StringId is_buffer_id_;
- StringId jank_tag_none_id_;
- StringId jank_tag_self_id_;
- StringId jank_tag_other_id_;
- StringId jank_tag_dropped_id_;
- StringId jank_tag_buffer_stuffing_id_;
- StringId jank_tag_sf_stuffing_id_;
+ const StringId jank_tag_none_id_;
+ const StringId jank_tag_self_id_;
+ const StringId jank_tag_other_id_;
+ const StringId jank_tag_dropped_id_;
+ const StringId jank_tag_buffer_stuffing_id_;
+ const StringId jank_tag_sf_stuffing_id_;
// upid -> set of tokens map. The expected timeline is the same for a given
// token no matter how many times its seen. We can safely ignore duplicates
diff --git a/src/trace_processor/importers/proto/gpu_event_parser.cc b/src/trace_processor/importers/proto/gpu_event_parser.cc
index b725ae6..2070e53 100644
--- a/src/trace_processor/importers/proto/gpu_event_parser.cc
+++ b/src/trace_processor/importers/proto/gpu_event_parser.cc
@@ -20,7 +20,6 @@
#include <cinttypes>
#include <cstddef>
#include <cstdint>
-#include <limits>
#include <optional>
#include <string>
#include <vector>
@@ -43,7 +42,6 @@
#include "src/trace_processor/storage/stats.h"
#include "src/trace_processor/storage/trace_storage.h"
#include "src/trace_processor/tables/profiler_tables_py.h"
-#include "src/trace_processor/tables/slice_tables_py.h"
#include "src/trace_processor/tables/track_tables_py.h"
#include "src/trace_processor/types/trace_processor_context.h"
#include "src/trace_processor/types/variadic.h"
@@ -121,21 +119,27 @@
tracks::StringDimensionBlueprint("hwqueue_name")),
tracks::DynamicNameBlueprint());
-constexpr auto kVulkanEventsBlueprint =
- tracks::SliceBlueprint("vulkan_events",
- tracks::DimensionBlueprints(),
- tracks::StaticNameBlueprint("Vulkan Events"));
-
-constexpr auto kGpuLogBlueprint =
- tracks::SliceBlueprint("gpu_log",
- tracks::DimensionBlueprints(),
- tracks::StaticNameBlueprint("GPU Log"));
-
} // anonymous namespace
GpuEventParser::GpuEventParser(TraceProcessorContext* context)
: context_(context),
vulkan_memory_tracker_(context),
+ context_id_id_(context->storage->InternString("context_id")),
+ render_target_id_(context->storage->InternString("render_target")),
+ render_target_name_id_(
+ context->storage->InternString("render_target_name")),
+ render_pass_id_(context->storage->InternString("render_pass")),
+ render_pass_name_id_(context->storage->InternString("render_pass_name")),
+ render_subpasses_id_(context->storage->InternString("render_subpasses")),
+ command_buffer_id_(context->storage->InternString("command_buffer")),
+ command_buffer_name_id_(
+ context->storage->InternString("command_buffer_name")),
+ frame_id_id_(context->storage->InternString("frame_id")),
+ submission_id_id_(context->storage->InternString("submission_id")),
+ hw_queue_id_id_(context->storage->InternString("hw_queue_id")),
+ upid_id_(context->storage->InternString("upid")),
+ pid_id_(context_->storage->InternString("pid")),
+ tid_id_(context_->storage->InternString("tid")),
description_id_(context->storage->InternString("description")),
tag_id_(context_->storage->InternString("tag")),
log_message_id_(context->storage->InternString("message")),
@@ -385,37 +389,6 @@
}
}
- auto args_callback = [this, &event,
- sequence_state](ArgsTracker::BoundInserter* inserter) {
- if (event.has_stage_iid()) {
- size_t stage_iid = static_cast<size_t>(event.stage_iid());
- auto* decoder = sequence_state->LookupInternedMessage<
- protos::pbzero::InternedData::kGpuSpecificationsFieldNumber,
- protos::pbzero::InternedGpuRenderStageSpecification>(stage_iid);
- if (decoder) {
- // TODO: Add RenderStageCategory to gpu_slice table.
- inserter->AddArg(description_id_,
- Variadic::String(context_->storage->InternString(
- decoder->description())));
- }
- } else if (event.has_stage_id()) {
- size_t stage_id = static_cast<size_t>(event.stage_id());
- if (stage_id < gpu_render_stage_ids_.size()) {
- auto description = gpu_render_stage_ids_[stage_id].second;
- if (description != kNullStringId) {
- inserter->AddArg(description_id_, Variadic::String(description));
- }
- }
- }
- for (auto it = event.extra_data(); it; ++it) {
- protos::pbzero::GpuRenderStageEvent_ExtraData_Decoder datum(*it);
- StringId name_id = context_->storage->InternString(datum.name());
- StringId value = context_->storage->InternString(
- datum.has_value() ? datum.value() : base::StringView());
- inserter->AddArg(name_id, Variadic::String(value));
- }
- };
-
if (event.has_event_id()) {
TrackId track_id;
uint64_t hw_queue_id = 0;
@@ -428,7 +401,7 @@
// Skip
return;
}
- // TODO: Add RenderStageCategory to gpu_track table.
+ // TODO: Add RenderStageCategory to track table.
track_id = context_->track_tracker->InternTrack(
kRenderStageBlueprint,
tracks::Dimensions("iid", hw_queue_id, decoder->name()),
@@ -484,28 +457,78 @@
? context_->storage->InternString(
command_buffer_name.value().c_str())
: kNullStringId;
+ StringId name_id;
+ if (event.has_submission_id()) {
+ name_id = context_->storage->InternString(
+ std::to_string(event.submission_id()).c_str());
+ } else {
+ name_id = GetFullStageName(sequence_state, event);
+ }
+ context_->slice_tracker->Scoped(
+ ts, track_id, kNullStringId, name_id,
+ static_cast<int64_t>(event.duration()),
+ [&](ArgsTracker::BoundInserter* inserter) {
+ if (event.has_stage_iid()) {
+ auto stage_iid = static_cast<size_t>(event.stage_iid());
+ auto* decoder = sequence_state->LookupInternedMessage<
+ protos::pbzero::InternedData::kGpuSpecificationsFieldNumber,
+ protos::pbzero::InternedGpuRenderStageSpecification>(stage_iid);
+ if (decoder) {
+ // TODO: Add RenderStageCategory to gpu_slice table.
+ inserter->AddArg(description_id_,
+ Variadic::String(context_->storage->InternString(
+ decoder->description())));
+ }
+ } else if (event.has_stage_id()) {
+ size_t stage_id = static_cast<size_t>(event.stage_id());
+ if (stage_id < gpu_render_stage_ids_.size()) {
+ auto description = gpu_render_stage_ids_[stage_id].second;
+ if (description != kNullStringId) {
+ inserter->AddArg(description_id_,
+ Variadic::String(description));
+ }
+ }
+ }
+ for (auto it = event.extra_data(); it; ++it) {
+ protos::pbzero::GpuRenderStageEvent_ExtraData_Decoder datum(*it);
+ StringId name_id = context_->storage->InternString(datum.name());
+ StringId value = context_->storage->InternString(
+ datum.has_value() ? datum.value() : base::StringView());
+ inserter->AddArg(name_id, Variadic::String(value));
+ }
- tables::GpuSliceTable::Row row;
- row.ts = ts;
- row.track_id = track_id;
- row.name = GetFullStageName(sequence_state, event);
- row.dur = static_cast<int64_t>(event.duration());
- // TODO: Create table for graphics context and lookup
- // InternedGraphicsContext.
- row.context_id = static_cast<int64_t>(event.context());
- row.render_target = static_cast<int64_t>(event.render_target_handle());
- row.render_target_name = render_target_name_id;
- row.render_pass = static_cast<int64_t>(event.render_pass_handle());
- row.render_pass_name = render_pass_name_id;
- row.render_subpasses = ParseRenderSubpasses(event);
- row.command_buffer = static_cast<int64_t>(event.command_buffer_handle());
- row.command_buffer_name = command_buffer_name_id;
- row.submission_id = event.submission_id();
- row.hw_queue_id = static_cast<int64_t>(hw_queue_id);
- row.upid = context_->process_tracker->GetOrCreateProcess(
- static_cast<uint32_t>(pid));
- context_->slice_tracker->ScopedTyped(
- context_->storage->mutable_gpu_slice_table(), row, args_callback);
+ // TODO: Create table for graphics context and lookup
+ // InternedGraphicsContext.
+ inserter->AddArg(
+ context_id_id_,
+ Variadic::Integer(static_cast<int64_t>(event.context())));
+ inserter->AddArg(render_target_id_,
+ Variadic::Integer(static_cast<int64_t>(
+ event.render_target_handle())));
+ inserter->AddArg(render_target_name_id_,
+ Variadic::String(render_target_name_id));
+ inserter->AddArg(render_pass_id_,
+ Variadic::Integer(static_cast<int64_t>(
+ event.render_pass_handle())));
+ inserter->AddArg(render_pass_name_id_,
+ Variadic::String(render_pass_name_id));
+ inserter->AddArg(render_subpasses_id_,
+ Variadic::String(ParseRenderSubpasses(event)));
+ inserter->AddArg(command_buffer_id_,
+ Variadic::Integer(static_cast<int64_t>(
+ event.command_buffer_handle())));
+ inserter->AddArg(command_buffer_name_id_,
+ Variadic::String(command_buffer_name_id));
+ inserter->AddArg(submission_id_id_,
+ Variadic::Integer(event.submission_id()));
+ inserter->AddArg(
+ hw_queue_id_id_,
+ Variadic::Integer(static_cast<int64_t>(hw_queue_id)));
+ inserter->AddArg(
+ upid_id_,
+ Variadic::Integer(context_->process_tracker->GetOrCreateProcess(
+ static_cast<uint32_t>(pid))));
+ });
}
}
@@ -723,20 +746,17 @@
void GpuEventParser::ParseGpuLog(int64_t ts, ConstBytes blob) {
protos::pbzero::GpuLog::Decoder event(blob);
+ static constexpr auto kGpuLogBlueprint =
+ tracks::SliceBlueprint("gpu_log", tracks::DimensionBlueprints(),
+ tracks::StaticNameBlueprint("GPU Log"));
TrackId track_id = context_->track_tracker->InternTrack(kGpuLogBlueprint);
auto severity = static_cast<size_t>(event.severity());
StringId severity_id =
severity < log_severity_ids_.size()
? log_severity_ids_[static_cast<size_t>(event.severity())]
: log_severity_ids_[log_severity_ids_.size() - 1];
-
- tables::GpuSliceTable::Row row;
- row.ts = ts;
- row.track_id = track_id;
- row.name = severity_id;
- row.dur = 0;
- context_->slice_tracker->ScopedTyped(
- context_->storage->mutable_gpu_slice_table(), row,
+ context_->slice_tracker->Scoped(
+ ts, track_id, kNullStringId, severity_id, 0,
[this, &event](ArgsTracker::BoundInserter* inserter) {
if (event.has_tag()) {
inserter->AddArg(
@@ -759,32 +779,33 @@
debug_marker_names_[event.object_type()][event.object()] =
event.object_name().ToStdString();
}
- if (vk_event.has_vk_queue_submit()) {
- protos::pbzero::VulkanApiEvent_VkQueueSubmit::Decoder event(
- vk_event.vk_queue_submit());
- // Once flow table is implemented, we can create a nice UI that link the
- // vkQueueSubmit to GpuRenderStageEvent. For now, just add it as in a GPU
- // track so that they can appear close to the render stage slices.
- TrackId track_id =
- context_->track_tracker->InternTrack(kVulkanEventsBlueprint);
- tables::GpuSliceTable::Row row;
- row.ts = ts;
- row.dur = static_cast<int64_t>(event.duration_ns());
- row.track_id = track_id;
- row.name = vk_queue_submit_id_;
- if (event.has_vk_command_buffers()) {
- row.command_buffer = static_cast<int64_t>(*event.vk_command_buffers());
- }
- row.submission_id = event.submission_id();
- auto args_callback = [this, &event](ArgsTracker::BoundInserter* inserter) {
- inserter->AddArg(context_->storage->InternString("pid"),
- Variadic::Integer(event.pid()));
- inserter->AddArg(context_->storage->InternString("tid"),
- Variadic::Integer(event.tid()));
- };
- context_->slice_tracker->ScopedTyped(
- context_->storage->mutable_gpu_slice_table(), row, args_callback);
+ if (!vk_event.has_vk_queue_submit()) {
+ return;
}
+ protos::pbzero::VulkanApiEvent_VkQueueSubmit::Decoder event(
+ vk_event.vk_queue_submit());
+ // Once flow table is implemented, we can create a nice UI that link the
+ // vkQueueSubmit to GpuRenderStageEvent. For now, just add it as in a GPU
+ // track so that they can appear close to the render stage slices.
+ static constexpr auto kVulkanEventsBlueprint =
+ tracks::SliceBlueprint("vulkan_events", tracks::DimensionBlueprints(),
+ tracks::StaticNameBlueprint("Vulkan Events"));
+ TrackId track_id =
+ context_->track_tracker->InternTrack(kVulkanEventsBlueprint);
+ context_->slice_tracker->Scoped(
+ ts, track_id, kNullStringId, vk_queue_submit_id_,
+ static_cast<int64_t>(event.duration_ns()),
+ [this, &event](ArgsTracker::BoundInserter* inserter) {
+ inserter->AddArg(pid_id_, Variadic::Integer(event.pid()));
+ inserter->AddArg(tid_id_, Variadic::Integer(event.tid()));
+ if (event.has_vk_command_buffers()) {
+ inserter->AddArg(command_buffer_id_,
+ Variadic::Integer(static_cast<int64_t>(
+ *event.vk_command_buffers())));
+ }
+ inserter->AddArg(submission_id_id_,
+ Variadic::Integer(event.submission_id()));
+ });
}
void GpuEventParser::ParseGpuMemTotalEvent(int64_t ts, ConstBytes blob) {
diff --git a/src/trace_processor/importers/proto/gpu_event_parser.h b/src/trace_processor/importers/proto/gpu_event_parser.h
index 11bcb76..324b066 100644
--- a/src/trace_processor/importers/proto/gpu_event_parser.h
+++ b/src/trace_processor/importers/proto/gpu_event_parser.h
@@ -91,13 +91,32 @@
TraceProcessorContext* const context_;
VulkanMemoryTracker vulkan_memory_tracker_;
+
+ const StringId context_id_id_;
+ const StringId render_target_id_;
+ const StringId render_target_name_id_;
+ const StringId render_pass_id_;
+ const StringId render_pass_name_id_;
+ const StringId render_subpasses_id_;
+ const StringId command_buffer_id_;
+ const StringId command_buffer_name_id_;
+ const StringId frame_id_id_;
+ const StringId submission_id_id_;
+ const StringId hw_queue_id_id_;
+ const StringId upid_id_;
+ const StringId pid_id_;
+ const StringId tid_id_;
+
// For GpuCounterEvent
std::unordered_map<uint32_t, TrackId> gpu_counter_track_ids_;
+
// For GpuRenderStageEvent
const StringId description_id_;
std::vector<std::optional<TrackId>> gpu_hw_queue_ids_;
+
// Map of stage ID -> pair(stage name, stage description)
std::vector<std::pair<StringId, StringId>> gpu_render_stage_ids_;
+
// For VulkanMemoryEvent
std::unordered_map<protos::pbzero::VulkanMemoryEvent::AllocationScope,
int64_t /*counter_value*/,
@@ -107,16 +126,20 @@
vulkan_device_memory_counters_allocate_;
std::unordered_map<uint32_t /*memory_type*/, int64_t /*counter_value*/>
vulkan_device_memory_counters_bind_;
+
// For GpuLog
const StringId tag_id_;
const StringId log_message_id_;
std::array<StringId, 7> log_severity_ids_;
+
// For Vulkan events.
// For VulkanApiEvent.VkDebugUtilsObjectName.
// Map of vk handle -> vk object name.
using DebugMarkerMap = std::unordered_map<uint64_t, std::string>;
+
// Map of VkObjectType -> DebugMarkerMap.
std::unordered_map<int32_t, DebugMarkerMap> debug_marker_names_;
+
// For VulkanApiEvent.VkQueueSubmit.
StringId vk_event_track_id_;
StringId vk_queue_submit_id_;
diff --git a/src/trace_processor/importers/proto/graphics_frame_event_parser.cc b/src/trace_processor/importers/proto/graphics_frame_event_parser.cc
index 8506637..077c8c2 100644
--- a/src/trace_processor/importers/proto/graphics_frame_event_parser.cc
+++ b/src/trace_processor/importers/proto/graphics_frame_event_parser.cc
@@ -20,11 +20,14 @@
#include <cstddef>
#include <cstdint>
#include <optional>
+#include <string>
+#include <utility>
+#include <variant>
#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/string_utils.h"
#include "perfetto/ext/base/string_view.h"
-#include "perfetto/ext/base/string_writer.h"
-#include "perfetto/ext/base/utils.h"
+#include "src/trace_processor/importers/common/args_tracker.h"
#include "src/trace_processor/importers/common/event_tracker.h"
#include "src/trace_processor/importers/common/slice_tracker.h"
#include "src/trace_processor/importers/common/track_tracker.h"
@@ -33,8 +36,8 @@
#include "src/trace_processor/storage/stats.h"
#include "src/trace_processor/storage/trace_storage.h"
#include "src/trace_processor/tables/slice_tables_py.h"
-#include "src/trace_processor/tables/track_tables_py.h"
#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/types/variadic.h"
#include "protos/perfetto/trace/android/graphics_frame_event.pbzero.h"
@@ -59,6 +62,14 @@
unknown_event_name_id_(context->storage->InternString("unknown_event")),
no_layer_name_name_id_(context->storage->InternString("no_layer_name")),
layer_name_key_id_(context->storage->InternString("layer_name")),
+ queue_lost_message_id_(context->storage->InternString(kQueueLostMessage)),
+ frame_number_id_(context->storage->InternString("frame_number")),
+ queue_to_acquire_time_id_(
+ context->storage->InternString("queue_to_acquire_time")),
+ acquire_to_latch_time_id_(
+ context->storage->InternString("acquire_to_latch_time")),
+ latch_to_present_time_id_(
+ context->storage->InternString("latch_to_present_time")),
event_type_name_ids_{
{context->storage->InternString(
"unspecified_event") /* UNSPECIFIED */,
@@ -79,338 +90,7 @@
context->storage->InternString("Modify") /* MODIFY */,
context->storage->InternString("Detach") /* DETACH */,
context->storage->InternString("Attach") /* ATTACH */,
- context->storage->InternString("Cancel") /* CANCEL */}},
- queue_lost_message_id_(
- context->storage->InternString(kQueueLostMessage)) {}
-
-bool GraphicsFrameEventParser::CreateBufferEvent(
- int64_t timestamp,
- GraphicsFrameEventDecoder& event) {
- if (!event.has_buffer_id()) {
- context_->storage->IncrementStats(
- stats::graphics_frame_event_parser_errors);
- PERFETTO_ELOG("GraphicsFrameEvent with missing buffer id field.");
- return false;
- }
-
- // Use buffer id + layer name as key because sometimes the same buffer can be
- // used by different layers.
- char event_key_buffer[4096];
- base::StringWriter event_key_str(event_key_buffer,
- base::ArraySize(event_key_buffer));
- const uint32_t buffer_id = event.buffer_id();
- StringId layer_name_id;
- event_key_str.AppendUnsignedInt(buffer_id);
-
- if (event.has_layer_name()) {
- layer_name_id = context_->storage->InternString(event.layer_name());
- event_key_str.AppendString(base::StringView(event.layer_name()));
- } else {
- layer_name_id = no_layer_name_name_id_;
- }
- StringId event_key =
- context_->storage->InternString(event_key_str.GetStringView());
-
- StringId event_name_id = unknown_event_name_id_;
- if (event.has_type()) {
- const auto type = static_cast<size_t>(event.type());
- if (type < event_type_name_ids_.size()) {
- event_name_id = event_type_name_ids_[type];
- graphics_frame_stats_map_[event_key][type] = timestamp;
- } else {
- context_->storage->IncrementStats(
- stats::graphics_frame_event_parser_errors);
- PERFETTO_ELOG("GraphicsFrameEvent with unknown type %zu.", type);
- }
- } else {
- context_->storage->IncrementStats(
- stats::graphics_frame_event_parser_errors);
- PERFETTO_ELOG("GraphicsFrameEvent with missing type field.");
- }
-
- char buffer[4096];
- base::StringWriter track_name(buffer, base::ArraySize(buffer));
- track_name.AppendLiteral("Buffer: ");
- track_name.AppendUnsignedInt(buffer_id);
- track_name.AppendLiteral(" ");
- track_name.AppendString(base::StringView(event.layer_name()));
-
- const int64_t duration =
- event.has_duration_ns() ? static_cast<int64_t>(event.duration_ns()) : 0;
- uint32_t frame_number = event.has_frame_number() ? event.frame_number() : 0;
-
- TrackId track_id = context_->track_tracker->InternTrack(
- kGraphicFrameEventBlueprint,
- tracks::Dimensions(track_name.GetStringView()),
- tracks::DynamicName(
- context_->storage->InternString(track_name.GetStringView())));
-
- auto* graphics_frame_slice_table =
- context_->storage->mutable_graphics_frame_slice_table();
- {
- tables::GraphicsFrameSliceTable::Row row;
- row.ts = timestamp;
- row.track_id = track_id;
- row.name = event_name_id;
- row.dur = duration;
- row.frame_number = frame_number;
- row.layer_name = layer_name_id;
- if (event.type() == GraphicsFrameEvent::PRESENT_FENCE) {
- auto acquire_ts =
- graphics_frame_stats_map_[event_key]
- [GraphicsFrameEvent::ACQUIRE_FENCE];
- auto queue_ts =
- graphics_frame_stats_map_[event_key][GraphicsFrameEvent::QUEUE];
- auto latch_ts =
- graphics_frame_stats_map_[event_key][GraphicsFrameEvent::LATCH];
-
- row.queue_to_acquire_time =
- std::max(acquire_ts - queue_ts, static_cast<int64_t>(0));
- row.acquire_to_latch_time = latch_ts - acquire_ts;
- row.latch_to_present_time = timestamp - latch_ts;
- }
- std::optional<SliceId> opt_slice_id =
- context_->slice_tracker->ScopedTyped(graphics_frame_slice_table, row);
- if (event.type() == GraphicsFrameEvent::DEQUEUE) {
- if (opt_slice_id) {
- dequeue_slice_ids_[event_key] = *opt_slice_id;
- }
- } else if (event.type() == GraphicsFrameEvent::QUEUE) {
- auto it = dequeue_slice_ids_.find(event_key);
- if (it != dequeue_slice_ids_.end()) {
- auto rr = graphics_frame_slice_table->FindById(it->second);
- rr->set_frame_number(frame_number);
- }
- }
- }
- return true;
-}
-
-void GraphicsFrameEventParser::InvalidatePhaseEvent(int64_t timestamp,
- TrackId track_id,
- bool reset_name) {
- const auto opt_slice_id = context_->slice_tracker->End(timestamp, track_id);
-
- if (opt_slice_id) {
- auto* graphics_frame_slice_table =
- context_->storage->mutable_graphics_frame_slice_table();
- auto rr = *graphics_frame_slice_table->FindById(*opt_slice_id);
- if (reset_name) {
- // Set the name (frame_number) to be 0 since there is no frame number
- // associated, example : dequeue event.
- StringId frame_name_id = context_->storage->InternString("0");
- rr.set_name(frame_name_id);
- rr.set_frame_number(0);
- }
-
- // Set the duration to -1 so that this slice will be ignored by the
- // UI. Setting any other duration results in wrong data which we want
- // to avoid at all costs.
- rr.set_dur(-1);
- }
-}
-
-// Here we convert the buffer events into Phases(slices)
-// APP: Dequeue to Queue
-// Wait for GPU: Queue to Acquire
-// SurfaceFlinger (SF): Latch to Present
-// Display: Present to next Present (of the same layer)
-void GraphicsFrameEventParser::CreatePhaseEvent(
- int64_t timestamp,
- GraphicsFrameEventDecoder& event) {
- // Use buffer id + layer name as key because sometimes the same buffer can be
- // used by different layers.
- char event_key_buffer[4096];
- base::StringWriter event_key_str(event_key_buffer,
- base::ArraySize(event_key_buffer));
- const uint32_t buffer_id = event.buffer_id();
- uint32_t frame_number = event.has_frame_number() ? event.frame_number() : 0;
- event_key_str.AppendUnsignedInt(buffer_id);
- StringId layer_name_id;
- if (event.has_layer_name()) {
- layer_name_id = context_->storage->InternString(event.layer_name());
- event_key_str.AppendString(base::StringView(event.layer_name()));
- } else {
- layer_name_id = no_layer_name_name_id_;
- }
- StringId event_key =
- context_->storage->InternString(event_key_str.GetStringView());
-
- char track_buffer[4096];
- char slice_buffer[4096];
- // We'll be using the name StringWriter and name_id for writing track names
- // and slice names.
- base::StringWriter track_name(track_buffer, base::ArraySize(track_buffer));
- base::StringWriter slice_name(slice_buffer, base::ArraySize(slice_buffer));
- StringId track_name_id;
- TrackId track_id;
- bool start_slice = true;
-
- // Close the previous phase before starting the new phase
- switch (event.type()) {
- case GraphicsFrameEvent::DEQUEUE: {
- track_name.reset();
- track_name.AppendLiteral("APP_");
- track_name.AppendUnsignedInt(buffer_id);
- track_name.AppendLiteral(" ");
- track_name.AppendString(base::StringView(event.layer_name()));
- track_name_id =
- context_->storage->InternString(track_name.GetStringView());
-
- track_id = context_->track_tracker->InternTrack(
- kGraphicFrameEventBlueprint,
- tracks::Dimensions(track_name.GetStringView()),
- tracks::DynamicName(
- context_->storage->InternString(track_name.GetStringView())));
-
- // Error handling
- auto dequeue_time = dequeue_map_.find(event_key);
- if (dequeue_time != dequeue_map_.end()) {
- InvalidatePhaseEvent(timestamp, dequeue_time->second, true);
- dequeue_map_.erase(dequeue_time);
- }
- auto queue_time = queue_map_.find(event_key);
- if (queue_time != queue_map_.end()) {
- InvalidatePhaseEvent(timestamp, queue_time->second);
- queue_map_.erase(queue_time);
- }
-
- dequeue_map_[event_key] = track_id;
- last_dequeued_[event_key] = timestamp;
- break;
- }
-
- case GraphicsFrameEvent::QUEUE: {
- auto dequeue_time = dequeue_map_.find(event_key);
- if (dequeue_time != dequeue_map_.end()) {
- const auto opt_slice_id =
- context_->slice_tracker->End(timestamp, dequeue_time->second);
- slice_name.reset();
- slice_name.AppendUnsignedInt(frame_number);
- if (opt_slice_id) {
- auto* graphics_frame_slice_table =
- context_->storage->mutable_graphics_frame_slice_table();
- // Set the name of the slice to be the frame number since dequeue did
- // not have a frame number at that time.
- auto rr = *graphics_frame_slice_table->FindById(*opt_slice_id);
- rr.set_name(
- context_->storage->InternString(slice_name.GetStringView()));
- rr.set_frame_number(frame_number);
- dequeue_map_.erase(dequeue_time);
- }
- }
- // The AcquireFence might be signaled before receiving a QUEUE event
- // sometimes. In that case, we shouldn't start a slice.
- if (last_acquired_[event_key] > last_dequeued_[event_key] &&
- last_acquired_[event_key] < timestamp) {
- start_slice = false;
- break;
- }
- track_name.reset();
- track_name.AppendLiteral("GPU_");
- track_name.AppendUnsignedInt(buffer_id);
- track_name.AppendLiteral(" ");
- track_name.AppendString(base::StringView(event.layer_name()));
- track_name_id =
- context_->storage->InternString(track_name.GetStringView());
-
- track_id = context_->track_tracker->InternTrack(
- kGraphicFrameEventBlueprint,
- tracks::Dimensions(track_name.GetStringView()),
- tracks::DynamicName(
- context_->storage->InternString(track_name.GetStringView())));
- queue_map_[event_key] = track_id;
- break;
- }
- case GraphicsFrameEvent::ACQUIRE_FENCE: {
- auto queue_time = queue_map_.find(event_key);
- if (queue_time != queue_map_.end()) {
- context_->slice_tracker->End(timestamp, queue_time->second);
- queue_map_.erase(queue_time);
- }
- last_acquired_[event_key] = timestamp;
- start_slice = false;
- break;
- }
- case GraphicsFrameEvent::LATCH: {
- // b/157578286 - Sometimes Queue event goes missing. To prevent having a
- // wrong slice info, we try to close any existing APP slice.
- auto dequeue_time = dequeue_map_.find(event_key);
- if (dequeue_time != dequeue_map_.end()) {
- InvalidatePhaseEvent(timestamp, dequeue_time->second, true);
- dequeue_map_.erase(dequeue_time);
- }
- track_name.reset();
- track_name.AppendLiteral("SF_");
- track_name.AppendUnsignedInt(buffer_id);
- track_name.AppendLiteral(" ");
- track_name.AppendString(base::StringView(event.layer_name()));
- track_name_id =
- context_->storage->InternString(track_name.GetStringView());
-
- track_id = context_->track_tracker->InternTrack(
- kGraphicFrameEventBlueprint,
- tracks::Dimensions(track_name.GetStringView()),
- tracks::DynamicName(
- context_->storage->InternString(track_name.GetStringView())));
- latch_map_[event_key] = track_id;
- break;
- }
-
- case GraphicsFrameEvent::PRESENT_FENCE: {
- auto latch_time = latch_map_.find(event_key);
- if (latch_time != latch_map_.end()) {
- context_->slice_tracker->End(timestamp, latch_time->second);
- latch_map_.erase(latch_time);
- }
- auto display_time = display_map_.find(layer_name_id);
- if (display_time != display_map_.end()) {
- context_->slice_tracker->End(timestamp, display_time->second);
- display_map_.erase(display_time);
- }
- base::StringView layerName(event.layer_name());
- track_name.reset();
- track_name.AppendLiteral("Display_");
- track_name.AppendString(layerName.substr(0, 10));
- track_name_id =
- context_->storage->InternString(track_name.GetStringView());
-
- track_id = context_->track_tracker->InternTrack(
- kGraphicFrameEventBlueprint,
- tracks::Dimensions(track_name.GetStringView()),
- tracks::DynamicName(
- context_->storage->InternString(track_name.GetStringView())));
- display_map_[layer_name_id] = track_id;
- break;
- }
-
- default:
- start_slice = false;
- }
-
- // Start the new phase if needed.
- if (start_slice) {
- tables::GraphicsFrameSliceTable::Row slice;
- slice.ts = timestamp;
- slice.track_id = track_id;
- slice.layer_name = layer_name_id;
- slice_name.reset();
- // If the frame_number is known, set it as the name of the slice.
- // If not known (DEQUEUE), set the name as the timestamp.
- // Timestamp is chosen here because the stack_id is hashed based on the name
- // of the slice. To not have any conflicting stack_id with any of the
- // existing slices, we use timestamp as the temporary name.
- if (frame_number != 0) {
- slice_name.AppendUnsignedInt(frame_number);
- } else {
- slice_name.AppendInt(timestamp);
- }
- slice.name = context_->storage->InternString(slice_name.GetStringView());
- slice.frame_number = frame_number;
- context_->slice_tracker->BeginTyped(
- context_->storage->mutable_graphics_frame_slice_table(), slice);
- }
-}
+ context->storage->InternString("Cancel") /* CANCEL */}} {}
void GraphicsFrameEventParser::ParseGraphicsFrameEvent(int64_t timestamp,
ConstBytes blob) {
@@ -421,10 +101,267 @@
protos::pbzero::GraphicsFrameEvent::BufferEvent::Decoder event(
frame_event.buffer_event());
- if (CreateBufferEvent(timestamp, event)) {
- // Create a phase event only if the buffer event finishes successfully
- CreatePhaseEvent(timestamp, event);
+ if (!event.has_buffer_id()) {
+ context_->storage->IncrementStats(
+ stats::graphics_frame_event_parser_errors);
+ return;
}
+
+ // Use buffer id + layer name as key because sometimes the same buffer can be
+ // used by different layers.
+ StringId layer_name_id;
+ StringId event_key;
+ if (event.has_layer_name()) {
+ layer_name_id = context_->storage->InternString(event.layer_name());
+ base::StackString<1024> key_str("%u%.*s", event.buffer_id(),
+ int(event.layer_name().size),
+ event.layer_name().data);
+ event_key = context_->storage->InternString(key_str.string_view());
+ } else {
+ layer_name_id = no_layer_name_name_id_;
+ event_key = context_->storage->InternString(
+ base::StackString<1024>("%u", event.buffer_id()).string_view());
+ }
+
+ CreateBufferEvent(timestamp, event, layer_name_id, event_key);
+ CreatePhaseEvent(timestamp, event, layer_name_id, event_key);
+}
+
+void GraphicsFrameEventParser::CreateBufferEvent(
+ int64_t timestamp,
+ const GraphicsFrameEventDecoder& event,
+ StringId layer_name_id,
+ StringId event_key) {
+ auto* it = buffer_event_map_.Insert(event_key, {}).first;
+ switch (event.type()) {
+ case GraphicsFrameEvent::DEQUEUE:
+ break;
+ case GraphicsFrameEvent::ACQUIRE_FENCE:
+ it->acquire_ts = timestamp;
+ break;
+ case GraphicsFrameEvent::QUEUE:
+ it->queue_ts = timestamp;
+ break;
+ case GraphicsFrameEvent::LATCH:
+ it->latch_ts = timestamp;
+ break;
+ default:
+ context_->storage->IncrementStats(
+ stats::graphics_frame_event_parser_errors);
+ PERFETTO_ELOG("GraphicsFrameEvent with unknown type %d.", event.type());
+ break;
+ }
+ bool prev_is_dequeue = it->is_most_recent_dequeue_;
+ it->is_most_recent_dequeue_ =
+ event.type() ==
+ protos::pbzero::GraphicsFrameEvent::BufferEventType::DEQUEUE;
+
+ StringId event_name_id;
+ if (event.has_type() &&
+ static_cast<uint32_t>(event.type()) < event_type_name_ids_.size()) {
+ event_name_id = event_type_name_ids_[static_cast<uint32_t>(event.type())];
+ } else {
+ event_name_id = unknown_event_name_id_;
+ }
+
+ base::StackString<4096> track_name("Buffer: %u %.*s", event.buffer_id(),
+ int(event.layer_name().size),
+ event.layer_name().data);
+ TrackId track_id = context_->track_tracker->InternTrack(
+ kGraphicFrameEventBlueprint, tracks::Dimensions(track_name.string_view()),
+ tracks::DynamicName(
+ context_->storage->InternString(track_name.string_view())));
+
+ // Update the frame number for the previous dequeue event.
+ uint32_t frame_number = event.has_frame_number() ? event.frame_number() : 0;
+ if (event.type() == GraphicsFrameEvent::QUEUE && prev_is_dequeue) {
+ context_->slice_tracker->AddArgs(
+ track_id, kNullStringId, kNullStringId,
+ [&](ArgsTracker::BoundInserter* inserter) {
+ inserter->AddArg(frame_number_id_, Variadic::Integer(frame_number));
+ });
+ }
+
+ const int64_t duration =
+ event.has_duration_ns() ? static_cast<int64_t>(event.duration_ns()) : 0;
+ context_->slice_tracker->Scoped(
+ timestamp, track_id, kNullStringId, event_name_id, duration,
+ [&](ArgsTracker::BoundInserter* inserter) {
+ inserter->AddArg(frame_number_id_, Variadic::Integer(frame_number));
+ inserter->AddArg(layer_name_key_id_, Variadic::String(layer_name_id));
+ inserter->AddArg(
+ queue_to_acquire_time_id_,
+ Variadic::Integer(std::max(it->acquire_ts - it->queue_ts,
+ static_cast<int64_t>(0))));
+ inserter->AddArg(acquire_to_latch_time_id_,
+ Variadic::Integer(it->latch_ts - it->acquire_ts));
+ inserter->AddArg(latch_to_present_time_id_,
+ Variadic::Integer(timestamp - it->latch_ts));
+ });
+}
+
+// Here we convert the buffer events into Phases(slices)
+// APP: Dequeue to Queue
+// Wait for GPU: Queue to Acquire
+// SurfaceFlinger (SF): Latch to Present
+// Display: Present to next Present (of the same layer)
+void GraphicsFrameEventParser::CreatePhaseEvent(
+ int64_t timestamp,
+ const GraphicsFrameEventDecoder& event,
+ StringId layer_name_id,
+ StringId event_key) {
+ auto* slices = context_->storage->mutable_slice_table();
+ auto [it, inserted] = phase_event_map_.Insert(event_key, {});
+ switch (event.type()) {
+ case GraphicsFrameEvent::DEQUEUE: {
+ if (auto* d = std::get_if<DequeueInfo>(&it->most_recent_event)) {
+ // Error handling
+ auto rr = d->slice_row.ToRowReference(slices);
+ rr.set_name(context_->storage->InternString("0"));
+ context_->slice_tracker->AddArgs(
+ rr.track_id(), kNullStringId, kNullStringId,
+ [&](ArgsTracker::BoundInserter* inserter) {
+ inserter->AddArg(frame_number_id_, Variadic::Integer(0));
+ });
+ it->most_recent_event = std::monostate();
+ }
+
+ base::StackString<1024> track_name("APP_%u %.*s", event.buffer_id(),
+ int(event.layer_name().size),
+ event.layer_name().data);
+ TrackId track_id = context_->track_tracker->InternTrack(
+ kGraphicFrameEventBlueprint,
+ tracks::Dimensions(track_name.string_view()),
+ tracks::DynamicName(
+ context_->storage->InternString(track_name.string_view())));
+ auto res = InsertPhaseSlice(timestamp, event, track_id, layer_name_id);
+ if (res) {
+ it->most_recent_event = DequeueInfo{*res, timestamp};
+ }
+ break;
+ }
+ case GraphicsFrameEvent::QUEUE: {
+ if (auto* d = std::get_if<DequeueInfo>(&it->most_recent_event)) {
+ auto slice_rr = d->slice_row.ToRowReference(slices);
+ context_->slice_tracker->End(
+ timestamp, slice_rr.track_id(), kNullStringId, kNullStringId,
+ [&](ArgsTracker::BoundInserter* inserter) {
+ inserter->AddArg(frame_number_id_,
+ Variadic::Integer(event.frame_number()));
+ });
+
+ // Set the name of the slice to be the frame number since dequeue did
+ // not have a frame number at that time.
+ slice_rr.set_name(context_->storage->InternString(
+ std::to_string(event.frame_number())));
+
+ // The AcquireFence might be signaled before receiving a QUEUE event
+ // sometimes. In that case, we shouldn't start a slice.
+ if (it->last_acquire_ts && *it->last_acquire_ts > d->timestamp) {
+ it->most_recent_event = std::monostate();
+ return;
+ }
+ }
+ base::StackString<1024> track_name("GPU_%u %.*s", event.buffer_id(),
+ int(event.layer_name().size),
+ event.layer_name().data);
+ StringId track_name_id =
+ context_->storage->InternString(track_name.string_view());
+ TrackId track_id = context_->track_tracker->InternTrack(
+ kGraphicFrameEventBlueprint,
+ tracks::Dimensions(track_name.string_view()),
+ tracks::DynamicName(track_name_id));
+ InsertPhaseSlice(timestamp, event, track_id, layer_name_id);
+ it->most_recent_event = QueueInfo{track_id};
+ break;
+ }
+ case GraphicsFrameEvent::ACQUIRE_FENCE: {
+ if (auto* q = std::get_if<QueueInfo>(&it->most_recent_event)) {
+ context_->slice_tracker->End(timestamp, q->track);
+ it->most_recent_event = std::monostate();
+ }
+ it->last_acquire_ts = timestamp;
+ break;
+ }
+ case GraphicsFrameEvent::LATCH: {
+ // b/157578286 - Sometimes Queue event goes missing. To prevent having a
+ // wrong slice info, we try to close any existing APP slice.
+ if (auto* d = std::get_if<DequeueInfo>(&it->most_recent_event)) {
+ auto rr = d->slice_row.ToRowReference(slices);
+ rr.set_name(context_->storage->InternString("0"));
+ context_->slice_tracker->AddArgs(
+ rr.track_id(), kNullStringId, kNullStringId,
+ [&](ArgsTracker::BoundInserter* inserter) {
+ inserter->AddArg(frame_number_id_, Variadic::Integer(0));
+ });
+ }
+ base::StackString<1024> track_name("SF_%u %.*s", event.buffer_id(),
+ int(event.layer_name().size),
+ event.layer_name().data);
+ TrackId track_id = context_->track_tracker->InternTrack(
+ kGraphicFrameEventBlueprint,
+ tracks::Dimensions(track_name.string_view()),
+ tracks::DynamicName(
+ context_->storage->InternString(track_name.string_view())));
+ InsertPhaseSlice(timestamp, event, track_id, layer_name_id);
+ it->most_recent_event = LatchInfo{track_id};
+ break;
+ }
+ case GraphicsFrameEvent::PRESENT_FENCE: {
+ if (auto* l = std::get_if<LatchInfo>(&it->most_recent_event)) {
+ context_->slice_tracker->End(timestamp, l->track);
+ it->most_recent_event = std::monostate();
+ }
+ auto [d_it, d_inserted] = display_map_.Insert(layer_name_id, {});
+ if (d_it) {
+ context_->slice_tracker->End(timestamp, *d_it);
+ }
+ base::StackString<1024> track_name("Display_%.*s",
+ int(event.layer_name().size),
+ event.layer_name().data);
+ TrackId track_id = context_->track_tracker->InternTrack(
+ kGraphicFrameEventBlueprint,
+ tracks::Dimensions(track_name.string_view()),
+ tracks::DynamicName(
+ context_->storage->InternString(track_name.string_view())));
+ InsertPhaseSlice(timestamp, event, track_id, layer_name_id);
+ *d_it = track_id;
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+std::optional<GraphicsFrameEventParser::SliceRowNumber>
+GraphicsFrameEventParser::InsertPhaseSlice(
+ int64_t timestamp,
+ const GraphicsFrameEventDecoder& event,
+ TrackId track_id,
+ StringId layer_name_id) {
+ // If the frame_number is known, set it as the name of the slice.
+ // If not known (DEQUEUE), set the name as the timestamp.
+ // Timestamp is chosen here because the stack_id is hashed based on the name
+ // of the slice. To not have any conflicting stack_id with any of the
+ // existing slices, we use timestamp as the temporary name.
+ StringId slice_name;
+ if (event.frame_number() != 0) {
+ slice_name =
+ context_->storage->InternString(std::to_string(event.frame_number()));
+ } else {
+ slice_name = context_->storage->InternString(std::to_string(timestamp));
+ }
+ auto slice_id = context_->slice_tracker->Begin(
+ timestamp, track_id, kNullStringId, slice_name,
+ [&](ArgsTracker::BoundInserter* inserter) {
+ inserter->AddArg(frame_number_id_,
+ Variadic::Integer(event.frame_number()));
+ inserter->AddArg(layer_name_key_id_, Variadic::String(layer_name_id));
+ });
+ if (slice_id) {
+ return context_->storage->slice_table().FindById(*slice_id)->ToRowNumber();
+ }
+ return std::nullopt;
}
} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/graphics_frame_event_parser.h b/src/trace_processor/importers/proto/graphics_frame_event_parser.h
index fbf4b0e..563365f 100644
--- a/src/trace_processor/importers/proto/graphics_frame_event_parser.h
+++ b/src/trace_processor/importers/proto/graphics_frame_event_parser.h
@@ -20,20 +20,16 @@
#include <array>
#include <cstdint>
#include <optional>
-#include <unordered_map>
-#include <vector>
+#include <variant>
-#include "perfetto/ext/base/string_writer.h"
+#include "perfetto/ext/base/flat_hash_map.h"
#include "perfetto/protozero/field.h"
-#include "src/trace_processor/importers/common/args_tracker.h"
-#include "src/trace_processor/importers/proto/vulkan_memory_tracker.h"
#include "src/trace_processor/storage/trace_storage.h"
#include "protos/perfetto/trace/android/graphics_frame_event.pbzero.h"
+#include "src/trace_processor/tables/slice_tables_py.h"
-namespace perfetto {
-
-namespace trace_processor {
+namespace perfetto::trace_processor {
class TraceProcessorContext;
@@ -46,45 +42,68 @@
void ParseGraphicsFrameEvent(int64_t timestamp, ConstBytes);
private:
+ using SliceRowNumber = tables::SliceTable::RowNumber;
+ struct BufferEvent {
+ int64_t acquire_ts = 0;
+ int64_t queue_ts = 0;
+ int64_t latch_ts = 0;
+ bool is_most_recent_dequeue_ = false;
+ };
+ struct DequeueInfo {
+ tables::SliceTable::RowNumber slice_row;
+ int64_t timestamp;
+ };
+ struct QueueInfo {
+ TrackId track;
+ };
+ struct LatchInfo {
+ TrackId track;
+ };
+ struct PhaseEvent {
+ std::variant<std::monostate, DequeueInfo, QueueInfo, LatchInfo>
+ most_recent_event;
+ std::optional<int64_t> last_acquire_ts;
+ };
+
using GraphicsFrameEventDecoder =
protos::pbzero::GraphicsFrameEvent_BufferEvent_Decoder;
using GraphicsFrameEvent = protos::pbzero::GraphicsFrameEvent;
- bool CreateBufferEvent(int64_t timestamp, GraphicsFrameEventDecoder& event);
- void CreatePhaseEvent(int64_t timestamp, GraphicsFrameEventDecoder& event);
- // Invalidate a phase slice that has one of the events missing
- void InvalidatePhaseEvent(int64_t timestamp,
- TrackId track_id,
- bool reset_name = false);
+
+ void CreateBufferEvent(int64_t timestamp,
+ const GraphicsFrameEventDecoder&,
+ StringId layer_name_id,
+ StringId event_key);
+ void CreatePhaseEvent(int64_t timestamp,
+ const GraphicsFrameEventDecoder&,
+ StringId layer_name_id,
+ StringId event_key);
+
+ std::optional<SliceRowNumber> InsertPhaseSlice(
+ int64_t timestamp,
+ const GraphicsFrameEventDecoder&,
+ TrackId track_id,
+ StringId layer_name_id);
TraceProcessorContext* const context_;
const StringId unknown_event_name_id_;
const StringId no_layer_name_name_id_;
const StringId layer_name_key_id_;
- std::array<StringId, 14> event_type_name_ids_;
const StringId queue_lost_message_id_;
- // Map of (buffer ID + layer name) -> slice id of the dequeue event
- std::unordered_map<StringId, SliceId> dequeue_slice_ids_;
+ const StringId frame_number_id_;
+ const StringId queue_to_acquire_time_id_;
+ const StringId acquire_to_latch_time_id_;
+ const StringId latch_to_present_time_id_;
+ std::array<StringId, 14> event_type_name_ids_;
- // Row indices of frame stats table. Used to populate the slice_id after
- // inserting the rows.
- std::vector<uint32_t> graphics_frame_stats_idx_;
- // Map of (buffer ID + layer name)
- // -> (Map of GraphicsFrameEvent -> ts of that event)
- std::unordered_map<StringId, std::unordered_map<uint64_t, int64_t>>
- graphics_frame_stats_map_;
+ // Map of (buffer ID + layer name) -> BufferEvent
+ base::FlatHashMap<StringId, BufferEvent> buffer_event_map_;
// Maps of (buffer id + layer name) -> track id
- std::unordered_map<StringId, TrackId> dequeue_map_;
- std::unordered_map<StringId, TrackId> queue_map_;
- std::unordered_map<StringId, TrackId> latch_map_;
- // Map of layer name -> track id
- std::unordered_map<StringId, TrackId> display_map_;
+ base::FlatHashMap<StringId, PhaseEvent> phase_event_map_;
- // Maps of (buffer id + layer name) -> timestamp
- std::unordered_map<StringId, int64_t> last_dequeued_;
- std::unordered_map<StringId, int64_t> last_acquired_;
+ // Map of layer name -> track id
+ base::FlatHashMap<StringId, TrackId> display_map_;
};
-} // namespace trace_processor
-} // namespace perfetto
+} // namespace perfetto::trace_processor
#endif // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_GRAPHICS_FRAME_EVENT_PARSER_H_
diff --git a/src/trace_processor/importers/proto/metadata_module.cc b/src/trace_processor/importers/proto/metadata_module.cc
index 92f5ef6..e9c26ef 100644
--- a/src/trace_processor/importers/proto/metadata_module.cc
+++ b/src/trace_processor/importers/proto/metadata_module.cc
@@ -29,7 +29,6 @@
#include "src/trace_processor/importers/common/slice_tracker.h"
#include "src/trace_processor/importers/common/track_tracker.h"
#include "src/trace_processor/importers/common/tracks.h"
-#include "src/trace_processor/importers/proto/config.descriptor.h"
#include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
#include "src/trace_processor/importers/proto/proto_importer_module.h"
#include "src/trace_processor/storage/metadata.h"
@@ -69,6 +68,7 @@
RegisterForField(TracePacket::kUiStateFieldNumber, context);
RegisterForField(TracePacket::kTriggerFieldNumber, context);
RegisterForField(TracePacket::kChromeTriggerFieldNumber, context);
+ RegisterForField(TracePacket::kCloneSnapshotTriggerFieldNumber, context);
RegisterForField(TracePacket::kTraceUuidFieldNumber, context);
}
@@ -117,14 +117,20 @@
// We handle triggers at parse time rather at tokenization because
// we add slices to tables which need to happen post-sorting.
if (field_id == TracePacket::kTriggerFieldNumber) {
- ParseTrigger(ts, decoder.trigger());
+ ParseTrigger(ts, decoder.trigger(), TraceTriggerPacketType::kTraceTrigger);
}
if (field_id == TracePacket::kChromeTriggerFieldNumber) {
ParseChromeTrigger(ts, decoder.chrome_trigger());
}
+ if (field_id == TracePacket::kCloneSnapshotTriggerFieldNumber) {
+ ParseTrigger(ts, decoder.clone_snapshot_trigger(),
+ TraceTriggerPacketType::kCloneSnapshot);
+ }
}
-void MetadataModule::ParseTrigger(int64_t ts, ConstBytes blob) {
+void MetadataModule::ParseTrigger(int64_t ts,
+ ConstBytes blob,
+ TraceTriggerPacketType packetType) {
protos::pbzero::Trigger::Decoder trigger(blob.data, blob.size);
StringId cat_id = kNullStringId;
TrackId track_id =
@@ -145,6 +151,18 @@
Variadic::Integer(trigger.trusted_producer_uid()));
}
});
+
+ if (packetType == TraceTriggerPacketType::kCloneSnapshot &&
+ trace_trigger_packet_type_ != TraceTriggerPacketType::kCloneSnapshot) {
+ trace_trigger_packet_type_ = TraceTriggerPacketType::kCloneSnapshot;
+ context_->metadata_tracker->SetMetadata(metadata::trace_trigger,
+ Variadic::String(name_id));
+ } else if (packetType == TraceTriggerPacketType::kTraceTrigger &&
+ trace_trigger_packet_type_ == TraceTriggerPacketType::kNone) {
+ trace_trigger_packet_type_ = TraceTriggerPacketType::kTraceTrigger;
+ context_->metadata_tracker->SetMetadata(metadata::trace_trigger,
+ Variadic::String(name_id));
+ }
}
void MetadataModule::ParseChromeTrigger(int64_t ts, ConstBytes blob) {
@@ -218,12 +236,8 @@
Variadic::String(id));
}
- DescriptorPool pool;
- pool.AddFromFileDescriptorSet(kConfigDescriptor.data(),
- kConfigDescriptor.size());
-
std::string text = protozero_to_text::ProtozeroToText(
- pool, ".perfetto.protos.TraceConfig",
+ *context_->descriptor_pool_, ".perfetto.protos.TraceConfig",
protozero::ConstBytes{
trace_config.begin(),
static_cast<uint32_t>(trace_config.end() - trace_config.begin())},
diff --git a/src/trace_processor/importers/proto/metadata_module.h b/src/trace_processor/importers/proto/metadata_module.h
index aabea03..bb6f4f8 100644
--- a/src/trace_processor/importers/proto/metadata_module.h
+++ b/src/trace_processor/importers/proto/metadata_module.h
@@ -47,11 +47,19 @@
void ParseTraceConfig(const protos::pbzero::TraceConfig_Decoder&) override;
private:
- void ParseTrigger(int64_t ts, ConstBytes);
+ enum class TraceTriggerPacketType {
+ kNone,
+ kTraceTrigger,
+ kCloneSnapshot,
+ };
+
+ void ParseTrigger(int64_t ts, ConstBytes, TraceTriggerPacketType);
void ParseChromeTrigger(int64_t ts, ConstBytes);
void ParseTraceUuid(ConstBytes);
TraceProcessorContext* context_;
+ TraceTriggerPacketType trace_trigger_packet_type_ =
+ TraceTriggerPacketType::kNone;
const StringId producer_name_key_id_;
const StringId trusted_producer_uid_key_id_;
diff --git a/src/trace_processor/importers/proto/multi_machine_trace_manager.cc b/src/trace_processor/importers/proto/multi_machine_trace_manager.cc
index b030eb9..5738910 100644
--- a/src/trace_processor/importers/proto/multi_machine_trace_manager.cc
+++ b/src/trace_processor/importers/proto/multi_machine_trace_manager.cc
@@ -15,31 +15,19 @@
*/
#include "src/trace_processor/importers/proto/multi_machine_trace_manager.h"
-#include <memory>
-#include "src/trace_processor/importers/common/args_translation_table.h"
-#include "src/trace_processor/importers/common/clock_converter.h"
-#include "src/trace_processor/importers/common/clock_tracker.h"
-#include "src/trace_processor/importers/common/event_tracker.h"
-#include "src/trace_processor/importers/common/flow_tracker.h"
-#include "src/trace_processor/importers/common/machine_tracker.h"
-#include "src/trace_processor/importers/common/mapping_tracker.h"
+#include <memory>
+#include <utility>
+
+#include "perfetto/base/logging.h"
#include "src/trace_processor/importers/common/process_tracker.h"
-#include "src/trace_processor/importers/common/sched_event_tracker.h"
-#include "src/trace_processor/importers/common/slice_tracker.h"
-#include "src/trace_processor/importers/common/stack_profile_tracker.h"
-#include "src/trace_processor/importers/common/track_compressor.h"
-#include "src/trace_processor/importers/common/track_tracker.h"
#include "src/trace_processor/importers/proto/default_modules.h"
-#include "src/trace_processor/importers/proto/perf_sample_tracker.h"
-#include "src/trace_processor/importers/proto/proto_importer_module.h"
#include "src/trace_processor/importers/proto/proto_trace_parser_impl.h"
#include "src/trace_processor/importers/proto/proto_trace_reader.h"
#include "src/trace_processor/sorter/trace_sorter.h"
#include "src/trace_processor/types/trace_processor_context.h"
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
MultiMachineTraceManager::MultiMachineTraceManager(
TraceProcessorContext* default_context)
@@ -93,5 +81,4 @@
return remote_machine_contexts_[raw_machine_id].reader.get();
}
-} // namespace trace_processor
-} // namespace perfetto
+} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/network_trace_module_unittest.cc b/src/trace_processor/importers/proto/network_trace_module_unittest.cc
index 64fa9ee..79e9174 100644
--- a/src/trace_processor/importers/proto/network_trace_module_unittest.cc
+++ b/src/trace_processor/importers/proto/network_trace_module_unittest.cc
@@ -14,12 +14,12 @@
* limitations under the License.
*/
+#include "src/trace_processor/importers/proto/network_trace_module.h"
+
#include <cstdint>
#include <memory>
#include <vector>
-#include "src/trace_processor/importers/proto/network_trace_module.h"
-
#include "perfetto/base/status.h"
#include "perfetto/ext/base/string_view.h"
#include "perfetto/protozero/packed_repeated_fields.h"
@@ -135,15 +135,18 @@
ASSERT_EQ(slices.row_count(), 1u);
EXPECT_EQ(slices[0].ts(), 123);
- EXPECT_TRUE(HasArg(2u, "packet_length", Variadic::Integer(72)));
- EXPECT_TRUE(HasArg(2u, "socket_uid", Variadic::Integer(1010)));
- EXPECT_TRUE(HasArg(2u, "local_port", Variadic::Integer(5100)));
- EXPECT_TRUE(HasArg(2u, "remote_port", Variadic::Integer(443)));
- EXPECT_TRUE(HasArg(2u, "packet_transport",
+ EXPECT_TRUE(slices[0].arg_set_id().has_value());
+
+ uint32_t arg_set_id = *slices[0].arg_set_id();
+ EXPECT_TRUE(HasArg(arg_set_id, "packet_length", Variadic::Integer(72)));
+ EXPECT_TRUE(HasArg(arg_set_id, "socket_uid", Variadic::Integer(1010)));
+ EXPECT_TRUE(HasArg(arg_set_id, "local_port", Variadic::Integer(5100)));
+ EXPECT_TRUE(HasArg(arg_set_id, "remote_port", Variadic::Integer(443)));
+ EXPECT_TRUE(HasArg(arg_set_id, "packet_transport",
Variadic::String(storage_->InternString("IPPROTO_TCP"))));
- EXPECT_TRUE(HasArg(2u, "socket_tag",
+ EXPECT_TRUE(HasArg(arg_set_id, "socket_tag",
Variadic::String(storage_->InternString("0x407"))));
- EXPECT_TRUE(HasArg(2u, "packet_tcp_flags",
+ EXPECT_TRUE(HasArg(arg_set_id, "packet_tcp_flags",
Variadic::String(storage_->InternString(".s..a..."))));
}
@@ -175,8 +178,13 @@
EXPECT_EQ(slices[0].ts(), 123);
EXPECT_EQ(slices[1].ts(), 133);
- EXPECT_TRUE(HasArg(2u, "packet_length", Variadic::Integer(72)));
- EXPECT_TRUE(HasArg(3u, "packet_length", Variadic::Integer(100)));
+ EXPECT_TRUE(slices[0].arg_set_id().has_value());
+ EXPECT_TRUE(slices[1].arg_set_id().has_value());
+
+ EXPECT_TRUE(
+ HasArg(*slices[0].arg_set_id(), "packet_length", Variadic::Integer(72)));
+ EXPECT_TRUE(
+ HasArg(*slices[1].arg_set_id(), "packet_length", Variadic::Integer(100)));
}
TEST_F(NetworkTraceModuleTest, TokenizeAndParseAggregateBundle) {
@@ -200,8 +208,11 @@
EXPECT_EQ(slices[0].ts(), 123);
EXPECT_EQ(slices[0].dur(), 10);
- EXPECT_TRUE(HasArg(2u, "packet_length", Variadic::Integer(172)));
- EXPECT_TRUE(HasArg(2u, "packet_count", Variadic::Integer(2)));
+ EXPECT_TRUE(slices[0].arg_set_id().has_value());
+
+ uint32_t arg_set_id = *slices[0].arg_set_id();
+ EXPECT_TRUE(HasArg(arg_set_id, "packet_length", Variadic::Integer(172)));
+ EXPECT_TRUE(HasArg(arg_set_id, "packet_count", Variadic::Integer(2)));
}
} // namespace
diff --git a/src/trace_processor/importers/proto/proto_importer_module.h b/src/trace_processor/importers/proto/proto_importer_module.h
index c8375dc..b383072 100644
--- a/src/trace_processor/importers/proto/proto_importer_module.h
+++ b/src/trace_processor/importers/proto/proto_importer_module.h
@@ -54,7 +54,7 @@
class ModuleResult {
public:
- // Allow auto conversion from util::Status to Handled / Error result.
+ // Allow auto conversion from base::Status to Handled / Error result.
ModuleResult(const base::Status& status)
: ignored_(false),
error_(status.ok() ? std::nullopt
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_impl.cc b/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
index f1ed9df..5492e19 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
@@ -45,6 +45,7 @@
#include "src/trace_processor/importers/proto/track_event_module.h"
#include "src/trace_processor/storage/stats.h"
#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
#include "src/trace_processor/types/trace_processor_context.h"
#include "src/trace_processor/types/variadic.h"
@@ -164,10 +165,10 @@
protos::pbzero::ChromeEventBundle::Decoder bundle(blob);
ArgsTracker args(context_);
if (bundle.has_metadata()) {
- auto ucpu = context_->cpu_tracker->GetOrCreateCpu(0);
- RawId id = storage->mutable_raw_table()
- ->Insert({ts, raw_chrome_metadata_event_id_, 0, 0, 0, ucpu})
- .id;
+ tables::ChromeRawTable::Id id =
+ storage->mutable_chrome_raw_table()
+ ->Insert({ts, raw_chrome_metadata_event_id_, 0, 0})
+ .id;
auto inserter = args.AddArgsTo(id);
uint32_t bundle_index =
@@ -212,11 +213,10 @@
}
if (bundle.has_legacy_ftrace_output()) {
- auto ucpu = context_->cpu_tracker->GetOrCreateCpu(0);
- RawId id = storage->mutable_raw_table()
- ->Insert({ts, raw_chrome_legacy_system_trace_event_id_, 0, 0,
- 0, ucpu})
- .id;
+ tables::ChromeRawTable::Id id =
+ storage->mutable_chrome_raw_table()
+ ->Insert({ts, raw_chrome_legacy_system_trace_event_id_, 0, 0})
+ .id;
std::string data;
for (auto it = bundle.legacy_ftrace_output(); it; ++it) {
@@ -234,11 +234,10 @@
protos::pbzero::ChromeLegacyJsonTrace::USER_TRACE) {
continue;
}
- auto ucpu = context_->cpu_tracker->GetOrCreateCpu(0);
- RawId id = storage->mutable_raw_table()
- ->Insert({ts, raw_chrome_legacy_user_trace_event_id_, 0, 0,
- 0, ucpu})
- .id;
+ tables::ChromeRawTable::Id id =
+ storage->mutable_chrome_raw_table()
+ ->Insert({ts, raw_chrome_legacy_user_trace_event_id_, 0, 0})
+ .id;
Variadic value =
Variadic::String(storage->InternString(legacy_trace.data()));
args.AddArgsTo(id).AddArg(data_name_id_, value);
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc b/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc
index b0a5355..190f31f 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc
@@ -19,14 +19,12 @@
#include <cmath>
#include <cstdint>
#include <cstring>
-#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
-#include "perfetto/base/logging.h"
#include "perfetto/base/status.h"
#include "perfetto/ext/base/string_view.h"
#include "perfetto/protozero/scattered_heap_buffer.h"
@@ -55,6 +53,7 @@
#include "src/trace_processor/importers/proto/additional_modules.h"
#include "src/trace_processor/importers/proto/default_modules.h"
#include "src/trace_processor/importers/proto/proto_trace_reader.h"
+#include "src/trace_processor/importers/proto/trace.descriptor.h"
#include "src/trace_processor/sorter/trace_sorter.h"
#include "src/trace_processor/storage/metadata.h"
#include "src/trace_processor/storage/stats.h"
@@ -233,45 +232,6 @@
ArgsTracker tracker_;
};
-class MockSliceTracker : public SliceTracker {
- public:
- explicit MockSliceTracker(TraceProcessorContext* context)
- : SliceTracker(context) {}
-
- MOCK_METHOD(std::optional<SliceId>,
- Begin,
- (int64_t timestamp,
- TrackId track_id,
- StringId cat,
- StringId name,
- SetArgsCallback args_callback),
- (override));
- MOCK_METHOD(std::optional<SliceId>,
- End,
- (int64_t timestamp,
- TrackId track_id,
- StringId cat,
- StringId name,
- SetArgsCallback args_callback),
- (override));
- MOCK_METHOD(std::optional<SliceId>,
- Scoped,
- (int64_t timestamp,
- TrackId track_id,
- StringId cat,
- StringId name,
- int64_t duration,
- SetArgsCallback args_callback),
- (override));
- MOCK_METHOD(std::optional<SliceId>,
- StartSlice,
- (int64_t timestamp,
- TrackId track_id,
- SetArgsCallback args_callback,
- std::function<SliceId()> inserter),
- (override));
-};
-
class ProtoTraceParserTest : public ::testing::Test {
public:
ProtoTraceParserTest() {
@@ -297,8 +257,7 @@
context_.process_tracker.reset(process_);
context_.process_track_translation_table.reset(
new ProcessTrackTranslationTable(storage_));
- slice_ = new NiceMock<MockSliceTracker>(&context_);
- context_.slice_tracker.reset(slice_);
+ context_.slice_tracker = std::make_unique<SliceTracker>(&context_);
context_.slice_translation_table =
std::make_unique<SliceTranslationTable>(storage_);
clock_ = new ClockTracker(&context_);
@@ -309,6 +268,8 @@
context_.sorter = std::make_shared<TraceSorter>(
&context_, TraceSorter::SortingMode::kFullSort);
context_.descriptor_pool_ = std::make_unique<DescriptorPool>();
+ context_.descriptor_pool_->AddFromFileDescriptorSet(
+ kTraceDescriptor.data(), kTraceDescriptor.size());
context_.perf_sample_tracker.reset(new PerfSampleTracker(&context_));
@@ -358,7 +319,6 @@
MockEventTracker* event_;
MockSchedEventTracker* sched_;
MockProcessTracker* process_;
- MockSliceTracker* slice_;
ClockTracker* clock_;
TraceStorage* storage_;
};
@@ -391,7 +351,7 @@
context_.sorter->ExtractEventsForced();
}
-TEST_F(ProtoTraceParserTest, LoadEventsIntoRaw) {
+TEST_F(ProtoTraceParserTest, LoadEventsIntoFtraceEvent) {
auto* bundle = trace_->add_packet()->set_ftrace_events();
bundle->set_cpu(10);
@@ -422,7 +382,7 @@
Tokenize();
context_.sorter->ExtractEventsForced();
- const auto& raw = context_.storage->raw_table();
+ const auto& raw = context_.storage->ftrace_event_table();
ASSERT_EQ(raw.row_count(), 2u);
const auto& args = context_.storage->arg_table();
ASSERT_EQ(args.row_count(), 6u);
@@ -475,7 +435,7 @@
Tokenize();
context_.sorter->ExtractEventsForced();
- const auto& raw = storage_->raw_table();
+ const auto& raw = storage_->ftrace_event_table();
ASSERT_EQ(raw.row_count(), 1u);
ASSERT_EQ(raw[raw.row_count() - 1].ts(), 100);
@@ -978,28 +938,16 @@
MockBoundInserter inserter;
- StringId unknown_cat = storage_->InternString("unknown(1)");
-
- constexpr TrackId track{0u};
constexpr TrackId thread_time_track{1u};
InSequence in_sequence; // Below slices should be sorted by timestamp.
// Only the begin thread time can be imported into the counter table.
EXPECT_CALL(*event_, PushCounter(1005000, testing::DoubleEq(2003000),
thread_time_track));
- EXPECT_CALL(*slice_, StartSlice(1005000, track, _, _))
- .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
- InvokeArgument<2>(&inserter), Return(SliceId(0u))));
EXPECT_CALL(*event_, PushCounter(1010000, testing::DoubleEq(2005000),
thread_time_track));
- EXPECT_CALL(*slice_, StartSlice(1010000, track, _, _))
- .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
- InvokeArgument<2>(&inserter), Return(SliceId(1u))));
EXPECT_CALL(*event_, PushCounter(1020000, testing::DoubleEq(2010000),
thread_time_track));
- EXPECT_CALL(*slice_, End(1020000, track, unknown_cat, kNullStringId, _))
- .WillOnce(DoAll(InvokeArgument<4>(&inserter), Return(SliceId(1u))));
-
context_.sorter->ExtractEventsForced();
EXPECT_EQ(storage_->slice_table().row_count(), 2u);
@@ -1068,26 +1016,15 @@
MockBoundInserter inserter;
- StringId unknown_cat1 = storage_->InternString("unknown(1)");
-
- constexpr TrackId track{0u};
constexpr TrackId thread_time_track{1u};
InSequence in_sequence; // Below slices should be sorted by timestamp.
EXPECT_CALL(*event_, PushCounter(1010000, testing::DoubleEq(2005000),
thread_time_track));
- EXPECT_CALL(*slice_, StartSlice(1010000, track, _, _))
- .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
- InvokeArgument<2>(&inserter), Return(SliceId(0u))));
EXPECT_CALL(*event_, PushCounter(1015000, testing::DoubleEq(2007000),
thread_time_track));
- EXPECT_CALL(*slice_, StartSlice(1015000, track, _, _))
- .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
- InvokeArgument<2>(&inserter), Return(SliceId(1u))));
EXPECT_CALL(*event_, PushCounter(1020000, testing::DoubleEq(2010000),
thread_time_track));
- EXPECT_CALL(*slice_, End(1020000, track, unknown_cat1, kNullStringId, _))
- .WillOnce(DoAll(InvokeArgument<4>(&inserter), Return(SliceId(0u))));
context_.sorter->ExtractEventsForced();
@@ -1240,13 +1177,8 @@
row.upid = 2u;
storage_->mutable_thread_table()->Insert(row);
- constexpr TrackId thread_1_track{0u};
constexpr TrackId thread_time_track{1u};
constexpr TrackId thread_instruction_count_track{2u};
- constexpr TrackId process_2_track{3u};
-
- StringId cat_1 = storage_->InternString("cat1");
- StringId ev_1 = storage_->InternString("ev1");
InSequence in_sequence; // Below slices should be sorted by timestamp.
@@ -1256,41 +1188,25 @@
thread_time_track));
EXPECT_CALL(*event_, PushCounter(1005000, testing::DoubleEq(3010),
thread_instruction_count_track));
- EXPECT_CALL(*slice_, StartSlice(1005000, thread_1_track, _, _))
- .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
- InvokeArgument<2>(&inserter), Return(SliceId(0u))));
EXPECT_CALL(*event_, PushCounter(1010000, testing::DoubleEq(2005000),
thread_time_track));
EXPECT_CALL(*event_, PushCounter(1010000, testing::DoubleEq(3020),
thread_instruction_count_track));
- EXPECT_CALL(*slice_, StartSlice(1010000, thread_1_track, _, _))
- .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
- InvokeArgument<2>(&inserter), Return(SliceId(1u))));
EXPECT_CALL(*event_, PushCounter(1020000, testing::DoubleEq(2010000),
thread_time_track));
EXPECT_CALL(*event_, PushCounter(1020000, testing::DoubleEq(3040),
thread_instruction_count_track));
- EXPECT_CALL(*slice_, End(1020000, thread_1_track, cat_1, ev_1, _))
- .WillOnce(DoAll(InvokeArgument<4>(&inserter), Return(SliceId(1u))));
EXPECT_CALL(*event_, PushCounter(1040000, testing::DoubleEq(2030000),
thread_time_track));
EXPECT_CALL(*event_, PushCounter(1040000, testing::DoubleEq(3100),
thread_instruction_count_track));
- EXPECT_CALL(*slice_, StartSlice(1040000, thread_1_track, _, _))
- .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
- InvokeArgument<2>(&inserter), Return(SliceId(2u))));
-
- EXPECT_CALL(*slice_, Scoped(1050000, process_2_track, cat_1, ev_1, 0, _))
- .WillOnce(DoAll(InvokeArgument<5>(&inserter), Return(SliceId(3u))));
- // Second slice should have a legacy_event.passthrough_utid arg.
- EXPECT_CALL(inserter, AddArg(_, _, Variadic::UnsignedInteger(1u), _));
context_.sorter->ExtractEventsForced();
- EXPECT_EQ(storage_->slice_table().row_count(), 3u);
+ EXPECT_EQ(storage_->slice_table().row_count(), 4u);
auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
EXPECT_TRUE(rr_0);
EXPECT_EQ(rr_0->thread_ts(), 2003000);
@@ -1413,9 +1329,7 @@
row.upid = 1u;
storage_->mutable_thread_table()->Insert(row);
- StringId cat_1 = storage_->InternString("cat1");
StringId ev_1 = storage_->InternString("ev1");
- StringId cat_2 = storage_->InternString("cat2");
StringId ev_2 = storage_->InternString("ev2");
TrackId thread_time_track{2u};
@@ -1427,17 +1341,10 @@
thread_time_track));
EXPECT_CALL(*event_, PushCounter(1010000, testing::DoubleEq(3020),
thread_instruction_count_track));
- EXPECT_CALL(*slice_, Begin(1010000, TrackId{1}, cat_1, ev_1, _))
- .WillOnce(Return(SliceId(0u)));
- EXPECT_CALL(*slice_, Scoped(1015000, TrackId{1}, cat_1, ev_2, 0, _));
- EXPECT_CALL(*slice_, Scoped(1018000, TrackId{4}, cat_2, ev_2, 0, _));
EXPECT_CALL(*event_, PushCounter(1020000, testing::DoubleEq(2010000),
thread_time_track));
EXPECT_CALL(*event_, PushCounter(1020000, testing::DoubleEq(3040),
thread_instruction_count_track));
- EXPECT_CALL(*slice_, End(1020000, TrackId{1}, cat_1, ev_1, _))
- .WillOnce(Return(SliceId(SliceId(0u))));
- EXPECT_CALL(*slice_, Scoped(1030000, TrackId{5}, cat_2, ev_2, 0, _));
context_.sorter->ExtractEventsForced();
@@ -1602,27 +1509,13 @@
Tokenize();
- StringId cat_1 = storage_->InternString("cat1");
- StringId ev_1 = storage_->InternString("ev1");
-
InSequence in_sequence; // Below slices should be sorted by timestamp.
- EXPECT_CALL(*slice_, Begin(1010000, TrackId{1}, cat_1, ev_1, _))
- .WillOnce(Return(SliceId(2u)));
-
EXPECT_CALL(*event_,
PushCounter(1015000, testing::DoubleEq(2007000), TrackId{3}));
- EXPECT_CALL(*slice_, StartSlice(1015000, TrackId{0}, _, _))
- .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()), Return(SliceId(0u))));
EXPECT_CALL(*event_,
PushCounter(1016000, testing::DoubleEq(2008000), TrackId{4}));
- EXPECT_CALL(*slice_, StartSlice(1016000, TrackId{2}, _, _))
- .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()), Return(SliceId(1u))));
-
- EXPECT_CALL(*slice_,
- End(1020000, TrackId{1}, kNullStringId, kNullStringId, _))
- .WillOnce(Return(SliceId(2u)));
context_.sorter->ExtractEventsForced();
@@ -1640,7 +1533,7 @@
EXPECT_EQ(storage_->track_table()[4].utid(), 2u);
EXPECT_EQ(storage_->virtual_track_slices().slice_count(), 1u);
- EXPECT_EQ(storage_->virtual_track_slices().slice_ids()[0], SliceId(2u));
+ EXPECT_EQ(storage_->virtual_track_slices().slice_ids()[0], SliceId(0u));
EXPECT_EQ(storage_->virtual_track_slices().thread_timestamp_ns()[0], 2005000);
EXPECT_EQ(storage_->virtual_track_slices().thread_duration_ns()[0], 5000);
EXPECT_EQ(storage_->virtual_track_slices().thread_instruction_counts()[0],
@@ -1648,15 +1541,15 @@
EXPECT_EQ(storage_->virtual_track_slices().thread_instruction_deltas()[0],
20);
- EXPECT_EQ(storage_->slice_table().row_count(), 2u);
- auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+ EXPECT_EQ(storage_->slice_table().row_count(), 3u);
+ auto rr_0 = storage_->slice_table().FindById(SliceId(1u));
EXPECT_TRUE(rr_0);
EXPECT_EQ(rr_0->thread_ts(), 2007000);
EXPECT_EQ(rr_0->thread_dur(), 0);
// There was no thread instructions in the packets above.
EXPECT_FALSE(rr_0->thread_instruction_count());
EXPECT_FALSE(rr_0->thread_instruction_delta());
- auto rr_1 = storage_->slice_table().FindById(SliceId(1u));
+ auto rr_1 = storage_->slice_table().FindById(SliceId(2u));
EXPECT_TRUE(rr_1);
EXPECT_EQ(rr_1->thread_ts(), 2008000);
EXPECT_EQ(rr_1->thread_dur(), 0);
@@ -1737,13 +1630,9 @@
EXPECT_CALL(*event_,
PushCounter(1000, testing::DoubleEq(1000000), TrackId{1}));
- EXPECT_CALL(*slice_, StartSlice(1000, TrackId{0}, _, _))
- .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()), Return(SliceId(0u))));
EXPECT_CALL(*event_,
PushCounter(1100, testing::DoubleEq(1010000), TrackId{1}));
- EXPECT_CALL(*slice_, End(1100, TrackId{0}, kNullStringId, kNullStringId, _))
- .WillOnce(Return(SliceId(0u)));
EXPECT_CALL(*process_,
UpdateThreadNameByUtid(1u, storage_->InternString("t1"),
@@ -1816,9 +1705,13 @@
StringId cat1 = storage_->InternString("cat1");
StringId ev2 = storage_->InternString("ev2");
- EXPECT_CALL(*slice_, Scoped(2100000, TrackId{0}, cat1, ev2, 0, _))
- .WillOnce(Return(SliceId(0u)));
context_.sorter->ExtractEventsForced();
+
+ EXPECT_EQ(storage_->slice_table().row_count(), 1u);
+ auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+ EXPECT_TRUE(rr_0);
+ EXPECT_EQ(rr_0->category(), cat1);
+ EXPECT_EQ(rr_0->name(), ev2);
}
TEST_F(ProtoTraceParserTest, TrackEventWithoutThreadDescriptor) {
@@ -1855,9 +1748,16 @@
StringId cat1 = storage_->InternString("cat1");
StringId ev1 = storage_->InternString("ev1");
- EXPECT_CALL(*slice_, Scoped(2000000, TrackId{0}, cat1, ev1, 0, _))
- .WillOnce(Return(SliceId(0u)));
context_.sorter->ExtractEventsForced();
+
+ EXPECT_EQ(storage_->slice_table().row_count(), 1u);
+ auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+ EXPECT_TRUE(rr_0);
+ EXPECT_EQ(rr_0->ts(), 2000000);
+ EXPECT_EQ(rr_0->track_id(), TrackId{0});
+ EXPECT_EQ(rr_0->dur(), 0);
+ EXPECT_EQ(rr_0->category(), cat1);
+ EXPECT_EQ(rr_0->name(), ev1);
}
TEST_F(ProtoTraceParserTest, TrackEventWithDataLoss) {
@@ -1945,10 +1845,17 @@
StringId unknown_cat = storage_->InternString("unknown(1)");
constexpr TrackId track{0u};
InSequence in_sequence; // Below slices should be sorted by timestamp.
- EXPECT_CALL(*slice_, StartSlice(1010000, track, _, _));
- EXPECT_CALL(*slice_, End(2010000, track, unknown_cat, kNullStringId, _));
context_.sorter->ExtractEventsForced();
+
+ EXPECT_EQ(storage_->slice_table().row_count(), 1u);
+ auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+ EXPECT_TRUE(rr_0);
+ EXPECT_EQ(rr_0->ts(), 1010000);
+ EXPECT_EQ(rr_0->track_id(), track);
+ EXPECT_EQ(rr_0->dur(), 1000000);
+ EXPECT_EQ(rr_0->category(), unknown_cat);
+ EXPECT_EQ(rr_0->name(), std::nullopt);
}
TEST_F(ProtoTraceParserTest, TrackEventMultipleSequences) {
@@ -2048,12 +1955,24 @@
constexpr TrackId thread_1_track{1u};
InSequence in_sequence; // Below slices should be sorted by timestamp.
- EXPECT_CALL(*slice_, StartSlice(1005000, thread_2_track, _, _));
- EXPECT_CALL(*slice_, StartSlice(1010000, thread_1_track, _, _));
- EXPECT_CALL(*slice_, End(1015000, thread_2_track, cat_1, ev_2, _));
- EXPECT_CALL(*slice_, End(1020000, thread_1_track, cat_1, ev_1, _));
-
context_.sorter->ExtractEventsForced();
+
+ EXPECT_EQ(storage_->slice_table().row_count(), 2u);
+ auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+ EXPECT_TRUE(rr_0);
+ EXPECT_EQ(rr_0->ts(), 1005000);
+ EXPECT_EQ(rr_0->track_id(), thread_2_track);
+ EXPECT_EQ(rr_0->dur(), 10000);
+ EXPECT_EQ(rr_0->category(), cat_1);
+ EXPECT_EQ(rr_0->name(), ev_2);
+
+ auto rr_1 = storage_->slice_table().FindById(SliceId(1u));
+ EXPECT_TRUE(rr_1);
+ EXPECT_EQ(rr_1->ts(), 1010000);
+ EXPECT_EQ(rr_1->track_id(), thread_1_track);
+ EXPECT_EQ(rr_1->dur(), 10000);
+ EXPECT_EQ(rr_1->category(), cat_1);
+ EXPECT_EQ(rr_1->name(), ev_1);
}
TEST_F(ProtoTraceParserTest, TrackEventWithDebugAnnotations) {
@@ -2186,75 +2105,19 @@
StringId cat_1 = storage_->InternString("cat1");
StringId ev_1 = storage_->InternString("ev1");
- StringId debug_an_1 = storage_->InternString("debug.an1");
- StringId debug_an_2_child_1 = storage_->InternString("debug.an2.child1");
- StringId debug_an_2_child_2 = storage_->InternString("debug.an2.child2");
- StringId debug_an_2_child_2_0 = storage_->InternString("debug.an2.child2[0]");
- StringId child21 = storage_->InternString("child21");
- StringId debug_an_2_child_2_1 = storage_->InternString("debug.an2.child2[1]");
- StringId debug_an_2_child_2_2 = storage_->InternString("debug.an2.child2[2]");
- StringId debug_an_3 = storage_->InternString("debug.an3");
- StringId debug_an_4 = storage_->InternString("debug.an4");
- StringId debug_an_5 = storage_->InternString("debug.an5");
- StringId debug_an_6 = storage_->InternString("debug.an6");
- StringId debug_an_7 = storage_->InternString("debug.an7");
- StringId val_7 = storage_->InternString("val7");
- StringId debug_an_8_val8_a = storage_->InternString("debug.an8.val8.a");
- StringId debug_an_8_val8_b = storage_->InternString("debug.an8.val8.b");
- StringId val_8b = storage_->InternString("val8b");
- StringId debug_an_8_arr8 = storage_->InternString("debug.an8.arr8");
- StringId debug_an_8_arr8_0 = storage_->InternString("debug.an8.arr8[0]");
- StringId debug_an_8_arr8_1 = storage_->InternString("debug.an8.arr8[1]");
- StringId debug_an_8_arr8_2 = storage_->InternString("debug.an8.arr8[2]");
- StringId debug_an_8_foo = storage_->InternString("debug.an8_foo");
constexpr TrackId track{0u};
- EXPECT_CALL(*slice_, StartSlice(1010000, track, _, _))
- .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
- InvokeArgument<2>(&inserter), Return(SliceId(0u))));
- EXPECT_CALL(inserter, AddArg(debug_an_1, debug_an_1,
- Variadic::UnsignedInteger(10u), _));
-
- EXPECT_CALL(inserter, AddArg(debug_an_2_child_1, debug_an_2_child_1,
- Variadic::Boolean(true), _));
-
- EXPECT_CALL(inserter, AddArg(debug_an_2_child_2, debug_an_2_child_2_0,
- Variadic::String(child21), _));
-
- EXPECT_CALL(inserter, AddArg(debug_an_2_child_2, debug_an_2_child_2_1,
- Variadic::Real(2.2), _));
-
- EXPECT_CALL(inserter, AddArg(debug_an_2_child_2, debug_an_2_child_2_2,
- Variadic::Integer(23), _));
-
- EXPECT_CALL(*slice_, End(1020000, track, cat_1, ev_1, _))
- .WillOnce(DoAll(InvokeArgument<4>(&inserter), Return(SliceId(0u))));
-
- EXPECT_CALL(inserter,
- AddArg(debug_an_3, debug_an_3, Variadic::Integer(-3), _));
- EXPECT_CALL(inserter,
- AddArg(debug_an_4, debug_an_4, Variadic::Boolean(true), _));
- EXPECT_CALL(inserter,
- AddArg(debug_an_5, debug_an_5, Variadic::Real(-5.5), _));
- EXPECT_CALL(inserter,
- AddArg(debug_an_6, debug_an_6, Variadic::Pointer(20u), _));
- EXPECT_CALL(inserter,
- AddArg(debug_an_7, debug_an_7, Variadic::String(val_7), _));
- EXPECT_CALL(inserter, AddArg(debug_an_8_val8_a, debug_an_8_val8_a,
- Variadic::Integer(42), _));
- EXPECT_CALL(inserter, AddArg(debug_an_8_val8_b, debug_an_8_val8_b,
- Variadic::String(val_8b), _));
- EXPECT_CALL(inserter, AddArg(debug_an_8_arr8, debug_an_8_arr8_0,
- Variadic::Integer(1), _));
- EXPECT_CALL(inserter, AddArg(debug_an_8_arr8, debug_an_8_arr8_1,
- Variadic::Integer(2), _));
- EXPECT_CALL(inserter, AddArg(debug_an_8_arr8, debug_an_8_arr8_2,
- Variadic::Integer(3), _));
- EXPECT_CALL(inserter,
- AddArg(debug_an_8_foo, debug_an_8_foo, Variadic::Integer(15), _));
-
context_.sorter->ExtractEventsForced();
+
+ EXPECT_EQ(storage_->slice_table().row_count(), 1u);
+ auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+ EXPECT_TRUE(rr_0);
+ EXPECT_EQ(rr_0->ts(), 1010000);
+ EXPECT_EQ(rr_0->track_id(), track);
+ EXPECT_EQ(rr_0->dur(), 10000);
+ EXPECT_EQ(rr_0->category(), cat_1);
+ EXPECT_EQ(rr_0->name(), ev_1);
}
TEST_F(ProtoTraceParserTest, TrackEventWithTaskExecution) {
@@ -2303,20 +2166,15 @@
constexpr TrackId track{0u};
- StringId file_1 = storage_->InternString("file1");
- StringId func_1 = storage_->InternString("func1");
-
InSequence in_sequence; // Below slices should be sorted by timestamp.
- MockBoundInserter inserter;
- EXPECT_CALL(*slice_, StartSlice(1010000, track, _, _))
- .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
- InvokeArgument<2>(&inserter), Return(SliceId(0u))));
- EXPECT_CALL(inserter, AddArg(_, _, Variadic::String(file_1), _));
- EXPECT_CALL(inserter, AddArg(_, _, Variadic::String(func_1), _));
- EXPECT_CALL(inserter, AddArg(_, _, Variadic::UnsignedInteger(42), _));
-
context_.sorter->ExtractEventsForced();
+
+ EXPECT_EQ(storage_->slice_table().row_count(), 1u);
+ auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+ EXPECT_TRUE(rr_0);
+ EXPECT_EQ(rr_0->ts(), 1010000);
+ EXPECT_EQ(rr_0->track_id(), track);
}
TEST_F(ProtoTraceParserTest, TrackEventWithLogMessage) {
@@ -2373,26 +2231,19 @@
storage_->mutable_thread_table()->Insert(row);
StringId body_1 = storage_->InternString("body1");
- StringId file_1 = storage_->InternString("file1");
- StringId func_1 = storage_->InternString("func1");
StringId source_location_id = storage_->InternString("file1:1");
constexpr TrackId track{0};
InSequence in_sequence; // Below slices should be sorted by timestamp.
- MockBoundInserter inserter;
- EXPECT_CALL(*slice_, StartSlice(1010000, track, _, _))
- .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
- InvokeArgument<2>(&inserter), Return(SliceId(0u))));
-
- // Call with logMessageBody (body1 in this case).
- EXPECT_CALL(inserter, AddArg(_, _, Variadic::String(body_1), _));
- EXPECT_CALL(inserter, AddArg(_, _, Variadic::String(file_1), _));
- EXPECT_CALL(inserter, AddArg(_, _, Variadic::String(func_1), _));
- EXPECT_CALL(inserter, AddArg(_, _, Variadic::Integer(1), _));
-
context_.sorter->ExtractEventsForced();
+ EXPECT_EQ(storage_->slice_table().row_count(), 1u);
+ auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+ EXPECT_TRUE(rr_0);
+ EXPECT_EQ(rr_0->ts(), 1010000);
+ EXPECT_EQ(rr_0->track_id(), track);
+
EXPECT_GT(context_.storage->android_log_table().row_count(), 0u);
EXPECT_EQ(context_.storage->android_log_table()[0].ts(), 1010000);
EXPECT_EQ(context_.storage->android_log_table()[0].msg(), body_1);
@@ -2466,38 +2317,41 @@
::testing::Mock::VerifyAndClearExpectations(storage_);
// Verify raw_table and args contents.
- const auto& raw_table = storage_->raw_table();
+ const auto& raw_table = storage_->chrome_raw_table();
EXPECT_EQ(raw_table.row_count(), 1u);
EXPECT_EQ(raw_table[0].ts(), 1010000);
EXPECT_EQ(raw_table[0].name(),
storage_->InternString("track_event.legacy_event"));
- auto ucpu = raw_table[0].ucpu();
- const auto& cpu_table = storage_->cpu_table();
- EXPECT_EQ(cpu_table[ucpu.value].cpu(), 0u);
EXPECT_EQ(raw_table[0].utid(), 1u);
- EXPECT_EQ(raw_table[0].arg_set_id(), 2u);
+ EXPECT_TRUE(raw_table[0].arg_set_id());
- EXPECT_TRUE(HasArg(2u, storage_->InternString("legacy_event.category"),
+ uint32_t arg_set_id = raw_table[0].arg_set_id();
+ EXPECT_TRUE(HasArg(arg_set_id,
+ storage_->InternString("legacy_event.category"),
Variadic::String(cat_1)));
- EXPECT_TRUE(HasArg(2u, storage_->InternString("legacy_event.name"),
+ EXPECT_TRUE(HasArg(arg_set_id, storage_->InternString("legacy_event.name"),
Variadic::String(ev_1)));
- EXPECT_TRUE(HasArg(2u, storage_->InternString("legacy_event.phase"),
+ EXPECT_TRUE(HasArg(arg_set_id, storage_->InternString("legacy_event.phase"),
Variadic::String(question)));
- EXPECT_TRUE(HasArg(2u, storage_->InternString("legacy_event.duration_ns"),
+ EXPECT_TRUE(HasArg(arg_set_id,
+ storage_->InternString("legacy_event.duration_ns"),
Variadic::Integer(23000)));
- EXPECT_TRUE(HasArg(2u,
+ EXPECT_TRUE(HasArg(arg_set_id,
storage_->InternString("legacy_event.thread_timestamp_ns"),
Variadic::Integer(2005000)));
- EXPECT_TRUE(HasArg(2u,
+ EXPECT_TRUE(HasArg(arg_set_id,
storage_->InternString("legacy_event.thread_duration_ns"),
Variadic::Integer(15000)));
- EXPECT_TRUE(HasArg(2u, storage_->InternString("legacy_event.use_async_tts"),
+ EXPECT_TRUE(HasArg(arg_set_id,
+ storage_->InternString("legacy_event.use_async_tts"),
Variadic::Boolean(true)));
- EXPECT_TRUE(HasArg(2u, storage_->InternString("legacy_event.global_id"),
+ EXPECT_TRUE(HasArg(arg_set_id,
+ storage_->InternString("legacy_event.global_id"),
Variadic::UnsignedInteger(99u)));
- EXPECT_TRUE(HasArg(2u, storage_->InternString("legacy_event.id_scope"),
+ EXPECT_TRUE(HasArg(arg_set_id,
+ storage_->InternString("legacy_event.id_scope"),
Variadic::String(scope_1)));
- EXPECT_TRUE(HasArg(2u, debug_an_1, Variadic::UnsignedInteger(10u)));
+ EXPECT_TRUE(HasArg(arg_set_id, debug_an_1, Variadic::UnsignedInteger(10u)));
}
TEST_F(ProtoTraceParserTest, TrackEventLegacyTimestampsWithClockSnapshot) {
@@ -2533,12 +2387,14 @@
storage_->mutable_thread_table()->Insert(row);
constexpr TrackId track{0u};
- InSequence in_sequence; // Below slices should be sorted by timestamp.
-
- // Timestamp should be adjusted to trace time (BOOTTIME).
- EXPECT_CALL(*slice_, StartSlice(10000, track, _, _));
context_.sorter->ExtractEventsForced();
+
+ EXPECT_EQ(storage_->slice_table().row_count(), 1u);
+ auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+ EXPECT_TRUE(rr_0);
+ EXPECT_EQ(rr_0->ts(), 10000);
+ EXPECT_EQ(rr_0->track_id(), track);
}
TEST_F(ProtoTraceParserTest, ParseEventWithClockIdButWithoutClockSnapshot) {
@@ -2558,7 +2414,7 @@
context_.sorter->ExtractEventsForced();
// Metadata should have created a raw event.
- const auto& raw_table = storage_->raw_table();
+ const auto& raw_table = storage_->chrome_raw_table();
EXPECT_EQ(raw_table.row_count(), 1u);
}
@@ -2586,16 +2442,16 @@
context_.sorter->ExtractEventsForced();
// Verify raw_table and args contents.
- const auto& raw_table = storage_->raw_table();
+ const auto& raw_table = storage_->chrome_raw_table();
EXPECT_EQ(raw_table.row_count(), 1u);
EXPECT_EQ(raw_table[0].name(),
storage_->InternString("chrome_event.metadata"));
- EXPECT_EQ(raw_table[0].arg_set_id(), 1u);
+ uint32_t arg_set_id = raw_table[0].arg_set_id();
EXPECT_EQ(storage_->arg_table().row_count(), 2u);
- EXPECT_TRUE(HasArg(1u, storage_->InternString(kStringName),
+ EXPECT_TRUE(HasArg(arg_set_id, storage_->InternString(kStringName),
Variadic::String(storage_->InternString(kStringValue))));
- EXPECT_TRUE(HasArg(1u, storage_->InternString(kIntName),
+ EXPECT_TRUE(HasArg(arg_set_id, storage_->InternString(kIntName),
Variadic::Integer(kIntValue)));
}
@@ -2617,14 +2473,14 @@
context_.sorter->ExtractEventsForced();
// Verify raw_table and args contents.
- const auto& raw_table = storage_->raw_table();
+ const auto& raw_table = storage_->chrome_raw_table();
EXPECT_EQ(raw_table.row_count(), 1u);
EXPECT_EQ(raw_table[0].name(),
storage_->InternString("chrome_event.legacy_system_trace"));
- EXPECT_EQ(raw_table[0].arg_set_id(), 1u);
EXPECT_EQ(storage_->arg_table().row_count(), 1u);
- EXPECT_TRUE(HasArg(1u, storage_->InternString("data"),
+ uint32_t arg_set_id = raw_table[0].arg_set_id();
+ EXPECT_TRUE(HasArg(arg_set_id, storage_->InternString("data"),
Variadic::String(storage_->InternString(kFullData))));
}
@@ -2645,15 +2501,15 @@
context_.sorter->ExtractEventsForced();
// Verify raw_table and args contents.
- const auto& raw_table = storage_->raw_table();
+ const auto& raw_table = storage_->chrome_raw_table();
EXPECT_EQ(raw_table.row_count(), 1u);
EXPECT_EQ(raw_table[0].name(),
storage_->InternString("chrome_event.legacy_user_trace"));
- EXPECT_EQ(raw_table[0].arg_set_id(), 1u);
+ uint32_t arg_set_id = raw_table[0].arg_set_id();
EXPECT_EQ(storage_->arg_table().row_count(), 1u);
EXPECT_TRUE(
- HasArg(1u, storage_->InternString("data"),
+ HasArg(arg_set_id, storage_->InternString("data"),
Variadic::String(storage_->InternString(kUserTraceEvent))));
}
diff --git a/src/trace_processor/importers/proto/proto_trace_reader_unittest.cc b/src/trace_processor/importers/proto/proto_trace_reader_unittest.cc
index 166640c..7c111ce 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader_unittest.cc
+++ b/src/trace_processor/importers/proto/proto_trace_reader_unittest.cc
@@ -44,7 +44,7 @@
proto_trace_reader_ = std::make_unique<ProtoTraceReader>(&context_);
}
- util::Status Tokenize() {
+ base::Status Tokenize() {
trace_->Finalize();
std::vector<uint8_t> trace_bytes = trace_.SerializeAsArray();
std::unique_ptr<uint8_t[]> raw_trace(new uint8_t[trace_bytes.size()]);
diff --git a/src/trace_processor/importers/proto/proto_trace_tokenizer.cc b/src/trace_processor/importers/proto/proto_trace_tokenizer.cc
index b6b5937..67414bc 100644
--- a/src/trace_processor/importers/proto/proto_trace_tokenizer.cc
+++ b/src/trace_processor/importers/proto/proto_trace_tokenizer.cc
@@ -24,7 +24,7 @@
ProtoTraceTokenizer::ProtoTraceTokenizer() = default;
-util::Status ProtoTraceTokenizer::Decompress(TraceBlobView input,
+base::Status ProtoTraceTokenizer::Decompress(TraceBlobView input,
TraceBlobView* output) {
PERFETTO_DCHECK(util::IsGzipSupported());
@@ -41,13 +41,13 @@
});
if (ret == ResultCode::kError || ret == ResultCode::kNeedsMoreInput) {
- return util::ErrStatus("Failed to decompress (error code: %d)",
+ return base::ErrStatus("Failed to decompress (error code: %d)",
static_cast<int>(ret));
}
TraceBlob out_blob = TraceBlob::CopyFrom(data.data(), data.size());
*output = TraceBlobView(std::move(out_blob));
- return util::OkStatus();
+ return base::OkStatus();
}
} // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/statsd_module.cc b/src/trace_processor/importers/proto/statsd_module.cc
index 36d4fb3..5897f7b 100644
--- a/src/trace_processor/importers/proto/statsd_module.cc
+++ b/src/trace_processor/importers/proto/statsd_module.cc
@@ -37,7 +37,6 @@
#include "src/trace_processor/importers/common/args_tracker.h"
#include "src/trace_processor/importers/common/parser_types.h"
#include "src/trace_processor/importers/common/slice_tracker.h"
-#include "src/trace_processor/importers/common/track_compressor.h"
#include "src/trace_processor/importers/common/track_tracker.h"
#include "src/trace_processor/importers/common/tracks.h"
#include "src/trace_processor/importers/proto/args_parser.h"
@@ -108,23 +107,17 @@
using perfetto::protos::pbzero::StatsdAtom;
using perfetto::protos::pbzero::TracePacket;
-PoolAndDescriptor::PoolAndDescriptor(const uint8_t* data,
- size_t size,
- const char* name) {
- pool_.AddFromFileDescriptorSet(data, size);
- std::optional<uint32_t> opt_idx = pool_.FindDescriptorIdx(name);
- if (opt_idx.has_value()) {
- descriptor_ = &pool_.descriptors()[opt_idx.value()];
- }
-}
-
-PoolAndDescriptor::~PoolAndDescriptor() = default;
-
StatsdModule::StatsdModule(TraceProcessorContext* context)
- : context_(context),
- pool_(kAtomsDescriptor.data(), kAtomsDescriptor.size(), kAtomProtoName),
- args_parser_(*(pool_.pool())) {
+ : context_(context), args_parser_(*context_->descriptor_pool_) {
RegisterForField(TracePacket::kStatsdAtomFieldNumber, context);
+ context_->descriptor_pool_->AddFromFileDescriptorSet(kAtomsDescriptor.data(),
+ kAtomsDescriptor.size());
+
+ std::optional<uint32_t> opt_idx =
+ context_->descriptor_pool_->FindDescriptorIdx(kAtomProtoName);
+ if (opt_idx.has_value()) {
+ descriptor_ = &context_->descriptor_pool_->descriptors()[opt_idx.value()];
+ }
}
StatsdModule::~StatsdModule() = default;
@@ -204,7 +197,7 @@
[&](ArgsTracker::BoundInserter* inserter) {
ArgsParser delegate(ts, *inserter, *context_->storage);
- const auto& fields = pool_.descriptor()->fields();
+ const auto& fields = descriptor_->fields();
const auto& field_it = fields.find(nested_field_id);
base::Status status;
@@ -232,13 +225,13 @@
StringId StatsdModule::GetAtomName(uint32_t atom_field_id) {
StringId* cached_name = atom_names_.Find(atom_field_id);
if (cached_name == nullptr) {
- if (pool_.descriptor() == nullptr) {
+ if (descriptor_ == nullptr) {
context_->storage->IncrementStats(stats::atom_unknown);
return context_->storage->InternString("Could not load atom descriptor");
}
StringId name_id;
- const auto& fields = pool_.descriptor()->fields();
+ const auto& fields = descriptor_->fields();
const auto& field_it = fields.find(atom_field_id);
if (field_it == fields.end()) {
base::StackString<255> name("atom_%u", atom_field_id);
diff --git a/src/trace_processor/importers/proto/statsd_module.h b/src/trace_processor/importers/proto/statsd_module.h
index bfcc86c..1d8658a 100644
--- a/src/trace_processor/importers/proto/statsd_module.h
+++ b/src/trace_processor/importers/proto/statsd_module.h
@@ -25,7 +25,6 @@
#include "perfetto/trace_processor/ref_counted.h"
#include "protos/perfetto/trace/trace_packet.pbzero.h"
#include "src/trace_processor/importers/common/trace_parser.h"
-#include "src/trace_processor/importers/common/track_compressor.h"
#include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
#include "src/trace_processor/importers/proto/proto_importer_module.h"
#include "src/trace_processor/storage/trace_storage.h"
@@ -35,27 +34,6 @@
namespace perfetto::trace_processor {
-// Wraps a DescriptorPool and a pointer into that pool. This prevents
-// common bugs where moving/changing the pool invalidates the pointer.
-class PoolAndDescriptor {
- public:
- PoolAndDescriptor(const uint8_t* data, size_t size, const char* name);
- virtual ~PoolAndDescriptor();
-
- PoolAndDescriptor(const PoolAndDescriptor&) = delete;
- PoolAndDescriptor& operator=(const PoolAndDescriptor&) = delete;
- PoolAndDescriptor(PoolAndDescriptor&&) = delete;
- PoolAndDescriptor& operator=(PoolAndDescriptor&&) = delete;
-
- const DescriptorPool* pool() const { return &pool_; }
-
- const ProtoDescriptor* descriptor() const { return descriptor_; }
-
- private:
- DescriptorPool pool_;
- const ProtoDescriptor* descriptor_{};
-};
-
class StatsdModule : public ProtoImporterModule {
public:
explicit StatsdModule(TraceProcessorContext* context);
@@ -80,7 +58,7 @@
TraceProcessorContext* context_;
base::FlatHashMap<uint32_t, StringId> atom_names_;
- PoolAndDescriptor pool_;
+ const ProtoDescriptor* descriptor_ = nullptr;
util::ProtoToArgsParser args_parser_;
std::optional<TrackId> track_id_;
};
diff --git a/src/trace_processor/importers/proto/track_event_module.cc b/src/trace_processor/importers/proto/track_event_module.cc
index 0ae00f2..80912f5 100644
--- a/src/trace_processor/importers/proto/track_event_module.cc
+++ b/src/trace_processor/importers/proto/track_event_module.cc
@@ -22,8 +22,11 @@
#include "perfetto/trace_processor/ref_counted.h"
#include "perfetto/trace_processor/trace_blob_view.h"
#include "src/trace_processor/importers/common/parser_types.h"
+#include "src/trace_processor/importers/proto/android_track_event.descriptor.h"
+#include "src/trace_processor/importers/proto/chrome_track_event.descriptor.h"
#include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
#include "src/trace_processor/importers/proto/proto_importer_module.h"
+#include "src/trace_processor/importers/proto/track_event.descriptor.h"
#include "src/trace_processor/importers/proto/track_event_tracker.h"
#include "src/trace_processor/types/trace_processor_context.h"
@@ -42,6 +45,13 @@
RegisterForField(TracePacket::kTrackDescriptorFieldNumber, context);
RegisterForField(TracePacket::kThreadDescriptorFieldNumber, context);
RegisterForField(TracePacket::kProcessDescriptorFieldNumber, context);
+
+ context->descriptor_pool_->AddFromFileDescriptorSet(
+ kTrackEventDescriptor.data(), kTrackEventDescriptor.size());
+ context->descriptor_pool_->AddFromFileDescriptorSet(
+ kChromeTrackEventDescriptor.data(), kChromeTrackEventDescriptor.size());
+ context->descriptor_pool_->AddFromFileDescriptorSet(
+ kAndroidTrackEventDescriptor.data(), kAndroidTrackEventDescriptor.size());
}
TrackEventModule::~TrackEventModule() = default;
diff --git a/src/trace_processor/importers/proto/track_event_parser.cc b/src/trace_processor/importers/proto/track_event_parser.cc
index 4833a74..b9ea5e5 100644
--- a/src/trace_processor/importers/proto/track_event_parser.cc
+++ b/src/trace_processor/importers/proto/track_event_parser.cc
@@ -53,6 +53,7 @@
#include "src/trace_processor/importers/proto/track_event_tracker.h"
#include "src/trace_processor/storage/stats.h"
#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
#include "src/trace_processor/tables/slice_tables_py.h"
#include "src/trace_processor/types/variadic.h"
#include "src/trace_processor/util/debug_annotation_parser.h"
@@ -66,7 +67,6 @@
#include "protos/perfetto/trace/track_event/chrome_histogram_sample.pbzero.h"
#include "protos/perfetto/trace/track_event/chrome_process_descriptor.pbzero.h"
#include "protos/perfetto/trace/track_event/chrome_thread_descriptor.pbzero.h"
-#include "protos/perfetto/trace/track_event/counter_descriptor.pbzero.h"
#include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
#include "protos/perfetto/trace/track_event/log_message.pbzero.h"
#include "protos/perfetto/trace/track_event/process_descriptor.pbzero.h"
@@ -450,7 +450,8 @@
upid_ = storage_->thread_table()[*utid_].upid();
track_id_ = track_tracker->InternThreadTrack(*utid_);
} else {
- track_id_ = track_event_tracker_->GetOrCreateDefaultDescriptorTrack();
+ track_id_ = *track_event_tracker_->GetDescriptorTrack(
+ TrackEventTracker::kDefaultDescriptorTrackUuid);
}
}
@@ -689,12 +690,18 @@
"TrackEvent with phase B without thread association");
}
- auto* thread_slices = storage_->mutable_slice_table();
- auto opt_slice_id = context_->slice_tracker->BeginTyped(
- thread_slices, MakeThreadSliceRow(),
+ auto opt_slice_id = context_->slice_tracker->Begin(
+ ts_, track_id_, category_id_, name_id_,
[this](BoundInserter* inserter) { ParseTrackEventArgs(inserter); });
-
if (opt_slice_id.has_value()) {
+ auto rr =
+ context_->storage->mutable_slice_table()->FindById(*opt_slice_id);
+ if (thread_timestamp_) {
+ rr->set_thread_ts(*thread_timestamp_);
+ }
+ if (thread_instruction_count_) {
+ rr->set_thread_instruction_count(*thread_instruction_count_);
+ }
MaybeParseFlowEvents(opt_slice_id.value());
}
return base::OkStatus();
@@ -747,20 +754,22 @@
if (duration_ns < 0)
return base::ErrStatus("TrackEvent with phase X with negative duration");
- auto* thread_slices = storage_->mutable_slice_table();
- tables::SliceTable::Row row = MakeThreadSliceRow();
- row.dur = duration_ns;
- if (legacy_event_.has_thread_duration_us()) {
- row.thread_dur = legacy_event_.thread_duration_us() * 1000;
- }
- if (legacy_event_.has_thread_instruction_delta()) {
- row.thread_instruction_delta = legacy_event_.thread_instruction_delta();
- }
- auto opt_slice_id = context_->slice_tracker->ScopedTyped(
- thread_slices, row,
+ auto opt_slice_id = context_->slice_tracker->Scoped(
+ ts_, track_id_, category_id_, name_id_, duration_ns,
[this](BoundInserter* inserter) { ParseTrackEventArgs(inserter); });
-
if (opt_slice_id.has_value()) {
+ auto rr =
+ context_->storage->mutable_slice_table()->FindById(*opt_slice_id);
+ PERFETTO_CHECK(rr);
+ if (thread_timestamp_) {
+ rr->set_thread_ts(*thread_timestamp_);
+ rr->set_thread_dur(legacy_event_.thread_duration_us() * 1000);
+ }
+ if (thread_instruction_count_) {
+ rr->set_thread_instruction_count(*thread_instruction_count_);
+ rr->set_thread_instruction_delta(
+ legacy_event_.thread_instruction_delta());
+ }
MaybeParseFlowEvents(opt_slice_id.value());
}
return base::OkStatus();
@@ -879,25 +888,23 @@
Variadic::String(phase_id));
}
};
+ opt_slice_id =
+ context_->slice_tracker->Scoped(ts_, track_id_, category_id_, name_id_,
+ duration_ns, std::move(args_inserter));
+ if (!opt_slice_id) {
+ return base::OkStatus();
+ }
if (utid_) {
- auto* thread_slices = storage_->mutable_slice_table();
- tables::SliceTable::Row row = MakeThreadSliceRow();
- row.dur = duration_ns;
+ auto rr =
+ context_->storage->mutable_slice_table()->FindById(*opt_slice_id);
if (thread_timestamp_) {
- row.thread_dur = duration_ns;
+ rr->set_thread_ts(*thread_timestamp_);
+ rr->set_thread_dur(duration_ns);
}
if (thread_instruction_count_) {
- row.thread_instruction_delta = tidelta;
+ rr->set_thread_instruction_count(*thread_instruction_count_);
+ rr->set_thread_instruction_delta(tidelta);
}
- opt_slice_id = context_->slice_tracker->ScopedTyped(
- thread_slices, row, std::move(args_inserter));
- } else {
- opt_slice_id = context_->slice_tracker->Scoped(
- ts_, track_id_, category_id_, name_id_, duration_ns,
- std::move(args_inserter));
- }
- if (!opt_slice_id.has_value()) {
- return base::OkStatus();
}
MaybeParseFlowEvents(opt_slice_id.value());
return base::OkStatus();
@@ -1059,10 +1066,9 @@
if (!utid_)
return base::ErrStatus("raw legacy event without thread association");
- auto ucpu = context_->cpu_tracker->GetOrCreateCpu(0);
- RawId id =
- storage_->mutable_raw_table()
- ->Insert({ts_, parser_->raw_legacy_event_id_, *utid_, 0, 0, ucpu})
+ tables::ChromeRawTable::Id id =
+ storage_->mutable_chrome_raw_table()
+ ->Insert({ts_, parser_->raw_legacy_event_id_, *utid_, 0})
.id;
auto inserter = context_->args_tracker->AddArgsTo(id);
@@ -1336,19 +1342,6 @@
return base::OkStatus();
}
- tables::SliceTable::Row MakeThreadSliceRow() {
- tables::SliceTable::Row row;
- row.ts = ts_;
- row.track_id = track_id_;
- row.category = category_id_;
- row.name = name_id_;
- row.thread_ts = thread_timestamp_;
- row.thread_dur = std::nullopt;
- row.thread_instruction_count = thread_instruction_count_;
- row.thread_instruction_delta = std::nullopt;
- return row;
- }
-
TraceProcessorContext* context_;
TrackEventTracker* track_event_tracker_;
TraceStorage* storage_;
@@ -1554,13 +1547,15 @@
if (decoder.has_thread()) {
UniqueTid utid = ParseThreadDescriptor(decoder.thread());
- if (decoder.has_chrome_thread())
+ if (decoder.has_chrome_thread()) {
ParseChromeThreadDescriptor(utid, decoder.chrome_thread());
+ }
} else if (decoder.has_process()) {
UniquePid upid =
ParseProcessDescriptor(packet_timestamp, decoder.process());
- if (decoder.has_chrome_process())
+ if (decoder.has_chrome_process()) {
ParseChromeProcessDescriptor(upid, decoder.chrome_process());
+ }
}
// Override the name with the most recent name seen (after sorting by ts).
@@ -1572,7 +1567,7 @@
} else if (decoder.has_atrace_name()) {
name = decoder.atrace_name();
}
- if (name.data != nullptr) {
+ if (name.data) {
auto* tracks = context_->storage->mutable_track_table();
const StringId raw_name_id = context_->storage->InternString(name);
const StringId name_id =
diff --git a/src/trace_processor/importers/proto/track_event_tokenizer.cc b/src/trace_processor/importers/proto/track_event_tokenizer.cc
index bc7417f..7f77d60 100644
--- a/src/trace_processor/importers/proto/track_event_tokenizer.cc
+++ b/src/trace_processor/importers/proto/track_event_tokenizer.cc
@@ -88,7 +88,7 @@
context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
return ModuleResult::Handled();
}
- track_event_tracker_->SetRangeOfInterestStartUs(range_of_interest.start_us());
+ track_event_tracker_->set_range_of_interest_us(range_of_interest.start_us());
context_->metadata_tracker->SetMetadata(
metadata::range_of_interest_start_us,
Variadic::Integer(range_of_interest.start_us()));
@@ -241,9 +241,12 @@
auto unit = static_cast<uint32_t>(counter.unit());
if (counter.type() == CounterDescriptor::COUNTER_THREAD_TIME_NS) {
counter_details.unit = counter_unit_ids_[CounterDescriptor::UNIT_TIME_NS];
+ counter_details.builtin_type_str = counter_name_thread_time_id_;
} else if (counter.type() ==
CounterDescriptor::COUNTER_THREAD_INSTRUCTION_COUNT) {
counter_details.unit = counter_unit_ids_[CounterDescriptor::UNIT_COUNT];
+ counter_details.builtin_type_str =
+ counter_name_thread_instruction_count_id_;
} else if (unit < counter_unit_ids_.size() &&
unit != CounterDescriptor::COUNTER_UNSPECIFIED) {
counter_details.unit = counter_unit_ids_[unit];
diff --git a/src/trace_processor/importers/proto/track_event_tracker.cc b/src/trace_processor/importers/proto/track_event_tracker.cc
index 227b3d8..3a5c213 100644
--- a/src/trace_processor/importers/proto/track_event_tracker.cc
+++ b/src/trace_processor/importers/proto/track_event_tracker.cc
@@ -17,15 +17,13 @@
#include "src/trace_processor/importers/proto/track_event_tracker.h"
#include <algorithm>
+#include <array>
#include <cinttypes>
#include <cstddef>
#include <cstdint>
-#include <map>
#include <memory>
#include <optional>
-#include <tuple>
-#include <utility>
-#include <vector>
+#include <unordered_set>
#include "perfetto/base/logging.h"
#include "src/trace_processor/importers/common/args_tracker.h"
@@ -59,7 +57,7 @@
tracks::DynamicNameBlueprint());
constexpr auto kGlobalCounterTrackBlueprint = tracks::CounterBlueprint(
- "global_track_event",
+ "global_counter_track_event",
tracks::DynamicUnitBlueprint(),
tracks::DimensionBlueprints(tracks::LongDimensionBlueprint("track_uuid")),
tracks::DynamicNameBlueprint());
@@ -88,6 +86,8 @@
source_id_key_(context->storage->InternString("trace_id")),
is_root_in_scope_key_(context->storage->InternString("is_root_in_scope")),
category_key_(context->storage->InternString("category")),
+ builtin_counter_type_key_(
+ context->storage->InternString("builtin_counter_type")),
has_first_packet_on_sequence_key_id_(
context->storage->InternString("has_first_packet_on_sequence")),
child_ordering_key_(context->storage->InternString("child_ordering")),
@@ -104,293 +104,165 @@
void TrackEventTracker::ReserveDescriptorTrack(
uint64_t uuid,
const DescriptorTrackReservation& reservation) {
- std::map<uint64_t, DescriptorTrackReservation>::iterator it;
- bool inserted;
- std::tie(it, inserted) =
- reserved_descriptor_tracks_.insert(std::make_pair<>(uuid, reservation));
-
- if (inserted)
+ if (uuid == kDefaultDescriptorTrackUuid && reservation.parent_uuid) {
+ PERFETTO_DLOG(
+ "Default track (uuid 0) cannot have a parent uui specified. Ignoring "
+ "the descriptor.");
+ context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
return;
+ }
- if (!it->second.IsForSameTrack(reservation)) {
- PERFETTO_DLOG("New track reservation for process track with uuid %" PRIu64
+ auto [it, inserted] = reserved_descriptor_tracks_.Insert(uuid, reservation);
+ if (inserted) {
+ return;
+ }
+
+ if (!it->IsForSameTrack(reservation)) {
+ PERFETTO_DLOG("New track reservation for track with uuid %" PRIu64
" doesn't match earlier one",
uuid);
context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
return;
}
- it->second.min_timestamp =
- std::min(it->second.min_timestamp, reservation.min_timestamp);
+ it->min_timestamp = std::min(it->min_timestamp, reservation.min_timestamp);
}
-std::optional<TrackId> TrackEventTracker::GetDescriptorTrack(
+std::optional<TrackEventTracker::ResolvedDescriptorTrack>
+TrackEventTracker::GetDescriptorTrackImpl(
uint64_t uuid,
StringId event_name,
std::optional<uint32_t> packet_sequence_id) {
- std::optional<TrackId> track_id =
- GetDescriptorTrackImpl(uuid, packet_sequence_id);
- if (!track_id || event_name.is_null())
- return track_id;
+ auto* resolved_ptr = resolved_descriptor_tracks_.Find(uuid);
+ if (resolved_ptr) {
+ if (event_name.is_null()) {
+ return *resolved_ptr;
+ }
- // Update the name of the track if unset and the track is not the primary
- // track of a process/thread or a counter track.
- auto rr = *context_->storage->mutable_track_table()->FindById(*track_id);
- if (!rr.name().is_null()) {
- return track_id;
- }
-
- // Check reservation for track type.
- auto reservation_it = reserved_descriptor_tracks_.find(uuid);
- PERFETTO_CHECK(reservation_it != reserved_descriptor_tracks_.end());
-
- if (reservation_it->second.pid || reservation_it->second.tid ||
- reservation_it->second.is_counter) {
- return track_id;
- }
- rr.set_name(
- context_->process_track_translation_table->TranslateName(event_name));
- return track_id;
-}
-
-std::optional<TrackId> TrackEventTracker::GetDescriptorTrackImpl(
- uint64_t uuid,
- std::optional<uint32_t> packet_sequence_id) {
- auto it = descriptor_tracks_.find(uuid);
- if (it != descriptor_tracks_.end())
- return it->second;
-
- std::optional<ResolvedDescriptorTrack> resolved_track =
- ResolveDescriptorTrack(uuid, nullptr);
- if (!resolved_track)
- return std::nullopt;
-
- // The reservation must exist as |resolved_track| would have been std::nullopt
- // otherwise.
- auto reserved_it = reserved_descriptor_tracks_.find(uuid);
- PERFETTO_CHECK(reserved_it != reserved_descriptor_tracks_.end());
-
- const auto& reservation = reserved_it->second;
-
- // We resolve parent_id here to ensure that it's going to be smaller
- // than the id of the child.
- std::optional<TrackId> parent_id;
- if (reservation.parent_uuid != 0) {
- parent_id = GetDescriptorTrackImpl(reservation.parent_uuid);
- }
-
- TrackId track_id = CreateTrackFromResolved(uuid, packet_sequence_id,
- reservation, *resolved_track);
- descriptor_tracks_[uuid] = track_id;
-
- auto row_ref = *context_->storage->mutable_track_table()->FindById(track_id);
- if (!row_ref.source_arg_set_id().has_value()) {
- auto inserter = context_->args_tracker->AddArgsTo(track_id);
- AddTrackArgs(uuid, packet_sequence_id, reservation, *resolved_track,
- inserter);
- }
- if (parent_id) {
- row_ref.set_parent_id(*parent_id);
- }
- if (!reservation.name.is_null()) {
- // Initialize the track name here, so that, if a name was given in the
- // reservation, it is set immediately after resolution takes place.
- row_ref.set_name(reservation.name);
- }
- return track_id;
-}
-
-TrackId TrackEventTracker::CreateTrackFromResolved(
- uint64_t uuid,
- std::optional<uint32_t> packet_sequence_id,
- const DescriptorTrackReservation& reservation,
- const ResolvedDescriptorTrack& track) {
- if (track.is_root_in_scope()) {
- switch (track.scope()) {
- case ResolvedDescriptorTrack::Scope::kThread: {
- if (track.use_separate_track()) {
- auto it = thread_tracks_.find(track.utid());
- if (it != thread_tracks_.end()) {
- return it->second;
- }
- TrackId id = context_->track_tracker->InternTrack(
- kThreadTrackBlueprint,
- tracks::Dimensions(track.utid(), static_cast<int64_t>(uuid)),
- tracks::DynamicName(kNullStringId));
- thread_tracks_[track.utid()] = id;
- return id;
- }
- return context_->track_tracker->InternThreadTrack(track.utid());
+ // Update the name to match the first non-null and valid event name. We need
+ // this because TrackEventParser calls |GetDescriptorTrack| with
+ // kNullStringId which means we cannot just have the code below for updating
+ // the name
+ DescriptorTrackReservation* reservation_ptr =
+ reserved_descriptor_tracks_.Find(uuid);
+ PERFETTO_CHECK(reservation_ptr);
+ auto* tracks = context_->storage->mutable_track_table();
+ auto rr = *tracks->FindById(resolved_ptr->track_id());
+ bool is_root_thread_process_or_counter = reservation_ptr->pid ||
+ reservation_ptr->tid ||
+ reservation_ptr->is_counter;
+ if (rr.name().is_null() && !is_root_thread_process_or_counter) {
+ if (resolved_ptr->scope() == ResolvedDescriptorTrack::Scope::kProcess) {
+ rr.set_name(context_->process_track_translation_table->TranslateName(
+ event_name));
+ } else {
+ rr.set_name(event_name);
}
- case ResolvedDescriptorTrack::Scope::kProcess: {
- return context_->track_tracker->InternTrack(
- kProcessTrackBlueprint,
- tracks::Dimensions(track.upid(), static_cast<int64_t>(uuid)),
- tracks::DynamicName(kNullStringId));
- }
- case ResolvedDescriptorTrack::Scope::kGlobal:
- // Will be handled below.
- break;
}
+ return *resolved_ptr;
}
- if (track.is_counter()) {
- switch (track.scope()) {
- case ResolvedDescriptorTrack::Scope::kThread:
- return context_->track_tracker->InternTrack(
- kThreadCounterTrackBlueprint,
- tracks::Dimensions(track.utid(), static_cast<int64_t>(uuid)),
- tracks::DynamicName(kNullStringId),
- [&, this](ArgsTracker::BoundInserter& inserter) {
- AddTrackArgs(uuid, packet_sequence_id, reservation, track,
- inserter);
- },
- tracks::DynamicUnit(reservation.counter_details->unit));
- case ResolvedDescriptorTrack::Scope::kProcess:
- return context_->track_tracker->InternTrack(
- kProcessCounterTrackBlueprint,
- tracks::Dimensions(track.upid(), static_cast<int64_t>(uuid)),
- tracks::DynamicName(kNullStringId),
- [&, this](ArgsTracker::BoundInserter& inserter) {
- AddTrackArgs(uuid, packet_sequence_id, reservation, track,
- inserter);
- },
- tracks::DynamicUnit(reservation.counter_details->unit));
- case ResolvedDescriptorTrack::Scope::kGlobal:
- return context_->track_tracker->InternTrack(
- kGlobalCounterTrackBlueprint,
- tracks::Dimensions(static_cast<int64_t>(uuid)),
- tracks::DynamicName(kNullStringId),
- [&, this](ArgsTracker::BoundInserter& inserter) {
- AddTrackArgs(uuid, packet_sequence_id, reservation, track,
- inserter);
- },
- tracks::DynamicUnit(reservation.counter_details->unit));
+ DescriptorTrackReservation* reservation_ptr =
+ reserved_descriptor_tracks_.Find(uuid);
+ if (!reservation_ptr) {
+ if (uuid != kDefaultDescriptorTrackUuid) {
+ return std::nullopt;
}
+
+ // For the default track, if there's no reservation, forcefully create it
+ // as it's always allowed to emit events on it, even without emitting a
+ // descriptor.
+ DescriptorTrackReservation r;
+ r.parent_uuid = 0;
+ r.name = default_descriptor_track_name_;
+ ReserveDescriptorTrack(kDefaultDescriptorTrackUuid, r);
+
+ reservation_ptr = reserved_descriptor_tracks_.Find(uuid);
+ PERFETTO_CHECK(reservation_ptr);
}
- switch (track.scope()) {
- case ResolvedDescriptorTrack::Scope::kThread: {
- return context_->track_tracker->InternTrack(
- kThreadTrackBlueprint,
- tracks::Dimensions(track.utid(), static_cast<int64_t>(uuid)),
- tracks::DynamicName(kNullStringId));
- }
- case ResolvedDescriptorTrack::Scope::kProcess: {
- return context_->track_tracker->InternTrack(
- kProcessTrackBlueprint,
- tracks::Dimensions(track.upid(), static_cast<int64_t>(uuid)),
- tracks::DynamicName(kNullStringId));
- }
- case ResolvedDescriptorTrack::Scope::kGlobal: {
- return context_->track_tracker->InternTrack(
- kGlobalTrackBlueprint, tracks::Dimensions(static_cast<int64_t>(uuid)),
- tracks::DynamicName(kNullStringId));
- }
- }
- PERFETTO_FATAL("For GCC");
-}
-
-std::optional<TrackEventTracker::ResolvedDescriptorTrack>
-TrackEventTracker::ResolveDescriptorTrack(
- uint64_t uuid,
- std::vector<uint64_t>* descendent_uuids) {
- auto it = resolved_descriptor_tracks_.find(uuid);
- if (it != resolved_descriptor_tracks_.end())
- return it->second;
-
- auto reservation_it = reserved_descriptor_tracks_.find(uuid);
- if (reservation_it == reserved_descriptor_tracks_.end())
+ // Before trying to resolve anything, ensure that the hierarchy of tracks is
+ // well defined.
+ if (!IsTrackHierarchyValid(uuid)) {
return std::nullopt;
+ }
// Resolve process and thread id for tracks produced from within a pid
// namespace.
+ //
// Get the root-level trusted_pid for the process that produces the track
// event.
- auto opt_trusted_pid = context_->process_tracker->GetTrustedPid(uuid);
- auto& reservation = reservation_it->second;
+ std::optional<uint32_t> trusted_pid =
+ context_->process_tracker->GetTrustedPid(uuid);
+ DescriptorTrackReservation& reservation = *reservation_ptr;
+
// Try to resolve to root-level pid and tid if the process is pid-namespaced.
- if (opt_trusted_pid && reservation.tid) {
- auto opt_resolved_tid = context_->process_tracker->ResolveNamespacedTid(
- *opt_trusted_pid, *reservation.tid);
- if (opt_resolved_tid)
- reservation.tid = *opt_resolved_tid;
+ if (trusted_pid && reservation.tid) {
+ std::optional<uint32_t> resolved_tid =
+ context_->process_tracker->ResolveNamespacedTid(*trusted_pid,
+ *reservation.tid);
+ if (resolved_tid) {
+ reservation.tid = *resolved_tid;
+ }
}
- if (opt_trusted_pid && reservation.pid) {
- auto opt_resolved_pid = context_->process_tracker->ResolveNamespacedTid(
- *opt_trusted_pid, *reservation.pid);
- if (opt_resolved_pid)
- reservation.pid = *opt_resolved_pid;
+ if (trusted_pid && reservation.pid) {
+ std::optional<uint32_t> resolved_pid =
+ context_->process_tracker->ResolveNamespacedTid(*trusted_pid,
+ *reservation.pid);
+ if (resolved_pid) {
+ reservation.pid = *resolved_pid;
+ }
}
- std::optional<ResolvedDescriptorTrack> resolved_track =
- ResolveDescriptorTrackImpl(uuid, reservation, descendent_uuids);
- if (!resolved_track) {
- return std::nullopt;
+ bool is_root_thread_process_or_counter = reservation_ptr->pid ||
+ reservation_ptr->tid ||
+ reservation_ptr->is_counter;
+ if (reservation.name.is_null() && !is_root_thread_process_or_counter) {
+ reservation.name = event_name;
}
- resolved_descriptor_tracks_[uuid] = *resolved_track;
- return resolved_track;
+
+ // If the reservation does not have a name specified, name it the same
+ // as the first event on the track. Note this only applies for non-root and
+ // non-counter tracks.
+ auto [it, inserted] = resolved_descriptor_tracks_.Insert(
+ uuid, ResolveDescriptorTrack(uuid, reservation, packet_sequence_id));
+ PERFETTO_CHECK(inserted);
+ return *it;
}
-std::optional<TrackEventTracker::ResolvedDescriptorTrack>
-TrackEventTracker::ResolveDescriptorTrackImpl(
+TrackEventTracker::ResolvedDescriptorTrack
+TrackEventTracker::ResolveDescriptorTrack(
uint64_t uuid,
const DescriptorTrackReservation& reservation,
- std::vector<uint64_t>* descendent_uuids) {
- static constexpr size_t kMaxAncestors = 10;
+ std::optional<uint32_t> packet_sequence_id) {
+ TrackTracker::SetArgsCallback args_fn_root =
+ [&, this](ArgsTracker::BoundInserter& inserter) {
+ AddTrackArgs(uuid, packet_sequence_id, reservation, true /* is_root*/,
+ inserter);
+ };
+ TrackTracker::SetArgsCallback args_fn_non_root =
+ [&, this](ArgsTracker::BoundInserter& inserter) {
+ AddTrackArgs(uuid, packet_sequence_id, reservation, false /* is_root*/,
+ inserter);
+ };
// Try to resolve any parent tracks recursively, too.
std::optional<ResolvedDescriptorTrack> parent_resolved_track;
- if (reservation.parent_uuid) {
- // Input data may contain loops or extremely long ancestor track chains. To
- // avoid stack overflow in these situations, we keep track of the ancestors
- // seen in the recursion.
- std::unique_ptr<std::vector<uint64_t>> owned_descendent_uuids;
- if (!descendent_uuids) {
- owned_descendent_uuids = std::make_unique<std::vector<uint64_t>>();
- descendent_uuids = owned_descendent_uuids.get();
- }
- descendent_uuids->push_back(uuid);
-
- if (descendent_uuids->size() > kMaxAncestors) {
- PERFETTO_ELOG(
- "Too many ancestors in parent_track_uuid hierarchy at track %" PRIu64
- " with parent %" PRIu64,
- uuid, reservation.parent_uuid);
- return std::nullopt;
- }
-
- if (std::find(descendent_uuids->begin(), descendent_uuids->end(),
- reservation.parent_uuid) != descendent_uuids->end()) {
- PERFETTO_ELOG(
- "Loop detected in parent_track_uuid hierarchy at track %" PRIu64
- " with parent %" PRIu64,
- uuid, reservation.parent_uuid);
- return std::nullopt;
- }
-
- parent_resolved_track =
- ResolveDescriptorTrack(reservation.parent_uuid, descendent_uuids);
- if (!parent_resolved_track) {
- PERFETTO_ELOG("Unknown parent track %" PRIu64 " for track %" PRIu64,
- reservation.parent_uuid, uuid);
- }
-
- descendent_uuids->pop_back();
- if (owned_descendent_uuids)
- descendent_uuids = nullptr;
+ if (reservation.parent_uuid != kDefaultDescriptorTrackUuid) {
+ parent_resolved_track = GetDescriptorTrackImpl(
+ reservation.parent_uuid, kNullStringId, packet_sequence_id);
}
if (reservation.tid) {
UniqueTid utid = context_->process_tracker->UpdateThread(*reservation.tid,
*reservation.pid);
- auto it_and_inserted =
- descriptor_uuids_by_utid_.insert(std::make_pair<>(utid, uuid));
- if (!it_and_inserted.second) {
+ auto [it, inserted] = descriptor_uuids_by_utid_.Insert(utid, uuid);
+ if (!inserted) {
// We already saw a another track with a different uuid for this thread.
// Since there should only be one descriptor track for each thread, we
// assume that its tid was reused. So, start a new thread.
- uint64_t old_uuid = it_and_inserted.first->second;
+ uint64_t old_uuid = *it;
PERFETTO_DCHECK(old_uuid != uuid); // Every track is only resolved once.
+ *it = uuid;
PERFETTO_DLOG("Detected tid reuse (pid: %" PRIu32 " tid: %" PRIu32
") from track descriptors (old uuid: %" PRIu64
@@ -398,31 +270,46 @@
*reservation.pid, *reservation.tid, old_uuid, uuid,
reservation.min_timestamp);
+ // Associate the new thread with its process.
utid = context_->process_tracker->StartNewThread(std::nullopt,
*reservation.tid);
-
- // Associate the new thread with its process.
- PERFETTO_CHECK(context_->process_tracker->UpdateThread(
- *reservation.tid, *reservation.pid) == utid);
-
- descriptor_uuids_by_utid_[utid] = uuid;
+ UniqueTid updated_utid = context_->process_tracker->UpdateThread(
+ *reservation.tid, *reservation.pid);
+ PERFETTO_CHECK(updated_utid == utid);
}
- return ResolvedDescriptorTrack::Thread(utid, false /* is_counter */,
- true /* is_root*/,
- reservation.use_separate_track);
+
+ TrackId id;
+ if (reservation.is_counter) {
+ id = context_->track_tracker->InternTrack(
+ kThreadCounterTrackBlueprint,
+ tracks::Dimensions(utid, static_cast<int64_t>(uuid)),
+ tracks::DynamicName(reservation.name), args_fn_root,
+ tracks::DynamicUnit(reservation.counter_details->unit));
+ } else if (reservation.use_separate_track) {
+ id = context_->track_tracker->InternTrack(
+ kThreadTrackBlueprint,
+ tracks::Dimensions(utid, static_cast<int64_t>(uuid)),
+ tracks::DynamicName(reservation.name), args_fn_root);
+ } else {
+ id = context_->track_tracker->InternThreadTrack(utid);
+ return ResolvedDescriptorTrack::Thread(id, utid, reservation.is_counter,
+ true);
+ }
+ return ResolvedDescriptorTrack::Thread(id, utid, reservation.is_counter,
+ false);
}
if (reservation.pid) {
UniquePid upid =
context_->process_tracker->GetOrCreateProcess(*reservation.pid);
- auto it_and_inserted =
- descriptor_uuids_by_upid_.insert(std::make_pair<>(upid, uuid));
- if (!it_and_inserted.second) {
+ auto [it, inserted] = descriptor_uuids_by_upid_.Insert(upid, uuid);
+ if (!inserted) {
// We already saw a another track with a different uuid for this process.
- // Since there should only be one descriptor track for each process, we
- // assume that its pid was reused. So, start a new process.
- uint64_t old_uuid = it_and_inserted.first->second;
+ // Since there should only be one descriptor track for each process,
+ // we assume that its pid was reused. So, start a new process.
+ uint64_t old_uuid = *it;
PERFETTO_DCHECK(old_uuid != uuid); // Every track is only resolved once.
+ *it = uuid;
PERFETTO_DLOG("Detected pid reuse (pid: %" PRIu32
") from track descriptors (old uuid: %" PRIu64
@@ -433,85 +320,127 @@
upid = context_->process_tracker->StartNewProcess(
std::nullopt, std::nullopt, *reservation.pid, kNullStringId,
ThreadNamePriority::kTrackDescriptor);
-
- descriptor_uuids_by_upid_[upid] = uuid;
}
- return ResolvedDescriptorTrack::Process(upid, false /* is_counter */,
- true /* is_root*/);
+ StringId translated_name =
+ context_->process_track_translation_table->TranslateName(
+ reservation.name);
+ TrackId id;
+ if (reservation.is_counter) {
+ id = context_->track_tracker->InternTrack(
+ kProcessCounterTrackBlueprint,
+ tracks::Dimensions(upid, static_cast<int64_t>(uuid)),
+ tracks::DynamicName(translated_name), args_fn_root,
+ tracks::DynamicUnit(reservation.counter_details->unit));
+ } else {
+ id = context_->track_tracker->InternTrack(
+ kProcessTrackBlueprint,
+ tracks::Dimensions(upid, static_cast<int64_t>(uuid)),
+ tracks::DynamicName(translated_name), args_fn_root);
+ }
+ return ResolvedDescriptorTrack::Process(id, upid, reservation.is_counter);
}
+ auto set_parent_id = [&](TrackId id) {
+ if (parent_resolved_track) {
+ auto rr = context_->storage->mutable_track_table()->FindById(id);
+ PERFETTO_CHECK(rr);
+ rr->set_parent_id(parent_resolved_track->track_id());
+ }
+ };
+
if (parent_resolved_track) {
switch (parent_resolved_track->scope()) {
- case ResolvedDescriptorTrack::Scope::kThread:
+ case ResolvedDescriptorTrack::Scope::kThread: {
// If parent is a thread track, create another thread-associated track.
+ TrackId id;
+ if (reservation.is_counter) {
+ id = context_->track_tracker->InternTrack(
+ kThreadCounterTrackBlueprint,
+ tracks::Dimensions(parent_resolved_track->utid(),
+ static_cast<int64_t>(uuid)),
+ tracks::DynamicName(reservation.name), args_fn_non_root,
+ tracks::DynamicUnit(reservation.counter_details->unit));
+ } else {
+ id = context_->track_tracker->InternTrack(
+ kThreadTrackBlueprint,
+ tracks::Dimensions(parent_resolved_track->utid(),
+ static_cast<int64_t>(uuid)),
+ tracks::DynamicName(reservation.name), args_fn_non_root);
+ }
+ // If the parent is the default thread scoped track, promote this track
+ // to also be a root thread level track: this is because the default
+ // thread scoped track is *not* owned by track_event and so we cannot
+ // make ourselves a child of it without making the semantics very
+ // strange.
+ if (!parent_resolved_track->is_default_thead_slice_track()) {
+ set_parent_id(id);
+ }
return ResolvedDescriptorTrack::Thread(
- parent_resolved_track->utid(), reservation.is_counter,
- false /* is_root*/, parent_resolved_track->use_separate_track());
- case ResolvedDescriptorTrack::Scope::kProcess:
+ id, parent_resolved_track->utid(), reservation.is_counter, false);
+ }
+ case ResolvedDescriptorTrack::Scope::kProcess: {
// If parent is a process track, create another process-associated
// track.
- return ResolvedDescriptorTrack::Process(parent_resolved_track->upid(),
- reservation.is_counter,
- false /* is_root*/);
+ StringId translated_name =
+ context_->process_track_translation_table->TranslateName(
+ reservation.name);
+ TrackId id;
+ if (reservation.is_counter) {
+ id = context_->track_tracker->InternTrack(
+ kProcessCounterTrackBlueprint,
+ tracks::Dimensions(parent_resolved_track->upid(),
+ static_cast<int64_t>(uuid)),
+ tracks::DynamicName(translated_name), args_fn_non_root,
+ tracks::DynamicUnit(reservation.counter_details->unit));
+ } else {
+ id = context_->track_tracker->InternTrack(
+ kProcessTrackBlueprint,
+ tracks::Dimensions(parent_resolved_track->upid(),
+ static_cast<int64_t>(uuid)),
+ tracks::DynamicName(translated_name), args_fn_non_root);
+ }
+ set_parent_id(id);
+ return ResolvedDescriptorTrack::Process(
+ id, parent_resolved_track->upid(), reservation.is_counter);
+ }
case ResolvedDescriptorTrack::Scope::kGlobal:
break;
}
}
- // Otherwise create a global track.
-
- // The global track with no uuid is the default global track (e.g. for
- // global instant events). Any other global tracks are considered children
- // of the default track.
- bool is_root_in_scope = !parent_resolved_track;
- if (!parent_resolved_track && uuid) {
- // Detect loops where the default track has a parent that itself is a
- // global track (and thus should be parent of the default track).
- if (descendent_uuids &&
- std::find(descendent_uuids->begin(), descendent_uuids->end(),
- kDefaultDescriptorTrackUuid) != descendent_uuids->end()) {
- PERFETTO_ELOG(
- "Loop detected in parent_track_uuid hierarchy at track %" PRIu64
- " with parent %" PRIu64,
- uuid, kDefaultDescriptorTrackUuid);
- return std::nullopt;
- }
-
- // This track will be implicitly a child of the default global track.
- is_root_in_scope = false;
+ // root_in_scope only matters for legacy JSON export. This is somewhat related
+ // but intentionally distinct from our handling of parent_id relationships.
+ bool is_root_in_scope = uuid == kDefaultDescriptorTrackUuid;
+ TrackId id;
+ if (reservation.is_counter) {
+ id = context_->track_tracker->InternTrack(
+ kGlobalCounterTrackBlueprint,
+ tracks::Dimensions(static_cast<int64_t>(uuid)),
+ tracks::DynamicName(reservation.name),
+ is_root_in_scope ? args_fn_root : args_fn_non_root,
+ tracks::DynamicUnit(reservation.counter_details->unit));
+ } else {
+ id = context_->track_tracker->InternTrack(
+ kGlobalTrackBlueprint, tracks::Dimensions(static_cast<int64_t>(uuid)),
+ tracks::DynamicName(reservation.name),
+ is_root_in_scope ? args_fn_root : args_fn_non_root);
}
- return ResolvedDescriptorTrack::Global(reservation.is_counter,
- is_root_in_scope);
-}
-
-TrackId TrackEventTracker::GetOrCreateDefaultDescriptorTrack() {
- // If the default track was already reserved (e.g. because a producer emitted
- // a descriptor for it) or created, resolve and return it.
- std::optional<TrackId> track_id =
- GetDescriptorTrack(kDefaultDescriptorTrackUuid);
- if (track_id)
- return *track_id;
-
- // Otherwise reserve a new track and resolve it.
- DescriptorTrackReservation r;
- r.parent_uuid = 0;
- r.name = default_descriptor_track_name_;
- ReserveDescriptorTrack(kDefaultDescriptorTrackUuid, r);
- return *GetDescriptorTrack(kDefaultDescriptorTrackUuid);
+ set_parent_id(id);
+ return ResolvedDescriptorTrack::Global(id, reservation.is_counter);
}
std::optional<double> TrackEventTracker::ConvertToAbsoluteCounterValue(
uint64_t counter_track_uuid,
uint32_t packet_sequence_id,
double value) {
- auto reservation_it = reserved_descriptor_tracks_.find(counter_track_uuid);
- if (reservation_it == reserved_descriptor_tracks_.end()) {
+ auto* reservation_ptr = reserved_descriptor_tracks_.Find(counter_track_uuid);
+ if (!reservation_ptr) {
PERFETTO_DLOG("Unknown counter track with uuid %" PRIu64,
counter_track_uuid);
return std::nullopt;
}
- DescriptorTrackReservation& reservation = reservation_it->second;
+ DescriptorTrackReservation& reservation = *reservation_ptr;
if (!reservation.is_counter) {
PERFETTO_DLOG("Track with uuid %" PRIu64 " is not a counter track",
counter_track_uuid);
@@ -539,7 +468,6 @@
c_details.latest_value += value;
value = c_details.latest_value;
}
-
return value;
}
@@ -548,8 +476,8 @@
// of packet sequences, incremental state clearing at O(trace second), and
// total number of tracks in O(thousands), a linear scan through all tracks
// here might not be fast enough.
- for (auto& entry : reserved_descriptor_tracks_) {
- DescriptorTrackReservation& reservation = entry.second;
+ for (auto it = reserved_descriptor_tracks_.GetIterator(); it; ++it) {
+ DescriptorTrackReservation& reservation = it.value();
// Only consider incremental counter tracks for current sequence.
if (!reservation.is_counter || !reservation.counter_details ||
!reservation.counter_details->is_incremental ||
@@ -569,16 +497,22 @@
uint64_t uuid,
std::optional<uint32_t> packet_sequence_id,
const DescriptorTrackReservation& reservation,
- const ResolvedDescriptorTrack& track,
+ bool is_root_in_scope,
ArgsTracker::BoundInserter& args) {
args.AddArg(source_key_, Variadic::String(descriptor_source_))
.AddArg(source_id_key_, Variadic::Integer(static_cast<int64_t>(uuid)))
- .AddArg(is_root_in_scope_key_,
- Variadic::Boolean(track.is_root_in_scope()));
- if (reservation.counter_details &&
- !reservation.counter_details->category.is_null())
- args.AddArg(category_key_,
- Variadic::String(reservation.counter_details->category));
+ .AddArg(is_root_in_scope_key_, Variadic::Boolean(is_root_in_scope));
+ if (reservation.counter_details) {
+ if (!reservation.counter_details->category.is_null()) {
+ args.AddArg(category_key_,
+ Variadic::String(reservation.counter_details->category));
+ }
+ if (!reservation.counter_details->builtin_type_str.is_null()) {
+ args.AddArg(
+ builtin_counter_type_key_,
+ Variadic::String(reservation.counter_details->builtin_type_str));
+ }
+ }
if (packet_sequence_id &&
sequences_with_first_packet_.find(*packet_sequence_id) !=
sequences_with_first_packet_.end()) {
@@ -605,39 +539,68 @@
}
}
+bool TrackEventTracker::IsTrackHierarchyValid(uint64_t uuid) {
+ // Do a basic tree walking algorithm to find if there are duplicate ids or
+ // the path to the root is longer than kMaxAncestors.
+ static constexpr size_t kMaxAncestors = 10;
+ std::array<uint64_t, kMaxAncestors> seen;
+ uint64_t current_uuid = uuid;
+ for (uint32_t i = 0; i < kMaxAncestors; ++i) {
+ if (current_uuid == 0) {
+ return true;
+ }
+ for (uint32_t j = 0; j < i; ++j) {
+ if (current_uuid == seen[j]) {
+ PERFETTO_ELOG("Loop detected in hierarchy for track %" PRIu64, uuid);
+ return false;
+ }
+ }
+ auto* reservation_ptr = reserved_descriptor_tracks_.Find(current_uuid);
+ if (!reservation_ptr) {
+ PERFETTO_ELOG("Missing uuid in hierarchy for track %" PRIu64, uuid);
+ return false;
+ }
+ seen[i] = current_uuid;
+ current_uuid = reservation_ptr->parent_uuid;
+ }
+ PERFETTO_ELOG("Too many ancestors in hierarchy for track %" PRIu64, uuid);
+ return false;
+}
+
TrackEventTracker::ResolvedDescriptorTrack
-TrackEventTracker::ResolvedDescriptorTrack::Process(UniquePid upid,
- bool is_counter,
- bool is_root) {
+TrackEventTracker::ResolvedDescriptorTrack::Process(TrackId track_id,
+ UniquePid upid,
+ bool is_counter) {
ResolvedDescriptorTrack track;
+ track.track_id_ = track_id;
track.scope_ = Scope::kProcess;
track.is_counter_ = is_counter;
- track.is_root_in_scope_ = is_root;
track.upid_ = upid;
return track;
}
TrackEventTracker::ResolvedDescriptorTrack
-TrackEventTracker::ResolvedDescriptorTrack::Thread(UniqueTid utid,
- bool is_counter,
- bool is_root,
- bool use_separate_track) {
+TrackEventTracker::ResolvedDescriptorTrack::Thread(
+ TrackId track_id,
+ UniqueTid utid,
+ bool is_counter,
+ bool is_default_thead_slice_track) {
ResolvedDescriptorTrack track;
+ track.track_id_ = track_id;
track.scope_ = Scope::kThread;
track.is_counter_ = is_counter;
- track.is_root_in_scope_ = is_root;
track.utid_ = utid;
- track.use_separate_track_ = use_separate_track;
+ track.is_default_thead_slice_track_ = is_default_thead_slice_track;
return track;
}
TrackEventTracker::ResolvedDescriptorTrack
-TrackEventTracker::ResolvedDescriptorTrack::Global(bool is_counter,
- bool is_root) {
+TrackEventTracker::ResolvedDescriptorTrack::Global(TrackId track_id,
+ bool is_counter) {
ResolvedDescriptorTrack track;
+ track.track_id_ = track_id;
track.scope_ = Scope::kGlobal;
track.is_counter_ = is_counter;
- track.is_root_in_scope_ = is_root;
return track;
}
diff --git a/src/trace_processor/importers/proto/track_event_tracker.h b/src/trace_processor/importers/proto/track_event_tracker.h
index 7848eac..c035a8d 100644
--- a/src/trace_processor/importers/proto/track_event_tracker.h
+++ b/src/trace_processor/importers/proto/track_event_tracker.h
@@ -18,13 +18,13 @@
#define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_TRACKER_H_
#include <cstdint>
-#include <map>
#include <optional>
#include <tuple>
#include <unordered_set>
-#include <vector>
#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "protos/perfetto/trace/track_event/counter_descriptor.pbzero.h"
#include "src/trace_processor/importers/common/args_tracker.h"
#include "src/trace_processor/storage/trace_storage.h"
#include "src/trace_processor/types/trace_processor_context.h"
@@ -34,6 +34,8 @@
// Tracks and stores tracks based on track types, ids and scopes.
class TrackEventTracker {
public:
+ static constexpr uint64_t kDefaultDescriptorTrackUuid = 0u;
+
// Data from TrackDescriptor proto used to reserve a track before interning it
// with |TrackTracker|.
struct DescriptorTrackReservation {
@@ -51,19 +53,20 @@
uint32_t packet_sequence_id = 0;
double latest_value = 0;
StringId unit = kNullStringId;
+ StringId builtin_type_str;
- bool operator==(const CounterDetails& o) const {
+ bool IsForSameTrack(const CounterDetails& o) const {
return std::tie(category, unit_multiplier, is_incremental,
- packet_sequence_id, latest_value) ==
+ packet_sequence_id, builtin_type_str) ==
std::tie(o.category, o.unit_multiplier, o.is_incremental,
- o.packet_sequence_id, o.latest_value);
+ o.packet_sequence_id, o.builtin_type_str);
}
};
uint64_t parent_uuid = 0;
std::optional<uint32_t> pid;
std::optional<uint32_t> tid;
- int64_t min_timestamp = 0; // only set if |pid| and/or |tid| is set.
+ int64_t min_timestamp = 0;
StringId name = kNullStringId;
bool use_separate_track = false;
bool is_counter = false;
@@ -78,11 +81,16 @@
// Whether |other| is a valid descriptor for this track reservation. A track
// should always remain nested underneath its original parent.
bool IsForSameTrack(const DescriptorTrackReservation& other) {
- // Note that |min_timestamp|, |latest_value|, and |name| are ignored for
- // this comparison.
- return std::tie(parent_uuid, pid, tid, is_counter, counter_details) ==
- std::tie(other.parent_uuid, other.pid, other.tid, other.is_counter,
- other.counter_details);
+ if (counter_details.has_value() != other.counter_details.has_value()) {
+ return false;
+ }
+ if (counter_details &&
+ !counter_details->IsForSameTrack(*other.counter_details)) {
+ return false;
+ }
+ return std::tie(parent_uuid, pid, tid, is_counter) ==
+ std::tie(other.parent_uuid, other.pid, other.tid,
+ other.is_counter);
}
};
explicit TrackEventTracker(TraceProcessorContext*);
@@ -104,12 +112,16 @@
// the |uuid|. If the track is a child track and doesn't have a name yet,
// updates the track's name to event_name. Returns std::nullopt if no track
// for a descriptor with this |uuid| has been reserved.
- // TODO(lalitm): this method needs to be split up and moved back to
- // TrackTracker.
std::optional<TrackId> GetDescriptorTrack(
uint64_t uuid,
StringId event_name = kNullStringId,
- std::optional<uint32_t> packet_sequence_id = std::nullopt);
+ std::optional<uint32_t> packet_sequence_id = std::nullopt) {
+ auto res = GetDescriptorTrackImpl(uuid, event_name, packet_sequence_id);
+ if (!res) {
+ return std::nullopt;
+ }
+ return res->track_id();
+ }
// Converts the given counter value to an absolute value in the unit of the
// counter, applying incremental delta encoding or unit multipliers as
@@ -121,11 +133,6 @@
uint32_t packet_sequence_id,
double value);
- // Returns the ID of the implicit trace-global default TrackDescriptor track.
- // TODO(lalitm): this method needs to be moved back to TrackTracker once
- // GetDescriptorTrack is moved back.
- TrackId GetOrCreateDefaultDescriptorTrack();
-
// Called by ProtoTraceReader whenever incremental state is cleared on a
// packet sequence. Resets counter values for any incremental counters of
// the sequence identified by |packet_sequence_id|.
@@ -133,14 +140,14 @@
void OnFirstPacketOnSequence(uint32_t packet_sequence_id);
- void SetRangeOfInterestStartUs(int64_t range_of_interest_start_us) {
- range_of_interest_start_us_ = range_of_interest_start_us;
- }
-
std::optional<int64_t> range_of_interest_start_us() const {
return range_of_interest_start_us_;
}
+ void set_range_of_interest_us(int64_t range_of_interest_start_us) {
+ range_of_interest_start_us_ = range_of_interest_start_us;
+ }
+
private:
class ResolvedDescriptorTrack {
public:
@@ -150,77 +157,71 @@
kGlobal,
};
- static ResolvedDescriptorTrack Process(UniquePid upid,
- bool is_counter,
- bool is_root);
- static ResolvedDescriptorTrack Thread(UniqueTid utid,
+ static ResolvedDescriptorTrack Process(TrackId,
+ UniquePid upid,
+ bool is_counter);
+ static ResolvedDescriptorTrack Thread(TrackId,
+ UniqueTid utid,
bool is_counter,
- bool is_root,
- bool use_separate_track);
- static ResolvedDescriptorTrack Global(bool is_counter, bool is_root);
+ bool is_default_thead_slice_track);
+ static ResolvedDescriptorTrack Global(TrackId, bool is_counter);
+ TrackId track_id() const { return track_id_; }
Scope scope() const { return scope_; }
bool is_counter() const { return is_counter_; }
UniqueTid utid() const {
PERFETTO_DCHECK(scope() == Scope::kThread);
return utid_;
}
+ bool is_default_thead_slice_track() const {
+ PERFETTO_DCHECK(scope() == Scope::kThread);
+ return is_default_thead_slice_track_;
+ }
UniquePid upid() const {
PERFETTO_DCHECK(scope() == Scope::kProcess);
return upid_;
}
- UniqueTid is_root_in_scope() const { return is_root_in_scope_; }
- bool use_separate_track() const { return use_separate_track_; }
private:
+ TrackId track_id_;
Scope scope_;
bool is_counter_;
- bool is_root_in_scope_;
- bool use_separate_track_;
// Only set when |scope| == |Scope::kThread|.
UniqueTid utid_;
+ bool is_default_thead_slice_track_ = false;
// Only set when |scope| == |Scope::kProcess|.
UniquePid upid_;
};
- std::optional<TrackId> GetDescriptorTrackImpl(
+ std::optional<ResolvedDescriptorTrack> GetDescriptorTrackImpl(
uint64_t uuid,
- std::optional<uint32_t> packet_sequence_id = std::nullopt);
- TrackId CreateTrackFromResolved(uint64_t uuid,
- std::optional<uint32_t> packet_sequence_id,
- const DescriptorTrackReservation&,
- const ResolvedDescriptorTrack&);
- std::optional<ResolvedDescriptorTrack> ResolveDescriptorTrack(
+ StringId event_name,
+ std::optional<uint32_t> packet_sequence_id);
+
+ ResolvedDescriptorTrack ResolveDescriptorTrack(
uint64_t uuid,
- std::vector<uint64_t>* descendent_uuids);
- std::optional<ResolvedDescriptorTrack> ResolveDescriptorTrackImpl(
- uint64_t uuid,
- const DescriptorTrackReservation&,
- std::vector<uint64_t>* descendent_uuids);
+ const DescriptorTrackReservation& reservation,
+ std::optional<uint32_t> packet_sequence_id);
+
+ bool IsTrackHierarchyValid(uint64_t uuid);
void AddTrackArgs(uint64_t uuid,
std::optional<uint32_t> packet_sequence_id,
const DescriptorTrackReservation&,
- const ResolvedDescriptorTrack&,
+ bool,
ArgsTracker::BoundInserter&);
- static constexpr uint64_t kDefaultDescriptorTrackUuid = 0u;
-
- std::map<UniqueTid, TrackId> thread_tracks_;
- std::map<UniquePid, TrackId> process_tracks_;
-
- std::map<uint64_t /* uuid */, DescriptorTrackReservation>
+ base::FlatHashMap<uint64_t /* uuid */, DescriptorTrackReservation>
reserved_descriptor_tracks_;
- std::map<uint64_t /* uuid */, ResolvedDescriptorTrack>
+ base::FlatHashMap<uint64_t /* uuid */, ResolvedDescriptorTrack>
resolved_descriptor_tracks_;
- std::map<uint64_t /* uuid */, TrackId> descriptor_tracks_;
// Stores the descriptor uuid used for the primary process/thread track
// for the given upid / utid. Used for pid/tid reuse detection.
- std::map<UniquePid, uint64_t /*uuid*/> descriptor_uuids_by_upid_;
- std::map<UniqueTid, uint64_t /*uuid*/> descriptor_uuids_by_utid_;
+ base::FlatHashMap<UniquePid, uint64_t /*uuid*/> descriptor_uuids_by_upid_;
+ base::FlatHashMap<UniqueTid, uint64_t /*uuid*/> descriptor_uuids_by_utid_;
std::unordered_set<uint32_t> sequences_with_first_packet_;
@@ -228,19 +229,17 @@
const StringId source_id_key_;
const StringId is_root_in_scope_key_;
const StringId category_key_;
+ const StringId builtin_counter_type_key_;
const StringId has_first_packet_on_sequence_key_id_;
const StringId child_ordering_key_;
const StringId explicit_id_;
const StringId lexicographic_id_;
const StringId chronological_id_;
const StringId sibling_order_rank_key_;
-
const StringId descriptor_source_;
-
const StringId default_descriptor_track_name_;
std::optional<int64_t> range_of_interest_start_us_;
-
TraceProcessorContext* const context_;
};
diff --git a/src/trace_processor/importers/proto/v8_module.cc b/src/trace_processor/importers/proto/v8_module.cc
index 88d8f53..600e2b1 100644
--- a/src/trace_processor/importers/proto/v8_module.cc
+++ b/src/trace_processor/importers/proto/v8_module.cc
@@ -240,7 +240,7 @@
}
void V8Module::ParseV8CodeMove(protozero::ConstBytes bytes,
- int64_t,
+ int64_t ts,
const TracePacketData& data) {
V8SequenceState& state =
*data.sequence_state->GetCustomState<V8SequenceState>();
@@ -252,7 +252,13 @@
return;
}
- // TODO(carlscab): Implement
+ std::optional<UniqueTid> utid =
+ GetUtid(*data.sequence_state, *isolate_id, v8_code_move);
+ if (!utid) {
+ return;
+ }
+
+ v8_tracker_->MoveCode(ts, *utid, *isolate_id, v8_code_move);
}
} // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/v8_tracker.cc b/src/trace_processor/importers/proto/v8_tracker.cc
index d60bef1..2866638 100644
--- a/src/trace_processor/importers/proto/v8_tracker.cc
+++ b/src/trace_processor/importers/proto/v8_tracker.cc
@@ -51,6 +51,7 @@
using ::perfetto::protos::pbzero::InternedV8JsFunction;
using ::perfetto::protos::pbzero::InternedV8JsScript;
using ::perfetto::protos::pbzero::InternedV8WasmScript;
+using ::perfetto::protos::pbzero::V8CodeMove;
using ::perfetto::protos::pbzero::V8InternalCode;
using ::perfetto::protos::pbzero::V8JsCode;
using ::perfetto::protos::pbzero::V8RegExpCode;
@@ -341,10 +342,13 @@
row.is_toplevel = function.is_toplevel();
row.kind =
context_->storage->InternString(JsFunctionKindToString(function.kind()));
- // TODO(carlscab): Line and column are hard. Offset is in bytes, line and
- // column are in characters and we potentially have a multi byte encoding
- // (UTF16). Good luck!
- if (function.has_byte_offset()) {
+ if (function.has_line() && function.has_column()) {
+ row.line = function.line();
+ row.col = function.column();
+ } else if (function.has_byte_offset()) {
+ // TODO(carlscab): Line and column are hard. Offset is in bytes, line and
+ // column are in characters and we potentially have a multi byte encoding
+ // (UTF16). Good luck!
row.line = 1;
row.col = function.byte_offset();
}
@@ -520,6 +524,24 @@
{jit_code_id, isolate_id, pattern});
}
+void V8Tracker::MoveCode(int64_t timestamp,
+ UniqueTid utid,
+ IsolateId isolate_id,
+ const V8CodeMove::Decoder& code) {
+ if (!code.has_from_instruction_start_address())
+ return;
+
+ const AddressRange code_range = AddressRange::FromStartAndSize(
+ code.from_instruction_start_address(), code.instruction_size_bytes());
+ JitCache* const jit_cache = FindJitCache(isolate_id, code_range);
+ if (!jit_cache) {
+ return;
+ }
+
+ jit_cache->MoveCode(timestamp, utid, code.from_instruction_start_address(),
+ code.to_instruction_start_address());
+}
+
StringId V8Tracker::InternV8String(const V8String::Decoder& v8_string) {
auto& storage = *context_->storage;
if (v8_string.has_latin1()) {
diff --git a/src/trace_processor/importers/proto/v8_tracker.h b/src/trace_processor/importers/proto/v8_tracker.h
index 0abcc30..03176d6 100644
--- a/src/trace_processor/importers/proto/v8_tracker.h
+++ b/src/trace_processor/importers/proto/v8_tracker.h
@@ -87,6 +87,11 @@
IsolateId v8_isolate_id,
const protos::pbzero::V8RegExpCode::Decoder& code);
+ void MoveCode(int64_t timestamp,
+ UniqueTid utid,
+ IsolateId v8_isolate_id,
+ const protos::pbzero::V8CodeMove::Decoder& code_move);
+
private:
struct JsFunctionHash {
size_t operator()(const tables::V8JsFunctionTable::Row& v) const {
diff --git a/src/trace_processor/importers/proto/winscope/winscope_module.cc b/src/trace_processor/importers/proto/winscope/winscope_module.cc
index a25a05d..c4ddab4 100644
--- a/src/trace_processor/importers/proto/winscope/winscope_module.cc
+++ b/src/trace_processor/importers/proto/winscope/winscope_module.cc
@@ -15,23 +15,35 @@
*/
#include "src/trace_processor/importers/proto/winscope/winscope_module.h"
+
+#include <cstdint>
+
+#include "perfetto/base/status.h"
#include "perfetto/ext/base/base64.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/protozero/field.h"
+#include "perfetto/trace_processor/ref_counted.h"
#include "protos/perfetto/trace/android/winscope_extensions.pbzero.h"
#include "protos/perfetto/trace/android/winscope_extensions_impl.pbzero.h"
+#include "src/trace_processor/importers/common/args_tracker.h"
+#include "src/trace_processor/importers/common/parser_types.h"
#include "src/trace_processor/importers/proto/args_parser.h"
+#include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
+#include "src/trace_processor/importers/proto/proto_importer_module.h"
#include "src/trace_processor/importers/proto/winscope/viewcapture_args_parser.h"
#include "src/trace_processor/importers/proto/winscope/winscope.descriptor.h"
+#include "src/trace_processor/storage/stats.h"
+#include "src/trace_processor/tables/winscope_tables_py.h"
#include "src/trace_processor/util/winscope_proto_mapping.h"
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
using perfetto::protos::pbzero::TracePacket;
using perfetto::protos::pbzero::WinscopeExtensionsImpl;
WinscopeModule::WinscopeModule(TraceProcessorContext* context)
: context_{context},
- args_parser_{*context->descriptor_pool_.get()},
+ args_parser_{*context->descriptor_pool_},
surfaceflinger_layers_parser_(context),
surfaceflinger_transactions_parser_(context),
shell_transitions_parser_(context),
@@ -56,7 +68,6 @@
int64_t /*packet_timestamp*/,
RefPtr<PacketSequenceStateGeneration> /*state*/,
uint32_t field_id) {
-
switch (field_id) {
case TracePacket::kProtologViewerConfigFieldNumber:
protolog_parser_.ParseAndAddViewerConfigToMessageDecoder(
@@ -144,7 +155,7 @@
ArgsTracker tracker(context_);
auto inserter = tracker.AddArgsTo(rowId);
- ArgsParser writer(timestamp, inserter, *context_->storage.get());
+ ArgsParser writer(timestamp, inserter, *context_->storage);
base::Status status =
args_parser_.ParseMessage(blob,
*util::winscope_proto_mapping::GetProtoName(
@@ -170,7 +181,7 @@
ArgsTracker tracker(context_);
auto inserter = tracker.AddArgsTo(rowId);
- ArgsParser writer(timestamp, inserter, *context_->storage.get());
+ ArgsParser writer(timestamp, inserter, *context_->storage);
base::Status status = args_parser_.ParseMessage(
blob,
*util::winscope_proto_mapping::GetProtoName(
@@ -194,7 +205,7 @@
ArgsTracker tracker(context_);
auto inserter = tracker.AddArgsTo(rowId);
- ArgsParser writer(timestamp, inserter, *context_->storage.get());
+ ArgsParser writer(timestamp, inserter, *context_->storage);
base::Status status =
args_parser_.ParseMessage(blob,
*util::winscope_proto_mapping::GetProtoName(
@@ -219,7 +230,7 @@
ArgsTracker tracker(context_);
auto inserter = tracker.AddArgsTo(rowId);
- ViewCaptureArgsParser writer(timestamp, inserter, *context_->storage.get(),
+ ViewCaptureArgsParser writer(timestamp, inserter, *context_->storage,
sequence_state);
base::Status status =
args_parser_.ParseMessage(blob,
@@ -242,7 +253,7 @@
ArgsTracker tracker(context_);
auto inserter = tracker.AddArgsTo(rowId);
- ArgsParser writer(timestamp, inserter, *context_->storage.get());
+ ArgsParser writer(timestamp, inserter, *context_->storage);
base::Status status =
args_parser_.ParseMessage(blob,
*util::winscope_proto_mapping::GetProtoName(
@@ -254,5 +265,4 @@
}
}
-} // namespace trace_processor
-} // namespace perfetto
+} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/systrace/systrace_line_parser.cc b/src/trace_processor/importers/systrace/systrace_line_parser.cc
index 74980e7..2906b1f 100644
--- a/src/trace_processor/importers/systrace/systrace_line_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_line_parser.cc
@@ -56,7 +56,7 @@
waker_utid_id_(ctx->storage->InternString("waker_utid")),
unknown_thread_name_id_(ctx->storage->InternString("<...>")) {}
-util::Status SystraceLineParser::ParseLine(const SystraceLine& line) {
+base::Status SystraceLineParser::ParseLine(const SystraceLine& line) {
const StringId line_task_id{
context_->storage->InternString(base::StringView(line.task))};
auto utid = context_->process_tracker->UpdateThreadName(
@@ -109,7 +109,7 @@
if (!(prev_pid.has_value() && prev_prio.has_value() &&
next_pid.has_value() && next_prio.has_value())) {
- return util::Status("Could not parse sched_switch");
+ return base::Status("Could not parse sched_switch");
}
FtraceSchedEventTracker::GetOrCreate(context_)->PushSchedSwitch(
@@ -123,7 +123,7 @@
auto comm = args["comm"];
std::optional<uint32_t> wakee_pid = base::StringToUInt32(args["pid"]);
if (!wakee_pid.has_value()) {
- return util::Status("Could not convert wakee_pid");
+ return base::Status("Could not convert wakee_pid");
}
StringId name_id = context_->storage->InternString(base::StringView(comm));
@@ -137,10 +137,10 @@
std::optional<uint32_t> event_cpu = base::StringToUInt32(args["cpu_id"]);
std::optional<double> new_state = base::StringToDouble(args["state"]);
if (!event_cpu.has_value()) {
- return util::Status("Could not convert event cpu");
+ return base::Status("Could not convert event cpu");
}
if (!event_cpu.has_value()) {
- return util::Status("Could not convert state");
+ return base::Status("Could not convert state");
}
TrackId track = context_->track_tracker->InternTrack(
@@ -150,10 +150,10 @@
std::optional<uint32_t> event_cpu = base::StringToUInt32(args["cpu_id"]);
std::optional<double> new_state = base::StringToDouble(args["state"]);
if (!event_cpu.has_value()) {
- return util::Status("Could not convert event cpu");
+ return base::Status("Could not convert event cpu");
}
if (!event_cpu.has_value()) {
- return util::Status("Could not convert state");
+ return base::Status("Could not convert state");
}
TrackId track = context_->track_tracker->InternTrack(
@@ -171,16 +171,16 @@
std::string code_str = args["code"] + " Java Layer Dependent";
StringId code = context_->storage->InternString(base::StringView(code_str));
if (!dest_tgid.has_value()) {
- return util::Status("Could not convert dest_tgid");
+ return base::Status("Could not convert dest_tgid");
}
if (!dest_tid.has_value()) {
- return util::Status("Could not convert dest_tid");
+ return base::Status("Could not convert dest_tid");
}
if (!id.has_value()) {
- return util::Status("Could not convert transaction id");
+ return base::Status("Could not convert transaction id");
}
if (!dest_node.has_value()) {
- return util::Status("Could not covert dest node");
+ return base::Status("Could not covert dest node");
}
BinderTracker::GetOrCreate(context_)->Transaction(
line.ts, line.pid, id.value(), dest_node.value(), dest_tgid.value(),
@@ -188,21 +188,21 @@
} else if (line.event_name == "binder_transaction_received") {
auto id = base::StringToInt32(args["transaction"]);
if (!id.has_value()) {
- return util::Status("Could not convert transaction id");
+ return base::Status("Could not convert transaction id");
}
BinderTracker::GetOrCreate(context_)->TransactionReceived(line.ts, line.pid,
id.value());
} else if (line.event_name == "binder_command") {
auto id = base::StringToUInt32(args["cmd"], 0);
if (!id.has_value()) {
- return util::Status("Could not convert cmd ");
+ return base::Status("Could not convert cmd ");
}
BinderTracker::GetOrCreate(context_)->CommandToKernel(line.ts, line.pid,
id.value());
} else if (line.event_name == "binder_return") {
auto id = base::StringToUInt32(args["cmd"], 0);
if (!id.has_value()) {
- return util::Status("Could not convert cmd");
+ return base::Status("Could not convert cmd");
}
BinderTracker::GetOrCreate(context_)->ReturnFromKernel(line.ts, line.pid,
id.value());
@@ -216,17 +216,17 @@
auto data_size = base::StringToUInt64(args["data_size"]);
auto offsets_size = base::StringToUInt64(args["offsets_size"]);
if (!data_size.has_value()) {
- return util::Status("Could not convert data size");
+ return base::Status("Could not convert data size");
}
if (!offsets_size.has_value()) {
- return util::Status("Could not convert offsets size");
+ return base::Status("Could not convert offsets size");
}
BinderTracker::GetOrCreate(context_)->TransactionAllocBuf(
line.ts, line.pid, data_size.value(), offsets_size.value());
} else if (line.event_name == "clock_set_rate") {
auto rate = base::StringToUInt32(args["state"]);
if (!rate.has_value()) {
- return util::Status("Could not convert state");
+ return base::Status("Could not convert state");
}
TrackId track = context_->track_tracker->InternTrack(
tracks::kClockFrequencyBlueprint,
@@ -236,7 +236,7 @@
line.event_name == "clock_disable") {
auto rate = base::StringToUInt32(args["state"]);
if (!rate.has_value()) {
- return util::Status("Could not convert state");
+ return base::Status("Could not convert state");
}
TrackId track = context_->track_tracker->InternTrack(
tracks::kClockStateBlueprint,
@@ -257,7 +257,7 @@
tracks::Dimensions(base::StringView(args["thermal_zone"])));
auto temp = base::StringToInt32(args["temp"]);
if (!temp.has_value()) {
- return util::Status("Could not convert temp");
+ return base::Status("Could not convert temp");
}
context_->event_tracker->PushCounter(line.ts, temp.value(), track);
} else if (line.event_name == "cdev_update") {
@@ -266,18 +266,18 @@
tracks::Dimensions(base::StringView(args["type"])));
auto target = base::StringToDouble(args["target"]);
if (!target.has_value()) {
- return util::Status("Could not convert target");
+ return base::Status("Could not convert target");
}
context_->event_tracker->PushCounter(line.ts, target.value(), track);
} else if (line.event_name == "sched_blocked_reason") {
auto wakee_pid = base::StringToUInt32(args["pid"]);
if (!wakee_pid.has_value()) {
- return util::Status("sched_blocked_reason: could not parse wakee_pid");
+ return base::Status("sched_blocked_reason: could not parse wakee_pid");
}
auto wakee_utid = context_->process_tracker->GetOrCreateThread(*wakee_pid);
auto io_wait = base::StringToInt32(args["iowait"]);
if (!io_wait.has_value()) {
- return util::Status("sched_blocked_reason: could not parse io_wait");
+ return base::Status("sched_blocked_reason: could not parse io_wait");
}
StringId blocked_function =
context_->storage->InternString(base::StringView(args["caller"]));
@@ -290,10 +290,10 @@
auto mm_id = base::StringToInt64(args["mm_id"]);
auto opt_curr = base::StringToUInt32(args["curr"]);
if (!size.has_value()) {
- return util::Status("rss_stat: could not parse size");
+ return base::Status("rss_stat: could not parse size");
}
if (!member.has_value()) {
- return util::Status("rss_stat: could not parse member");
+ return base::Status("rss_stat: could not parse member");
}
std::optional<bool> curr;
if (!opt_curr.has_value()) {
@@ -303,7 +303,7 @@
mm_id);
}
- return util::OkStatus();
+ return base::OkStatus();
}
} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/systrace/systrace_line_parser.h b/src/trace_processor/importers/systrace/systrace_line_parser.h
index 4ff2b83..bf53d94 100644
--- a/src/trace_processor/importers/systrace/systrace_line_parser.h
+++ b/src/trace_processor/importers/systrace/systrace_line_parser.h
@@ -31,7 +31,7 @@
public:
explicit SystraceLineParser(TraceProcessorContext*);
- util::Status ParseLine(const SystraceLine&);
+ base::Status ParseLine(const SystraceLine&);
private:
TraceProcessorContext* const context_;
diff --git a/src/trace_processor/importers/systrace/systrace_line_tokenizer.cc b/src/trace_processor/importers/systrace/systrace_line_tokenizer.cc
index 054bc3e..6daf6de 100644
--- a/src/trace_processor/importers/systrace/systrace_line_tokenizer.cc
+++ b/src/trace_processor/importers/systrace/systrace_line_tokenizer.cc
@@ -46,7 +46,7 @@
// TODO(hjd): This should be more robust to being passed random input.
// This can happen if we mess up detecting a gzip trace for example.
-util::Status SystraceLineTokenizer::Tokenize(const std::string& buffer,
+base::Status SystraceLineTokenizer::Tokenize(const std::string& buffer,
SystraceLine* line) {
// An example line from buffer looks something like the following:
// kworker/u16:1-77 ( 77) [004] .... 316.196720: 0:
@@ -65,7 +65,7 @@
std::smatch matches;
bool matched = std::regex_search(buffer, matches, line_matcher_);
if (!matched) {
- return util::ErrStatus("Not a known systrace event format (line: %s)",
+ return base::ErrStatus("Not a known systrace event format (line: %s)",
buffer.c_str());
}
@@ -80,23 +80,23 @@
std::optional<uint32_t> maybe_pid = base::StringToUInt32(pid_str);
if (!maybe_pid.has_value()) {
- return util::Status("Could not convert pid " + pid_str);
+ return base::Status("Could not convert pid " + pid_str);
}
line->pid = maybe_pid.value();
std::optional<uint32_t> maybe_cpu = base::StringToUInt32(cpu_str);
if (!maybe_cpu.has_value()) {
- return util::Status("Could not convert cpu " + cpu_str);
+ return base::Status("Could not convert cpu " + cpu_str);
}
line->cpu = maybe_cpu.value();
std::optional<double> maybe_ts = base::StringToDouble(ts_str);
if (!maybe_ts.has_value()) {
- return util::Status("Could not convert ts");
+ return base::Status("Could not convert ts");
}
line->ts = static_cast<int64_t>(maybe_ts.value() * 1e9);
- return util::OkStatus();
+ return base::OkStatus();
}
} // namespace trace_processor
diff --git a/src/trace_processor/importers/systrace/systrace_line_tokenizer.h b/src/trace_processor/importers/systrace/systrace_line_tokenizer.h
index 10c4a28..b861c96 100644
--- a/src/trace_processor/importers/systrace/systrace_line_tokenizer.h
+++ b/src/trace_processor/importers/systrace/systrace_line_tokenizer.h
@@ -30,7 +30,7 @@
public:
SystraceLineTokenizer();
- util::Status Tokenize(const std::string& line, SystraceLine*);
+ base::Status Tokenize(const std::string& line, SystraceLine*);
private:
const std::regex line_matcher_;
diff --git a/src/trace_processor/importers/systrace/systrace_trace_parser.cc b/src/trace_processor/importers/systrace/systrace_trace_parser.cc
index a28800c..e52f867 100644
--- a/src/trace_processor/importers/systrace/systrace_trace_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_trace_parser.cc
@@ -66,9 +66,9 @@
: line_parser_(ctx), ctx_(ctx) {}
SystraceTraceParser::~SystraceTraceParser() = default;
-util::Status SystraceTraceParser::Parse(TraceBlobView blob) {
+base::Status SystraceTraceParser::Parse(TraceBlobView blob) {
if (state_ == ParseState::kEndOfSystrace)
- return util::OkStatus();
+ return base::OkStatus();
partial_buf_.insert(partial_buf_.end(), blob.data(),
blob.data() + blob.size());
@@ -122,7 +122,7 @@
break;
} else if (!base::StartsWith(buffer, "#") && !buffer.empty()) {
SystraceLine line;
- util::Status status = line_tokenizer_.Tokenize(buffer, &line);
+ base::Status status = line_tokenizer_.Tokenize(buffer, &line);
if (status.ok()) {
line_parser_.ParseLine(std::move(line));
} else {
@@ -156,7 +156,7 @@
static_cast<size_t>((buffer.data() + buffer.size()) - cmd_start));
if (!pid || !ppid) {
PERFETTO_ELOG("Could not parse line '%s'", buffer.c_str());
- return util::ErrStatus("Could not parse PROCESS DUMP line");
+ return base::ErrStatus("Could not parse PROCESS DUMP line");
}
ctx_->process_tracker->SetProcessMetadata(pid.value(), ppid, name,
base::StringView());
@@ -177,7 +177,7 @@
ctx_->storage->mutable_string_pool()->InternString(cmd);
if (!tid || !tgid) {
PERFETTO_ELOG("Could not parse line '%s'", buffer.c_str());
- return util::ErrStatus("Could not parse PROCESS DUMP line");
+ return base::ErrStatus("Could not parse PROCESS DUMP line");
}
UniqueTid utid =
ctx_->process_tracker->UpdateThread(tid.value(), tgid.value());
@@ -198,7 +198,7 @@
} else {
partial_buf_.erase(partial_buf_.begin(), start_it);
}
- return util::OkStatus();
+ return base::OkStatus();
}
base::Status SystraceTraceParser::NotifyEndOfFile() {
diff --git a/src/trace_processor/importers/systrace/systrace_trace_parser.h b/src/trace_processor/importers/systrace/systrace_trace_parser.h
index e6b3596..c16d77d 100644
--- a/src/trace_processor/importers/systrace/systrace_trace_parser.h
+++ b/src/trace_processor/importers/systrace/systrace_trace_parser.h
@@ -35,7 +35,7 @@
~SystraceTraceParser() override;
// ChunkedTraceReader implementation.
- util::Status Parse(TraceBlobView) override;
+ base::Status Parse(TraceBlobView) override;
base::Status NotifyEndOfFile() override;
private:
diff --git a/src/trace_processor/metrics/sql/android/BUILD.gn b/src/trace_processor/metrics/sql/android/BUILD.gn
index 5ad8a64..252cdb1 100644
--- a/src/trace_processor/metrics/sql/android/BUILD.gn
+++ b/src/trace_processor/metrics/sql/android/BUILD.gn
@@ -26,6 +26,7 @@
"android_batt.sql",
"android_binder.sql",
"android_blocking_calls_cuj_metric.sql",
+ "android_blocking_calls_cuj_per_frame_metric.sql",
"android_blocking_calls_unagg.sql",
"android_boot.sql",
"android_boot_unagg.sql",
diff --git a/src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_per_frame_metric.sql b/src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_per_frame_metric.sql
new file mode 100644
index 0000000..dd4ce90
--- /dev/null
+++ b/src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_per_frame_metric.sql
@@ -0,0 +1,205 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+-- Create the base table (`android_jank_cuj`) containing all completed CUJs
+-- found in the trace.
+-- This script will use the `android_jank_cuj_main_thread_frame_boundary`,
+-- containing bounds of frames within jank CUJs.
+SELECT RUN_METRIC('android/android_jank_cuj.sql');
+
+INCLUDE PERFETTO MODULE android.slices;
+INCLUDE PERFETTO MODULE android.binder;
+INCLUDE PERFETTO MODULE android.critical_blocking_calls;
+INCLUDE PERFETTO MODULE android.frames.timeline;
+
+-- TODO(b/296349525): Add this to the perfetto standard library.
+DROP TABLE IF EXISTS android_cujs;
+CREATE TABLE android_cujs AS
+ SELECT
+ cuj_id,
+ cuj.upid,
+ t.utid AS ui_thread,
+ process_name,
+ process_metadata,
+ cuj_name,
+ cuj.layer_id,
+ tb.ts,
+ tb.dur,
+ tb.ts_end,
+ begin_vsync,
+ end_vsync
+ FROM android_jank_cuj_main_thread_cuj_boundary tb
+ JOIN android_jank_cuj cuj USING (cuj_id)
+ JOIN android_jank_cuj_main_thread t USING (cuj_id);
+
+DROP TABLE IF EXISTS _android_frames_layers_with_end_ts;
+CREATE PERFETTO TABLE _android_frames_layers_with_end_ts AS
+ SELECT
+ ts,
+ dur,
+ frame_id,
+ layer_id,
+ upid,
+ ui_thread_utid,
+ (ts + dur) AS ts_end
+ FROM android_frames_layers;
+
+-- While calculating the metric, there are two possibilities for a blocking call:
+-- 1. Blocking call is completely within a frame boundary.
+-- 2. Blocking call crosses the frame boundary into the next frame.
+-- For the first case, the blocking call duration is the 'dur' field value itself. But for the
+-- second case, only the part within the frame is considered.
+DROP TABLE IF EXISTS blocking_calls_per_frame;
+CREATE PERFETTO TABLE blocking_calls_per_frame AS
+SELECT
+ MIN(
+ bc.dur,
+ frame.ts_end - bc.ts,
+ bc.ts_end - frame.ts
+ ) AS dur,
+ MAX(frame.ts, bc.ts) AS ts,
+ bc.upid,
+ bc.name,
+ bc.process_name,
+ bc.utid,
+ frame.frame_id,
+ frame.layer_id
+FROM _android_critical_blocking_calls bc
+JOIN _android_frames_layers_with_end_ts frame
+ON bc.utid = frame.ui_thread_utid
+ -- The following condition to accommodate blocking call crossing frame boundary. The blocking
+ -- call starts in a frame and ends in a frame. It can either be the same frame or a different
+ -- frame.
+WHERE (bc.ts >= frame.ts AND bc.ts <= frame.ts_end) -- Blocking call starts within the frame.
+ OR (bc.ts_end >= frame.ts AND bc.ts_end <= frame.ts_end); -- Blocking call ends within the frame.
+
+-- Table capturing the full and partial frames within a CUJ boundary. This table captures the
+-- layer ID associated with the actual frame too.
+DROP TABLE IF EXISTS frames_in_cuj;
+CREATE PERFETTO TABLE frames_in_cuj AS
+SELECT
+ cuj.cuj_name,
+ cuj.upid,
+ cuj.process_name,
+ frame.layer_id,
+ frame.frame_id,
+ cuj.cuj_id,
+ MAX(frame.ts, cuj.ts) AS frame_ts,
+ MIN(
+ frame.dur,
+ cuj.ts_end - frame.ts,
+ frame.ts_end - cuj.ts
+ ) AS dur
+FROM _android_frames_layers_with_end_ts frame
+JOIN android_cujs cuj
+ON frame.upid = cuj.upid
+ AND frame.layer_id = cuj.layer_id
+ AND frame.ui_thread_utid = cuj.ui_thread
+-- Check whether the frame_id falls within the begin and end vsync of the cuj.
+-- Also check if the frame start or end timestamp falls within the cuj boundary.
+WHERE
+ -- frame withtin cuj vsync boundary
+ frame_id >= begin_vsync AND frame_id <= end_vsync
+ AND (
+ -- frame start within cuj
+ (frame.ts >= cuj.ts AND frame.ts <= cuj.ts_end)
+ OR
+ -- frame end within cuj
+ (frame.ts_end >= cuj.ts AND frame.ts_end <= cuj.ts_end)
+ );
+
+-- Combine the above two tables to get blocking calls within frame within CUJ.
+DROP TABLE IF EXISTS blocking_calls_frame_cuj;
+CREATE PERFETTO TABLE blocking_calls_frame_cuj AS
+SELECT
+ b.upid,
+ b.frame_id,
+ b.layer_id,
+ b.name,
+ frame_cuj.cuj_name,
+ b.ts,
+ b.dur,
+ b.process_name
+FROM frames_in_cuj frame_cuj
+JOIN blocking_calls_per_frame b
+USING (upid, frame_id, layer_id);
+
+-- Calculate the mean/max values for duration and count for blocking calls per frame.
+DROP TABLE IF EXISTS android_blocking_calls_cuj_per_frame_calls;
+CREATE PERFETTO TABLE android_blocking_calls_cuj_per_frame_calls AS
+WITH blocking_calls_aggregate_values AS (
+ -- Aggregate the count and sum for each blocking call by grouping on CUJ name, blocking
+ -- call name and frame ID(vsync).
+ SELECT
+ COUNT(*) AS cnt,
+ SUM(dur) AS total_dur_per_frame_ns,
+ MAX(dur) AS max_dur_per_frame_ns,
+ cuj_name,
+ upid,
+ process_name,
+ name
+ FROM blocking_calls_frame_cuj
+ GROUP BY cuj_name, name, frame_id
+),
+frame_cnt_per_cuj AS (
+ -- Calculate the total number of frames for all CUJs across all instances(eg. multiple
+ -- instances for the same CUJ).
+ SELECT
+ COUNT(*) AS frame_cnt,
+ cuj_name
+ FROM frames_in_cuj
+ GROUP BY cuj_name
+)
+SELECT
+ cast_double!(SUM(cnt)) / frame_cnt AS mean_cnt_per_frame,
+ MAX(cnt) AS max_cnt_per_frame,
+ SUM(total_dur_per_frame_ns) / frame_cnt AS mean_dur_per_frame_ns,
+ MAX(max_dur_per_frame_ns) AS max_dur_per_frame_ns,
+ name,
+ upid,
+ bc.cuj_name,
+ process_name
+FROM blocking_calls_aggregate_values bc
+JOIN frame_cnt_per_cuj fc
+USING(cuj_name)
+GROUP BY bc.cuj_name, name;
+
+DROP VIEW IF EXISTS android_blocking_calls_cuj_per_frame_metric_output;
+CREATE PERFETTO VIEW android_blocking_calls_cuj_per_frame_metric_output AS
+SELECT AndroidCujBlockingCallsPerFrameMetric('cuj', (
+ SELECT RepeatedField(
+ AndroidCujBlockingCallsPerFrameMetric_Cuj(
+ 'name', cuj_name,
+ 'process', process_metadata,
+ 'blocking_calls', (
+ SELECT RepeatedField(
+ AndroidBlockingCallPerFrame(
+ 'name', b.name,
+ 'max_dur_per_frame_ms', CAST(max_dur_per_frame_ns / 1e6 AS INT),
+ 'max_dur_per_frame_ns', b.max_dur_per_frame_ns,
+ 'mean_dur_per_frame_ms', CAST(mean_dur_per_frame_ns / 1e6 AS INT),
+ 'mean_dur_per_frame_ns', b.mean_dur_per_frame_ns,
+ 'max_cnt_per_frame', CAST(b.max_cnt_per_frame AS INT),
+ 'mean_cnt_per_frame', b.mean_cnt_per_frame
+ )
+ )
+ FROM android_blocking_calls_cuj_per_frame_calls b
+ WHERE b.cuj_name = cuj.cuj_name and b.upid = cuj.upid
+ GROUP BY b.cuj_name
+ )
+ )
+ )
+ FROM (SELECT DISTINCT cuj_name, upid, process_metadata FROM android_cujs) cuj
+));
diff --git a/src/trace_processor/metrics/sql/android/jank/frames.sql b/src/trace_processor/metrics/sql/android/jank/frames.sql
index 481bb4b..aad4c41 100644
--- a/src/trace_processor/metrics/sql/android/jank/frames.sql
+++ b/src/trace_processor/metrics/sql/android/jank/frames.sql
@@ -56,9 +56,8 @@
MAX(timeline.layer_name) as frame_layer_name
FROM android_jank_cuj_vsync_boundary boundary
JOIN actual_timeline_with_vsync timeline
- ON boundary.upid = timeline.upid
- AND vsync >= vsync_min
- AND vsync <= vsync_max
+ ON vsync >= vsync_min
+ AND vsync <= vsync_max
LEFT JOIN expected_frame_timeline_slice expected
ON expected.upid = timeline.upid AND expected.name = timeline.name
LEFT JOIN vsync_missed_callback missed_callback USING(vsync)
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_event_metadata.sql b/src/trace_processor/metrics/sql/chrome/chrome_event_metadata.sql
index a340d82..eef18ee 100644
--- a/src/trace_processor/metrics/sql/chrome/chrome_event_metadata.sql
+++ b/src/trace_processor/metrics/sql/chrome/chrome_event_metadata.sql
@@ -21,7 +21,7 @@
CREATE PERFETTO VIEW chrome_event_metadata AS
WITH metadata (arg_set_id) AS (
SELECT arg_set_id
- FROM raw
+ FROM __intrinsic_chrome_raw
WHERE name = "chrome_event.metadata"
)
-- TODO(b/173201788): Once this is fixed, extract all the fields.
diff --git a/src/trace_processor/metrics/sql/experimental/frame_times.sql b/src/trace_processor/metrics/sql/experimental/frame_times.sql
index a3a73c5..9b6f527 100644
--- a/src/trace_processor/metrics/sql/experimental/frame_times.sql
+++ b/src/trace_processor/metrics/sql/experimental/frame_times.sql
@@ -24,7 +24,7 @@
SELECT
ts,
EXTRACT_ARG(arg_set_id, 'legacy_event.phase') AS phase
-FROM raw
+FROM __intrinsic_chrome_raw
WHERE EXTRACT_ARG(arg_set_id, 'legacy_event.name') = 'SyntheticGestureController::running';
-- Convert pairs of 'S' and 'F' events into slices with ts and dur.
diff --git a/src/trace_processor/metrics/sql/trace_metadata.sql b/src/trace_processor/metrics/sql/trace_metadata.sql
index 1e45573..b90cda9 100644
--- a/src/trace_processor/metrics/sql/trace_metadata.sql
+++ b/src/trace_processor/metrics/sql/trace_metadata.sql
@@ -43,6 +43,10 @@
FROM track JOIN slice ON track.id = slice.track_id
WHERE track.name = 'Trace Triggers'
),
+ 'trace_causal_trigger', (
+ SELECT str_value FROM metadata
+ WHERE name = 'trace_trigger'
+ ),
'trace_config_pbtxt', (
SELECT str_value FROM metadata
WHERE name = 'trace_config_pbtxt'
diff --git a/src/trace_processor/perfetto_sql/engine/created_function.cc b/src/trace_processor/perfetto_sql/engine/created_function.cc
index 1c01c17..e6cef14 100644
--- a/src/trace_processor/perfetto_sql/engine/created_function.cc
+++ b/src/trace_processor/perfetto_sql/engine/created_function.cc
@@ -109,7 +109,7 @@
// the destructors run correctly for non-trivial members of the
// union.
using Data =
- std::variant<int64_t, double, OwnedString, OwnedBytes, nullptr_t>;
+ std::variant<int64_t, double, OwnedString, OwnedBytes, std::nullptr_t>;
StoredSqlValue(SqlValue value) {
switch (value.type) {
@@ -134,7 +134,7 @@
}
SqlValue AsSqlValue() {
- if (std::holds_alternative<nullptr_t>(data)) {
+ if (std::holds_alternative<std::nullptr_t>(data)) {
return SqlValue();
} else if (std::holds_alternative<int64_t>(data)) {
return SqlValue::Long(std::get<int64_t>(data));
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
index 700854f..7d26fed 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
@@ -731,7 +731,7 @@
record->AddArg("cols", base::Join(index.col_names, ", "));
});
- Table* t = GetMutableTableOrNull(index.table_name);
+ Table* t = GetTableOrNull(index.table_name);
if (!t) {
return base::ErrStatus("CREATE PERFETTO INDEX: Table '%s' not found",
index.table_name.c_str());
@@ -759,7 +759,7 @@
record->AddArg("table_name", index.table_name);
});
- Table* t = GetMutableTableOrNull(index.table_name);
+ Table* t = GetTableOrNull(index.table_name);
if (!t) {
return base::ErrStatus("DROP PERFETTO INDEX: Table '%s' not found",
index.table_name.c_str());
@@ -1114,8 +1114,7 @@
return state ? state->runtime_table.get() : nullptr;
}
-RuntimeTable* PerfettoSqlEngine::GetMutableRuntimeTableOrNull(
- std::string_view name) {
+RuntimeTable* PerfettoSqlEngine::GetRuntimeTableOrNull(std::string_view name) {
auto* state = runtime_table_context_->manager.FindStateByName(name);
return state ? state->runtime_table.get() : nullptr;
}
@@ -1126,7 +1125,7 @@
return state ? state->static_table : nullptr;
}
-Table* PerfettoSqlEngine::GetMutableStaticTableOrNull(std::string_view name) {
+Table* PerfettoSqlEngine::GetStaticTableOrNull(std::string_view name) {
auto* state = static_table_context_->manager.FindStateByName(name);
return state ? state->static_table : nullptr;
}
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
index f09d8c8..0828427 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
@@ -231,33 +231,12 @@
// Find table (Static or Runtime) registered with engine with provided name.
const Table* GetTableOrNull(std::string_view name) const {
- if (auto maybe_runtime = GetRuntimeTableOrNull(name); maybe_runtime) {
- return maybe_runtime;
+ if (const auto* r = GetRuntimeTableOrNull(name); r) {
+ return r;
}
return GetStaticTableOrNull(name);
}
- // Find RuntimeTable registered with engine with provided name.
- const RuntimeTable* GetRuntimeTableOrNull(std::string_view) const;
-
- // Find static table registered with engine with provided name.
- const Table* GetStaticTableOrNull(std::string_view) const;
-
- // Find table (Static or Runtime) registered with engine with provided name.
- Table* GetMutableTableOrNull(std::string_view name) {
- if (auto maybe_runtime = GetMutableRuntimeTableOrNull(name);
- maybe_runtime) {
- return maybe_runtime;
- }
- return GetMutableStaticTableOrNull(name);
- }
-
- // Find RuntimeTable registered with engine with provided name.
- RuntimeTable* GetMutableRuntimeTableOrNull(std::string_view);
-
- // Find static table registered with engine with provided name.
- Table* GetMutableStaticTableOrNull(std::string_view);
-
private:
base::Status ExecuteCreateFunction(const PerfettoSqlParser::CreateFunction&);
@@ -329,6 +308,26 @@
const std::string& key,
const PerfettoSqlParser&);
+ // Find table (Static or Runtime) registered with engine with provided name.
+ Table* GetTableOrNull(std::string_view name) {
+ if (auto* maybe_runtime = GetRuntimeTableOrNull(name); maybe_runtime) {
+ return maybe_runtime;
+ }
+ return GetStaticTableOrNull(name);
+ }
+
+ // Find RuntimeTable registered with engine with provided name.
+ RuntimeTable* GetRuntimeTableOrNull(std::string_view);
+
+ // Find static table registered with engine with provided name.
+ Table* GetStaticTableOrNull(std::string_view);
+
+ // Find RuntimeTable registered with engine with provided name.
+ const RuntimeTable* GetRuntimeTableOrNull(std::string_view) const;
+
+ // Find static table registered with engine with provided name.
+ const Table* GetStaticTableOrNull(std::string_view) const;
+
StringPool* pool_ = nullptr;
// If true, engine will perform additional consistency checks when e.g.
// creating tables and views.
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine_unittest.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine_unittest.cc
index a1e8f54..b698326 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine_unittest.cc
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine_unittest.cc
@@ -317,29 +317,6 @@
ASSERT_FALSE(engine_.FindPackage("bar")->modules["bar.bar"].included);
}
-TEST_F(PerfettoSqlEngineTest, MismatchedRange) {
- tables::SliceTable parent(&pool_);
- tables::ExpectedFrameTimelineSliceTable child(&pool_, &parent);
-
- engine_.RegisterStaticTable(&parent, "parent",
- tables::SliceTable::ComputeStaticSchema());
- engine_.RegisterStaticTable(
- &child, "child",
- tables::ExpectedFrameTimelineSliceTable::ComputeStaticSchema());
-
- for (uint32_t i = 0; i < 5; i++) {
- child.Insert({});
- }
-
- for (uint32_t i = 0; i < 10; i++) {
- parent.Insert({});
- }
-
- auto res = engine_.Execute(
- SqlSource::FromExecuteQuery("SELECT * FROM child WHERE ts > 3"));
- ASSERT_TRUE(res.ok()) << res.status().c_message();
-}
-
} // namespace
} // namespace trace_processor
} // namespace perfetto
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.cc
index 89f7e58..7ddded9 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.cc
@@ -84,7 +84,7 @@
if (!builder_.AddSample(stack, sample_values_)) {
return base::ErrStatus("Failed to add callstack");
}
- return util::OkStatus();
+ return base::OkStatus();
}
void Final(sqlite3_context* ctx) {
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/stack_functions.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/stack_functions.cc
index 1babe3d..f3a8cfd 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/stack_functions.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/stack_functions.cc
@@ -44,7 +44,7 @@
using protos::pbzero::Stack;
-util::Status SetBytesOutputValue(const std::vector<uint8_t>& src,
+base::Status SetBytesOutputValue(const std::vector<uint8_t>& src,
SqlValue& out,
SqlFunction::Destructors& destructors) {
void* dest = malloc(src.size());
@@ -54,7 +54,7 @@
memcpy(dest, src.data(), src.size());
out = SqlValue::Bytes(dest, src.size());
destructors.bytes_destructor = free;
- return util::OkStatus();
+ return base::OkStatus();
}
// CAT_STACKS(root BLOB/STRING, level_1 BLOB/STRING, …, leaf BLOB/STRING)
@@ -223,7 +223,7 @@
}
if (value->is_null()) {
- return util::OkStatus();
+ return base::OkStatus();
}
if (value->AsLong() > std::numeric_limits<uint32_t>::max() ||
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc
index 7898fb9..2adb7bc 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc
@@ -615,7 +615,7 @@
SystraceSerializer::ScopedCString SystraceSerializer::SerializeToString(
uint32_t raw_row) {
- const auto& raw = storage_->raw_table();
+ const auto& raw = storage_->ftrace_event_table();
char line[4096];
base::StringWriter writer(line, sizeof(line));
@@ -648,7 +648,7 @@
void SystraceSerializer::SerializePrefix(uint32_t raw_row,
base::StringWriter* writer) {
- const auto& raw = storage_->raw_table();
+ const auto& raw = storage_->ftrace_event_table();
const auto& cpu_table = storage_->cpu_table();
int64_t ts = raw.ts()[raw_row];
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h b/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h
index e3d1263..ebaef3f 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h
@@ -264,7 +264,7 @@
out = SqlValue::Long(int_len);
- return util::OkStatus();
+ return base::OkStatus();
}
struct ExtractArg : public SqlFunction {
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn b/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn
index 0ca4f37..cb5757e 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn
@@ -55,19 +55,26 @@
":etm",
":etm_impl",
]
- sources = [ "etm_decode_trace_vtable.h" ]
+ sources = [
+ "etm_decode_trace_vtable.h",
+ "etm_iterate_range_vtable.h",
+ ]
deps = [
"../../../../../gn:default_deps",
"../../../../../gn:sqlite",
"../../../../../include/perfetto/base",
"../../../../../include/perfetto/ext/base:base",
"../../../sqlite",
+ "../../../storage",
]
}
source_set("etm_impl") {
visibility = [ ":etm" ]
- sources = [ "etm_decode_trace_vtable.cc" ]
+ sources = [
+ "etm_decode_trace_vtable.cc",
+ "etm_iterate_range_vtable.cc",
+ ]
deps = [
":etm_hdr",
"../../../../../gn:default_deps",
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc b/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc
index 83321ce..b96ddee 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc
@@ -102,8 +102,7 @@
}
do {
int64_t ts = sqlite3_column_int64(res->stmt.sqlite_stmt(), 0);
- auto value =
- static_cast<float>(sqlite3_column_double(res->stmt.sqlite_stmt(), 1));
+ auto value = sqlite3_column_double(res->stmt.sqlite_stmt(), 1);
state->timestamps.push_back(ts);
state->forest.Push(Counter{value, value});
} while (res->stmt.Step());
@@ -239,10 +238,10 @@
const auto& res = c->counters[c->index];
switch (N) {
case ColumnIndex::kMinValue:
- sqlite::result::Double(ctx, static_cast<double>(res.min_max_counter.min));
+ sqlite::result::Double(ctx, res.min_max_counter.min);
return SQLITE_OK;
case ColumnIndex::kMaxValue:
- sqlite::result::Double(ctx, static_cast<double>(res.min_max_counter.max));
+ sqlite::result::Double(ctx, res.min_max_counter.max);
return SQLITE_OK;
case ColumnIndex::kLastTs:
sqlite::result::Long(ctx, res.last_ts);
@@ -250,7 +249,7 @@
case ColumnIndex::kLastValue:
PERFETTO_DCHECK(
std::equal_to<>()(res.last_counter.min, res.last_counter.max));
- sqlite::result::Double(ctx, static_cast<double>(res.last_counter.min));
+ sqlite::result::Double(ctx, res.last_counter.min);
return SQLITE_OK;
default:
return sqlite::utils::SetError(t, "Bad column");
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h b/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h
index 7c1337b..fa5723b 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h
@@ -53,8 +53,8 @@
// [1] https://en.wikipedia.org/wiki/Mipmap
struct CounterMipmapOperator : sqlite::Module<CounterMipmapOperator> {
struct Counter {
- float min;
- float max;
+ double min;
+ double max;
};
struct Agg {
Counter operator()(const Counter& a, const Counter& b) {
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/etm_decode_trace_vtable.cc b/src/trace_processor/perfetto_sql/intrinsics/operators/etm_decode_trace_vtable.cc
index 9decb3c..59815e4 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/etm_decode_trace_vtable.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/etm_decode_trace_vtable.cc
@@ -29,6 +29,7 @@
#include "src/trace_processor/importers/etm/element_cursor.h"
#include "src/trace_processor/importers/etm/mapping_version.h"
#include "src/trace_processor/importers/etm/opencsd.h"
+#include "src/trace_processor/importers/etm/sql_values.h"
#include "src/trace_processor/importers/etm/util.h"
#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
#include "src/trace_processor/sqlite/sqlite_utils.h"
@@ -82,7 +83,8 @@
isa TEXT,
start_address INTEGER,
end_address INTEGER,
- mapping_id INTEGER
+ mapping_id INTEGER,
+ instruction_range BLOB HIDDEN
)
)";
@@ -98,7 +100,8 @@
kIsa,
kStartAddress,
kEndAddress,
- kMappingId
+ kMappingId,
+ kInstructionRange
};
constexpr char kTraceIdEqArg = 't';
@@ -237,6 +240,12 @@
sqlite::result::Long(ctx, cursor_.mapping()->id().value);
}
break;
+ case ColumnIndex::kInstructionRange:
+ if (cursor_.has_instruction_range()) {
+ sqlite::result::UniquePointer(ctx, cursor_.GetInstructionRange(),
+ InstructionRangeSqlValue::kPtrType);
+ }
+ break;
}
return SQLITE_OK;
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.cc b/src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.cc
new file mode 100644
index 0000000..a97069c
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.cc
@@ -0,0 +1,290 @@
+/*
+ * 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.
+ */
+
+#include "src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.h"
+#include <opencsd/ocsd_if_types.h>
+
+#include <cstring>
+#include <memory>
+
+#include "perfetto/base/logging.h"
+#include "src/trace_processor/importers/etm/opencsd.h"
+#include "src/trace_processor/importers/etm/sql_values.h"
+#include "src/trace_processor/importers/etm/storage_handle.h"
+#include "src/trace_processor/importers/etm/types.h"
+#include "src/trace_processor/importers/etm/util.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_module.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_value.h"
+#include "src/trace_processor/sqlite/sqlite_utils.h"
+#include "src/trace_processor/storage/trace_storage.h"
+
+namespace perfetto::trace_processor::etm {
+namespace {
+
+static constexpr char kSchema[] = R"(
+ CREATE TABLE x(
+ instruction_index INTEGER,
+ address INTEGER,
+ opcode INTEGER,
+ type TEXT,
+ branch_address INTEGER,
+ is_conditional INTEGER,
+ is_link INTEGER,
+ sub_type TEXT,
+ instruction_range BLOB HIDDEN
+ )
+ )";
+
+enum class ColumnIndex {
+ kInstructionIndex,
+ kAddress,
+ kOpcode,
+ kType,
+ kBranchAddress,
+ kIsConditional,
+ kIsLink,
+ kSubType,
+ kInstructionRange
+};
+
+constexpr char kInstructionRangeEqArg = 'r';
+
+class IntructionCursor : public sqlite::Module<EtmIterateRangeVtable>::Cursor {
+ public:
+ explicit IntructionCursor(TraceStorage* storage) : storage_(storage) {}
+ int Filter(int, const char* idxStr, int argc, sqlite3_value** argv) {
+ std::optional<const InstructionRangeSqlValue*> range;
+ if (argc != static_cast<int>(strlen(idxStr))) {
+ return sqlite::utils::SetError(pVtab, "Invalid idxStr");
+ }
+ for (; *idxStr != 0; ++idxStr, ++argv) {
+ switch (*idxStr) {
+ case kInstructionRangeEqArg: {
+ range = sqlite::value::Pointer<InstructionRangeSqlValue>(
+ *argv, InstructionRangeSqlValue::kPtrType);
+ break;
+ }
+ default:
+ return sqlite::utils::SetError(pVtab, "Invalid idxStr");
+ }
+ }
+
+ if (!range.has_value()) {
+ return sqlite::utils::SetError(pVtab, "Invalid idxStr, no range");
+ }
+
+ Reset(*range);
+ return SQLITE_OK;
+ }
+
+ void Next() {
+ ++instruction_index_;
+ ptr_ += instr_info_.instr_size;
+ if (ptr_ == end_) {
+ return;
+ }
+
+ instr_info_.instr_addr += instr_info_.instr_size;
+ instr_info_.isa = instr_info_.next_isa;
+ FeedDecoder();
+ }
+
+ bool Eof() { return ptr_ == end_; }
+
+ int Column(sqlite3_context* ctx, int raw_n) {
+ switch (static_cast<ColumnIndex>(raw_n)) {
+ case ColumnIndex::kInstructionIndex:
+ sqlite::result::Long(ctx, instruction_index_);
+ break;
+ case ColumnIndex::kAddress:
+ sqlite::result::Long(ctx, static_cast<int64_t>(instr_info_.instr_addr));
+ break;
+ case ColumnIndex::kOpcode:
+ sqlite::result::Long(ctx, instr_info_.opcode);
+ break;
+ case ColumnIndex::kType:
+ sqlite::result::StaticString(ctx, ToString(instr_info_.type));
+ break;
+ case ColumnIndex::kBranchAddress:
+ if (instr_info_.type == OCSD_INSTR_BR ||
+ instr_info_.type == OCSD_INSTR_BR_INDIRECT) {
+ sqlite::result::Long(ctx,
+ static_cast<int64_t>(instr_info_.branch_addr));
+ }
+ break;
+ case ColumnIndex::kIsConditional:
+ sqlite::result::Long(ctx, instr_info_.is_conditional);
+ break;
+ case ColumnIndex::kIsLink:
+ sqlite::result::Long(ctx, instr_info_.is_link);
+ break;
+ case ColumnIndex::kSubType:
+ sqlite::result::StaticString(ctx, ToString(instr_info_.sub_type));
+ break;
+ case ColumnIndex::kInstructionRange:
+ break;
+ }
+
+ return SQLITE_OK;
+ }
+
+ private:
+ void FeedDecoder() {
+ PERFETTO_CHECK(static_cast<size_t>(end_ - ptr_) >=
+ sizeof(instr_info_.opcode));
+ memcpy(&instr_info_.opcode, ptr_, sizeof(instr_info_.opcode));
+ inst_decoder_.DecodeInstruction(&instr_info_);
+ }
+
+ void Reset(const InstructionRangeSqlValue* range) {
+ if (!range) {
+ ptr_ = nullptr;
+ end_ = nullptr;
+ return;
+ }
+ const auto& config =
+ StorageHandle(storage_).GetEtmV4Config(range->config_id);
+ instr_info_.pe_type.arch = config.etm_v4_config().archVersion();
+ instr_info_.pe_type.profile = config.etm_v4_config().coreProfile();
+ instr_info_.dsb_dmb_waypoints = 0; // Not used in ETM
+ instr_info_.wfi_wfe_branch = config.etm_v4_config().wfiwfeBranch();
+ instr_info_.isa = range->isa;
+ instr_info_.instr_addr = range->st_addr;
+
+ ptr_ = range->start;
+ end_ = range->end;
+ instruction_index_ = 0;
+ FeedDecoder();
+ }
+
+ TraceStorage* storage_;
+ const uint8_t* ptr_ = nullptr;
+ const uint8_t* end_ = nullptr;
+ ocsd_instr_info instr_info_;
+ TrcIDecode inst_decoder_;
+ uint32_t instruction_index_ = 0;
+};
+
+IntructionCursor* GetInstructionCursor(sqlite3_vtab_cursor* cursor) {
+ return static_cast<IntructionCursor*>(cursor);
+}
+
+} // namespace
+
+int EtmIterateRangeVtable::Connect(sqlite3* db,
+ void* ctx,
+ int,
+ const char* const*,
+ sqlite3_vtab** vtab,
+ char**) {
+ if (int ret = sqlite3_declare_vtab(db, kSchema); ret != SQLITE_OK) {
+ return ret;
+ }
+ std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
+ res->storage = GetContext(ctx);
+ *vtab = res.release();
+ return SQLITE_OK;
+}
+
+int EtmIterateRangeVtable::Disconnect(sqlite3_vtab* vtab) {
+ delete GetVtab(vtab);
+ return SQLITE_OK;
+}
+
+int EtmIterateRangeVtable::BestIndex(sqlite3_vtab* tab,
+ sqlite3_index_info* info) {
+ bool seen_range = false;
+ int argv_index = 1;
+ std::string idx_str;
+ for (int i = 0; i < info->nConstraint; ++i) {
+ auto& in = info->aConstraint[i];
+ auto& out = info->aConstraintUsage[i];
+
+ if (in.iColumn == static_cast<int>(ColumnIndex::kInstructionRange)) {
+ if (!in.usable) {
+ return SQLITE_CONSTRAINT;
+ }
+ if (in.op != SQLITE_INDEX_CONSTRAINT_EQ) {
+ return sqlite::utils::SetError(
+ tab, "instruction_range only supports equality constraints");
+ }
+ idx_str += kInstructionRangeEqArg;
+ out.argvIndex = argv_index++;
+ out.omit = true;
+ seen_range = true;
+ continue;
+ }
+ }
+
+ if (!seen_range) {
+ return sqlite::utils::SetError(tab,
+ "Constraint required on instruction_range");
+ }
+
+ info->idxStr = sqlite3_mprintf("%s", idx_str.c_str());
+ info->needToFreeIdxStr = true;
+
+ if (info->nOrderBy == 1 &&
+ info->aOrderBy[0].iColumn ==
+ static_cast<int>(ColumnIndex::kInstructionIndex) &&
+ !info->aOrderBy[0].desc) {
+ info->orderByConsumed = true;
+ }
+
+ return SQLITE_OK;
+}
+
+int EtmIterateRangeVtable::Open(sqlite3_vtab* vtab,
+ sqlite3_vtab_cursor** cursor) {
+ *cursor = new IntructionCursor(GetVtab(vtab)->storage);
+ return SQLITE_OK;
+}
+
+int EtmIterateRangeVtable::Close(sqlite3_vtab_cursor* cursor) {
+ delete GetInstructionCursor(cursor);
+ return SQLITE_OK;
+}
+
+int EtmIterateRangeVtable::Filter(sqlite3_vtab_cursor* cur,
+ int idxNum,
+ const char* idxStr,
+ int argc,
+ sqlite3_value** argv) {
+ GetInstructionCursor(cur)->Filter(idxNum, idxStr, argc, argv);
+ return SQLITE_OK;
+}
+
+int EtmIterateRangeVtable::Next(sqlite3_vtab_cursor* cur) {
+ GetInstructionCursor(cur)->Next();
+ return SQLITE_OK;
+}
+
+int EtmIterateRangeVtable::Eof(sqlite3_vtab_cursor* cur) {
+ return GetInstructionCursor(cur)->Eof();
+}
+
+int EtmIterateRangeVtable::Column(sqlite3_vtab_cursor* cur,
+ sqlite3_context* ctx,
+ int raw_n) {
+ return GetInstructionCursor(cur)->Column(ctx, raw_n);
+}
+
+int EtmIterateRangeVtable::Rowid(sqlite3_vtab_cursor*, sqlite_int64*) {
+ return SQLITE_ERROR;
+}
+
+} // namespace perfetto::trace_processor::etm
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.h b/src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.h
new file mode 100644
index 0000000..b9b7aab
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_ETM_ITERATE_RANGE_VTABLE_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_ETM_ITERATE_RANGE_VTABLE_H_
+
+#include <cstdint>
+
+#include "src/trace_processor/sqlite/bindings/sqlite_module.h"
+#include "src/trace_processor/storage/trace_storage.h"
+
+namespace perfetto::trace_processor::etm {
+
+struct InstructionRangeSqlValue;
+
+struct EtmIterateRangeVtable : sqlite::Module<EtmIterateRangeVtable> {
+ using Context = TraceStorage;
+ struct Vtab : sqlite::Module<EtmIterateRangeVtable>::Vtab {
+ TraceStorage* storage;
+ };
+
+ static constexpr auto kType = kEponymousOnly;
+ static constexpr bool kSupportsWrites = false;
+ static constexpr bool kDoesOverloadFunctions = false;
+
+ static int Connect(sqlite3*,
+ void*,
+ int,
+ const char* const*,
+ sqlite3_vtab**,
+ char**);
+ static int Disconnect(sqlite3_vtab*);
+
+ static int BestIndex(sqlite3_vtab*, sqlite3_index_info*);
+
+ static int Open(sqlite3_vtab*, sqlite3_vtab_cursor**);
+ static int Close(sqlite3_vtab_cursor*);
+
+ static int Filter(sqlite3_vtab_cursor*,
+ int,
+ const char*,
+ int,
+ sqlite3_value**);
+ static int Next(sqlite3_vtab_cursor*);
+ static int Eof(sqlite3_vtab_cursor*);
+ static int Column(sqlite3_vtab_cursor*, sqlite3_context*, int);
+ static int Rowid(sqlite3_vtab_cursor*, sqlite_int64*);
+
+ // This needs to happen at the end as it depends on the functions
+ // defined above.
+ static constexpr sqlite3_module kModule = CreateModule();
+};
+
+} // namespace perfetto::trace_processor::etm
+
+#endif // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_ETM_ITERATE_RANGE_VTABLE_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn b/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn
index a73c7d9..3853faa 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn
@@ -29,22 +29,18 @@
"dfs_weight_bounded.h",
"experimental_annotated_stack.cc",
"experimental_annotated_stack.h",
- "experimental_counter_dur.cc",
- "experimental_counter_dur.h",
"experimental_flamegraph.cc",
"experimental_flamegraph.h",
"experimental_flat_slice.cc",
"experimental_flat_slice.h",
- "experimental_sched_upid.cc",
- "experimental_sched_upid.h",
"experimental_slice_layout.cc",
"experimental_slice_layout.h",
"flamegraph_construction_algorithms.cc",
"flamegraph_construction_algorithms.h",
- "winscope_proto_to_args_with_defaults.cc",
- "winscope_proto_to_args_with_defaults.h",
"table_info.cc",
"table_info.h",
+ "winscope_proto_to_args_with_defaults.cc",
+ "winscope_proto_to_args_with_defaults.h",
]
deps = [
":tables",
@@ -96,7 +92,6 @@
"ancestor_unittest.cc",
"connected_flow_unittest.cc",
"descendant_unittest.cc",
- "experimental_counter_dur_unittest.cc",
"experimental_flat_slice_unittest.cc",
"experimental_slice_layout_unittest.cc",
]
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.cc
deleted file mode 100644
index 48407cc..0000000
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.cc
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.h"
-
-#include <cstdint>
-#include <memory>
-#include <string>
-#include <unordered_map>
-#include <vector>
-
-#include "perfetto/base/logging.h"
-#include "perfetto/ext/base/status_or.h"
-#include "perfetto/trace_processor/basic_types.h"
-#include "src/trace_processor/db/column_storage.h"
-#include "src/trace_processor/db/table.h"
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/tables_py.h"
-#include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/tables/counter_tables_py.h"
-
-namespace perfetto::trace_processor {
-namespace tables {
-
-ExperimentalCounterDurTable::~ExperimentalCounterDurTable() = default;
-
-} // namespace tables
-
-ExperimentalCounterDur::ExperimentalCounterDur(
- const tables::CounterTable& table)
- : counter_table_(&table) {}
-ExperimentalCounterDur::~ExperimentalCounterDur() = default;
-
-Table::Schema ExperimentalCounterDur::CreateSchema() {
- return tables::ExperimentalCounterDurTable::ComputeStaticSchema();
-}
-
-std::string ExperimentalCounterDur::TableName() {
- return tables::ExperimentalCounterDurTable::Name();
-}
-
-uint32_t ExperimentalCounterDur::EstimateRowCount() {
- return counter_table_->row_count();
-}
-
-base::StatusOr<std::unique_ptr<Table>> ExperimentalCounterDur::ComputeTable(
- const std::vector<SqlValue>& arguments) {
- PERFETTO_CHECK(arguments.empty());
- if (!counter_dur_table_) {
- counter_dur_table_ = tables::ExperimentalCounterDurTable::ExtendParent(
- *counter_table_, ComputeDurColumn(*counter_table_),
- ComputeDeltaColumn(*counter_table_));
- }
- return std::make_unique<Table>(counter_dur_table_->Copy());
-}
-
-// static
-ColumnStorage<int64_t> ExperimentalCounterDur::ComputeDurColumn(
- const CounterTable& table) {
- // Keep track of the last seen row for each track id.
- std::unordered_map<TrackId, CounterTable::RowNumber> last_row_for_track_id;
- ColumnStorage<int64_t> dur;
-
- for (auto table_it = table.IterateRows(); table_it; ++table_it) {
- // Check if we already have a previous row for the current track id.
- TrackId track_id = table_it.track_id();
- auto it = last_row_for_track_id.find(track_id);
- if (it == last_row_for_track_id.end()) {
- // This means we don't have any row - start tracking this row for the
- // future.
- last_row_for_track_id.emplace(track_id, table_it.row_number());
- } else {
- // This means we have an previous row for the current track id. Update
- // the duration of the previous row to be up to the current ts.
- CounterTable::RowNumber old_row = it->second;
- it->second = table_it.row_number();
- dur.Set(old_row.row_number(),
- table_it.ts() - old_row.ToRowReference(table).ts());
- }
- // Append -1 to mark this event as not having been finished. On a later
- // row, we may set this to have the correct value.
- dur.Append(-1);
- }
- return dur;
-}
-
-// static
-ColumnStorage<double> ExperimentalCounterDur::ComputeDeltaColumn(
- const CounterTable& table) {
- // Keep track of the last seen row for each track id.
- std::unordered_map<TrackId, CounterTable::RowNumber> last_row_for_track_id;
- ColumnStorage<double> delta;
-
- for (auto table_it = table.IterateRows(); table_it; ++table_it) {
- // Check if we already have a previous row for the current track id.
- TrackId track_id = table_it.track_id();
- auto it = last_row_for_track_id.find(track_id);
- if (it == last_row_for_track_id.end()) {
- // This means we don't have any row - start tracking this row for the
- // future.
- last_row_for_track_id.emplace(track_id, table_it.row_number());
- } else {
- // This means we have an previous row for the current track id. Update
- // the duration of the previous row to be up to the current ts.
- CounterTable::RowNumber old_row = it->second;
- it->second = table_it.row_number();
- delta.Set(old_row.row_number(),
- table_it.value() - old_row.ToRowReference(table).value());
- }
- delta.Append(0);
- }
- return delta;
-}
-
-} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.h b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.h
deleted file mode 100644
index 3e1999b..0000000
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.h
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_EXPERIMENTAL_COUNTER_DUR_H_
-#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_EXPERIMENTAL_COUNTER_DUR_H_
-
-#include <cstdint>
-#include <memory>
-#include <string>
-#include <vector>
-
-#include "perfetto/ext/base/status_or.h"
-#include "perfetto/trace_processor/basic_types.h"
-#include "src/trace_processor/db/column_storage.h"
-#include "src/trace_processor/db/table.h"
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/static_table_function.h"
-#include "src/trace_processor/tables/counter_tables_py.h"
-
-namespace perfetto::trace_processor {
-
-class ExperimentalCounterDur : public StaticTableFunction {
- public:
- using CounterTable = tables::CounterTable;
-
- explicit ExperimentalCounterDur(const CounterTable& table);
- virtual ~ExperimentalCounterDur() override;
-
- Table::Schema CreateSchema() override;
- std::string TableName() override;
- uint32_t EstimateRowCount() override;
- base::StatusOr<std::unique_ptr<Table>> ComputeTable(
- const std::vector<SqlValue>& arguments) override;
-
- // public + static for testing
- static ColumnStorage<int64_t> ComputeDurColumn(const CounterTable& table);
- static ColumnStorage<double> ComputeDeltaColumn(const CounterTable& table);
-
- private:
- const CounterTable* counter_table_ = nullptr;
- std::unique_ptr<Table> counter_dur_table_;
-};
-
-} // namespace perfetto::trace_processor
-
-#endif // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_EXPERIMENTAL_COUNTER_DUR_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur_unittest.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur_unittest.cc
deleted file mode 100644
index 783c2a0..0000000
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur_unittest.cc
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.h"
-
-#include <cstdint>
-
-#include "src/trace_processor/containers/string_pool.h"
-#include "src/trace_processor/tables/counter_tables_py.h"
-#include "src/trace_processor/tables/track_tables_py.h"
-#include "test/gtest_and_gmock.h"
-
-namespace perfetto::trace_processor {
-namespace {
-
-tables::CounterTable::Row CounterRow(int64_t ts, uint32_t track_id) {
- tables::CounterTable::Row row;
- row.ts = ts;
- row.track_id = tables::TrackTable::Id{track_id};
- return row;
-}
-
-TEST(ExperimentalCounterDur, SmokeDur) {
- StringPool pool;
- tables::CounterTable table(&pool);
-
- table.Insert(CounterRow(100 /* ts */, 1 /* track_id */));
- table.Insert(CounterRow(102 /* ts */, 2 /* track_id */));
- table.Insert(CounterRow(105 /* ts */, 1 /* track_id */));
- table.Insert(CounterRow(105 /* ts */, 3 /* track_id */));
- table.Insert(CounterRow(105 /* ts */, 2 /* track_id */));
- table.Insert(CounterRow(110 /* ts */, 2 /* track_id */));
-
- auto dur = ExperimentalCounterDur::ComputeDurColumn(table);
- ASSERT_EQ(dur.size(), table.row_count());
-
- ASSERT_EQ(dur.Get(0), 5);
- ASSERT_EQ(dur.Get(1), 3);
- ASSERT_EQ(dur.Get(2), -1);
- ASSERT_EQ(dur.Get(3), -1);
- ASSERT_EQ(dur.Get(4), 5);
- ASSERT_EQ(dur.Get(5), -1);
-}
-
-} // namespace
-} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.cc
index 6d4ff85..ccd9bf5 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.cc
@@ -86,7 +86,7 @@
row.track_id = track_id;
row.category = kNullStringId;
row.name = kNullStringId;
- row.arg_set_id = kInvalidArgSetId;
+ row.arg_set_id = std::nullopt;
row.source_id = std::nullopt;
row.start_bound = start_bound;
row.end_bound = end_bound;
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.cc
deleted file mode 100644
index 6c4b43c..0000000
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.cc
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.h"
-
-#include <cstdint>
-#include <memory>
-#include <optional>
-#include <string>
-#include <vector>
-
-#include "perfetto/base/logging.h"
-#include "perfetto/ext/base/status_or.h"
-#include "perfetto/trace_processor/basic_types.h"
-#include "src/trace_processor/db/column_storage.h"
-#include "src/trace_processor/db/table.h"
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/tables_py.h"
-#include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/tables/metadata_tables_py.h"
-#include "src/trace_processor/tables/sched_tables_py.h"
-
-namespace perfetto::trace_processor {
-namespace tables {
-
-ExperimentalSchedUpidTable::~ExperimentalSchedUpidTable() = default;
-
-} // namespace tables
-
-ExperimentalSchedUpid::ExperimentalSchedUpid(
- const tables::SchedSliceTable& sched,
- const tables::ThreadTable& thread)
- : sched_slice_table_(&sched), thread_table_(&thread) {}
-ExperimentalSchedUpid::~ExperimentalSchedUpid() = default;
-
-Table::Schema ExperimentalSchedUpid::CreateSchema() {
- return tables::ExperimentalSchedUpidTable::ComputeStaticSchema();
-}
-
-std::string ExperimentalSchedUpid::TableName() {
- return tables::ExperimentalSchedUpidTable::Name();
-}
-
-uint32_t ExperimentalSchedUpid::EstimateRowCount() {
- return sched_slice_table_->row_count();
-}
-
-base::StatusOr<std::unique_ptr<Table>> ExperimentalSchedUpid::ComputeTable(
- const std::vector<SqlValue>& arguments) {
- PERFETTO_CHECK(arguments.empty());
- if (!sched_upid_table_) {
- sched_upid_table_ = tables::ExperimentalSchedUpidTable::ExtendParent(
- *sched_slice_table_, ComputeUpidColumn());
- }
- return std::make_unique<Table>(sched_upid_table_->Copy());
-}
-
-ColumnStorage<std::optional<UniquePid>>
-ExperimentalSchedUpid::ComputeUpidColumn() {
- ColumnStorage<std::optional<UniquePid>> upid;
- for (uint32_t i = 0; i < sched_slice_table_->row_count(); ++i) {
- upid.Append(thread_table_->upid()[sched_slice_table_->utid()[i]]);
- }
- return upid;
-}
-
-} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.h b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.h
deleted file mode 100644
index 0eecc4f..0000000
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.h
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_EXPERIMENTAL_SCHED_UPID_H_
-#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_EXPERIMENTAL_SCHED_UPID_H_
-
-#include <cstdint>
-#include <memory>
-#include <optional>
-#include <string>
-#include <vector>
-
-#include "perfetto/ext/base/status_or.h"
-#include "perfetto/trace_processor/basic_types.h"
-#include "src/trace_processor/db/column_storage.h"
-#include "src/trace_processor/db/table.h"
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/static_table_function.h"
-#include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/tables/metadata_tables_py.h"
-#include "src/trace_processor/tables/sched_tables_py.h"
-
-namespace perfetto::trace_processor {
-
-class ExperimentalSchedUpid : public StaticTableFunction {
- public:
- ExperimentalSchedUpid(const tables::SchedSliceTable&,
- const tables::ThreadTable&);
- virtual ~ExperimentalSchedUpid() override;
-
- Table::Schema CreateSchema() override;
- std::string TableName() override;
- uint32_t EstimateRowCount() override;
- base::StatusOr<std::unique_ptr<Table>> ComputeTable(
- const std::vector<SqlValue>& arguments) override;
-
- private:
- ColumnStorage<std::optional<UniquePid>> ComputeUpidColumn();
-
- const tables::SchedSliceTable* sched_slice_table_;
- const tables::ThreadTable* thread_table_;
- std::unique_ptr<Table> sched_upid_table_;
-};
-
-} // namespace perfetto::trace_processor
-
-#endif // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_EXPERIMENTAL_SCHED_UPID_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.cc
index 401de7b..282d27c 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.cc
@@ -104,27 +104,15 @@
auto table = std::make_unique<TableInfoTable>(string_pool_);
auto table_name_id = string_pool_->InternString(table_name.c_str());
- // Find static table
- const Table* static_table = engine_->GetStaticTableOrNull(table_name);
- if (static_table) {
- for (auto& row : GetColInfoRows(static_table->columns(), string_pool_)) {
+ // Find table
+ const Table* t = engine_->GetTableOrNull(table_name);
+ if (t) {
+ for (auto& row : GetColInfoRows(t->columns(), string_pool_)) {
row.table_name = table_name_id;
table->Insert(row);
}
return std::unique_ptr<Table>(std::move(table));
}
-
- // Find runtime table
- const RuntimeTable* runtime_table =
- engine_->GetRuntimeTableOrNull(table_name);
- if (runtime_table) {
- for (auto& row : GetColInfoRows(runtime_table->columns(), string_pool_)) {
- row.table_name = table_name_id;
- table->Insert(row);
- }
- return std::unique_ptr<Table>(std::move(table));
- }
-
return base::ErrStatus("Perfetto table '%s' not found.", table_name.c_str());
}
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py b/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
index 22999ee..0b63b7f 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
@@ -125,25 +125,6 @@
],
parent=STACK_PROFILE_CALLSITE_TABLE)
-EXPERIMENTAL_COUNTER_DUR_TABLE = Table(
- python_module=__file__,
- class_name="ExperimentalCounterDurTable",
- sql_name="experimental_counter_dur",
- columns=[
- C("dur", CppInt64()),
- C("delta", CppDouble()),
- ],
- parent=COUNTER_TABLE)
-
-EXPERIMENTAL_SCHED_UPID_TABLE = Table(
- python_module=__file__,
- class_name="ExperimentalSchedUpidTable",
- sql_name="__intrinsic_sched_upid",
- columns=[
- C("upid", CppOptional(CppTableId(PROCESS_TABLE))),
- ],
- parent=SCHED_SLICE_TABLE)
-
EXPERIMENTAL_SLICE_LAYOUT_TABLE = Table(
python_module=__file__,
class_name="ExperimentalSliceLayoutTable",
@@ -189,8 +170,6 @@
DESCENDANT_SLICE_TABLE,
DFS_WEIGHT_BOUNDED_TABLE,
EXPERIMENTAL_ANNOTATED_CALLSTACK_TABLE,
- EXPERIMENTAL_COUNTER_DUR_TABLE,
- EXPERIMENTAL_SCHED_UPID_TABLE,
EXPERIMENTAL_SLICE_LAYOUT_TABLE,
TABLE_INFO_TABLE,
]
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.cc
index ee8e600..0ba91d4 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.cc
@@ -16,9 +16,21 @@
#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h"
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include "perfetto/base/logging.h"
#include "perfetto/base/status.h"
#include "perfetto/ext/base/base64.h"
#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/protozero/field.h"
+#include "perfetto/trace_processor/basic_types.h"
#include "src/trace_processor/containers/string_pool.h"
#include "src/trace_processor/db/table.h"
#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
@@ -84,7 +96,7 @@
Row r;
SetColumnsAndInsertRow(key, r);
}
- void AddPointer(const Key&, const void*) override {
+ void AddPointer(const Key&, uint64_t) override {
PERFETTO_FATAL("Unsupported");
}
bool AddJson(const Key&, const protozero::ConstChars&) override {
@@ -150,7 +162,7 @@
WinscopeProtoToArgsWithDefaults::WinscopeProtoToArgsWithDefaults(
StringPool* string_pool,
- PerfettoSqlEngine* engine,
+ const PerfettoSqlEngine* engine,
TraceProcessorContext* context)
: string_pool_(string_pool), engine_(engine), context_(context) {}
@@ -165,7 +177,7 @@
}
std::string table_name = arguments[0].AsString();
- const Table* static_table = engine_->GetStaticTableOrNull(table_name);
+ const Table* static_table = engine_->GetTableOrNull(table_name);
if (!static_table) {
return base::ErrStatus("Failed to find %s table.", table_name.c_str());
}
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h b/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h
index 91ab8c8..84b3a47 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h
@@ -17,7 +17,13 @@
#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_WINSCOPE_PROTO_TO_ARGS_WITH_DEFAULTS_H_
#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_WINSCOPE_PROTO_TO_ARGS_WITH_DEFAULTS_H_
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <vector>
+
#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/basic_types.h"
#include "src/trace_processor/containers/string_pool.h"
#include "src/trace_processor/db/table.h"
#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
@@ -30,7 +36,7 @@
class WinscopeProtoToArgsWithDefaults : public StaticTableFunction {
public:
explicit WinscopeProtoToArgsWithDefaults(StringPool*,
- PerfettoSqlEngine*,
+ const PerfettoSqlEngine*,
TraceProcessorContext* context);
Table::Schema CreateSchema() override;
@@ -41,7 +47,7 @@
private:
StringPool* string_pool_ = nullptr;
- PerfettoSqlEngine* engine_ = nullptr;
+ const PerfettoSqlEngine* engine_ = nullptr;
TraceProcessorContext* context_ = nullptr;
};
diff --git a/src/trace_processor/perfetto_sql/stdlib/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
index 92150ae..09386af 100644
--- a/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
@@ -20,6 +20,7 @@
perfetto_amalgamated_sql_header("stdlib") {
deps = [
"android",
+ "appleos",
"callstacks",
"chrome:chrome_sql",
"counters",
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 6e5956b..b8d70c4 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
@@ -66,7 +66,8 @@
s.id,
s.process_name,
thread.utid,
- s.upid
+ s.upid,
+ s.ts + s.dur AS ts_end
FROM thread_slice s JOIN
thread USING (utid)
WHERE
@@ -81,6 +82,7 @@
tx.binder_txn_id AS id,
tx.client_process as process_name,
tx.client_utid as utid,
- tx.client_upid as upid
+ tx.client_upid as upid,
+ tx.client_ts + tx.client_dur AS ts_end
FROM android_binder_txns AS tx
WHERE aidl_name IS NOT NULL AND is_sync = 1;
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/frames/timeline.sql b/src/trace_processor/perfetto_sql/stdlib/android/frames/timeline.sql
index ff0c822..022260f 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/frames/timeline.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/frames/timeline.sql
@@ -24,7 +24,7 @@
glob_str STRING
) RETURNS TABLE (
-- Frame slice.
- id JOINID(slice.id),
+ id ID(slice.id),
-- Parsed frame id.
frame_id LONG,
-- Utid.
@@ -55,7 +55,7 @@
CREATE PERFETTO TABLE android_frames_choreographer_do_frame(
-- Choreographer#doFrame slice. Slice with the name "Choreographer#doFrame
-- {frame id}".
- id JOINID(slice.id),
+ id ID(slice.id),
-- Frame id. Taken as the value behind "Choreographer#doFrame" in slice
-- name.
frame_id LONG,
@@ -82,7 +82,7 @@
-- notifications).
CREATE PERFETTO TABLE android_frames_draw_frame(
-- DrawFrame slice. Slice with the name "DrawFrame {frame id}".
- id JOINID(slice.id),
+ id ID(slice.id),
-- Frame id. Taken as the value behind "DrawFrame" in slice
-- name.
frame_id LONG,
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/io.sql b/src/trace_processor/perfetto_sql/stdlib/android/io.sql
index e92dbcc..b122b91 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/io.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/io.sql
@@ -77,7 +77,7 @@
EXTRACT_ARG(arg_set_id, 'dev') AS dev,
EXTRACT_ARG(arg_set_id, 'ino') AS ino,
EXTRACT_ARG(arg_set_id, 'copied') AS copied
- FROM raw
+ FROM ftrace_event
WHERE name GLOB 'f2fs_write_end*'
)
SELECT
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/screenshots.sql b/src/trace_processor/perfetto_sql/stdlib/android/screenshots.sql
index 39c0786..6c06c87 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/screenshots.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/screenshots.sql
@@ -15,7 +15,7 @@
-- Screenshot slices, used in perfetto UI.
CREATE PERFETTO TABLE android_screenshots(
-- Id of the screenshot slice.
- id JOINID(slice.id),
+ id ID(slice.id),
-- Slice timestamp.
ts TIMESTAMP,
-- Slice duration, should be typically 0 since screeenshot slices are of instant
diff --git a/src/trace_processor/perfetto_sql/stdlib/appleos/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/appleos/BUILD.gn
new file mode 100644
index 0000000..0ddc0a0
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/appleos/BUILD.gn
@@ -0,0 +1,20 @@
+# Copyright (C) 2025 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("../../../../../gn/perfetto_sql.gni")
+
+perfetto_sql_source_set("appleos") {
+ sources = []
+ deps = [ "instruments" ]
+}
diff --git a/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/BUILD.gn
new file mode 100644
index 0000000..b964f2e
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/BUILD.gn
@@ -0,0 +1,19 @@
+# Copyright (C) 2025 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("../../../../../../gn/perfetto_sql.gni")
+
+perfetto_sql_source_set("instruments") {
+ sources = [ "samples.sql" ]
+}
diff --git a/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/samples.sql b/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/samples.sql
new file mode 100644
index 0000000..c0c1c40
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/samples.sql
@@ -0,0 +1,62 @@
+--
+-- Copyright 2025 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the 'License');
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an 'AS IS' BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+INCLUDE PERFETTO MODULE callstacks.stack_profile;
+
+CREATE PERFETTO TABLE _appleos_instruments_raw_callstacks AS
+SELECT *
+FROM _callstacks_for_callsites!((
+ SELECT p.callsite_id
+ FROM instruments_sample p
+)) c
+ORDER BY c.id;
+
+-- Table summarising the callstacks captured during all
+-- instruments samples in the trace.
+--
+-- Specifically, this table returns a tree containing all
+-- the callstacks seen during the trace with `self_count`
+-- equal to the number of samples with that frame as the
+-- leaf and `cumulative_count` equal to the number of
+-- samples with the frame anywhere in the tree.
+CREATE PERFETTO TABLE appleos_instruments_samples_summary_tree(
+ -- The id of the callstack. A callstack in this context
+ -- is a unique set of frames up to the root.
+ id LONG,
+ -- The id of the parent callstack for this callstack.
+ parent_id LONG,
+ -- The function name of the frame for this callstack.
+ name STRING,
+ -- The name of the mapping containing the frame. This
+ -- can be a native binary, library, or JIT.
+ mapping_name STRING,
+ -- The name of the file containing the function.
+ source_file STRING,
+ -- The line number in the file the function is located at.
+ line_number LONG,
+ -- The number of samples with this function as the leaf
+ -- frame.
+ self_count LONG,
+ -- The number of samples with this function appearing
+ -- anywhere on the callstack.
+ cumulative_count LONG
+) AS
+SELECT r.*, a.cumulative_count
+FROM _callstacks_self_to_cumulative!((
+ SELECT id, parent_id, self_count
+ FROM _appleos_instruments_raw_callstacks
+)) a
+JOIN _appleos_instruments_raw_callstacks r USING (id)
+ORDER BY r.id;
diff --git a/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql b/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql
index d8bc406..2785e51 100644
--- a/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql
@@ -15,6 +15,7 @@
INCLUDE PERFETTO MODULE graphs.hierarchy;
INCLUDE PERFETTO MODULE graphs.scan;
+INCLUDE PERFETTO MODULE v8.jit;
CREATE PERFETTO TABLE _callstack_spf_summary AS
SELECT
@@ -52,9 +53,11 @@
s.id - 1
) AS parent_symbol_id,
f.id AS frame_id,
+ jf.jit_code_id AS jit_code_id,
s.id IS f.max_symbol_id AS is_leaf
FROM stack_profile_callsite c
JOIN _callstack_spf_summary f ON c.frame_id = f.id
+LEFT JOIN __intrinsic_jit_frame jf ON jf.frame_id = f.id
LEFT JOIN stack_profile_symbol s USING (symbol_set_id)
LEFT JOIN stack_profile_callsite p ON c.parent_id = p.id
LEFT JOIN _callstack_spf_summary pf ON p.frame_id = pf.id
@@ -67,17 +70,29 @@
-- TODO(lalitm): consider demangling in a separate table as
-- demangling is suprisingly inefficient and is taking a
-- significant fraction of the runtime on big traces.
- IFNULL(
+ COALESCE(
+ 'JS: ' || IIF(jsf.name = "", "(anonymous)", jsf.name) || ':' || jsf.line || ':' || jsf.col || ' [' || LOWER(jsc.tier) || ']',
+ 'WASM: ' || wc.function_name || ' [' || LOWER(wc.tier) || ']',
+ 'REGEXP: ' || rc.pattern,
+ 'V8: ' || v8c.function_name,
+ 'JIT: ' || jc.function_name,
DEMANGLE(COALESCE(s.name, f.deobfuscated_name, f.name)),
COALESCE(s.name, f.deobfuscated_name, f.name, '[Unknown]')
) AS name,
f.mapping AS mapping_id,
s.source_file,
- s.line_number,
+ COALESCE(jsf.line, s.line_number) as line_number,
+ COALESCE(jsf.col, 0) as column_number,
c.callsite_id,
c.is_leaf AS is_leaf_function_in_callsite_frame
FROM _callstack_spc_raw_forest c
JOIN stack_profile_frame f ON c.frame_id = f.id
+LEFT JOIN _v8_js_code jsc USING(jit_code_id)
+LEFT JOIN v8_js_function jsf USING(v8_js_function_id)
+LEFT JOIN _v8_internal_code v8c USING(jit_code_id)
+LEFT JOIN _v8_wasm_code wc USING(jit_code_id)
+LEFT JOIN _v8_regexp_code rc USING(jit_code_id)
+LEFT JOIN __intrinsic_jit_code jc ON c.jit_code_id = jc.id
LEFT JOIN stack_profile_symbol s ON c.symbol_id = s.id
LEFT JOIN _callstack_spc_raw_forest p ON
p.callsite_id = c.parent_callsite_id
@@ -172,4 +187,4 @@
JOIN $callstacks r USING (id)
)
) a
-)
\ No newline at end of file
+)
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scrolls.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scrolls.sql
index d526f64..97d64fa 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scrolls.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scrolls.sql
@@ -155,7 +155,7 @@
-- Whether this is the first input that was presented in frame
-- `presented_in_frame_id`.
is_first_scroll_update_in_frame BOOL,
- -- Whether the corresponding input event was coalesced into another.
+ -- Input generation timestamp (from the Android system).
generation_ts TIMESTAMP,
-- Duration from input generation to when the browser received the input.
generation_to_browser_main_dur DURATION,
@@ -252,7 +252,13 @@
-- No applicable utid (duration between two threads).
-- No applicable slice id (duration between two threads).
generation_ts,
- touch_move_received_ts - generation_ts AS generation_to_browser_main_dur,
+ -- Flings don't have a touch move event so make GenerationToBrowserMain span
+ -- all the way to the creation of the gesture scroll update.
+ IIF(
+ is_inertial AND touch_move_received_ts IS NULL,
+ scroll_update_created_ts,
+ touch_move_received_ts
+ ) - generation_ts AS generation_to_browser_main_dur,
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
browser_utid,
touch_move_received_slice_id,
@@ -355,7 +361,6 @@
chrome_event_latency.buffer_available_timestamp,
chrome_event_latency.buffer_ready_timestamp,
chrome_event_latency.latch_timestamp,
- chrome_event_latency.swap_end_timestamp,
chrome_event_latency.presentation_timestamp
FROM _chrome_scroll_update_refs refs
LEFT JOIN chrome_event_latencies chrome_event_latency
@@ -484,12 +489,10 @@
viz_swap_buffers_to_latch_dur DURATION,
-- Timestamp for `EventLatency`'s `LatchToSwapEnd` step.
latch_timestamp TIMESTAMP,
- -- Duration of `EventLatency`'s `LatchToSwapEnd` step.
- viz_latch_to_swap_end_dur DURATION,
- -- Timestamp for `EventLatency`'s `SwapEndToPresentationCompositorFrame` step.
- swap_end_timestamp TIMESTAMP,
- -- Duration of `EventLatency`'s `SwapEndToPresentationCompositorFrame` step.
- swap_end_to_presentation_dur DURATION,
+ -- Duration of either `EventLatency`'s `LatchToSwapEnd` +
+ -- `SwapEndToPresentationCompositorFrame` steps or its `LatchToPresentation`
+ -- step.
+ viz_latch_to_presentation_dur DURATION,
-- Presentation timestamp for the frame.
presentation_timestamp TIMESTAMP
) AS
@@ -550,7 +553,6 @@
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- Timestamps
latch_timestamp,
- swap_end_timestamp,
presentation_timestamp
FROM _scroll_update_frame_timestamps_and_metadata
)
@@ -615,10 +617,7 @@
latch_timestamp - viz_swap_buffers_end_ts AS viz_swap_buffers_to_latch_dur,
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
latch_timestamp,
- swap_end_timestamp - latch_timestamp AS viz_latch_to_swap_end_dur,
- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
- swap_end_timestamp,
- presentation_timestamp - swap_end_timestamp AS swap_end_to_presentation_dur,
+ presentation_timestamp - latch_timestamp AS viz_latch_to_presentation_dur,
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
presentation_timestamp
FROM processed_timestamps_and_metadata;
@@ -841,12 +840,10 @@
viz_swap_buffers_to_latch_dur DURATION,
-- Timestamp for `EventLatency`'s `LatchToSwapEnd` step.
latch_timestamp TIMESTAMP,
- -- Duration of `EventLatency`'s `LatchToSwapEnd` step.
- viz_latch_to_swap_end_dur DURATION,
- -- Timestamp for `EventLatency`'s `SwapEndToPresentationCompositorFrame` step.
- swap_end_timestamp TIMESTAMP,
- -- Duration of `EventLatency`'s `SwapEndToPresentationCompositorFrame` step.
- swap_end_to_presentation_dur DURATION,
+ -- Duration of either `EventLatency`'s `LatchToSwapEnd` +
+ -- `SwapEndToPresentationCompositorFrame` steps or its `LatchToPresentation`
+ -- step.
+ viz_latch_to_presentation_dur DURATION,
-- Presentation timestamp for the frame.
presentation_timestamp TIMESTAMP)
AS
@@ -961,12 +958,123 @@
frame.viz_swap_buffers_to_latch_dur,
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
frame.latch_timestamp,
- frame.viz_latch_to_swap_end_dur,
- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
- frame.swap_end_timestamp,
- frame.swap_end_to_presentation_dur,
+ frame.viz_latch_to_presentation_dur,
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
frame.presentation_timestamp
FROM chrome_scroll_update_input_info AS input
LEFT JOIN chrome_scroll_update_frame_info AS frame
ON input.presented_in_frame_id = frame.id;
+
+-- Source of truth for the definition of the stages of a scroll. Mainly intended
+-- for visualization purposes (e.g. in Chrome Scroll Jank plugin).
+CREATE PERFETTO TABLE chrome_scroll_update_info_step_templates(
+ -- The name of a stage of a scroll.
+ step_name STRING,
+ -- The name of the column in `chrome_scroll_update_info` which contains the
+ -- timestamp of the stage.
+ ts_column_name STRING,
+ -- The name of the column in `chrome_scroll_update_info` which contains the
+ -- duration of the stage. NULL if the stage doesn't have a duration.
+ dur_column_name STRING
+) AS
+WITH steps(step_name, ts_column_name, dur_column_name)
+AS (
+ VALUES
+ (
+ 'GenerationToBrowserMain',
+ 'generation_ts',
+ 'generation_to_browser_main_dur'
+ ),
+ (
+ 'TouchMoveProcessing',
+ 'touch_move_received_ts',
+ 'touch_move_processing_dur'
+ ),
+ (
+ 'ScrollUpdateProcessing',
+ 'scroll_update_created_ts',
+ 'scroll_update_processing_dur'
+ ),
+ (
+ 'BrowserMainToRendererCompositor',
+ 'scroll_update_created_end_ts',
+ 'browser_to_compositor_delay_dur'
+ ),
+ (
+ 'RendererCompositorDispatch',
+ 'compositor_dispatch_ts',
+ 'compositor_dispatch_dur'
+ ),
+ (
+ 'RendererCompositorDispatchToOnBeginFrame',
+ 'compositor_dispatch_end_ts',
+ 'compositor_dispatch_to_on_begin_frame_delay_dur'
+ ),
+ (
+ 'RendererCompositorBeginFrame',
+ 'compositor_on_begin_frame_ts',
+ 'compositor_on_begin_frame_dur'
+ ),
+ (
+ 'RendererCompositorBeginToGenerateFrame',
+ 'compositor_on_begin_frame_end_ts',
+ 'compositor_on_begin_frame_to_generation_delay_dur'
+ ),
+ (
+ 'RendererCompositorGenerateToSubmitFrame',
+ 'compositor_generate_compositor_frame_ts',
+ 'compositor_generate_frame_to_submit_frame_dur'
+ ),
+ (
+ 'RendererCompositorSubmitFrame',
+ 'compositor_submit_compositor_frame_ts',
+ 'compositor_submit_frame_dur'
+ ),
+ (
+ 'RendererCompositorToViz',
+ 'compositor_submit_compositor_frame_end_ts',
+ 'compositor_to_viz_delay_dur'
+ ),
+ (
+ 'VizReceiveFrame',
+ 'viz_receive_compositor_frame_ts',
+ 'viz_receive_compositor_frame_dur'
+ ),
+ (
+ 'VizReceiveToDrawFrame',
+ 'viz_receive_compositor_frame_end_ts',
+ 'viz_wait_for_draw_dur'
+ ),
+ (
+ 'VizDrawToSwapFrame',
+ 'viz_draw_and_swap_ts',
+ 'viz_draw_and_swap_dur'
+ ),
+ (
+ 'VizToGpu',
+ 'viz_send_buffer_swap_end_ts',
+ 'viz_to_gpu_delay_dur'
+ ),
+ (
+ 'VizSwapBuffers',
+ 'viz_swap_buffers_ts',
+ 'viz_swap_buffers_dur'
+ ),
+ (
+ 'VizSwapBuffersToLatch',
+ 'viz_swap_buffers_end_ts',
+ 'viz_swap_buffers_to_latch_dur'
+ ),
+ (
+ 'VizLatchToPresentation',
+ 'latch_timestamp',
+ 'viz_latch_to_presentation_dur'
+ ),
+ (
+ 'Presentation',
+ 'presentation_timestamp',
+ NULL
+ )
+)
+SELECT step_name, ts_column_name, dur_column_name
+FROM steps;
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/event_latency.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/event_latency.sql
index d2e6330..82049b0 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/event_latency.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/event_latency.sql
@@ -99,7 +99,8 @@
buffer_available_timestamp LONG,
-- Timestamp of the BufferReadyToLatch substage.
buffer_ready_timestamp LONG,
- -- Timestamp of the LatchToSwapEnd substage.
+ -- Timestamp of the LatchToSwapEnd substage (or LatchToPresentation as a
+ -- fallback).
latch_timestamp LONG,
-- Timestamp of the SwapEndToPresentationCompositorFrame substage.
swap_end_timestamp LONG,
@@ -129,7 +130,10 @@
AS buffer_available_timestamp,
_descendant_slice_begin(slice.id, 'BufferReadyToLatch')
AS buffer_ready_timestamp,
- _descendant_slice_begin(slice.id, 'LatchToSwapEnd') AS latch_timestamp,
+ COALESCE(
+ _descendant_slice_begin(slice.id, 'LatchToSwapEnd'),
+ _descendant_slice_begin(slice.id, 'LatchToPresentation')
+ ) AS latch_timestamp,
_descendant_slice_begin(slice.id, 'SwapEndToPresentationCompositorFrame')
AS swap_end_timestamp,
_get_presentation_timestamp(slice.id) AS presentation_timestamp
diff --git a/src/trace_processor/perfetto_sql/stdlib/pixel/camera.sql b/src/trace_processor/perfetto_sql/stdlib/pixel/camera.sql
index 5daf24d..881f930 100644
--- a/src/trace_processor/perfetto_sql/stdlib/pixel/camera.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/pixel/camera.sql
@@ -21,7 +21,7 @@
-- provides timing information for each processing stage.
CREATE PERFETTO TABLE pixel_camera_frames(
-- Unique identifier for this slice.
- id JOINID(slice.id),
+ id ID(slice.id),
-- Start timestamp of the slice.
ts TIMESTAMP,
-- Duration of the slice execution.
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/tables_views.sql b/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/tables_views.sql
index 859918f..9704052 100644
--- a/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/tables_views.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/tables_views.sql
@@ -260,42 +260,6 @@
FROM
__intrinsic_thread_state;
--- Contains 'raw' events from the trace for some types of events. This table
--- only exists for debugging purposes and should not be relied on in production
--- usecases (i.e. metrics, standard library etc.)
-CREATE PERFETTO VIEW raw (
- -- Unique identifier for this raw event.
- id ID,
- -- The timestamp of this event.
- ts TIMESTAMP,
- -- The name of the event. For ftrace events, this will be the ftrace event
- -- name.
- name STRING,
- -- The CPU this event was emitted on (meaningful only in single machine
- -- traces). For multi-machine, join with the `cpu` table on `ucpu` to get the
- -- CPU identifier of each machine.
- cpu LONG,
- -- The thread this event was emitted on.
- utid JOINID(thread.id),
- -- The set of key/value pairs associated with this event.
- arg_set_id ARGSETID,
- -- Ftrace event flags for this event. Currently only emitted for sched_waking
- -- events.
- common_flags LONG,
- -- The unique CPU identifier that this event was emitted on.
- ucpu LONG
-) AS
-SELECT
- id,
- ts,
- name,
- ucpu AS cpu,
- utid,
- arg_set_id,
- common_flags,
- ucpu
-FROM
- __intrinsic_raw;
-- Contains all the ftrace events in the trace. This table exists only for
-- debugging purposes and should not be relied on in production usecases (i.e.
@@ -334,51 +298,37 @@
FROM
__intrinsic_ftrace_event;
--- The sched_slice table with the upid column.
-CREATE PERFETTO VIEW experimental_sched_upid (
- -- Unique identifier for this scheduling slice.
+-- This table is deprecated. Use `ftrace_event` instead which contains the same
+-- rows; this table is simply a (badly named) alias.
+CREATE PERFETTO VIEW raw (
+ -- Unique identifier for this raw event.
id ID,
- -- The timestamp at the start of the slice.
+ -- The timestamp of this event.
ts TIMESTAMP,
- -- The duration of the slice.
- dur DURATION,
- -- The CPU that the slice executed on (meaningful only in single machine
+ -- The name of the event. For ftrace events, this will be the ftrace event
+ -- name.
+ name STRING,
+ -- The CPU this event was emitted on (meaningful only in single machine
-- traces). For multi-machine, join with the `cpu` table on `ucpu` to get the
-- CPU identifier of each machine.
cpu LONG,
- -- The thread's unique id in the trace.
+ -- The thread this event was emitted on.
utid JOINID(thread.id),
- -- A string representing the scheduling state of the kernel thread at the end
- -- of the slice. The individual characters in the string mean the following: R
- -- (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T
- -- (suspended), t (being traced), X (exiting), P (parked), W (waking), I
- -- (idle), N (not contributing to the load average), K (wakeable on fatal
- -- signals) and Z (zombie, awaiting cleanup).
- end_state STRING,
- -- The kernel priority that the thread ran at.
- priority LONG,
- -- The unique CPU identifier that the slice executed on.
- ucpu LONG,
- -- The process's unique id in the trace.
- upid JOINID(process.id)
+ -- The set of key/value pairs associated with this event.
+ arg_set_id ARGSETID,
+ -- Ftrace event flags for this event. Currently only emitted for sched_waking
+ -- events.
+ common_flags LONG,
+ -- The unique CPU identifier that this event was emitted on.
+ ucpu LONG
) AS
-SELECT
- id,
- ts,
- dur,
- ucpu AS cpu,
- utid,
- end_state,
- priority,
- ucpu,
- upid
-FROM
- __intrinsic_sched_upid;
+SELECT *
+FROM ftrace_event;
-- Tracks which are associated to a single thread.
CREATE PERFETTO TABLE thread_track (
-- Unique identifier for this thread track.
- id ID,
+ id ID(track.id),
-- Name of the track.
name STRING,
-- The type of a track indicates the type of data the track contains.
@@ -415,7 +365,7 @@
-- Tracks which are associated to a single process.
CREATE PERFETTO TABLE process_track (
-- Unique identifier for this process track.
- id ID,
+ id ID(track.id),
-- Name of the track.
name STRING,
-- The type of a track indicates the type of data the track contains.
@@ -427,7 +377,7 @@
type STRING,
-- The track which is the "parent" of this track. Only non-null for tracks
-- created using Perfetto's track_event API.
- parent_id LONG,
+ parent_id JOINID(track.id),
-- Args for this track which store information about "source" of this track in
-- the trace. For example: whether this track orginated from atrace, Chrome
-- tracepoints etc.
@@ -452,7 +402,7 @@
-- Tracks which are associated to a single CPU.
CREATE PERFETTO TABLE cpu_track (
-- Unique identifier for this cpu track.
- id ID,
+ id ID(track.id),
-- Name of the track.
name STRING,
-- The type of a track indicates the type of data the track contains.
@@ -494,7 +444,7 @@
-- instead.
CREATE PERFETTO TABLE gpu_track (
-- Unique identifier for this cpu track.
- id ID,
+ id ID(track.id),
-- Name of the track.
name STRING,
-- The type of a track indicates the type of data the track contains.
@@ -506,7 +456,7 @@
type STRING,
-- The track which is the "parent" of this track. Only non-null for tracks
-- created using Perfetto's track_event API.
- parent_id LONG,
+ parent_id JOINID(track.id),
-- Args for this track which store information about "source" of this track in
-- the trace. For example: whether this track orginated from atrace, Chrome
-- tracepoints etc.
@@ -550,7 +500,7 @@
-- Tracks containing counter-like events.
CREATE PERFETTO VIEW counter_track (
-- Unique identifier for this cpu counter track.
- id ID,
+ id ID(track.id),
-- Name of the track.
name STRING,
-- The track which is the "parent" of this track. Only non-null for tracks
@@ -593,7 +543,7 @@
-- Tracks containing counter-like events associated to a CPU.
CREATE PERFETTO TABLE cpu_counter_track (
-- Unique identifier for this cpu counter track.
- id ID,
+ id ID(track.id),
-- Name of the track.
name STRING,
-- The type of a track indicates the type of data the track contains.
@@ -636,7 +586,7 @@
-- Tracks containing counter-like events associated to a GPU.
CREATE PERFETTO TABLE gpu_counter_track (
-- Unique identifier for this gpu counter track.
- id ID,
+ id ID(track.id),
-- Name of the track.
name STRING,
-- The type of a track indicates the type of data the track contains.
@@ -652,7 +602,7 @@
-- Args for this track which store information about "source" of this track in
-- the trace. For example: whether this track orginated from atrace, Chrome
-- tracepoints etc.
- source_arg_set_id LONG,
+ source_arg_set_id ARGSETID,
-- Machine identifier, non-null for tracks on a remote machine.
machine_id LONG,
-- The units of the counter. This column is rarely filled.
@@ -679,7 +629,7 @@
-- Tracks containing counter-like events associated to a process.
CREATE PERFETTO TABLE process_counter_track (
-- Unique identifier for this process counter track.
- id ID,
+ id ID(track.id),
-- Name of the track.
name STRING,
-- The type of a track indicates the type of data the track contains.
@@ -695,7 +645,7 @@
-- Args for this track which store information about "source" of this track in
-- the trace. For example: whether this track orginated from atrace, Chrome
-- tracepoints etc.
- source_arg_set_id LONG,
+ source_arg_set_id ARGSETID,
-- Machine identifier, non-null for tracks on a remote machine.
machine_id LONG,
-- The units of the counter. This column is rarely filled.
@@ -722,7 +672,7 @@
-- Tracks containing counter-like events associated to a thread.
CREATE PERFETTO TABLE thread_counter_track (
-- Unique identifier for this thread counter track.
- id ID,
+ id ID(track.id),
-- Name of the track.
name STRING,
-- The type of a track indicates the type of data the track contains.
@@ -738,7 +688,7 @@
-- Args for this track which store information about "source" of this track in
-- the trace. For example: whether this track orginated from atrace, Chrome
-- tracepoints etc.
- source_arg_set_id LONG,
+ source_arg_set_id JOINID(track.id),
-- Machine identifier, non-null for tracks on a remote machine.
machine_id LONG,
-- The units of the counter. This column is rarely filled.
@@ -765,7 +715,7 @@
-- Tracks containing counter-like events collected from Linux perf.
CREATE PERFETTO TABLE perf_counter_track (
-- Unique identifier for this thread counter track.
- id ID,
+ id ID(track.id),
-- Name of the track.
name STRING,
-- The type of a track indicates the type of data the track contains.
@@ -781,7 +731,7 @@
-- Args for this track which store information about "source" of this track in
-- the trace. For example: whether this track orginated from atrace, Chrome
-- tracepoints etc.
- source_arg_set_id LONG,
+ source_arg_set_id ARGSETID,
-- Machine identifier, non-null for tracks on a remote machine.
machine_id LONG,
-- The units of the counter. This column is rarely filled.
@@ -831,3 +781,243 @@
FROM counter v
JOIN counter_track t ON v.track_id = t.id
ORDER BY ts;
+
+-- Table containing graphics frame events on Android.
+CREATE PERFETTO VIEW frame_slice(
+ -- Alias of `slice.id`.
+ id ID(slice.id),
+ -- Alias of `slice.ts`.
+ ts TIMESTAMP,
+ -- Alias of `slice.dur`.
+ dur DURATION,
+ -- Alias of `slice.track_id`.
+ track_id JOINID(track.id),
+ -- Alias of `slice.category`.
+ category STRING,
+ -- Alias of `slice.name`.
+ name STRING,
+ -- Alias of `slice.depth`.
+ depth LONG,
+ -- Alias of `slice.parent_id`.
+ parent_id JOINID(frame_slice.id),
+ -- Alias of `slice.arg_set_id`.
+ arg_set_id LONG,
+ -- Name of the graphics layer this slice happened on.
+ layer_name STRING,
+ -- The frame number this slice is associated with.
+ frame_number LONG,
+ -- The time between queue and acquire for this buffer and layer.
+ queue_to_acquire_time LONG,
+ -- The time between acquire and latch for this buffer and layer.
+ acquire_to_latch_time LONG,
+ -- The time between latch and present for this buffer and layer.
+ latch_to_present_time LONG
+) AS
+SELECT
+ s.id,
+ s.ts,
+ s.dur,
+ s.track_id,
+ s.category,
+ s.name,
+ s.depth,
+ s.parent_id,
+ s.arg_set_id,
+ extract_arg(s.arg_set_id, 'layer_name') as layer_name,
+ extract_arg(s.arg_set_id, 'frame_number') as frame_number,
+ extract_arg(s.arg_set_id, 'queue_to_acquire_time') as queue_to_acquire_time,
+ extract_arg(s.arg_set_id, 'acquire_to_latch_time') as acquire_to_latch_time,
+ extract_arg(s.arg_set_id, 'latch_to_present_time') as latch_to_present_time
+FROM slice s
+JOIN track t ON s.track_id = t.id
+WHERE t.type = 'graphics_frame_event';
+
+-- Table containing graphics frame events on Android.
+CREATE PERFETTO VIEW gpu_slice(
+ -- Alias of `slice.id`.
+ id ID(slice.id),
+ -- Alias of `slice.ts`.
+ ts TIMESTAMP,
+ -- Alias of `slice.dur`.
+ dur DURATION,
+ -- Alias of `slice.track_id`.
+ track_id JOINID(track.id),
+ -- Alias of `slice.category`.
+ category STRING,
+ -- Alias of `slice.name`.
+ name STRING,
+ -- Alias of `slice.depth`.
+ depth LONG,
+ -- Alias of `slice.parent_id`.
+ parent_id JOINID(frame_slice.id),
+ -- Alias of `slice.arg_set_id`.
+ arg_set_id LONG,
+ -- Context ID.
+ context_id LONG,
+ -- Render target ID.
+ render_target LONG,
+ -- The name of the render target.
+ render_target_name STRING,
+ -- Render pass ID.
+ render_pass LONG,
+ -- The name of the render pass.
+ render_pass_name STRING,
+ -- The command buffer ID.
+ command_buffer LONG,
+ -- The name of the command buffer.
+ command_buffer_name STRING,
+ -- Frame id.
+ frame_id LONG,
+ -- The submission id.
+ submission_id LONG,
+ -- The hardware queue id.
+ hw_queue_id LONG,
+ -- The id of the process.
+ upid JOINID(process.id),
+ -- Render subpasses.
+ render_subpasses STRING
+) AS
+SELECT
+ s.id,
+ s.ts,
+ s.dur,
+ s.track_id,
+ s.category,
+ s.name,
+ s.depth,
+ s.parent_id,
+ s.arg_set_id,
+ extract_arg(s.arg_set_id, 'context_id') as context_id,
+ extract_arg(s.arg_set_id, 'render_target') as render_target,
+ extract_arg(s.arg_set_id, 'render_target_name') as render_target_name,
+ extract_arg(s.arg_set_id, 'render_pass') as render_pass,
+ extract_arg(s.arg_set_id, 'render_pass_name') as render_pass_name,
+ extract_arg(s.arg_set_id, 'command_buffer') as command_buffer,
+ extract_arg(s.arg_set_id, 'command_buffer_name') as command_buffer_name,
+ extract_arg(s.arg_set_id, 'frame_id') as frame_id,
+ extract_arg(s.arg_set_id, 'submission_id') as submission_id,
+ extract_arg(s.arg_set_id, 'hw_queue_id') as hw_queue_id,
+ extract_arg(s.arg_set_id, 'upid') as upid,
+ extract_arg(s.arg_set_id, 'render_subpasses') as render_subpasses
+FROM slice s
+JOIN track t ON s.track_id = t.id
+WHERE t.type IN ('gpu_render_stage', 'vulkan_events', 'gpu_log');
+
+-- This table contains information on the expected timeline of either a display
+-- frame or a surface frame.
+CREATE PERFETTO TABLE expected_frame_timeline_slice(
+ -- Alias of `slice.id`.
+ id ID(slice.id),
+ -- Alias of `slice.ts`.
+ ts TIMESTAMP,
+ -- Alias of `slice.dur`.
+ dur DURATION,
+ -- Alias of `slice.track_id`.
+ track_id JOINID(track.id),
+ -- Alias of `slice.category`.
+ category STRING,
+ -- Alias of `slice.name`.
+ name STRING,
+ -- Alias of `slice.depth`.
+ depth LONG,
+ -- Alias of `slice.parent_id`.
+ parent_id JOINID(frame_slice.id),
+ -- Alias of `slice.arg_set_id`.
+ arg_set_id LONG,
+ -- Display frame token (vsync id).
+ display_frame_token LONG,
+ -- Surface frame token (vsync id), null if this is a display frame.
+ surface_frame_token LONG,
+ -- Unique process id of the app that generates the surface frame.
+ upid JOINID(process.id),
+ -- Layer name if this is a surface frame.
+ layer_name STRING
+) AS
+SELECT
+ s.id,
+ s.ts,
+ s.dur,
+ s.track_id,
+ s.category,
+ s.name,
+ s.depth,
+ s.parent_id,
+ s.arg_set_id,
+ extract_arg(s.arg_set_id, 'Display frame token') as display_frame_token,
+ extract_arg(s.arg_set_id, 'Surface frame token') as surface_frame_token,
+ t.upid,
+ extract_arg(s.arg_set_id, 'Layer name') as layer_name
+FROM slice s
+JOIN process_track t ON s.track_id = t.id
+WHERE t.type = 'android_expected_frame_timeline';
+
+-- This table contains information on the actual timeline and additional
+-- analysis related to the performance of either a display frame or a surface
+-- frame.
+CREATE PERFETTO TABLE actual_frame_timeline_slice(
+ -- Alias of `slice.id`.
+ id ID(slice.id),
+ -- Alias of `slice.ts`.
+ ts TIMESTAMP,
+ -- Alias of `slice.dur`.
+ dur DURATION,
+ -- Alias of `slice.track_id`.
+ track_id JOINID(track.id),
+ -- Alias of `slice.category`.
+ category STRING,
+ -- Alias of `slice.name`.
+ name STRING,
+ -- Alias of `slice.depth`.
+ depth LONG,
+ -- Alias of `slice.parent_id`.
+ parent_id JOINID(frame_slice.id),
+ -- Alias of `slice.arg_set_id`.
+ arg_set_id LONG,
+ -- Display frame token (vsync id).
+ display_frame_token LONG,
+ -- Surface frame token (vsync id), null if this is a display frame.
+ surface_frame_token LONG,
+ -- Unique process id of the app that generates the surface frame.
+ upid JOINID(process.id),
+ -- Layer name if this is a surface frame.
+ layer_name STRING,
+ -- Frame's present type (eg. on time / early / late).
+ present_type STRING,
+ -- Whether the frame finishes on time.
+ on_time_finish LONG,
+ -- Whether the frame used gpu composition.
+ gpu_composition LONG,
+ -- Specify the jank types for this frame if there's jank, or none if no jank
+ -- occurred.
+ jank_type STRING,
+ -- Severity of the jank: none if no jank.
+ jank_severity_type STRING,
+ -- Frame's prediction type (eg. valid / expired).
+ prediction_type STRING,
+ -- Jank tag based on jank type, used for slice visualization.
+ jank_tag STRING
+) AS
+SELECT
+ s.id,
+ s.ts,
+ s.dur,
+ s.track_id,
+ s.category,
+ s.name,
+ s.depth,
+ s.parent_id,
+ s.arg_set_id,
+ extract_arg(s.arg_set_id, 'Display frame token') as display_frame_token,
+ extract_arg(s.arg_set_id, 'Surface frame token') as surface_frame_token,
+ t.upid,
+ extract_arg(s.arg_set_id, 'Layer name') as layer_name,
+ extract_arg(s.arg_set_id, 'Present type') as present_type,
+ extract_arg(s.arg_set_id, 'On time finish') as on_time_finish,
+ extract_arg(s.arg_set_id, 'GPU composition') as gpu_composition,
+ extract_arg(s.arg_set_id, 'Jank type') as jank_type,
+ extract_arg(s.arg_set_id, 'Jank severity type') as jank_severity_type,
+ extract_arg(s.arg_set_id, 'Prediction type') as prediction_type,
+ extract_arg(s.arg_set_id, 'Jank tag') as jank_tag
+FROM slice s
+JOIN process_track t ON s.track_id = t.id
+WHERE t.type = 'android_actual_frame_timeline';
diff --git a/src/trace_processor/perfetto_sql/stdlib/slices/cpu_time.sql b/src/trace_processor/perfetto_sql/stdlib/slices/cpu_time.sql
index a1cd9d6..e38ac97 100644
--- a/src/trace_processor/perfetto_sql/stdlib/slices/cpu_time.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/slices/cpu_time.sql
@@ -34,13 +34,13 @@
-- Duration of the time the slice was running.
cpu_time LONG) AS
SELECT
-id_0 AS id,
-name,
-ts.utid,
-thread_name,
-upid,
-process_name,
-SUM(ii.dur) AS cpu_time
+ id_0 AS id,
+ name,
+ ts.utid,
+ thread_name,
+ upid,
+ process_name,
+ SUM(ii.dur) AS cpu_time
FROM _interval_intersect!((
(SELECT * FROM thread_slice WHERE utid > 0 AND dur > 0),
(SELECT * FROM sched WHERE dur > 0)
diff --git a/src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql b/src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql
index 78a7e5d..aafdef0 100644
--- a/src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql
@@ -17,7 +17,7 @@
-- Where possible, use available view functions which filter this view.
CREATE PERFETTO VIEW thread_slice(
-- Slice
- id JOINID(slice.id),
+ id ID(slice.id),
-- Alias for `slice.ts`.
ts TIMESTAMP,
-- Alias for `slice.dur`.
@@ -84,7 +84,7 @@
-- Where possible, use available view functions which filter this view.
CREATE PERFETTO VIEW process_slice(
-- Slice
- id JOINID(slice.id),
+ id ID(slice.id),
-- Alias for `slice.ts`.
ts TIMESTAMP,
-- Alias for `slice.dur`.
diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/summary/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/viz/summary/BUILD.gn
index 3c2356f..5ce76ff 100644
--- a/src/trace_processor/perfetto_sql/stdlib/viz/summary/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/viz/summary/BUILD.gn
@@ -22,6 +22,6 @@
"threads.sql",
"threads_w_processes.sql",
"trace.sql",
- "tracks.sql",
+ "track_event.sql",
]
}
diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/summary/track_event.sql b/src/trace_processor/perfetto_sql/stdlib/viz/summary/track_event.sql
new file mode 100644
index 0000000..c59052e
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/viz/summary/track_event.sql
@@ -0,0 +1,111 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+INCLUDE PERFETTO MODULE viz.summary.slices;
+
+CREATE PERFETTO TABLE _track_event_tracks_unordered AS
+WITH extracted AS (
+ SELECT
+ t.id,
+ t.name,
+ t.parent_id,
+ EXTRACT_ARG(t.source_arg_set_id, 'child_ordering') AS ordering,
+ EXTRACT_ARG(t.source_arg_set_id, 'sibling_order_rank') AS rank
+ FROM track t
+ WHERE t.type GLOB '*_track_event'
+)
+SELECT
+ t.id,
+ t.name,
+ t.parent_id,
+ p.ordering AS parent_ordering,
+ IFNULL(t.rank, 0) AS rank
+FROM extracted t
+LEFT JOIN extracted p ON t.parent_id = p.id;
+
+CREATE PERFETTO TABLE _min_ts_per_track AS
+SELECT track_id AS id, min(ts) as min_ts
+FROM counter
+GROUP BY track_id
+UNION ALL
+SELECT track_id AS id, min(ts) as min_ts
+FROM slice
+GROUP BY track_id;
+
+CREATE PERFETTO TABLE _track_event_has_children AS
+SELECT DISTINCT t.parent_id AS id
+FROM track t
+WHERE t.type GLOB '*_track_event' AND t.parent_id IS NOT NULL;
+
+CREATE PERFETTO TABLE _track_event_tracks_ordered_groups AS
+WITH
+ lexicographic_and_none AS (
+ SELECT
+ id,
+ ROW_NUMBER() OVER (PARTITION BY parent_id ORDER BY name) AS order_id
+ FROM _track_event_tracks_unordered t
+ WHERE t.parent_ordering = 'lexicographic'
+ OR t.parent_ordering IS NULL
+ ),
+ explicit AS (
+ SELECT
+ id,
+ ROW_NUMBER() OVER (PARTITION BY parent_id ORDER BY rank) AS order_id
+ FROM _track_event_tracks_unordered t
+ WHERE t.parent_ordering = 'explicit'
+ ),
+ chronological AS (
+ SELECT
+ t.id,
+ ROW_NUMBER() OVER (PARTITION BY t.parent_id ORDER BY m.min_ts) AS order_id
+ FROM _track_event_tracks_unordered t
+ LEFT JOIN _min_ts_per_track m USING (id)
+ WHERE t.parent_ordering = 'chronological'
+ ),
+ unioned AS (
+ SELECT id, order_id
+ FROM lexicographic_and_none
+ UNION ALL
+ SELECT id, order_id
+ FROM explicit
+ UNION ALL
+ SELECT id, order_id
+ FROM chronological
+ )
+SELECT
+ extract_arg(track.dimension_arg_set_id, 'upid') AS upid,
+ extract_arg(track.dimension_arg_set_id, 'utid') AS utid,
+ track.parent_id,
+ track.type GLOB '*counter*' AS is_counter,
+ track.name,
+ MIN(counter_track.unit) AS unit,
+ MIN(extract_arg(track.source_arg_set_id, 'builtin_counter_type')) AS builtin_counter_type,
+ MAX(m.id IS NOT NULL) AS has_data,
+ MAX(c.id IS NOT NULL) AS has_children,
+ GROUP_CONCAT(unioned.id) as track_ids,
+ MIN(unioned.order_id) AS order_id
+FROM unioned
+JOIN track USING (id)
+LEFT JOIN counter_track USING (id)
+LEFT JOIN _track_event_has_children c USING (id)
+LEFT JOIN _min_ts_per_track m USING (id)
+GROUP BY
+ -- Merge by parent id if it exists or, if not, then by upid/utid scope.
+ coalesce(track.parent_id, upid, utid),
+ is_counter,
+ track.name,
+ -- Don't merge tracks by name which have children or are counters.
+ IIF(c.id IS NOT NULL OR is_counter, track.id, NULL)
+ORDER BY track.parent_id, unioned.order_id;
diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql b/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql
deleted file mode 100644
index 49fb3f0..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql
+++ /dev/null
@@ -1,128 +0,0 @@
---
--- Copyright 2024 The Android Open Source Project
---
--- Licensed under the Apache License, Version 2.0 (the "License");
--- you may not use this file except in compliance with the License.
--- You may obtain a copy of the License at
---
--- https://www.apache.org/licenses/LICENSE-2.0
---
--- Unless required by applicable law or agreed to in writing, software
--- distributed under the License is distributed on an "AS IS" BASIS,
--- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
--- See the License for the specific language governing permissions and
--- limitations under the License.
-
-INCLUDE PERFETTO MODULE viz.summary.slices;
-
-CREATE PERFETTO VIEW _track_event_tracks_unordered AS
-WITH extracted AS (
- SELECT
- t.id,
- t.parent_id,
- t.name,
- EXTRACT_ARG(t.source_arg_set_id, 'child_ordering') AS ordering,
- EXTRACT_ARG(t.source_arg_set_id, 'sibling_order_rank') AS rank
- FROM track t
-)
-SELECT
- t.id,
- t.parent_id,
- t.name,
- t.ordering,
- p.ordering AS parent_ordering,
- IFNULL(t.rank, 0) AS rank
-FROM extracted t
-LEFT JOIN extracted p ON t.parent_id = p.id
-WHERE p.ordering IS NOT NULL;
-
-CREATE PERFETTO TABLE _track_event_tracks_ordered AS
-WITH lexicographic_and_none AS (
- SELECT
- id, parent_id, name,
- ROW_NUMBER() OVER (ORDER BY parent_id, name) AS order_id
- FROM _track_event_tracks_unordered
- WHERE parent_ordering = 'lexicographic'
-),
-explicit AS (
-SELECT
- id, parent_id, name,
- ROW_NUMBER() OVER (ORDER BY parent_id, rank) AS order_id
-FROM _track_event_tracks_unordered
-WHERE parent_ordering = 'explicit'
-),
-slice_chronological AS (
- SELECT
- t.*,
- min(ts) AS min_ts
- FROM _track_event_tracks_unordered t
- JOIN slice s on t.id = s.track_id
- WHERE parent_ordering = 'chronological'
- GROUP BY track_id
-),
-counter_chronological AS (
- SELECT
- t.*,
- min(ts) AS min_ts
- FROM _track_event_tracks_unordered t
- JOIN counter s on t.id = s.track_id
- WHERE parent_ordering = 'chronological'
- GROUP BY track_id
-),
-slice_and_counter_chronological AS (
- SELECT t.*, u.min_ts
- FROM _track_event_tracks_unordered t
- LEFT JOIN (
- SELECT * FROM slice_chronological
- UNION ALL
- SELECT * FROM counter_chronological) u USING (id)
- WHERE t.parent_ordering = 'chronological'
-),
-chronological AS (
- SELECT
- id, parent_id, name,
- ROW_NUMBER() OVER (ORDER BY parent_id, min_ts) AS order_id
- FROM slice_and_counter_chronological
-),
-all_tracks AS (
- SELECT id, parent_id, name, order_id
- FROM lexicographic_and_none
- UNION
- SELECT id, parent_id, name, order_id
- FROM explicit
- UNION
- SELECT id, parent_id, name, order_id
- FROM chronological
-)
-SELECT id, order_id
-FROM all_tracks all_t
-ORDER BY parent_id, order_id;
-
-CREATE PERFETTO TABLE _thread_track_summary_by_utid_and_name AS
-SELECT
- utid,
- parent_id,
- name,
- -- Only meaningful when track_count == 1.
- id as track_id,
- -- Only meaningful when track_count == 1.
- max_depth as max_depth,
- GROUP_CONCAT(id) AS track_ids,
- COUNT() AS track_count
-FROM thread_track
-JOIN _slice_track_summary USING (id)
-LEFT JOIN _track_event_tracks_ordered USING (id)
-GROUP BY utid, parent_id, order_id, name;
-
-CREATE PERFETTO TABLE _process_track_summary_by_upid_and_parent_id_and_name AS
-SELECT
- id,
- parent_id,
- upid,
- name,
- GROUP_CONCAT(id) AS track_ids,
- COUNT() AS track_count
-FROM process_track
-JOIN _slice_track_summary USING (id)
-LEFT JOIN _track_event_tracks_ordered USING (id)
-GROUP BY upid, parent_id, order_id, name;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/wattson/BUILD.gn
index 185d6ef..e32094c 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/BUILD.gn
@@ -19,6 +19,7 @@
"arm_dsu.sql",
"cpu_freq.sql",
"cpu_freq_idle.sql",
+ "cpu_hotplug.sql",
"cpu_idle.sql",
"cpu_split.sql",
"curves/device.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq.sql
index 1256f3b..c211e8e 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq.sql
@@ -30,9 +30,9 @@
),
-- Get first freq transition per CPU
first_cpu_freq_slices AS (
- SELECT ts, cpu FROM _cpu_freq
+ SELECT MIN(ts) as ts, cpu
+ FROM _cpu_freq
GROUP BY cpu
- ORDER by ts ASC
)
-- Prepend NULL slices up to first freq events on a per CPU basis
SELECT
@@ -44,6 +44,7 @@
d_map.policy
FROM first_cpu_freq_slices as first_slices
JOIN _dev_cpu_policy_map as d_map ON first_slices.cpu = d_map.cpu
+WHERE dur > 0
UNION ALL
SELECT
ts,
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq_idle.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq_idle.sql
index 756ebed..dba6cca 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq_idle.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq_idle.sql
@@ -15,6 +15,7 @@
INCLUDE PERFETTO MODULE intervals.intersect;
INCLUDE PERFETTO MODULE wattson.cpu_freq;
+INCLUDE PERFETTO MODULE wattson.cpu_hotplug;
INCLUDE PERFETTO MODULE wattson.cpu_idle;
INCLUDE PERFETTO MODULE wattson.curves.utils;
INCLUDE PERFETTO MODULE wattson.device_infos;
@@ -57,17 +58,31 @@
CREATE PERFETTO TABLE _idle_freq_materialized
AS
SELECT
- ii.ts, ii.dur, ii.cpu, freq.policy, freq.freq, idle.idle, lut.curve_value
+ ii.ts, ii.dur, ii.cpu, freq.policy, freq.freq,
+ -- Set idle since subsequent calculations are based on number of idle/active
+ -- CPUs. If offline/suspended, set the CPU to the device specific deepest idle
+ -- state.
+ IIF(
+ suspend.suspended OR hotplug.offline,
+ (SELECT idle FROM _deepest_idle),
+ idle.idle
+ ) as idle,
+ -- If CPU is suspended or offline, set power estimate to 0
+ IIF(suspend.suspended OR hotplug.offline, 0, lut.curve_value) as curve_value
FROM _interval_intersect!(
(
_ii_subquery!(_valid_window),
_ii_subquery!(_adjusted_cpu_freq),
- _ii_subquery!(_adjusted_deep_idle)
+ _ii_subquery!(_adjusted_deep_idle),
+ _ii_subquery!(_gapless_hotplug_slices),
+ _ii_subquery!(_gapless_suspend_slices)
),
(cpu)
) ii
JOIN _adjusted_cpu_freq AS freq ON freq._auto_id = id_1
JOIN _adjusted_deep_idle AS idle ON idle._auto_id = id_2
+JOIN _gapless_hotplug_slices AS hotplug ON hotplug._auto_id = id_3
+JOIN _gapless_suspend_slices AS suspend ON suspend._auto_id = id_4
-- Left join since some CPUs may only match the 2D LUT
LEFT JOIN _filtered_curves_1d lut ON
freq.policy = lut.policy AND
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_hotplug.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_hotplug.sql
new file mode 100644
index 0000000..283c28e
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_hotplug.sql
@@ -0,0 +1,108 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+INCLUDE PERFETTO MODULE android.suspend;
+INCLUDE PERFETTO MODULE time.conversion;
+INCLUDE PERFETTO MODULE wattson.device_infos;
+
+-- Creates the hotplug slice(s) for each CPU defined to be the region when CPU
+-- is off
+CREATE PERFETTO TABLE _cpu_hotplug_offline AS
+WITH is_different_cpu AS (
+ -- Set flag for when hotplug CPU processing is being done on separate CPU
+ SELECT
+ s.ts,
+ s.dur,
+ EXTRACT_ARG(t.dimension_arg_set_id, 'cpu') AS hp_cpu,
+ EXTRACT_ARG(t.dimension_arg_set_id, 'cpu')
+ != EXTRACT_ARG(s.arg_set_id, 'action_cpu') AS is_different_cpu
+ FROM slice s
+ JOIN track t ON t.id = s.track_id
+ WHERE t.type = 'cpu_hotplug'
+),
+cpu_transitions AS (
+ -- AP CPU is CPU being hotplugged out, and BP CPU is CPU that assists the AP
+ -- CPU in hotplugging. The BP CPU could be the AP CPU itself or a different
+ -- CPU. Find the transition points where the BP CPU changes between AP and BP.
+ SELECT
+ ts,
+ hp_cpu,
+ is_different_cpu,
+ LAG(is_different_cpu) OVER (PARTITION BY hp_cpu) != is_different_cpu
+ AS is_cpu_transitions
+ FROM is_different_cpu
+),
+transitions_dur AS (
+ -- Calculates duration between transitions from AP -> BP and BP -> AP
+ SELECT
+ ts,
+ LEAD(ts, 1, trace_end()) OVER (PARTITION BY hp_cpu) - ts AS dur,
+ hp_cpu AS cpu,
+ is_different_cpu
+ FROM cpu_transitions
+ WHERE is_cpu_transitions
+)
+SELECT
+ ts, dur, cpu,
+ -- Sometimes the assignment of AP CPU during hotplugging creates short,
+ -- spurious "pockets" of hotplug events, so assign these slices that are
+ -- shorter than 100us as if they were on the same CPU.
+ IIF(
+ is_different_cpu AND dur < time_from_us(100),
+ FALSE,
+ is_different_cpu
+ ) AS is_different_cpu
+FROM transitions_dur;
+
+-- Fill gaps from beginning of trace to end of trace so that this table can be
+-- used by interval_intersect().
+CREATE PERFETTO TABLE _gapless_hotplug_slices AS
+WITH filled_gaps AS (
+ -- First slice from trace_start() to first offline slice per CPU
+ SELECT
+ cpu,
+ trace_start() AS ts,
+ MIN(ts) - trace_start() AS dur,
+ FALSE AS offline
+ FROM _cpu_hotplug_offline
+ GROUP BY cpu
+ UNION ALL
+ -- All online and offline regions as defined by cpuhp. This will have
+ -- continuous slices from somewhere in the middle to the end of the trace.
+ SELECT
+ cpu, ts, dur, is_different_cpu AS offline
+ FROM _cpu_hotplug_offline
+ UNION ALL
+ -- Creates a single online slice spanning the entire trace for CPUs that are
+ -- never offline. This is needed for interval_intersect() to not delete
+ -- undefined time periods
+ SELECT
+ cpu,
+ trace_start() AS ts,
+ trace_dur() AS dur,
+ FALSE AS offline
+ FROM _dev_cpu_policy_map
+ WHERE cpu NOT IN (SELECT cpu FROM _cpu_hotplug_offline)
+)
+SELECT ts, dur, cpu, offline
+FROM filled_gaps
+ORDER BY cpu, ts;
+
+-- Copies suspend state to each CPU defined, so that the suspend state can be
+-- partitioned by cpu during interval_intersect()
+CREATE PERFETTO TABLE _gapless_suspend_slices AS
+SELECT cpu, ts, dur, IIF(power_state = 'suspended', TRUE, FALSE) AS suspended
+FROM _dev_cpu_policy_map
+CROSS JOIN android_suspend_state;
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/device_infos.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/device_infos.sql
index a6941c8..e5a37d1 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/device_infos.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/device_infos.sql
@@ -195,3 +195,9 @@
FROM _idle_state_map as idle_map
JOIN _wattson_device as device
ON idle_map.device = device.name;
+
+-- Get the device specific deepest idle state if defined, otherwise use 1 as the
+-- deepest idle state
+CREATE PERFETTO TABLE _deepest_idle AS
+SELECT
+ IFNULL((SELECT MAX(override_idle) FROM _idle_state_map_override), 1) as idle;
diff --git a/src/trace_processor/read_trace_integrationtest.cc b/src/trace_processor/read_trace_integrationtest.cc
index 818eb6d..1bb4084 100644
--- a/src/trace_processor/read_trace_integrationtest.cc
+++ b/src/trace_processor/read_trace_integrationtest.cc
@@ -69,7 +69,7 @@
std::vector<uint8_t> decompressed;
decompressed.reserve(raw_trace.size());
- util::Status status = trace_processor::DecompressTrace(
+ base::Status status = trace_processor::DecompressTrace(
raw_trace.data(), raw_trace.size(), &decompressed);
ASSERT_TRUE(status.ok());
@@ -89,7 +89,7 @@
std::vector<uint8_t> raw_trace = ReadAllData(f);
std::vector<uint8_t> decompressed;
- util::Status status = trace_processor::DecompressTrace(
+ base::Status status = trace_processor::DecompressTrace(
raw_trace.data(), raw_trace.size(), &decompressed);
ASSERT_FALSE(status.ok());
}
@@ -100,7 +100,7 @@
std::vector<uint8_t> raw_compressed_trace = ReadAllData(f);
std::vector<uint8_t> decompressed;
- util::Status status = trace_processor::DecompressTrace(
+ base::Status status = trace_processor::DecompressTrace(
raw_compressed_trace.data(), raw_compressed_trace.size(), &decompressed);
ASSERT_TRUE(status.ok());
@@ -117,7 +117,7 @@
std::vector<uint8_t> raw_compressed_trace = ReadAllData(f);
std::vector<uint8_t> decompressed;
- util::Status status = trace_processor::DecompressTrace(
+ base::Status status = trace_processor::DecompressTrace(
raw_compressed_trace.data(), raw_compressed_trace.size(), &decompressed);
ASSERT_TRUE(status.ok()) << status.message();
diff --git a/src/trace_processor/read_trace_internal.cc b/src/trace_processor/read_trace_internal.cc
index f2b1a67..56bd11d 100644
--- a/src/trace_processor/read_trace_internal.cc
+++ b/src/trace_processor/read_trace_internal.cc
@@ -41,7 +41,7 @@
// 1MB chunk size seems the best tradeoff on a MacBook Pro 2013 - i7 2.8 GHz.
constexpr size_t kChunkSize = 1024 * 1024;
-util::Status ReadTraceUsingRead(
+base::Status ReadTraceUsingRead(
TraceProcessor* tp,
int fd,
uint64_t* file_size,
@@ -57,7 +57,7 @@
break;
if (rsize < 0) {
- return util::ErrStatus("Reading trace file failed (errno: %d, %s)", errno,
+ return base::ErrStatus("Reading trace file failed (errno: %d, %s)", errno,
strerror(errno));
}
@@ -65,11 +65,11 @@
TraceBlobView blob_view(std::move(blob), 0, static_cast<size_t>(rsize));
RETURN_IF_ERROR(tp->Parse(std::move(blob_view)));
}
- return util::OkStatus();
+ return base::OkStatus();
}
} // namespace
-util::Status ReadTraceUnfinalized(
+base::Status ReadTraceUnfinalized(
TraceProcessor* tp,
const char* filename,
const std::function<void(uint64_t parsed_size)>& progress_callback) {
@@ -102,7 +102,7 @@
if (bytes_read == 0) {
base::ScopedFile fd(base::OpenFile(filename, O_RDONLY));
if (!fd)
- return util::ErrStatus("Could not open trace file (path: %s)", filename);
+ return base::ErrStatus("Could not open trace file (path: %s)", filename);
RETURN_IF_ERROR(
ReadTraceUsingRead(tp, *fd, &bytes_read, progress_callback));
}
@@ -110,7 +110,7 @@
if (progress_callback)
progress_callback(bytes_read);
- return util::OkStatus();
+ return base::OkStatus();
}
} // namespace trace_processor
} // namespace perfetto
diff --git a/src/trace_processor/read_trace_internal.h b/src/trace_processor/read_trace_internal.h
index c946ad7..a54dc5c 100644
--- a/src/trace_processor/read_trace_internal.h
+++ b/src/trace_processor/read_trace_internal.h
@@ -30,7 +30,7 @@
class TraceProcessor;
// Reads trace without Flushing the data at the end.
-util::Status PERFETTO_EXPORT_COMPONENT ReadTraceUnfinalized(
+base::Status PERFETTO_EXPORT_COMPONENT ReadTraceUnfinalized(
TraceProcessor* tp,
const char* filename,
const std::function<void(uint64_t parsed_size)>& progress_callback = {});
diff --git a/src/trace_processor/sorter/trace_sorter.cc b/src/trace_processor/sorter/trace_sorter.cc
index 9703efe..d7bb07f 100644
--- a/src/trace_processor/sorter/trace_sorter.cc
+++ b/src/trace_processor/sorter/trace_sorter.cc
@@ -244,11 +244,6 @@
context.json_trace_parser->ParseJsonPacket(
event.ts, std::move(token_buffer_.Extract<JsonEvent>(id).value));
return;
- case TimestampedEvent::Type::kJsonValueWithDur:
- context.json_trace_parser->ParseJsonPacket(
- event.ts,
- std::move(token_buffer_.Extract<JsonWithDurEvent>(id).value));
- return;
case TimestampedEvent::Type::kSpeRecord:
context.spe_record_parser->ParseSpeRecord(
event.ts, token_buffer_.Extract<TraceBlobView>(id));
@@ -310,7 +305,6 @@
case TimestampedEvent::Type::kPerfRecord:
case TimestampedEvent::Type::kInstrumentsRow:
case TimestampedEvent::Type::kJsonValue:
- case TimestampedEvent::Type::kJsonValueWithDur:
case TimestampedEvent::Type::kFuchsiaRecord:
case TimestampedEvent::Type::kAndroidDumpstateEvent:
case TimestampedEvent::Type::kAndroidLogEvent:
@@ -348,7 +342,6 @@
case TimestampedEvent::Type::kPerfRecord:
case TimestampedEvent::Type::kInstrumentsRow:
case TimestampedEvent::Type::kJsonValue:
- case TimestampedEvent::Type::kJsonValueWithDur:
case TimestampedEvent::Type::kFuchsiaRecord:
case TimestampedEvent::Type::kAndroidDumpstateEvent:
case TimestampedEvent::Type::kAndroidLogEvent:
@@ -379,9 +372,6 @@
case TimestampedEvent::Type::kJsonValue:
base::ignore_result(token_buffer_.Extract<JsonEvent>(id));
return;
- case TimestampedEvent::Type::kJsonValueWithDur:
- base::ignore_result(token_buffer_.Extract<JsonWithDurEvent>(id));
- return;
case TimestampedEvent::Type::kSpeRecord:
base::ignore_result(token_buffer_.Extract<TraceBlobView>(id));
return;
diff --git a/src/trace_processor/sorter/trace_sorter.h b/src/trace_processor/sorter/trace_sorter.h
index 6727dcc..760f092 100644
--- a/src/trace_processor/sorter/trace_sorter.h
+++ b/src/trace_processor/sorter/trace_sorter.h
@@ -27,8 +27,10 @@
#include <tuple>
#include <type_traits>
#include <utility>
+#include <variant>
#include <vector>
+#include "perfetto/base/compiler.h"
#include "perfetto/base/logging.h"
#include "perfetto/ext/base/circular_queue.h"
#include "perfetto/public/compiler.h"
@@ -188,17 +190,15 @@
void PushJsonValue(int64_t timestamp,
std::string json_value,
- std::optional<int64_t> dur = std::nullopt) {
- if (dur.has_value()) {
- // We need to account for slices with duration by sorting them first: this
- // requires us to use the slower comparator which takes this into account.
+ const JsonEvent::Type& type) {
+ if (const auto* scoped = std::get_if<JsonEvent::Scoped>(&type); scoped) {
+ // We need to account for slices with duration by sorting them specially:
+ // this requires us to use the slower comparator which takes this into
+ // account.
use_slow_sorting_ = true;
- AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kJsonValueWithDur,
- JsonWithDurEvent{*dur, std::move(json_value)});
- return;
}
AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kJsonValue,
- JsonEvent{std::move(json_value)});
+ JsonEvent{std::move(json_value), type});
}
void PushFuchsiaRecord(int64_t timestamp, FuchsiaRecord fuchsia_record) {
@@ -357,7 +357,6 @@
kInlineSchedWaking,
kInstrumentsRow,
kJsonValue,
- kJsonValueWithDur,
kLegacyV8CpuProfileEvent,
kPerfRecord,
kSpeRecord,
@@ -410,18 +409,26 @@
bool operator()(const TimestampedEvent& a,
const TimestampedEvent& b) const {
int64_t a_key =
- a.type() == Type::kJsonValueWithDur
- ? std::numeric_limits<int64_t>::max() -
- buffer.Get<JsonWithDurEvent>(GetTokenBufferId(a))->dur
- : std::numeric_limits<int64_t>::max();
+ KeyForType(buffer.Get<JsonEvent>(GetTokenBufferId(a))->type);
int64_t b_key =
- b.type() == Type::kJsonValueWithDur
- ? std::numeric_limits<int64_t>::max() -
- buffer.Get<JsonWithDurEvent>(GetTokenBufferId(b))->dur
- : std::numeric_limits<int64_t>::max();
+ KeyForType(buffer.Get<JsonEvent>(GetTokenBufferId(b))->type);
return std::tie(a.ts, a_key, a.chunk_index, a.chunk_offset) <
std::tie(b.ts, b_key, b.chunk_index, b.chunk_offset);
}
+
+ static int64_t KeyForType(const JsonEvent::Type& type) {
+ switch (type.index()) {
+ case base::variant_index<JsonEvent::Type, JsonEvent::End>():
+ return std::numeric_limits<int64_t>::min();
+ case base::variant_index<JsonEvent::Type, JsonEvent::Scoped>():
+ return std::numeric_limits<int64_t>::max() -
+ std::get<JsonEvent::Scoped>(type).dur;
+ default:
+ return std::numeric_limits<int64_t>::max();
+ }
+ PERFETTO_FATAL("For GCC");
+ }
+
TraceTokenBuffer& buffer;
};
};
@@ -462,7 +469,6 @@
// after that index, instead, will need a sorting pass before moving
// events to the next pipeline stage.
if (sort_start_idx_ == 0) {
- PERFETTO_DCHECK(events_.size() >= 2);
sort_start_idx_ = events_.size() - 1;
sort_min_ts_ = ts;
} else {
diff --git a/src/trace_processor/sqlite/db_sqlite_table.cc b/src/trace_processor/sqlite/db_sqlite_table.cc
index c119d99..3b8e5fd 100644
--- a/src/trace_processor/sqlite/db_sqlite_table.cc
+++ b/src/trace_processor/sqlite/db_sqlite_table.cc
@@ -678,6 +678,21 @@
info->estimatedCost = cost_and_rows.cost;
info->estimatedRows = cost_and_rows.rows;
+ PERFETTO_TP_TRACE(
+ metatrace::Category::QUERY_TIMELINE, "DB_SQLITE_BEST_INDEX",
+ [&](metatrace::Record* record) {
+ record->AddArg("name", t->table_name.c_str());
+ record->AddArg("idxStr", info->idxStr);
+ record->AddArg("idxNum",
+ base::StackString<32>("%d", info->idxNum).c_str());
+ record->AddArg(
+ "estimatedCost",
+ base::StackString<32>("%f", info->estimatedCost).c_str());
+ record->AddArg(
+ "estimatedRows",
+ base::StackString<32>("%lld", info->estimatedRows).c_str());
+ });
+
return SQLITE_OK;
}
diff --git a/src/trace_processor/storage/metadata.h b/src/trace_processor/storage/metadata.h
index a690f84..10ed55d 100644
--- a/src/trace_processor/storage/metadata.h
+++ b/src/trace_processor/storage/metadata.h
@@ -65,7 +65,8 @@
F(tracing_disabled_ns, KeyType::kSingle, Variadic::kInt), \
F(tracing_started_ns, KeyType::kSingle, Variadic::kInt), \
F(ui_state, KeyType::kSingle, Variadic::kString), \
- F(unique_session_name, KeyType::kSingle, Variadic::kString)
+ F(unique_session_name, KeyType::kSingle, Variadic::kString), \
+ F(trace_trigger, KeyType::kSingle, Variadic::kString)
// clang-format on
// Compile time list of metadata items.
diff --git a/src/trace_processor/storage/trace_storage.h b/src/trace_processor/storage/trace_storage.h
index 8b29070..0cc9ce0 100644
--- a/src/trace_processor/storage/trace_storage.h
+++ b/src/trace_processor/storage/trace_storage.h
@@ -81,7 +81,6 @@
static const StringId kNullStringId = StringId::Null();
using ArgSetId = uint32_t;
-static const ArgSetId kInvalidArgSetId = 0;
using TrackId = tables::TrackTable::Id;
@@ -101,8 +100,6 @@
using MetadataId = tables::MetadataTable::Id;
-using RawId = tables::RawTable::Id;
-
using FlamegraphId = tables::ExperimentalFlamegraphTable::Id;
using VulkanAllocId = tables::VulkanMemoryAllocationsTable::Id;
@@ -349,7 +346,7 @@
track_table_.ShrinkToFit();
counter_table_.ShrinkToFit();
slice_table_.ShrinkToFit();
- raw_table_.ShrinkToFit();
+ ftrace_event_table_.ShrinkToFit();
sched_slice_table_.ShrinkToFit();
thread_state_table_.ShrinkToFit();
arg_table_.ShrinkToFit();
@@ -414,11 +411,6 @@
return &virtual_track_slices_;
}
- const tables::GpuSliceTable& gpu_slice_table() const {
- return gpu_slice_table_;
- }
- tables::GpuSliceTable* mutable_gpu_slice_table() { return &gpu_slice_table_; }
-
const tables::CounterTable& counter_table() const { return counter_table_; }
tables::CounterTable* mutable_counter_table() { return &counter_table_; }
@@ -480,8 +472,12 @@
const tables::ArgTable& arg_table() const { return arg_table_; }
tables::ArgTable* mutable_arg_table() { return &arg_table_; }
- const tables::RawTable& raw_table() const { return raw_table_; }
- tables::RawTable* mutable_raw_table() { return &raw_table_; }
+ const tables::ChromeRawTable& chrome_raw_table() const {
+ return chrome_raw_table_;
+ }
+ tables::ChromeRawTable* mutable_chrome_raw_table() {
+ return &chrome_raw_table_;
+ }
const tables::FtraceEventTable& ftrace_event_table() const {
return ftrace_event_table_;
@@ -625,14 +621,6 @@
return &vulkan_memory_allocations_table_;
}
- const tables::GraphicsFrameSliceTable& graphics_frame_slice_table() const {
- return graphics_frame_slice_table_;
- }
-
- tables::GraphicsFrameSliceTable* mutable_graphics_frame_slice_table() {
- return &graphics_frame_slice_table_;
- }
-
const tables::MemorySnapshotTable& memory_snapshot_table() const {
return memory_snapshot_table_;
}
@@ -662,25 +650,6 @@
return &memory_snapshot_edge_table_;
}
- const tables::ExpectedFrameTimelineSliceTable&
- expected_frame_timeline_slice_table() const {
- return expected_frame_timeline_slice_table_;
- }
-
- tables::ExpectedFrameTimelineSliceTable*
- mutable_expected_frame_timeline_slice_table() {
- return &expected_frame_timeline_slice_table_;
- }
-
- const tables::ActualFrameTimelineSliceTable&
- actual_frame_timeline_slice_table() const {
- return actual_frame_timeline_slice_table_;
- }
- tables::ActualFrameTimelineSliceTable*
- mutable_actual_frame_timeline_slice_table() {
- return &actual_frame_timeline_slice_table_;
- }
-
const tables::AndroidNetworkPacketsTable& android_network_packets_table()
const {
return android_network_packets_table_;
@@ -1042,18 +1011,14 @@
// NestableSlices).
VirtualTrackSlices virtual_track_slices_;
- // Additional attributes for gpu track slices (sub-type of
- // NestableSlices).
- tables::GpuSliceTable gpu_slice_table_{&string_pool_, &slice_table_};
-
// The values from the Counter events from the trace. This includes CPU
// frequency events as well systrace trace_marker counter events.
tables::CounterTable counter_table_{&string_pool_};
SqlStats sql_stats_;
- tables::RawTable raw_table_{&string_pool_};
- tables::FtraceEventTable ftrace_event_table_{&string_pool_, &raw_table_};
+ tables::ChromeRawTable chrome_raw_table_{&string_pool_};
+ tables::FtraceEventTable ftrace_event_table_{&string_pool_};
tables::MachineTable machine_table_{&string_pool_};
@@ -1097,9 +1062,6 @@
tables::VulkanMemoryAllocationsTable vulkan_memory_allocations_table_{
&string_pool_};
- tables::GraphicsFrameSliceTable graphics_frame_slice_table_{&string_pool_,
- &slice_table_};
-
// Metadata for memory snapshot.
tables::MemorySnapshotTable memory_snapshot_table_{&string_pool_};
tables::ProcessMemorySnapshotTable process_memory_snapshot_table_{
@@ -1107,12 +1069,6 @@
tables::MemorySnapshotNodeTable memory_snapshot_node_table_{&string_pool_};
tables::MemorySnapshotEdgeTable memory_snapshot_edge_table_{&string_pool_};
- // FrameTimeline tables
- tables::ExpectedFrameTimelineSliceTable expected_frame_timeline_slice_table_{
- &string_pool_, &slice_table_};
- tables::ActualFrameTimelineSliceTable actual_frame_timeline_slice_table_{
- &string_pool_, &slice_table_};
-
// AndroidNetworkPackets tables
tables::AndroidNetworkPacketsTable android_network_packets_table_{
&string_pool_, &slice_table_};
diff --git a/src/trace_processor/storage_minimal_smoke_test.cc b/src/trace_processor/storage_minimal_smoke_test.cc
index 2ff90c7..ff3e638 100644
--- a/src/trace_processor/storage_minimal_smoke_test.cc
+++ b/src/trace_processor/storage_minimal_smoke_test.cc
@@ -33,9 +33,9 @@
class JsonStringOutputWriter : public json::OutputWriter {
public:
- util::Status AppendString(const std::string& string) override {
+ base::Status AppendString(const std::string& string) override {
buffer += string;
- return util::OkStatus();
+ return base::OkStatus();
}
std::string buffer;
};
@@ -54,7 +54,7 @@
auto f = fopen(base::GetTestDataPath("test/data/gpu_trace.pb").c_str(), "rb");
std::unique_ptr<uint8_t[]> buf(new uint8_t[MAX_SIZE]);
auto rsize = fread(reinterpret_cast<char*>(buf.get()), 1, MAX_SIZE, f);
- util::Status status = storage_->Parse(std::move(buf), rsize);
+ base::Status status = storage_->Parse(std::move(buf), rsize);
ASSERT_TRUE(status.ok());
ASSERT_OK(storage_->NotifyEndOfFile());
@@ -78,7 +78,7 @@
fopen(base::GetTestDataPath("test/data/systrace.html").c_str(), "rb");
std::unique_ptr<uint8_t[]> buf(new uint8_t[MAX_SIZE]);
auto rsize = fread(reinterpret_cast<char*>(buf.get()), 1, MAX_SIZE, f);
- util::Status status = storage_->Parse(std::move(buf), rsize);
+ base::Status status = storage_->Parse(std::move(buf), rsize);
ASSERT_FALSE(status.ok());
}
@@ -88,7 +88,7 @@
auto f = fopen("test/data/track_event_typed_args.pb", "rb");
std::unique_ptr<uint8_t[]> buf(new uint8_t[MAX_SIZE]);
auto rsize = fread(reinterpret_cast<char*>(buf.get()), 1, MAX_SIZE, f);
- util::Status status = storage_->Parse(std::move(buf), rsize);
+ base::Status status = storage_->Parse(std::move(buf), rsize);
ASSERT_TRUE(status.ok());
ASSERT_OK(storage_->NotifyEndOfFile());
diff --git a/src/trace_processor/tables/BUILD.gn b/src/trace_processor/tables/BUILD.gn
index 29891e6..8fccef1 100644
--- a/src/trace_processor/tables/BUILD.gn
+++ b/src/trace_processor/tables/BUILD.gn
@@ -15,6 +15,30 @@
import("../../../gn/perfetto_tp_tables.gni")
import("../../../gn/test.gni")
+source_set("tables") {
+ sources = [ "table_destructors.cc" ]
+ deps = [
+ ":macros_internal",
+ "../../../gn:default_deps",
+ ]
+ public_deps = [ ":tables_python" ]
+}
+
+source_set("macros_internal") {
+ sources = [
+ "macros_internal.cc",
+ "macros_internal.h",
+ ]
+ deps = [
+ "../../../gn:default_deps",
+ "../../../include/perfetto/ext/base",
+ "../../../include/perfetto/trace_processor",
+ "../containers",
+ "../db:minimal",
+ "../db/column",
+ ]
+}
+
perfetto_tp_tables("tables_python") {
sources = [
"android_tables.py",
@@ -36,23 +60,6 @@
generate_docs = true
}
-source_set("tables") {
- sources = [
- "macros_internal.cc",
- "macros_internal.h",
- "table_destructors.cc",
- ]
- deps = [
- "../../../gn:default_deps",
- "../../../include/perfetto/ext/base",
- "../../../include/perfetto/trace_processor",
- "../containers",
- "../db:minimal",
- "../db/column",
- ]
- public_deps = [ ":tables_python" ]
-}
-
perfetto_tp_tables("py_tables_unittest") {
sources = [ "py_tables_unittest.py" ]
}
diff --git a/src/trace_processor/tables/android_tables.py b/src/trace_processor/tables/android_tables.py
index 65eae80..ab37535 100644
--- a/src/trace_processor/tables/android_tables.py
+++ b/src/trace_processor/tables/android_tables.py
@@ -164,7 +164,7 @@
columns=[
C('event_id', CppUint32()),
C('ts', CppInt64()),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
C('base64_proto', CppString()),
C('base64_proto_id', CppOptional(CppUint32())),
],
@@ -183,8 +183,10 @@
ColumnDoc(
doc='Details of the motion event parsed from the proto message.',
joinable='args.arg_set_id'),
- 'base64_proto': 'Raw proto message encoded in base64',
- 'base64_proto_id': 'String id for raw proto message',
+ 'base64_proto':
+ 'Raw proto message encoded in base64',
+ 'base64_proto_id':
+ 'String id for raw proto message',
}))
ANDROID_KEY_EVENTS_TABLE = Table(
@@ -194,7 +196,7 @@
columns=[
C('event_id', CppUint32()),
C('ts', CppInt64()),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
C('base64_proto', CppString()),
C('base64_proto_id', CppOptional(CppUint32())),
],
@@ -213,8 +215,10 @@
ColumnDoc(
doc='Details of the key event parsed from the proto message.',
joinable='args.arg_set_id'),
- 'base64_proto': 'Raw proto message encoded in base64',
- 'base64_proto_id': 'String id for raw proto message',
+ 'base64_proto':
+ 'Raw proto message encoded in base64',
+ 'base64_proto_id':
+ 'String id for raw proto message',
}))
ANDROID_INPUT_EVENT_DISPATCH_TABLE = Table(
@@ -223,7 +227,7 @@
sql_name='__intrinsic_android_input_event_dispatch',
columns=[
C('event_id', CppUint32()),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
C('vsync_id', CppInt64()),
C('window_id', CppInt32()),
C('base64_proto', CppString()),
@@ -252,8 +256,10 @@
''',
'window_id':
'The id of the window to which the event was dispatched.',
- 'base64_proto': 'Raw proto message encoded in base64',
- 'base64_proto_id': 'String id for raw proto message',
+ 'base64_proto':
+ 'Raw proto message encoded in base64',
+ 'base64_proto_id':
+ 'String id for raw proto message',
}))
# Keep this list sorted.
diff --git a/src/trace_processor/tables/counter_tables.py b/src/trace_processor/tables/counter_tables.py
index b749559..efbea0c 100644
--- a/src/trace_processor/tables/counter_tables.py
+++ b/src/trace_processor/tables/counter_tables.py
@@ -35,16 +35,7 @@
C('track_id', CppTableId(TRACK_TABLE)),
C('value', CppDouble()),
C('arg_set_id', CppOptional(CppUint32())),
- ],
- tabledoc=TableDoc(
- doc='''''',
- group='Events',
- columns={
- 'ts': '''''',
- 'track_id': '''''',
- 'value': '''''',
- 'arg_set_id': '''''',
- }))
+ ])
# Keep this list sorted.
ALL_TABLES = [
diff --git a/src/trace_processor/tables/flow_tables.py b/src/trace_processor/tables/flow_tables.py
index 7ed9652..7c9b370 100644
--- a/src/trace_processor/tables/flow_tables.py
+++ b/src/trace_processor/tables/flow_tables.py
@@ -31,7 +31,7 @@
C('slice_out', CppTableId(SLICE_TABLE)),
C('slice_in', CppTableId(SLICE_TABLE)),
C('trace_id', CppOptional(CppInt64())),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
],
tabledoc=TableDoc(
doc='''''',
diff --git a/src/trace_processor/tables/metadata_tables.py b/src/trace_processor/tables/metadata_tables.py
index 10da09b..459d90d 100644
--- a/src/trace_processor/tables/metadata_tables.py
+++ b/src/trace_processor/tables/metadata_tables.py
@@ -62,7 +62,7 @@
C('uid', CppOptional(CppUint32())),
C('android_appid', CppOptional(CppUint32())),
C('cmdline', CppOptional(CppString())),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
C('machine_id', CppOptional(CppTableId(MACHINE_TABLE))),
],
wrapping_sql_view=WrappingSqlView(view_name='process',),
@@ -231,26 +231,36 @@
'''Extra args associated with the CPU''',
}))
-RAW_TABLE = Table(
+CHROME_RAW_TABLE = Table(
python_module=__file__,
- class_name='RawTable',
- sql_name='__intrinsic_raw',
+ class_name='ChromeRawTable',
+ sql_name='__intrinsic_chrome_raw',
+ columns=[
+ C('ts', CppInt64(), flags=ColumnFlag.SORTED),
+ C('name', CppString()),
+ C('utid', CppTableId(THREAD_TABLE)),
+ C('arg_set_id', CppUint32()),
+ ])
+
+FTRACE_EVENT_TABLE = Table(
+ python_module=__file__,
+ class_name='FtraceEventTable',
+ sql_name='__intrinsic_ftrace_event',
columns=[
C('ts', CppInt64(), flags=ColumnFlag.SORTED),
C('name', CppString()),
C('utid', CppTableId(THREAD_TABLE)),
C('arg_set_id', CppUint32()),
C('common_flags', CppUint32()),
- C('ucpu', CppTableId(CPU_TABLE))
+ C('ucpu', CppTableId(CPU_TABLE)),
],
- wrapping_sql_view=WrappingSqlView('track'),
+ wrapping_sql_view=WrappingSqlView('ftrace_event'),
tabledoc=TableDoc(
doc='''
- Contains 'raw' events from the trace for some types of events. This
- table only exists for debugging purposes and should not be relied on
- in production usecases (i.e. metrics, standard library etc).
-
- If you are looking for ftrace_events: please use the ftrace_event table.
+ Contains all the ftrace events in the trace. This table exists only
+ for debugging purposes and should not be relied on in production
+ usecases (i.e. metrics, standard library etc). Note also that this
+ table might be empty if raw ftrace parsing has been disabled.
''',
group='Events',
columns={
@@ -278,29 +288,14 @@
''',
}))
-FTRACE_EVENT_TABLE = Table(
- python_module=__file__,
- class_name='FtraceEventTable',
- sql_name='__intrinsic_ftrace_event',
- parent=RAW_TABLE,
- columns=[],
- wrapping_sql_view=WrappingSqlView('ftrace_event'),
- tabledoc=TableDoc(
- doc='''
- Contains all the ftrace events in the trace. This table exists only
- for debugging purposes and should not be relied on in production
- usecases (i.e. metrics, standard library etc). Note also that this
- table might be empty if raw ftrace parsing has been disabled.
- ''',
- group='Events',
- columns={}))
-
ARG_TABLE = Table(
python_module=__file__,
class_name='ArgTable',
sql_name='__intrinsic_args',
columns=[
- C('arg_set_id', CppUint32(), flags=ColumnFlag.SORTED),
+ C('arg_set_id',
+ CppUint32(),
+ flags=ColumnFlag.SORTED | ColumnFlag.SET_ID),
C('flat_key', CppString()),
C('key', CppString()),
C('int_value', CppOptional(CppInt64())),
@@ -489,6 +484,7 @@
# Keep this list sorted.
ALL_TABLES = [
ARG_TABLE,
+ CHROME_RAW_TABLE,
CLOCK_SNAPSHOT_TABLE,
CPU_FREQ_TABLE,
CPU_TABLE,
@@ -498,7 +494,6 @@
MACHINE_TABLE,
METADATA_TABLE,
PROCESS_TABLE,
- RAW_TABLE,
THREAD_TABLE,
TRACE_FILE_TABLE,
]
diff --git a/src/trace_processor/tables/profiler_tables.py b/src/trace_processor/tables/profiler_tables.py
index 67e1018..9fdf061 100644
--- a/src/trace_processor/tables/profiler_tables.py
+++ b/src/trace_processor/tables/profiler_tables.py
@@ -290,8 +290,8 @@
columns=[
C('ts', CppInt64(), flags=ColumnFlag.SORTED),
C('utid', CppUint32()),
- C('cpu', CppOptional(CppUint32())),
C('callsite_id', CppOptional(CppTableId(STACK_PROFILE_CALLSITE_TABLE))),
+ C('cpu', CppOptional(CppUint32())),
],
tabledoc=TableDoc(
doc='''
@@ -303,10 +303,10 @@
'''Timestamp of the sample.''',
'utid':
'''Sampled thread.''',
- 'cpu':
- '''Core the sampled thread was running on.''',
'callsite_id':
'''If set, unwound callstack of the sampled thread.''',
+ 'cpu':
+ '''Core the sampled thread was running on.''',
}))
SYMBOL_TABLE = Table(
diff --git a/src/trace_processor/tables/py_tables_unittest.cc b/src/trace_processor/tables/py_tables_unittest.cc
index d13b65a..45415bf 100644
--- a/src/trace_processor/tables/py_tables_unittest.cc
+++ b/src/trace_processor/tables/py_tables_unittest.cc
@@ -15,6 +15,7 @@
*/
#include <cstdint>
+#include <optional>
#include <utility>
#include <vector>
@@ -55,7 +56,7 @@
ASSERT_EQ(TestEventTable::ColumnFlag::ts,
ColumnLegacy::Flag::kSorted | ColumnLegacy::Flag::kNonNull);
ASSERT_EQ(TestEventTable::ColumnFlag::arg_set_id,
- ColumnLegacy::Flag::kNonNull);
+ ColumnLegacy::Flag::kNoFlag);
}
TEST_F(PyTablesUnittest, ArgsTableProprties) {
@@ -79,11 +80,11 @@
TEST_F(PyTablesUnittest, InsertEventSpecifyCols) {
TestEventTable::Row row;
row.ts = 100;
- row.arg_set_id = 0;
+ row.arg_set_id = std::nullopt;
event_.Insert(row);
ASSERT_EQ(event_[0].ts(), 100);
- ASSERT_EQ(event_[0].arg_set_id(), 0u);
+ ASSERT_EQ(event_[0].arg_set_id(), std::nullopt);
}
TEST_F(PyTablesUnittest, MutableColumn) {
diff --git a/src/trace_processor/tables/py_tables_unittest.py b/src/trace_processor/tables/py_tables_unittest.py
index 8acc688..7b63198 100644
--- a/src/trace_processor/tables/py_tables_unittest.py
+++ b/src/trace_processor/tables/py_tables_unittest.py
@@ -13,7 +13,7 @@
# limitations under the License.
"""Contains tables for unittesting."""
-from python.generators.trace_processor_table.public import Column as C
+from python.generators.trace_processor_table.public import Column as C, CppOptional
from python.generators.trace_processor_table.public import ColumnFlag
from python.generators.trace_processor_table.public import CppInt64
from python.generators.trace_processor_table.public import Table
@@ -25,7 +25,7 @@
sql_name="event",
columns=[
C("ts", CppInt64(), flags=ColumnFlag.SORTED),
- C("arg_set_id", CppUint32()),
+ C("arg_set_id", CppOptional(CppUint32())),
])
EVENT_CHILD_TABLE = Table(
diff --git a/src/trace_processor/tables/slice_tables.py b/src/trace_processor/tables/slice_tables.py
index 99f64c8..e48402a 100644
--- a/src/trace_processor/tables/slice_tables.py
+++ b/src/trace_processor/tables/slice_tables.py
@@ -43,7 +43,7 @@
C('stack_id', CppInt64()),
C('parent_stack_id', CppInt64()),
C('parent_id', CppOptional(CppSelfTableId())),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
C('thread_ts', CppOptional(CppInt64())),
C('thread_dur', CppOptional(CppInt64())),
C('thread_instruction_count', CppOptional(CppInt64())),
@@ -120,170 +120,6 @@
''',
}))
-GPU_SLICE_TABLE = Table(
- python_module=__file__,
- class_name='GpuSliceTable',
- sql_name='gpu_slice',
- columns=[
- C('context_id', CppOptional(CppInt64())),
- C('render_target', CppOptional(CppInt64())),
- C('render_target_name', CppString()),
- C('render_pass', CppOptional(CppInt64())),
- C('render_pass_name', CppString()),
- C('command_buffer', CppOptional(CppInt64())),
- C('command_buffer_name', CppString()),
- C('frame_id', CppOptional(CppUint32())),
- C('submission_id', CppOptional(CppUint32())),
- C('hw_queue_id', CppOptional(CppInt64())),
- C('upid', CppOptional(CppUint32())),
- C('render_subpasses', CppString()),
- ],
- parent=SLICE_TABLE,
- tabledoc=TableDoc(
- doc='''''',
- group='Slice',
- columns={
- 'context_id':
- '''''',
- 'render_target':
- '''''',
- 'render_target_name':
- '''''',
- 'render_pass':
- '''''',
- 'render_pass_name':
- '''''',
- 'command_buffer':
- '''''',
- 'command_buffer_name':
- '''''',
- 'frame_id':
- '''''',
- 'submission_id':
- '''''',
- 'hw_queue_id':
- '''''',
- 'upid':
- '''
- Unique process id of the app that generates this gpu render
- stage event.
- ''',
- 'render_subpasses':
- ''''''
- }))
-
-GRAPHICS_FRAME_SLICE_TABLE = Table(
- python_module=__file__,
- class_name='GraphicsFrameSliceTable',
- sql_name='frame_slice',
- columns=[
- C('frame_number', CppUint32()),
- C('layer_name', CppString()),
- C('queue_to_acquire_time', CppInt64()),
- C('acquire_to_latch_time', CppInt64()),
- C('latch_to_present_time', CppInt64()),
- ],
- parent=SLICE_TABLE,
- tabledoc=TableDoc(
- doc='''''',
- group='Slice',
- columns={
- 'frame_number': '''''',
- 'layer_name': '''''',
- 'queue_to_acquire_time': '''''',
- 'acquire_to_latch_time': '''''',
- 'latch_to_present_time': ''''''
- }))
-
-EXPECTED_FRAME_TIMELINE_SLICE_TABLE = Table(
- python_module=__file__,
- class_name='ExpectedFrameTimelineSliceTable',
- sql_name='expected_frame_timeline_slice',
- columns=[
- C('display_frame_token', CppInt64()),
- C('surface_frame_token', CppInt64()),
- C('upid', CppUint32()),
- C('layer_name', CppString()),
- ],
- parent=SLICE_TABLE,
- tabledoc=TableDoc(
- doc='''
- This table contains information on the expected timeline of either
- a display frame or a surface frame.
- ''',
- group='Slice',
- columns={
- 'display_frame_token':
- 'Display frame token (vsync id).',
- 'surface_frame_token':
- '''
- Surface frame token (vsync id), null if this is a display frame.
- ''',
- 'upid':
- '''
- Unique process id of the app that generates the surface frame.
- ''',
- 'layer_name':
- 'Layer name if this is a surface frame.',
- }))
-
-ACTUAL_FRAME_TIMELINE_SLICE_TABLE = Table(
- python_module=__file__,
- class_name='ActualFrameTimelineSliceTable',
- sql_name='actual_frame_timeline_slice',
- columns=[
- C('display_frame_token', CppInt64()),
- C('surface_frame_token', CppInt64()),
- C('upid', CppUint32()),
- C('layer_name', CppString()),
- C('present_type', CppString()),
- C('on_time_finish', CppInt32()),
- C('gpu_composition', CppInt32()),
- C('jank_type', CppString()),
- C('jank_severity_type', CppString()),
- C('prediction_type', CppString()),
- C('jank_tag', CppString()),
- ],
- parent=SLICE_TABLE,
- tabledoc=TableDoc(
- doc='''
- This table contains information on the actual timeline and additional
- analysis related to the performance of either a display frame or a
- surface frame.
- ''',
- group='Slice',
- columns={
- 'display_frame_token':
- 'Display frame token (vsync id).',
- 'surface_frame_token':
- '''
- Surface frame token (vsync id), null if this is a display frame.
- ''',
- 'upid':
- '''
- Unique process id of the app that generates the surface frame.
- ''',
- 'layer_name':
- 'Layer name if this is a surface frame.',
- 'present_type':
- 'Frame\'s present type (eg. on time / early / late).',
- 'on_time_finish':
- 'Whether the frame finishes on time.',
- 'gpu_composition':
- 'Whether the frame used gpu composition.',
- 'jank_type':
- '''
- Specify the jank types for this frame if there's jank, or
- none if no jank occurred.
- ''',
- 'jank_severity_type':
- 'Severity of the jank: none if no jank.',
- 'prediction_type':
- 'Frame\'s prediction type (eg. valid / expired).',
- 'jank_tag':
- 'Jank tag based on jank type, used for slice visualization.'
- }))
-
EXPERIMENTAL_FLAT_SLICE_TABLE = Table(
python_module=__file__,
class_name='ExperimentalFlatSliceTable',
@@ -294,7 +130,7 @@
C('track_id', CppTableId(TRACK_TABLE)),
C('category', CppOptional(CppString())),
C('name', CppOptional(CppString())),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
C('source_id', CppOptional(CppTableId(SLICE_TABLE))),
C('start_bound', CppInt64(), flags=ColumnFlag.HIDDEN),
C('end_bound', CppInt64(), flags=ColumnFlag.HIDDEN),
@@ -351,56 +187,11 @@
C('packet_tcp_flags', CppOptional(CppUint32())),
C('packet_tcp_flags_str', CppOptional(CppString())),
],
- parent=SLICE_TABLE,
- wrapping_sql_view=WrappingSqlView('android_network_packets'),
- tabledoc=TableDoc(
- doc="""
- This table contains details on Android Network activity.
- """,
- group='Slice',
- columns={
- 'iface':
- 'The name of the network interface used',
- 'direction':
- 'The direction of traffic (Received or Transmitted)',
- 'packet_transport':
- 'The transport protocol of packets in this event',
- 'packet_length':
- 'The length (in bytes) of packets in this event',
- 'packet_count':
- 'The number of packets contained in this event',
- 'socket_tag':
- 'The Android network tag of the socket',
- 'socket_tag_str':
- 'The socket tag formatted as a hex string',
- 'socket_uid':
- 'The Linux user id of the socket',
- 'local_port':
- 'The local udp/tcp port',
- 'remote_port':
- 'The remote udp/tcp port',
- 'packet_icmp_type':
- 'The 1-byte ICMP type identifier',
- 'packet_icmp_code':
- 'The 1-byte ICMP code identifier',
- 'packet_tcp_flags':
- 'The TCP flags as an integer bitmask (FIN=0x1, SYN=0x2, etc)',
- 'packet_tcp_flags_str':
- '''
- The TCP flags formatted as a string bitmask (e.g. "f...a..." for
- FIN & ACK)
- ''',
- },
- ),
-)
+ parent=SLICE_TABLE)
# Keep this list sorted.
ALL_TABLES = [
- ACTUAL_FRAME_TIMELINE_SLICE_TABLE,
ANDROID_NETWORK_PACKETS_TABLE,
- EXPECTED_FRAME_TIMELINE_SLICE_TABLE,
EXPERIMENTAL_FLAT_SLICE_TABLE,
- GPU_SLICE_TABLE,
- GRAPHICS_FRAME_SLICE_TABLE,
SLICE_TABLE,
]
diff --git a/src/trace_processor/tables/table_destructors.cc b/src/trace_processor/tables/table_destructors.cc
index b538861..f3f7b20 100644
--- a/src/trace_processor/tables/table_destructors.cc
+++ b/src/trace_processor/tables/table_destructors.cc
@@ -61,7 +61,7 @@
JitFrameTable::~JitFrameTable() = default;
// metadata_tables_py.h
-RawTable::~RawTable() = default;
+ChromeRawTable::~ChromeRawTable() = default;
FtraceEventTable::~FtraceEventTable() = default;
ArgTable::~ArgTable() = default;
ExpMissingChromeProcTable::~ExpMissingChromeProcTable() = default;
@@ -106,10 +106,6 @@
// slice_tables_py.h
SliceTable::~SliceTable() = default;
FlowTable::~FlowTable() = default;
-GpuSliceTable::~GpuSliceTable() = default;
-GraphicsFrameSliceTable::~GraphicsFrameSliceTable() = default;
-ExpectedFrameTimelineSliceTable::~ExpectedFrameTimelineSliceTable() = default;
-ActualFrameTimelineSliceTable::~ActualFrameTimelineSliceTable() = default;
ExperimentalFlatSliceTable::~ExperimentalFlatSliceTable() = default;
AndroidNetworkPacketsTable::~AndroidNetworkPacketsTable() = default;
diff --git a/src/trace_processor/tables/winscope_tables.py b/src/trace_processor/tables/winscope_tables.py
index a218e6b..07dc4ee 100644
--- a/src/trace_processor/tables/winscope_tables.py
+++ b/src/trace_processor/tables/winscope_tables.py
@@ -29,7 +29,7 @@
sql_name='__intrinsic_inputmethod_clients',
columns=[
C('ts', CppInt64(), ColumnFlag.SORTED),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
C('base64_proto', CppString()),
C('base64_proto_id', CppOptional(CppUint32())),
],
@@ -49,7 +49,7 @@
sql_name='__intrinsic_inputmethod_manager_service',
columns=[
C('ts', CppInt64(), ColumnFlag.SORTED),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
C('base64_proto', CppString()),
C('base64_proto_id', CppOptional(CppUint32())),
],
@@ -69,7 +69,7 @@
sql_name='__intrinsic_inputmethod_service',
columns=[
C('ts', CppInt64(), ColumnFlag.SORTED),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
C('base64_proto', CppString()),
C('base64_proto_id', CppOptional(CppUint32())),
],
@@ -89,7 +89,7 @@
sql_name='surfaceflinger_layers_snapshot',
columns=[
C('ts', CppInt64(), ColumnFlag.SORTED),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
C('base64_proto', CppString()),
C('base64_proto_id', CppOptional(CppUint32())),
],
@@ -109,7 +109,7 @@
sql_name='surfaceflinger_layer',
columns=[
C('snapshot_id', CppTableId(SURFACE_FLINGER_LAYERS_SNAPSHOT_TABLE)),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
C('base64_proto', CppString()),
C('base64_proto_id', CppOptional(CppUint32())),
],
@@ -129,7 +129,7 @@
sql_name='surfaceflinger_transactions',
columns=[
C('ts', CppInt64(), ColumnFlag.SORTED),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
C('base64_proto', CppString()),
C('base64_proto_id', CppOptional(CppUint32())),
],
@@ -150,7 +150,7 @@
sql_name='__intrinsic_viewcapture',
columns=[
C('ts', CppInt64(), ColumnFlag.SORTED),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
C('base64_proto', CppString()),
C('base64_proto_id', CppOptional(CppUint32())),
],
@@ -171,7 +171,7 @@
columns=[
C('ts', CppInt64()),
C('transition_id', CppInt64(), ColumnFlag.SORTED),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
C('base64_proto', CppString()),
C('base64_proto_id', CppOptional(CppUint32())),
],
@@ -212,7 +212,7 @@
sql_name='__intrinsic_windowmanager',
columns=[
C('ts', CppInt64(), ColumnFlag.SORTED),
- C('arg_set_id', CppUint32()),
+ C('arg_set_id', CppOptional(CppUint32())),
C('base64_proto', CppString()),
C('base64_proto_id', CppOptional(CppUint32())),
],
diff --git a/src/trace_processor/trace_parsing_fuzzer.cc b/src/trace_processor/trace_parsing_fuzzer.cc
index 7e9a1e9..94cbadc 100644
--- a/src/trace_processor/trace_parsing_fuzzer.cc
+++ b/src/trace_processor/trace_parsing_fuzzer.cc
@@ -27,7 +27,7 @@
TraceProcessorStorage::CreateInstance(Config());
std::unique_ptr<uint8_t[]> buf(new uint8_t[size]);
memcpy(buf.get(), data, size);
- util::Status status = processor->Parse(std::move(buf), size);
+ base::Status status = processor->Parse(std::move(buf), size);
if (!status.ok())
return;
if (auto s = processor->NotifyEndOfFile(); !s.ok()) {
diff --git a/src/trace_processor/trace_processor_context.cc b/src/trace_processor/trace_processor_context.cc
index 6922757..d32658e 100644
--- a/src/trace_processor/trace_processor_context.cc
+++ b/src/trace_processor/trace_processor_context.cc
@@ -19,7 +19,6 @@
#include <memory>
#include <optional>
-#include "perfetto/base/logging.h"
#include "src/trace_processor/forwarding_trace_parser.h"
#include "src/trace_processor/importers/common/args_tracker.h"
#include "src/trace_processor/importers/common/args_translation_table.h"
@@ -42,12 +41,9 @@
#include "src/trace_processor/importers/common/trace_file_tracker.h"
#include "src/trace_processor/importers/common/track_compressor.h"
#include "src/trace_processor/importers/common/track_tracker.h"
-#include "src/trace_processor/importers/proto/android_track_event.descriptor.h"
-#include "src/trace_processor/importers/proto/chrome_track_event.descriptor.h"
#include "src/trace_processor/importers/proto/multi_machine_trace_manager.h"
#include "src/trace_processor/importers/proto/perf_sample_tracker.h"
#include "src/trace_processor/importers/proto/proto_importer_module.h"
-#include "src/trace_processor/importers/proto/track_event.descriptor.h"
#include "src/trace_processor/storage/trace_storage.h"
#include "src/trace_processor/trace_reader_registry.h"
@@ -57,48 +53,34 @@
: config(args.config), storage(args.storage) {
reader_registry = std::make_unique<TraceReaderRegistry>(this);
// Init the trackers.
- machine_tracker.reset(new MachineTracker(this, args.raw_machine_id));
+ machine_tracker = std::make_unique<MachineTracker>(this, args.raw_machine_id);
if (!machine_id()) {
- multi_machine_trace_manager.reset(new MultiMachineTraceManager(this));
+ multi_machine_trace_manager =
+ std::make_unique<MultiMachineTraceManager>(this);
}
- track_tracker.reset(new TrackTracker(this));
- track_compressor.reset(new TrackCompressor(this));
- args_tracker.reset(new ArgsTracker(this));
- args_translation_table.reset(new ArgsTranslationTable(storage.get()));
- slice_tracker.reset(new SliceTracker(this));
- slice_translation_table.reset(new SliceTranslationTable(storage.get()));
- flow_tracker.reset(new FlowTracker(this));
- event_tracker.reset(new EventTracker(this));
- sched_event_tracker.reset(new SchedEventTracker(this));
- process_tracker.reset(new ProcessTracker(this));
- process_track_translation_table.reset(
- new ProcessTrackTranslationTable(storage.get()));
- clock_tracker.reset(new ClockTracker(this));
- clock_converter.reset(new ClockConverter(this));
- mapping_tracker.reset(new MappingTracker(this));
- perf_sample_tracker.reset(new PerfSampleTracker(this));
- stack_profile_tracker.reset(new StackProfileTracker(this));
- metadata_tracker.reset(new MetadataTracker(storage.get()));
- cpu_tracker.reset(new CpuTracker(this));
- global_args_tracker.reset(new GlobalArgsTracker(storage.get()));
- {
- descriptor_pool_.reset(new DescriptorPool());
- auto status = descriptor_pool_->AddFromFileDescriptorSet(
- kTrackEventDescriptor.data(), kTrackEventDescriptor.size());
-
- PERFETTO_DCHECK(status.ok());
-
- status = descriptor_pool_->AddFromFileDescriptorSet(
- kChromeTrackEventDescriptor.data(), kChromeTrackEventDescriptor.size());
-
- PERFETTO_DCHECK(status.ok());
-
- status = descriptor_pool_->AddFromFileDescriptorSet(
- kAndroidTrackEventDescriptor.data(),
- kAndroidTrackEventDescriptor.size());
-
- PERFETTO_DCHECK(status.ok());
- }
+ track_tracker = std::make_unique<TrackTracker>(this);
+ track_compressor = std::make_unique<TrackCompressor>(this);
+ args_tracker = std::make_unique<ArgsTracker>(this);
+ args_translation_table =
+ std::make_unique<ArgsTranslationTable>(storage.get());
+ slice_tracker = std::make_unique<SliceTracker>(this);
+ slice_translation_table =
+ std::make_unique<SliceTranslationTable>(storage.get());
+ flow_tracker = std::make_unique<FlowTracker>(this);
+ event_tracker = std::make_unique<EventTracker>(this);
+ sched_event_tracker = std::make_unique<SchedEventTracker>(this);
+ process_tracker = std::make_unique<ProcessTracker>(this);
+ process_track_translation_table =
+ std::make_unique<ProcessTrackTranslationTable>(storage.get());
+ clock_tracker = std::make_unique<ClockTracker>(this);
+ clock_converter = std::make_unique<ClockConverter>(this);
+ mapping_tracker = std::make_unique<MappingTracker>(this);
+ perf_sample_tracker = std::make_unique<PerfSampleTracker>(this);
+ stack_profile_tracker = std::make_unique<StackProfileTracker>(this);
+ metadata_tracker = std::make_unique<MetadataTracker>(storage.get());
+ cpu_tracker = std::make_unique<CpuTracker>(this);
+ global_args_tracker = std::make_shared<GlobalArgsTracker>(storage.get());
+ descriptor_pool_ = std::make_unique<DescriptorPool>();
slice_tracker->SetOnSliceBeginCallback(
[this](TrackId track_id, SliceId slice_id) {
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index 1061081..6b7f35c 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -17,7 +17,6 @@
#include "src/trace_processor/trace_processor_impl.h"
#include <algorithm>
-#include <chrono>
#include <cinttypes>
#include <cstddef>
#include <cstdint>
@@ -46,8 +45,8 @@
#include "perfetto/trace_processor/iterator.h"
#include "perfetto/trace_processor/trace_blob_view.h"
#include "perfetto/trace_processor/trace_processor.h"
-#include "src/trace_processor/importers/android_bugreport/android_dumpstate_reader.h"
#include "src/trace_processor/importers/android_bugreport/android_dumpstate_event_parser_impl.h"
+#include "src/trace_processor/importers/android_bugreport/android_dumpstate_reader.h"
#include "src/trace_processor/importers/android_bugreport/android_log_event_parser_impl.h"
#include "src/trace_processor/importers/android_bugreport/android_log_reader.h"
#include "src/trace_processor/importers/archive/gzip_trace_parser.h"
@@ -67,6 +66,7 @@
#include "src/trace_processor/importers/json/json_utils.h"
#include "src/trace_processor/importers/ninja/ninja_log_parser.h"
#include "src/trace_processor/importers/perf/perf_data_tokenizer.h"
+#include "src/trace_processor/importers/perf/perf_event.h"
#include "src/trace_processor/importers/perf/perf_tracker.h"
#include "src/trace_processor/importers/perf/record_parser.h"
#include "src/trace_processor/importers/perf/spe_record_parser.h"
@@ -112,10 +112,8 @@
#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/descendant.h"
#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.h"
#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_annotated_stack.h"
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.h"
#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flamegraph.h"
#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.h"
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.h"
#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_slice_layout.h"
#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.h"
#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h"
@@ -139,6 +137,11 @@
#include "src/trace_processor/util/status_macros.h"
#include "src/trace_processor/util/trace_type.h"
+#include "protos/perfetto/trace/clock_snapshot.pbzero.h"
+#include "protos/perfetto/trace/perfetto/perfetto_metatrace.pbzero.h"
+#include "protos/perfetto/trace/trace.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
#if PERFETTO_BUILDFLAG(PERFETTO_TP_INSTRUMENTS)
#include "src/trace_processor/importers/instruments/instruments_xml_tokenizer.h"
#include "src/trace_processor/importers/instruments/row_parser.h"
@@ -149,14 +152,9 @@
#include "src/trace_processor/importers/etm/etm_v4_stream_demultiplexer.h"
#include "src/trace_processor/importers/etm/file_tracker.h"
#include "src/trace_processor/perfetto_sql/intrinsics/operators/etm_decode_trace_vtable.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.h"
#endif
-#include "protos/perfetto/common/builtin_clock.pbzero.h"
-#include "protos/perfetto/trace/clock_snapshot.pbzero.h"
-#include "protos/perfetto/trace/perfetto/perfetto_metatrace.pbzero.h"
-#include "protos/perfetto/trace/trace.pbzero.h"
-#include "protos/perfetto/trace/trace_packet.pbzero.h"
-
namespace perfetto::trace_processor {
namespace {
@@ -347,7 +345,7 @@
const TraceStorage& storage) {
int64_t start_ns = std::numeric_limits<int64_t>::max();
int64_t end_ns = std::numeric_limits<int64_t>::min();
- for (auto it = storage.raw_table().IterateRows(); it; ++it) {
+ for (auto it = storage.ftrace_event_table().IterateRows(); it; ++it) {
start_ns = std::min(it.ts(), start_ns);
end_ns = std::max(it.ts(), end_ns);
}
@@ -476,17 +474,11 @@
context_.reader_registry->RegisterTraceReader<TarTraceReader>(kTarTraceType);
- if (context_.config.analyze_trace_proto_content) {
- context_.content_analyzer =
- std::make_unique<ProtoContentAnalyzer>(&context_);
- }
-
#if PERFETTO_BUILDFLAG(PERFETTO_ENABLE_ETM_IMPORTER)
perf_importer::PerfTracker::GetOrCreate(&context_)->RegisterAuxTokenizer(
PERF_AUXTRACE_CS_ETM, etm::CreateEtmV4StreamDemultiplexer);
#endif
- // Add metrics to descriptor pool
const std::vector<std::string> sanitized_extension_paths =
SanitizeMetricMountPaths(config_.skip_builtin_metric_paths);
std::vector<std::string> skip_prefixes;
@@ -494,14 +486,16 @@
for (const auto& path : sanitized_extension_paths) {
skip_prefixes.push_back(kMetricProtoRoot + path);
}
- pool_.AddFromFileDescriptorSet(kMetricsDescriptor.data(),
- kMetricsDescriptor.size(), skip_prefixes);
- pool_.AddFromFileDescriptorSet(kAllChromeMetricsDescriptor.data(),
- kAllChromeMetricsDescriptor.size(),
- skip_prefixes);
- pool_.AddFromFileDescriptorSet(kAllWebviewMetricsDescriptor.data(),
- kAllWebviewMetricsDescriptor.size(),
- skip_prefixes);
+
+ // Add metrics to descriptor pool
+ metrics_descriptor_pool_.AddFromFileDescriptorSet(
+ kMetricsDescriptor.data(), kMetricsDescriptor.size(), skip_prefixes);
+ metrics_descriptor_pool_.AddFromFileDescriptorSet(
+ kAllChromeMetricsDescriptor.data(), kAllChromeMetricsDescriptor.size(),
+ skip_prefixes);
+ metrics_descriptor_pool_.AddFromFileDescriptorSet(
+ kAllWebviewMetricsDescriptor.data(), kAllWebviewMetricsDescriptor.size(),
+ skip_prefixes);
RegisterAdditionalModules(&context_);
InitPerfettoSqlEngine();
@@ -522,22 +516,15 @@
TraceProcessorImpl::~TraceProcessorImpl() = default;
+// =================================================================
+// | TraceProcessorStorage implementation starts here |
+// =================================================================
+
base::Status TraceProcessorImpl::Parse(TraceBlobView blob) {
bytes_parsed_ += blob.size();
return TraceProcessorStorageImpl::Parse(std::move(blob));
}
-std::string TraceProcessorImpl::GetCurrentTraceName() {
- if (current_trace_name_.empty())
- return "";
- auto size = " (" + std::to_string(bytes_parsed_ / 1024 / 1024) + " MB)";
- return current_trace_name_ + size;
-}
-
-void TraceProcessorImpl::SetCurrentTraceName(const std::string& name) {
- current_trace_name_ = name;
-}
-
void TraceProcessorImpl::Flush() {
TraceProcessorStorageImpl::Flush();
BuildBoundsTable(engine_->sqlite_engine()->db(),
@@ -587,19 +574,9 @@
return base::OkStatus();
}
-size_t TraceProcessorImpl::RestoreInitialTables() {
- // We should always have at least as many objects now as we did in the
- // constructor.
- uint64_t registered_count_before = engine_->SqliteRegisteredObjectCount();
- PERFETTO_CHECK(registered_count_before >= sqlite_objects_post_prelude_);
-
- InitPerfettoSqlEngine();
-
- // The registered count should now be the same as it was in the constructor.
- uint64_t registered_count_after = engine_->SqliteRegisteredObjectCount();
- PERFETTO_CHECK(registered_count_after == sqlite_objects_post_prelude_);
- return static_cast<size_t>(registered_count_before - registered_count_after);
-}
+// =================================================================
+// | PerfettoSQL related functionality starts here |
+// =================================================================
Iterator TraceProcessorImpl::ExecuteQuery(const std::string& sql) {
PERFETTO_TP_TRACE(metatrace::Category::API_TIMELINE, "EXECUTE_QUERY",
@@ -617,23 +594,6 @@
return Iterator(std::move(impl));
}
-void TraceProcessorImpl::InterruptQuery() {
- if (!engine_->sqlite_engine()->db())
- return;
- query_interrupted_.store(true);
- sqlite3_interrupt(engine_->sqlite_engine()->db());
-}
-
-bool TraceProcessorImpl::IsRootMetricField(const std::string& metric_name) {
- std::optional<uint32_t> desc_idx =
- pool_.FindDescriptorIdx(".perfetto.protos.TraceMetrics");
- if (!desc_idx.has_value())
- return false;
- const auto* field_idx =
- pool_.descriptors()[*desc_idx].FindFieldByName(metric_name);
- return field_idx != nullptr;
-}
-
base::Status TraceProcessorImpl::RegisterSqlPackage(SqlPackage sql_package) {
sql_modules::RegisteredPackage new_package;
std::string name = sql_package.name;
@@ -661,6 +621,151 @@
return base::OkStatus();
}
+base::Status TraceProcessorImpl::RegisterSqlModule(SqlModule module) {
+ SqlPackage package;
+ package.name = std::move(module.name);
+ package.modules = std::move(module.files);
+ package.allow_override = module.allow_module_override;
+ return RegisterSqlPackage(package);
+}
+
+// =================================================================
+// | Metatracing related functionality starts here |
+// =================================================================
+
+void TraceProcessorImpl::EnableMetatrace(MetatraceConfig config) {
+ metatrace::Enable(config);
+}
+
+namespace {
+
+class StringInterner {
+ public:
+ StringInterner(protos::pbzero::PerfettoMetatrace& event,
+ base::FlatHashMap<std::string, uint64_t>& interned_strings)
+ : event_(event), interned_strings_(interned_strings) {}
+
+ ~StringInterner() {
+ for (const auto& interned_string : new_interned_strings_) {
+ auto* interned_string_proto = event_.add_interned_strings();
+ interned_string_proto->set_iid(interned_string.first);
+ interned_string_proto->set_value(interned_string.second);
+ }
+ }
+
+ uint64_t InternString(const std::string& str) {
+ uint64_t new_iid = interned_strings_.size();
+ auto insert_result = interned_strings_.Insert(str, new_iid);
+ if (insert_result.second) {
+ new_interned_strings_.emplace_back(new_iid, str);
+ }
+ return *insert_result.first;
+ }
+
+ private:
+ protos::pbzero::PerfettoMetatrace& event_;
+ base::FlatHashMap<std::string, uint64_t>& interned_strings_;
+
+ base::SmallVector<std::pair<uint64_t, std::string>, 16> new_interned_strings_;
+};
+
+} // namespace
+
+base::Status TraceProcessorImpl::DisableAndReadMetatrace(
+ std::vector<uint8_t>* trace_proto) {
+ protozero::HeapBuffered<protos::pbzero::Trace> trace;
+
+ auto* clock_snapshot = trace->add_packet()->set_clock_snapshot();
+ for (const auto& [clock_id, ts] : base::CaptureClockSnapshots()) {
+ auto* clock = clock_snapshot->add_clocks();
+ clock->set_clock_id(clock_id);
+ clock->set_timestamp(ts);
+ }
+
+ auto tid = static_cast<uint32_t>(base::GetThreadId());
+ base::FlatHashMap<std::string, uint64_t> interned_strings;
+ metatrace::DisableAndReadBuffer(
+ [&trace, &interned_strings, tid](metatrace::Record* record) {
+ auto* packet = trace->add_packet();
+ packet->set_timestamp(record->timestamp_ns);
+ auto* evt = packet->set_perfetto_metatrace();
+
+ StringInterner interner(*evt, interned_strings);
+
+ evt->set_event_name_iid(interner.InternString(record->event_name));
+ evt->set_event_duration_ns(record->duration_ns);
+ evt->set_thread_id(tid);
+
+ if (record->args_buffer_size == 0)
+ return;
+
+ base::StringSplitter s(
+ record->args_buffer, record->args_buffer_size, '\0',
+ base::StringSplitter::EmptyTokenMode::ALLOW_EMPTY_TOKENS);
+ for (; s.Next();) {
+ auto* arg_proto = evt->add_args();
+ arg_proto->set_key_iid(interner.InternString(s.cur_token()));
+
+ bool has_next = s.Next();
+ PERFETTO_CHECK(has_next);
+ arg_proto->set_value_iid(interner.InternString(s.cur_token()));
+ }
+ });
+ *trace_proto = trace.SerializeAsArray();
+ return base::OkStatus();
+}
+
+// =================================================================
+// | Advanced functionality starts here |
+// =================================================================
+
+std::string TraceProcessorImpl::GetCurrentTraceName() {
+ if (current_trace_name_.empty())
+ return "";
+ auto size = " (" + std::to_string(bytes_parsed_ / 1024 / 1024) + " MB)";
+ return current_trace_name_ + size;
+}
+
+void TraceProcessorImpl::SetCurrentTraceName(const std::string& name) {
+ current_trace_name_ = name;
+}
+
+base::Status TraceProcessorImpl::RegisterFileContent(
+ [[maybe_unused]] const std::string& path,
+ [[maybe_unused]] TraceBlobView content) {
+#if PERFETTO_BUILDFLAG(PERFETTO_ENABLE_ETM_IMPORTER)
+ return etm::FileTracker::GetOrCreate(&context_)->AddFile(path,
+ std::move(content));
+#else
+ return base::OkStatus();
+#endif
+}
+
+void TraceProcessorImpl::InterruptQuery() {
+ if (!engine_->sqlite_engine()->db())
+ return;
+ query_interrupted_.store(true);
+ sqlite3_interrupt(engine_->sqlite_engine()->db());
+}
+
+size_t TraceProcessorImpl::RestoreInitialTables() {
+ // We should always have at least as many objects now as we did in the
+ // constructor.
+ uint64_t registered_count_before = engine_->SqliteRegisteredObjectCount();
+ PERFETTO_CHECK(registered_count_before >= sqlite_objects_post_prelude_);
+
+ InitPerfettoSqlEngine();
+
+ // The registered count should now be the same as it was in the constructor.
+ uint64_t registered_count_after = engine_->SqliteRegisteredObjectCount();
+ PERFETTO_CHECK(registered_count_after == sqlite_objects_post_prelude_);
+ return static_cast<size_t>(registered_count_before - registered_count_after);
+}
+
+// =================================================================
+// | Trace-based metrics (v1) related functionality starts here |
+// =================================================================
+
base::Status TraceProcessorImpl::RegisterMetric(const std::string& path,
const std::string& sql) {
// Check if the metric with the given path already exists and if it does,
@@ -726,22 +831,26 @@
const uint8_t* data,
size_t size,
const std::vector<std::string>& skip_prefixes) {
- RETURN_IF_ERROR(pool_.AddFromFileDescriptorSet(data, size, skip_prefixes));
+ RETURN_IF_ERROR(metrics_descriptor_pool_.AddFromFileDescriptorSet(
+ data, size, skip_prefixes));
RETURN_IF_ERROR(RegisterAllProtoBuilderFunctions(
- &pool_, &proto_fn_name_to_path_, engine_.get(), this));
+ &metrics_descriptor_pool_, &proto_fn_name_to_path_, engine_.get(), this));
return base::OkStatus();
}
base::Status TraceProcessorImpl::ComputeMetric(
const std::vector<std::string>& metric_names,
std::vector<uint8_t>* metrics_proto) {
- auto opt_idx = pool_.FindDescriptorIdx(".perfetto.protos.TraceMetrics");
+ auto opt_idx = metrics_descriptor_pool_.FindDescriptorIdx(
+ ".perfetto.protos.TraceMetrics");
if (!opt_idx.has_value())
return base::Status("Root metrics proto descriptor not found");
- const auto& root_descriptor = pool_.descriptors()[opt_idx.value()];
+ const auto& root_descriptor =
+ metrics_descriptor_pool_.descriptors()[opt_idx.value()];
return metrics::ComputeMetrics(engine_.get(), metric_names, sql_metrics_,
- pool_, root_descriptor, metrics_proto);
+ metrics_descriptor_pool_, root_descriptor,
+ metrics_proto);
}
base::Status TraceProcessorImpl::ComputeMetricText(
@@ -755,13 +864,13 @@
switch (format) {
case TraceProcessor::MetricResultFormat::kProtoText:
*metrics_string = protozero_to_text::ProtozeroToText(
- pool_, ".perfetto.protos.TraceMetrics",
+ metrics_descriptor_pool_, ".perfetto.protos.TraceMetrics",
protozero::ConstBytes{metrics_proto.data(), metrics_proto.size()},
protozero_to_text::kIncludeNewLines);
break;
case TraceProcessor::MetricResultFormat::kJson:
*metrics_string = protozero_to_json::ProtozeroToJson(
- pool_, ".perfetto.protos.TraceMetrics",
+ metrics_descriptor_pool_, ".perfetto.protos.TraceMetrics",
protozero::ConstBytes{metrics_proto.data(), metrics_proto.size()},
protozero_to_json::kPretty | protozero_to_json::kInlineErrors |
protozero_to_json::kInlineAnnotations);
@@ -771,16 +880,12 @@
}
std::vector<uint8_t> TraceProcessorImpl::GetMetricDescriptors() {
- return pool_.SerializeAsDescriptorSet();
-}
-
-void TraceProcessorImpl::EnableMetatrace(MetatraceConfig config) {
- metatrace::Enable(config);
+ return metrics_descriptor_pool_.SerializeAsDescriptorSet();
}
void TraceProcessorImpl::InitPerfettoSqlEngine() {
- engine_.reset(new PerfettoSqlEngine(context_.storage->mutable_string_pool(),
- config_.enable_extra_checks));
+ engine_ = std::make_unique<PerfettoSqlEngine>(
+ context_.storage->mutable_string_pool(), config_.enable_extra_checks);
sqlite3* db = engine_->sqlite_engine()->db();
sqlite3_str_split_init(db);
@@ -908,6 +1013,9 @@
engine_->sqlite_engine()
->RegisterVirtualTableModule<etm::EtmDecodeTraceVtable>(
"__intrinsic_etm_decode_trace", storage);
+ engine_->sqlite_engine()
+ ->RegisterVirtualTableModule<etm::EtmIterateRangeVtable>(
+ "__intrinsic_etm_iterate_instruction_range", storage);
#endif
// Register stdlib packages.
@@ -951,7 +1059,7 @@
// that table in TraceStorage::ShrinkToFitTables.
RegisterStaticTable(storage->mutable_machine_table());
RegisterStaticTable(storage->mutable_arg_table());
- RegisterStaticTable(storage->mutable_raw_table());
+ RegisterStaticTable(storage->mutable_chrome_raw_table());
RegisterStaticTable(storage->mutable_ftrace_event_table());
RegisterStaticTable(storage->mutable_thread_table());
RegisterStaticTable(storage->mutable_process_table());
@@ -963,7 +1071,6 @@
RegisterStaticTable(storage->mutable_sched_slice_table());
RegisterStaticTable(storage->mutable_spurious_sched_wakeup_table());
RegisterStaticTable(storage->mutable_thread_state_table());
- RegisterStaticTable(storage->mutable_gpu_slice_table());
RegisterStaticTable(storage->mutable_track_table());
@@ -996,11 +1103,6 @@
RegisterStaticTable(storage->mutable_vulkan_memory_allocations_table());
- RegisterStaticTable(storage->mutable_graphics_frame_slice_table());
-
- RegisterStaticTable(storage->mutable_expected_frame_timeline_slice_table());
- RegisterStaticTable(storage->mutable_actual_frame_timeline_slice_table());
-
RegisterStaticTable(storage->mutable_android_network_packets_table());
RegisterStaticTable(storage->mutable_v8_isolate_table());
@@ -1063,8 +1165,6 @@
engine_->RegisterStaticTableFunction(
std::make_unique<ExperimentalFlamegraph>(&context_));
engine_->RegisterStaticTableFunction(
- std::make_unique<ExperimentalCounterDur>(storage->counter_table()));
- engine_->RegisterStaticTableFunction(
std::make_unique<ExperimentalSliceLayout>(
context_.storage->mutable_string_pool(), &storage->slice_table()));
engine_->RegisterStaticTableFunction(std::make_unique<TableInfo>(
@@ -1085,8 +1185,6 @@
ConnectedFlow::Mode::kPrecedingFlow, context_.storage.get()));
engine_->RegisterStaticTableFunction(std::make_unique<ConnectedFlow>(
ConnectedFlow::Mode::kFollowingFlow, context_.storage.get()));
- engine_->RegisterStaticTableFunction(std::make_unique<ExperimentalSchedUpid>(
- storage->sched_slice_table(), storage->thread_table()));
engine_->RegisterStaticTableFunction(
std::make_unique<ExperimentalAnnotatedStack>(&context_));
engine_->RegisterStaticTableFunction(
@@ -1105,8 +1203,9 @@
// Metrics.
{
- auto status = RegisterAllProtoBuilderFunctions(
- &pool_, &proto_fn_name_to_path_, engine_.get(), this);
+ auto status = RegisterAllProtoBuilderFunctions(&metrics_descriptor_pool_,
+ &proto_fn_name_to_path_,
+ engine_.get(), this);
if (!status.ok()) {
PERFETTO_FATAL("%s", status.c_message());
}
@@ -1149,93 +1248,15 @@
}
}
-namespace {
-
-class StringInterner {
- public:
- StringInterner(protos::pbzero::PerfettoMetatrace& event,
- base::FlatHashMap<std::string, uint64_t>& interned_strings)
- : event_(event), interned_strings_(interned_strings) {}
-
- ~StringInterner() {
- for (const auto& interned_string : new_interned_strings_) {
- auto* interned_string_proto = event_.add_interned_strings();
- interned_string_proto->set_iid(interned_string.first);
- interned_string_proto->set_value(interned_string.second);
- }
- }
-
- uint64_t InternString(const std::string& str) {
- uint64_t new_iid = interned_strings_.size();
- auto insert_result = interned_strings_.Insert(str, new_iid);
- if (insert_result.second) {
- new_interned_strings_.emplace_back(new_iid, str);
- }
- return *insert_result.first;
- }
-
- private:
- protos::pbzero::PerfettoMetatrace& event_;
- base::FlatHashMap<std::string, uint64_t>& interned_strings_;
-
- base::SmallVector<std::pair<uint64_t, std::string>, 16> new_interned_strings_;
-};
-
-} // namespace
-
-base::Status TraceProcessorImpl::DisableAndReadMetatrace(
- std::vector<uint8_t>* trace_proto) {
- protozero::HeapBuffered<protos::pbzero::Trace> trace;
-
- auto* clock_snapshot = trace->add_packet()->set_clock_snapshot();
- for (const auto& [clock_id, ts] : base::CaptureClockSnapshots()) {
- auto* clock = clock_snapshot->add_clocks();
- clock->set_clock_id(clock_id);
- clock->set_timestamp(ts);
- }
-
- auto tid = static_cast<uint32_t>(base::GetThreadId());
- base::FlatHashMap<std::string, uint64_t> interned_strings;
- metatrace::DisableAndReadBuffer(
- [&trace, &interned_strings, tid](metatrace::Record* record) {
- auto* packet = trace->add_packet();
- packet->set_timestamp(record->timestamp_ns);
- auto* evt = packet->set_perfetto_metatrace();
-
- StringInterner interner(*evt, interned_strings);
-
- evt->set_event_name_iid(interner.InternString(record->event_name));
- evt->set_event_duration_ns(record->duration_ns);
- evt->set_thread_id(tid);
-
- if (record->args_buffer_size == 0)
- return;
-
- base::StringSplitter s(
- record->args_buffer, record->args_buffer_size, '\0',
- base::StringSplitter::EmptyTokenMode::ALLOW_EMPTY_TOKENS);
- for (; s.Next();) {
- auto* arg_proto = evt->add_args();
- arg_proto->set_key_iid(interner.InternString(s.cur_token()));
-
- bool has_next = s.Next();
- PERFETTO_CHECK(has_next);
- arg_proto->set_value_iid(interner.InternString(s.cur_token()));
- }
- });
- *trace_proto = trace.SerializeAsArray();
- return base::OkStatus();
-}
-
-base::Status TraceProcessorImpl::RegisterFileContent(
- [[maybe_unused]] const std::string& path,
- [[maybe_unused]] TraceBlobView content) {
-#if PERFETTO_BUILDFLAG(PERFETTO_ENABLE_ETM_IMPORTER)
- return etm::FileTracker::GetOrCreate(&context_)->AddFile(path,
- std::move(content));
-#else
- return base::OkStatus();
-#endif
+bool TraceProcessorImpl::IsRootMetricField(const std::string& metric_name) {
+ std::optional<uint32_t> desc_idx = metrics_descriptor_pool_.FindDescriptorIdx(
+ ".perfetto.protos.TraceMetrics");
+ if (!desc_idx.has_value())
+ return false;
+ const auto* field_idx =
+ metrics_descriptor_pool_.descriptors()[*desc_idx].FindFieldByName(
+ metric_name);
+ return field_idx != nullptr;
}
} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/trace_processor_impl.h b/src/trace_processor/trace_processor_impl.h
index d2a716a..0706cd3 100644
--- a/src/trace_processor/trace_processor_impl.h
+++ b/src/trace_processor/trace_processor_impl.h
@@ -25,6 +25,7 @@
#include <memory>
#include <string>
#include <unordered_map>
+#include <utility>
#include <vector>
#include "perfetto/base/status.h"
@@ -56,21 +57,55 @@
~TraceProcessorImpl() override;
- // TraceProcessorStorage implementation:
+ // =================================================================
+ // | TraceProcessorStorage implementation starts here |
+ // =================================================================
+
base::Status Parse(TraceBlobView) override;
void Flush() override;
base::Status NotifyEndOfFile() override;
- // TraceProcessor implementation:
+ // =================================================================
+ // | PerfettoSQL related functionality starts here |
+ // =================================================================
+
Iterator ExecuteQuery(const std::string& sql) override;
+ base::Status RegisterSqlPackage(SqlPackage) override;
+
+ base::Status RegisterSqlModule(SqlModule module) override;
+
+ // =================================================================
+ // | Metatracing related functionality starts here |
+ // =================================================================
+
+ void EnableMetatrace(MetatraceConfig config) override;
+
+ base::Status DisableAndReadMetatrace(
+ std::vector<uint8_t>* trace_proto) override;
+
+ // =================================================================
+ // | Advanced functionality starts here |
+ // =================================================================
+
+ std::string GetCurrentTraceName() override;
+ void SetCurrentTraceName(const std::string&) override;
+
+ base::Status RegisterFileContent(const std::string& path,
+ TraceBlobView content) override;
+
+ void InterruptQuery() override;
+
+ size_t RestoreInitialTables() override;
+
+ // =================================================================
+ // | Trace-based metrics (v1) related functionality starts here |
+ // =================================================================
+
base::Status RegisterMetric(const std::string& path,
const std::string& sql) override;
- base::Status RegisterSqlPackage(SqlPackage) override;
-
base::Status ExtendMetricsProto(const uint8_t* data, size_t size) override;
-
base::Status ExtendMetricsProto(
const uint8_t* data,
size_t size,
@@ -78,37 +113,12 @@
base::Status ComputeMetric(const std::vector<std::string>& metric_names,
std::vector<uint8_t>* metrics) override;
-
base::Status ComputeMetricText(const std::vector<std::string>& metric_names,
TraceProcessor::MetricResultFormat format,
std::string* metrics_string) override;
std::vector<uint8_t> GetMetricDescriptors() override;
- void InterruptQuery() override;
-
- size_t RestoreInitialTables() override;
-
- std::string GetCurrentTraceName() override;
- void SetCurrentTraceName(const std::string&) override;
-
- void EnableMetatrace(MetatraceConfig config) override;
-
- base::Status DisableAndReadMetatrace(
- std::vector<uint8_t>* trace_proto) override;
-
- base::Status RegisterSqlModule(SqlModule module) override {
- SqlPackage package;
- package.name = std::move(module.name);
- package.modules = std::move(module.files);
- package.allow_override = module.allow_module_override;
-
- return RegisterSqlPackage(package);
- }
-
- base::Status RegisterFileContent(const std::string& path,
- TraceBlobView content) override;
-
private:
// Needed for iterators to be able to access the context.
friend class IteratorImpl;
@@ -128,7 +138,7 @@
const Config config_;
std::unique_ptr<PerfettoSqlEngine> engine_;
- DescriptorPool pool_;
+ DescriptorPool metrics_descriptor_pool_;
std::vector<metrics::SqlMetricFile> sql_metrics_;
diff --git a/src/trace_processor/trace_processor_storage.cc b/src/trace_processor/trace_processor_storage.cc
index 36e6579..5a5fc1e 100644
--- a/src/trace_processor/trace_processor_storage.cc
+++ b/src/trace_processor/trace_processor_storage.cc
@@ -31,7 +31,7 @@
TraceProcessorStorage::~TraceProcessorStorage() = default;
-util::Status TraceProcessorStorage::Parse(std::unique_ptr<uint8_t[]> buf,
+base::Status TraceProcessorStorage::Parse(std::unique_ptr<uint8_t[]> buf,
size_t size) {
return Parse(TraceBlobView(TraceBlob::TakeOwnership(std::move(buf), size)));
}
diff --git a/src/trace_processor/trace_processor_storage_impl.cc b/src/trace_processor/trace_processor_storage_impl.cc
index da5540f..a3781f1 100644
--- a/src/trace_processor/trace_processor_storage_impl.cc
+++ b/src/trace_processor/trace_processor_storage_impl.cc
@@ -20,10 +20,8 @@
#include <cstddef>
#include <cstdint>
#include <memory>
-#include <optional>
#include <utility>
-#include "perfetto/base/logging.h"
#include "perfetto/base/status.h"
#include "perfetto/ext/base/string_view.h"
#include "perfetto/ext/base/uuid.h"
@@ -38,10 +36,8 @@
#include "src/trace_processor/importers/common/slice_tracker.h"
#include "src/trace_processor/importers/common/stack_profile_tracker.h"
#include "src/trace_processor/importers/common/trace_file_tracker.h"
-#include "src/trace_processor/importers/common/track_compressor.h"
#include "src/trace_processor/importers/proto/default_modules.h"
#include "src/trace_processor/importers/proto/packet_analyzer.h"
-#include "src/trace_processor/importers/proto/perf_sample_tracker.h"
#include "src/trace_processor/importers/proto/proto_importer_module.h"
#include "src/trace_processor/importers/proto/proto_trace_parser_impl.h"
#include "src/trace_processor/importers/proto/proto_trace_reader.h"
diff --git a/src/trace_processor/trace_processor_storage_impl.h b/src/trace_processor/trace_processor_storage_impl.h
index 9198d2f..a40493d 100644
--- a/src/trace_processor/trace_processor_storage_impl.h
+++ b/src/trace_processor/trace_processor_storage_impl.h
@@ -19,7 +19,6 @@
#include "perfetto/ext/base/hash.h"
#include "perfetto/trace_processor/basic_types.h"
-#include "perfetto/trace_processor/status.h"
#include "perfetto/trace_processor/trace_processor_storage.h"
#include "src/trace_processor/importers/common/trace_file_tracker.h"
#include "src/trace_processor/types/trace_processor_context.h"
@@ -34,7 +33,7 @@
explicit TraceProcessorStorageImpl(const Config&);
~TraceProcessorStorageImpl() override;
- util::Status Parse(TraceBlobView) override;
+ base::Status Parse(TraceBlobView) override;
void Flush() override;
base::Status NotifyEndOfFile() override;
diff --git a/src/trace_processor/util/debug_annotation_parser.cc b/src/trace_processor/util/debug_annotation_parser.cc
index 6669b4d..23a2820 100644
--- a/src/trace_processor/util/debug_annotation_parser.cc
+++ b/src/trace_processor/util/debug_annotation_parser.cc
@@ -94,8 +94,7 @@
}
delegate.AddString(context_name, decoder->str().ToStdString());
} else if (annotation.has_pointer_value()) {
- delegate.AddPointer(context_name, reinterpret_cast<const void*>(
- annotation.pointer_value()));
+ delegate.AddPointer(context_name, annotation.pointer_value());
} else if (annotation.has_dict_entries()) {
bool added_entry = false;
for (auto it = annotation.dict_entries(); it; ++it) {
diff --git a/src/trace_processor/util/debug_annotation_parser_unittest.cc b/src/trace_processor/util/debug_annotation_parser_unittest.cc
index 902e9da..c948a9d 100644
--- a/src/trace_processor/util/debug_annotation_parser_unittest.cc
+++ b/src/trace_processor/util/debug_annotation_parser_unittest.cc
@@ -97,10 +97,10 @@
args_.push_back(ss.str());
}
- void AddPointer(const Key& key, const void* value) override {
+ void AddPointer(const Key& key, uint64_t value) override {
std::stringstream ss;
- ss << key.flat_key << " " << key.key << " " << std::hex
- << reinterpret_cast<uintptr_t>(value) << std::dec;
+ ss << key.flat_key << " " << key.key << " " << std::hex << value
+ << std::dec;
args_.push_back(ss.str());
}
diff --git a/src/trace_processor/util/proto_to_args_parser.h b/src/trace_processor/util/proto_to_args_parser.h
index 427ed41..d16be7f 100644
--- a/src/trace_processor/util/proto_to_args_parser.h
+++ b/src/trace_processor/util/proto_to_args_parser.h
@@ -85,7 +85,7 @@
const protozero::ConstChars& value) = 0;
virtual void AddString(const Key& key, const std::string& value) = 0;
virtual void AddDouble(const Key& key, double value) = 0;
- virtual void AddPointer(const Key& key, const void* value) = 0;
+ virtual void AddPointer(const Key& key, uint64_t value) = 0;
virtual void AddBoolean(const Key& key, bool value) = 0;
virtual void AddBytes(const Key& key, const protozero::ConstBytes& value) {
// In the absence of a better implementation default to showing
diff --git a/src/trace_processor/util/proto_to_args_parser_unittest.cc b/src/trace_processor/util/proto_to_args_parser_unittest.cc
index 3dae057..ace2f64 100644
--- a/src/trace_processor/util/proto_to_args_parser_unittest.cc
+++ b/src/trace_processor/util/proto_to_args_parser_unittest.cc
@@ -104,10 +104,10 @@
args_.push_back(ss.str());
}
- void AddPointer(const Key& key, const void* value) override {
+ void AddPointer(const Key& key, uint64_t value) override {
std::stringstream ss;
- ss << key.flat_key << " " << key.key << " " << std::hex
- << reinterpret_cast<uintptr_t>(value) << std::dec;
+ ss << key.flat_key << " " << key.key << " " << std::hex << value
+ << std::dec;
args_.push_back(ss.str());
}
diff --git a/src/trace_processor/util/protozero_to_text.cc b/src/trace_processor/util/protozero_to_text.cc
index 269cb96..8ddc69f 100644
--- a/src/trace_processor/util/protozero_to_text.cc
+++ b/src/trace_processor/util/protozero_to_text.cc
@@ -15,8 +15,11 @@
*/
#include "src/trace_processor/util/protozero_to_text.h"
+#include <cstdint>
#include <optional>
+#include <string>
+#include "perfetto/base/logging.h"
#include "perfetto/ext/base/string_utils.h"
#include "perfetto/ext/base/string_view.h"
#include "perfetto/protozero/proto_decoder.h"
@@ -27,9 +30,7 @@
// This is the highest level that this protozero to text supports.
#include "src/trace_processor/importers/proto/track_event.descriptor.h"
-namespace perfetto {
-namespace trace_processor {
-namespace protozero_to_text {
+namespace perfetto::trace_processor::protozero_to_text {
namespace {
@@ -456,44 +457,6 @@
return final_result;
}
-std::string DebugTrackEventProtozeroToText(const std::string& type,
- protozero::ConstBytes protobytes) {
- DescriptorPool pool;
- auto status = pool.AddFromFileDescriptorSet(kTrackEventDescriptor.data(),
- kTrackEventDescriptor.size());
- PERFETTO_DCHECK(status.ok());
- return ProtozeroToText(pool, type, protobytes, kIncludeNewLines);
-}
-
-std::string ShortDebugTrackEventProtozeroToText(
- const std::string& type,
- protozero::ConstBytes protobytes) {
- DescriptorPool pool;
- auto status = pool.AddFromFileDescriptorSet(kTrackEventDescriptor.data(),
- kTrackEventDescriptor.size());
- PERFETTO_DCHECK(status.ok());
- return ProtozeroToText(pool, type, protobytes, kSkipNewLines);
-}
-
-std::string ProtozeroEnumToText(const std::string& type, int32_t enum_value) {
- DescriptorPool pool;
- auto status = pool.AddFromFileDescriptorSet(kTrackEventDescriptor.data(),
- kTrackEventDescriptor.size());
- PERFETTO_DCHECK(status.ok());
- auto opt_enum_descriptor_idx = pool.FindDescriptorIdx(type);
- if (!opt_enum_descriptor_idx) {
- // Fall back to the integer representation of the field.
- return std::to_string(enum_value);
- }
- auto opt_enum_string =
- pool.descriptors()[*opt_enum_descriptor_idx].FindEnumString(enum_value);
- if (!opt_enum_string) {
- // Fall back to the integer representation of the field.
- return std::to_string(enum_value);
- }
- return *opt_enum_string;
-}
-
std::string ProtozeroToText(const DescriptorPool& pool,
const std::string& type,
const std::vector<uint8_t>& protobytes,
@@ -503,6 +466,4 @@
new_lines_mode);
}
-} // namespace protozero_to_text
-} // namespace trace_processor
-} // namespace perfetto
+} // namespace perfetto::trace_processor::protozero_to_text
diff --git a/src/trace_processor/util/protozero_to_text.h b/src/trace_processor/util/protozero_to_text.h
index 9a0acf1..43e961a 100644
--- a/src/trace_processor/util/protozero_to_text.h
+++ b/src/trace_processor/util/protozero_to_text.h
@@ -17,12 +17,13 @@
#ifndef SRC_TRACE_PROCESSOR_UTIL_PROTOZERO_TO_TEXT_H_
#define SRC_TRACE_PROCESSOR_UTIL_PROTOZERO_TO_TEXT_H_
+#include <cstdint>
#include <string>
+#include <vector>
#include "perfetto/protozero/field.h"
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
class DescriptorPool;
@@ -36,18 +37,6 @@
};
// Given a protozero message |protobytes| which is of fully qualified name
-// |type| within TrackEvent proto messages, we will convert this into a text
-// proto format string.
-//
-// DebugTrackEventProtozeroToText will use new lines between fields, and
-// ShortDebugTrackEventProtozeroToText will use only a single space.
-std::string DebugTrackEventProtozeroToText(const std::string& type,
- protozero::ConstBytes protobytes);
-std::string ShortDebugTrackEventProtozeroToText(
- const std::string& type,
- protozero::ConstBytes protobytes);
-
-// Given a protozero message |protobytes| which is of fully qualified name
// |type|, convert this into a text proto format string. All types used in
// message definition of |type| must be available in |pool|.
std::string ProtozeroToText(
@@ -62,17 +51,7 @@
const std::vector<uint8_t>& protobytes,
NewLinesMode new_lines_mode);
-// Allow the conversion from a protozero enum to a string. The template is just
-// to allow easy enum passing since we will do the explicit cast to a int32_t
-// for the user.
-std::string ProtozeroEnumToText(const std::string& type, int32_t enum_value);
-template <typename Enum>
-std::string ProtozeroEnumToText(const std::string& type, Enum enum_value) {
- return ProtozeroEnumToText(type, static_cast<int32_t>(enum_value));
-}
-
} // namespace protozero_to_text
-} // namespace trace_processor
-} // namespace perfetto
+} // namespace perfetto::trace_processor
#endif // SRC_TRACE_PROCESSOR_UTIL_PROTOZERO_TO_TEXT_H_
diff --git a/src/trace_processor/util/protozero_to_text_unittests.cc b/src/trace_processor/util/protozero_to_text_unittests.cc
index 232fc75..70cf42b 100644
--- a/src/trace_processor/util/protozero_to_text_unittests.cc
+++ b/src/trace_processor/util/protozero_to_text_unittests.cc
@@ -42,79 +42,6 @@
using ::testing::Eq;
using ::testing::StartsWith;
-TEST(ProtozeroToTextTest, TrackEventBasic) {
- using perfetto::protos::pbzero::TrackEvent;
- protozero::HeapBuffered<TrackEvent> msg{kChunkSize, kChunkSize};
- msg->set_track_uuid(4);
- msg->set_timestamp_delta_us(3);
- auto binary_proto = msg.SerializeAsArray();
- EXPECT_EQ(
- "track_uuid: 4\ntimestamp_delta_us: 3",
- DebugTrackEventProtozeroToText(
- ".perfetto.protos.TrackEvent",
- protozero::ConstBytes{binary_proto.data(), binary_proto.size()}));
- EXPECT_EQ(
- "track_uuid: 4 timestamp_delta_us: 3",
- ShortDebugTrackEventProtozeroToText(
- ".perfetto.protos.TrackEvent",
- protozero::ConstBytes{binary_proto.data(), binary_proto.size()}));
-}
-
-TEST(ProtozeroToTextTest, TrackEventNestedMsg) {
- using perfetto::protos::pbzero::TrackEvent;
- protozero::HeapBuffered<TrackEvent> msg{kChunkSize, kChunkSize};
- msg->set_track_uuid(4);
- auto* state = msg->set_cc_scheduler_state();
- state->set_deadline_us(7);
- auto* machine = state->set_state_machine();
- auto* minor_state = machine->set_minor_state();
- minor_state->set_commit_count(8);
- state->set_observing_begin_frame_source(true);
- msg->set_timestamp_delta_us(3);
- auto binary_proto = msg.SerializeAsArray();
-
- EXPECT_EQ(
- R"(track_uuid: 4
-cc_scheduler_state {
- deadline_us: 7
- state_machine {
- minor_state {
- commit_count: 8
- }
- }
- observing_begin_frame_source: true
-}
-timestamp_delta_us: 3)",
- DebugTrackEventProtozeroToText(
- ".perfetto.protos.TrackEvent",
- protozero::ConstBytes{binary_proto.data(), binary_proto.size()}));
-
- EXPECT_EQ(
- "track_uuid: 4 cc_scheduler_state { deadline_us: 7 state_machine { "
- "minor_state { commit_count: 8 } } observing_begin_frame_source: true } "
- "timestamp_delta_us: 3",
- ShortDebugTrackEventProtozeroToText(
- ".perfetto.protos.TrackEvent",
- protozero::ConstBytes{binary_proto.data(), binary_proto.size()}));
-}
-
-TEST(ProtozeroToTextTest, TrackEventEnumNames) {
- using perfetto::protos::pbzero::TrackEvent;
- protozero::HeapBuffered<TrackEvent> msg{kChunkSize, kChunkSize};
- msg->set_type(TrackEvent::TYPE_SLICE_BEGIN);
- auto binary_proto = msg.SerializeAsArray();
- EXPECT_EQ(
- "type: TYPE_SLICE_BEGIN",
- DebugTrackEventProtozeroToText(
- ".perfetto.protos.TrackEvent",
- protozero::ConstBytes{binary_proto.data(), binary_proto.size()}));
- EXPECT_EQ(
- "type: TYPE_SLICE_BEGIN",
- DebugTrackEventProtozeroToText(
- ".perfetto.protos.TrackEvent",
- protozero::ConstBytes{binary_proto.data(), binary_proto.size()}));
-}
-
TEST(ProtozeroToTextTest, CustomDescriptorPoolBasic) {
using perfetto::protos::pbzero::TrackEvent;
protozero::HeapBuffered<TrackEvent> msg{kChunkSize, kChunkSize};
@@ -174,13 +101,6 @@
kSkipNewLines));
}
-TEST(ProtozeroToTextTest, ProtozeroEnumToText) {
- using perfetto::protos::pbzero::TrackEvent;
- EXPECT_EQ("TYPE_SLICE_END",
- ProtozeroEnumToText(".perfetto.protos.TrackEvent.Type",
- TrackEvent::TYPE_SLICE_END));
-}
-
// Sets up a descriptor pool with all the messages from
// "src/protozero/test/example_proto/test_messages.proto"
class ProtozeroToTextTestMessageTest : public testing::Test {
diff --git a/src/trace_processor/util/regex.h b/src/trace_processor/util/regex.h
index 120e4c9..167af55 100644
--- a/src/trace_processor/util/regex.h
+++ b/src/trace_processor/util/regex.h
@@ -48,7 +48,7 @@
regfree(®ex_.value());
}
}
- Regex(Regex&) = delete;
+ Regex(const Regex&) = delete;
Regex(Regex&& other) {
regex_ = std::move(other.regex_);
other.regex_ = std::nullopt;
diff --git a/src/trace_processor/util/status_macros.h b/src/trace_processor/util/status_macros.h
index c902bac..4412cd4 100644
--- a/src/trace_processor/util/status_macros.h
+++ b/src/trace_processor/util/status_macros.h
@@ -19,7 +19,7 @@
#include "perfetto/trace_processor/status.h"
-// Evaluates |expr|, which should return a util::Status. If the status is an
+// Evaluates |expr|, which should return a base::Status. If the status is an
// error status, returns the status from the current function.
#define RETURN_IF_ERROR(expr) \
do { \
diff --git a/src/traceconv/pprof_reader.cc b/src/traceconv/pprof_reader.cc
index f14cca7..612b7c7 100644
--- a/src/traceconv/pprof_reader.cc
+++ b/src/traceconv/pprof_reader.cc
@@ -16,8 +16,8 @@
#include "src/traceconv/pprof_reader.h"
+#include <algorithm>
#include <cinttypes>
-#include <fstream>
#include "perfetto/ext/base/file_utils.h"
diff --git a/src/traceconv/trace_to_pprof_integrationtest.cc b/src/traceconv/trace_to_pprof_integrationtest.cc
index bff3e26..0f8812c 100644
--- a/src/traceconv/trace_to_pprof_integrationtest.cc
+++ b/src/traceconv/trace_to_pprof_integrationtest.cc
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-#include <unistd.h>
#include "test/gtest_and_gmock.h"
#include <fstream>
diff --git a/src/traced/probes/ftrace/ftrace_controller.cc b/src/traced/probes/ftrace/ftrace_controller.cc
index 3dd5962..296d150 100644
--- a/src/traced/probes/ftrace/ftrace_controller.cc
+++ b/src/traced/probes/ftrace/ftrace_controller.cc
@@ -547,7 +547,13 @@
if (!data_sources_.empty())
return;
- if (!retain_ksyms_on_stop_) {
+ // The kernel symbol table is discarded by default to save memory as we run as
+ // a long-lived daemon. Check if the config asked to retain the symbols (e.g.
+ // lab tests). And in either case, reset a set-but-empty table to allow trying
+ // again next time a config requests symbols.
+ if (!retain_ksyms_on_stop_ ||
+ (symbolizer_.is_valid() &&
+ symbolizer_.GetOrCreateKernelSymbolMap()->num_syms() == 0)) {
symbolizer_.Destroy();
}
retain_ksyms_on_stop_ = false;
@@ -608,7 +614,7 @@
// buffers while doing the symbol parsing.
if (data_source->config().symbolize_ksyms()) {
symbolizer_.GetOrCreateKernelSymbolMap();
- // If at least one config sets the KSYMS_RETAIN flag, keep the ksysm map
+ // If at least one config sets the KSYMS_RETAIN flag, keep the ksyms map
// around in StopIfNeeded().
const auto KRET = FtraceConfig::KSYMS_RETAIN;
retain_ksyms_on_stop_ |= data_source->config().ksyms_mem_policy() == KRET;
diff --git a/src/tracing/BUILD.gn b/src/tracing/BUILD.gn
index fad6537..4bbfdf7 100644
--- a/src/tracing/BUILD.gn
+++ b/src/tracing/BUILD.gn
@@ -43,33 +43,16 @@
shared_library("client_api_no_backends_compile_test") {
deps = [
":client_api_without_backends",
- ":platform_fake",
"../../gn:default_deps",
]
}
}
-# Separate target because the embedder might not want this.
+# Some .gn build files outside of this repo (v8, webrtc) still reference this
+# target.
source_set("platform_impl") {
- deps = [
- "../../gn:default_deps",
- "../../include/perfetto/tracing",
- "../base",
- ]
- sources = [
- "platform_posix.cc",
- "platform_windows.cc",
- ]
-}
-
-# Fake platform that allows buiding the client lib on all OSes. You can only use
-# those parts of the client lib that do not use the platform.
-source_set("platform_fake") {
- deps = [
- "../../gn:default_deps",
- "../../include/perfetto/tracing",
- ]
- sources = [ "platform_fake.cc" ]
+ deps = [ "../../gn:default_deps" ]
+ sources = []
}
# Code that both public headers and other non-public sources (e.g.
@@ -116,6 +99,8 @@
"internal/track_event_internal.cc",
"internal/track_event_interned_fields.cc",
"platform.cc",
+ "platform_posix.cc",
+ "platform_windows.cc",
"traced_value.cc",
"tracing.cc",
"tracing_policy.cc",
@@ -152,6 +137,7 @@
perfetto_unittest_source_set("unittests") {
testonly = true
deps = [
+ ":client_api_without_backends",
"../../gn:default_deps",
"../../gn:gtest_and_gmock",
"../../protos/perfetto/trace:lite",
@@ -163,19 +149,9 @@
sources = []
- # TODO(primiano): remove the build_with_chromium conditional once the root
- # //BUILD.gn:libperfetto (in chromium) stops adding tracing:platform_fake.
- # The problem is the following: in chrome builds we end up with duplicate
- # symbol definitions in the test because both platforms (impl and fake) are
- # present: impl added here and fake coming from chromium's base (full path:
- # perfetto_unittests -> //(chromium)base:test_support -> //(chromium)base
- # -> libperfetto -> platform_fake.
+ # TODO(lalitm): this tests appear to be failing on Chromium for unknown
+ # reasons. Figure out why and reenable them.
if (!build_with_chromium) {
- deps += [
- ":client_api_without_backends",
- ":platform_impl",
- ]
-
sources += [
"internal/interceptor_trace_writer_unittest.cc",
"traced_proto_unittest.cc",
@@ -234,7 +210,6 @@
source_set("benchmarks") {
testonly = true
deps = [
- ":platform_impl",
"../..:libperfetto_client_experimental",
"../../gn:benchmark",
"../../gn:default_deps",
diff --git a/src/tracing/service/trace_buffer.cc b/src/tracing/service/trace_buffer.cc
index 69d66cc..111eecc 100644
--- a/src/tracing/service/trace_buffer.cc
+++ b/src/tracing/service/trace_buffer.cc
@@ -435,7 +435,8 @@
for (size_t i = 0; i < patches_size; i++) {
const size_t offset_untrusted = patches[i].offset_untrusted;
- if (offset_untrusted > payload_size - Patch::kSize) {
+ if (payload_size < Patch::kSize ||
+ offset_untrusted > payload_size - Patch::kSize) {
// Either the IPC was so slow and in the meantime the writer managed to
// wrap over |chunk_id| or the producer sent a malicious IPC.
stats_.set_patches_failed(stats_.patches_failed() + 1);
diff --git a/src/tracing/test/BUILD.gn b/src/tracing/test/BUILD.gn
index 3acedef..a562146 100644
--- a/src/tracing/test/BUILD.gn
+++ b/src/tracing/test/BUILD.gn
@@ -66,6 +66,7 @@
testonly = true
deps = [
":test_support",
+ "..:client_api_without_backends",
"../../../gn:default_deps",
"../../../gn:gtest_and_gmock",
"../../base",
@@ -77,18 +78,9 @@
]
sources = [ "tracing_integration_test.cc" ]
- # TODO(primiano): remove the build_with_chromium conditional once the root
- # //BUILD.gn:libperfetto (in chromium) stops adding tracing:platform_fake.
- # The problem is the following: in chrome builds we end up with duplicate
- # symbol definitions in the test because both platorm (impl and fake) are
- # present: impl added here and fake coming from chromium's base (full path:
- # perfetto_unittests -> //(chromium)base:test_support -> //(chromium)base
- # -> libperfetto -> platform_fake.
+ # TODO(lalitm): this tests appear to be failing on Chromium for unknown
+ # reasons. Figure out why and reenable them.
if (!build_with_chromium) {
- deps += [
- "..:client_api_without_backends",
- "..:platform_impl",
- ]
sources += [ "platform_unittest.cc" ]
}
}
@@ -100,7 +92,6 @@
deps = [
":api_test_support",
"../:client_api",
- "../:platform_impl",
"../../../:libperfetto_client_experimental",
"../../../gn:default_deps",
"../../../gn:gtest_and_gmock",
diff --git a/src/tracing/track.cc b/src/tracing/track.cc
index 4ac5d4e..aceb540 100644
--- a/src/tracing/track.cc
+++ b/src/tracing/track.cc
@@ -213,20 +213,21 @@
// static
uint64_t TrackRegistry::ComputeProcessUuid() {
+ base::Hasher hash;
// Use the process start time + pid as the unique identifier for this process.
// This ensures that if there are two independent copies of the Perfetto SDK
// in the same process (e.g., one in the app and another in a system
// framework), events emitted by each will be consistently interleaved on
// common thread and process tracks.
if (uint64_t start_time = GetProcessStartTime()) {
- base::Hasher hash;
hash.Update(start_time);
- hash.Update(Platform::GetCurrentProcessId());
- return hash.digest();
+ } else {
+ // Fall back to a randomly generated identifier.
+ static uint64_t random_once = static_cast<uint64_t>(base::Uuidv4().lsb());
+ hash.Update(random_once);
}
- // Fall back to a randomly generated identifier.
- static uint64_t random_once = static_cast<uint64_t>(base::Uuidv4().lsb());
- return random_once;
+ hash.Update(Platform::GetCurrentProcessId());
+ return hash.digest();
}
void TrackRegistry::ResetForTesting() {
diff --git a/test/ci/bazel_tests.sh b/test/ci/bazel_tests.sh
index 54f5c6e..5db3200 100755
--- a/test/ci/bazel_tests.sh
+++ b/test/ci/bazel_tests.sh
@@ -13,7 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-INSTALL_BUILD_DEPS_ARGS=""
source $(dirname ${BASH_SOURCE[0]})/common.sh
# Save CI time by skipping runs on {UI,docs,infra}-only changes
@@ -23,8 +22,8 @@
exit 0
fi
-bazel build //:all --verbose_failures
-bazel build //python:all --verbose_failures
+tools/bazel build //:all --verbose_failures
+tools/bazel build //python:all --verbose_failures
# Smoke test that processes run without crashing.
./bazel-bin/traced &
diff --git a/test/cts/BUILD.gn b/test/cts/BUILD.gn
index 3feee32..42db191 100644
--- a/test/cts/BUILD.gn
+++ b/test/cts/BUILD.gn
@@ -28,6 +28,7 @@
"../../protos/perfetto/config/process_stats:cpp",
"../../protos/perfetto/config/profiling:cpp",
"../../protos/perfetto/trace:cpp",
+ "../../protos/perfetto/trace/interned_data:cpp",
"../../protos/perfetto/trace/profiling:cpp",
"../../src/base:test_support",
"../../src/protozero/filtering:bytecode_generator",
diff --git a/test/cts/traced_perf_test_cts.cc b/test/cts/traced_perf_test_cts.cc
index 807c8e0..918ba88 100644
--- a/test/cts/traced_perf_test_cts.cc
+++ b/test/cts/traced_perf_test_cts.cc
@@ -32,6 +32,7 @@
#include "protos/perfetto/common/perf_events.gen.h"
#include "protos/perfetto/config/process_stats/process_stats_config.gen.h"
#include "protos/perfetto/config/profiling/perf_event_config.gen.h"
+#include "protos/perfetto/trace/interned_data/interned_data.gen.h"
#include "protos/perfetto/trace/profiling/profile_common.gen.h"
#include "protos/perfetto/trace/profiling/profile_packet.gen.h"
#include "protos/perfetto/trace/trace_packet.gen.h"
@@ -279,5 +280,74 @@
AssertNoStacksForPid(packets, target_pid);
}
+TEST(TracedPerfCtsTest, ProfileKernelCallstack) {
+ if (!HasPerfLsmHooks())
+ GTEST_SKIP() << "skipped due to lack of perf_event_open LSM hooks";
+
+ TraceConfig trace_config;
+ trace_config.add_buffers()->set_size_kb(1024);
+ trace_config.set_duration_ms(3000);
+ trace_config.set_data_source_stop_timeout_ms(8000);
+ trace_config.set_unique_session_name(RandomSessionName().c_str());
+
+ // Capture context switch callstacks from the rest of the device, as they have
+ // predictable function names for the test to assert.
+ protos::gen::PerfEventConfig perf_config;
+ auto* timebase = perf_config.mutable_timebase();
+ // We only need a few samples, and the kernel will record an early sample per
+ // core even at low "frequency" values.
+ timebase->set_frequency(1);
+ auto* tracepoint = timebase->mutable_tracepoint();
+ tracepoint->set_name("sched:sched_switch");
+
+ auto* callstacks = perf_config.mutable_callstack_sampling();
+ callstacks->set_kernel_frames(true);
+ callstacks->set_user_frames(protos::gen::PerfEventConfig::UNWIND_SKIP);
+
+ auto* ds_config = trace_config.add_data_sources()->mutable_config();
+ ds_config->set_name("linux.perf");
+ ds_config->set_perf_event_config_raw(perf_config.SerializeAsString());
+
+ // Collect trace.
+ base::TestTaskRunner task_runner;
+ auto packets = CollectTrace(&task_runner, trace_config);
+
+ // Assert that we're seeing the symbolised scheduling functions, while
+ // double-checking that the interning ids are unrelated to the raw addresses.
+ const uint64_t kSmallInternedId = 4096;
+ size_t unexpected_iids = 0;
+
+ size_t fname_count = 0;
+ bool found_schedule = false;
+ for (const auto& packet : packets) {
+ for (const auto& fname : packet.interned_data().function_names()) {
+ fname_count++;
+ if (fname.str() == "schedule" || fname.str() == "__schedule" ||
+ fname.str() == "preempt_schedule") {
+ found_schedule = true;
+ }
+ if (fname.iid() > kSmallInternedId) {
+ unexpected_iids++;
+ }
+ }
+ for (const auto& frame : packet.interned_data().frames()) {
+ if (frame.iid() > kSmallInternedId) {
+ unexpected_iids++;
+ }
+ }
+ if (packet.has_perf_sample() &&
+ packet.perf_sample().callstack_iid() > kSmallInternedId) {
+ unexpected_iids++;
+ }
+ }
+
+ EXPECT_TRUE(found_schedule)
+ << "Failed to find a scheduling kernel function symbol name in the "
+ "profile, total functions seen: "
+ << fname_count;
+
+ EXPECT_EQ(unexpected_iids, 0u) << "Unexpectedly high interning ids.";
+}
+
} // namespace
} // namespace perfetto
diff --git a/test/data/ui-screenshots/aggregation.test.ts/frametimeline/frame-timeline-aggregation.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/frametimeline/frame-timeline-aggregation.png.sha256
index 9d4f6dc..f694f5d 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/frametimeline/frame-timeline-aggregation.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/frametimeline/frame-timeline-aggregation.png.sha256
@@ -1 +1 @@
-0b104eeb550b136333511b072be56dc3abf63664f88249db075a52afd3492592
\ No newline at end of file
+476be67ad67952c63eb73b5ad68d96be476f57bc835f826103586a508b522560
\ No newline at end of file
diff --git a/test/data/ui-screenshots/aggregation.test.ts/gpu-counter/gpu-counter-aggregation.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/gpu-counter/gpu-counter-aggregation.png.sha256
index fcf00cc..f029dce 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/gpu-counter/gpu-counter-aggregation.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/gpu-counter/gpu-counter-aggregation.png.sha256
@@ -1 +1 @@
-dffb0fb8df8ce39dfc8ae01c1647ea30d93b190e527ff1ab87ed5590da80a9ea
\ No newline at end of file
+ac5cb189130bd8f57843c3b366ba8a12e1d27848d4722fb541c5405a3ef107d7
\ No newline at end of file
diff --git a/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-process.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-process.png.sha256
index 856d657..23223bb 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-process.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-process.png.sha256
@@ -1 +1 @@
-06e371ba0b66007d781de10e76e6703518468fab9bc8c3d6c4c593b66dbd811b
\ No newline at end of file
+009ae9d78994c22adaa2484909acd8d2627f61b76573412012e079406cde7253
\ No newline at end of file
diff --git a/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-thread.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-thread.png.sha256
index dd957d9e..db33e76 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-thread.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-thread.png.sha256
@@ -1 +1 @@
-6c82764ab14233d586c63b1aacef3f593b8359a39e3f369c3d2ae57cb04e16a6
\ No newline at end of file
+0889c04d6944c1704452294714be35dd1a044bf5eb2e85360d72143ba784b5d7
\ No newline at end of file
diff --git a/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-occurrences.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-occurrences.png.sha256
index cf5c700..21bd7d9 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-occurrences.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-occurrences.png.sha256
@@ -1 +1 @@
-0c0917bba2ce35b5f83833ea74200e4b19b58e75529e1087c08f6deb1d32a488
\ No newline at end of file
+b77e2ef33ad26be6ecd0a8ef71f87c69b488c4162d792f84f5204d4a8c0d95c5
\ No newline at end of file
diff --git a/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration-desc.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration-desc.png.sha256
index 244ec4c..ad36d05 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration-desc.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration-desc.png.sha256
@@ -1 +1 @@
-8cacaa219ce05bc2d59a905512a8798bb2d857f5676097f30d10d1349bc48aa5
\ No newline at end of file
+f77cea1dfa259570c77ac7836adb998217af7c9a650479b6da6673a4d5f08096
\ No newline at end of file
diff --git a/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration.png.sha256
index 6b6a626..d67923a 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration.png.sha256
@@ -1 +1 @@
-0e96c5e7384488f90e21223d2db51719094eb7a180c8885a0250d26478a5c526
\ No newline at end of file
+c2e55d4a9f1671e0168637821dba9dbb77e67266cd10b20366084e0790aef290
\ No newline at end of file
diff --git a/test/data/ui-screenshots/aggregation.test.ts/slices/slice-aggregation.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/slices/slice-aggregation.png.sha256
index 6270439..6ff98d0 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/slices/slice-aggregation.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/slices/slice-aggregation.png.sha256
@@ -1 +1 @@
-c597b3f217765c7cd3b4e5db6bc2b2ae6aed643968960f19bd88d20050a96060
\ No newline at end of file
+78bf1ad90eaeb54628b3fcde2684a842dac3b5241cc55f98b5d00cd05f0e2f1c
\ No newline at end of file
diff --git a/test/data/ui-screenshots/chrome_missing_track_names.test.ts/expand-all-tracks/all-tracks-expanded.png.sha256 b/test/data/ui-screenshots/chrome_missing_track_names.test.ts/expand-all-tracks/all-tracks-expanded.png.sha256
index f1bf92b..692a0bb 100644
--- a/test/data/ui-screenshots/chrome_missing_track_names.test.ts/expand-all-tracks/all-tracks-expanded.png.sha256
+++ b/test/data/ui-screenshots/chrome_missing_track_names.test.ts/expand-all-tracks/all-tracks-expanded.png.sha256
@@ -1 +1 @@
-0133a9353c03118bc3c6737aa1e5b799e8cd888b4693300d9ac4bdbb72636863
\ No newline at end of file
+c91cf9293896ca4979eb1ecf0369c6fcf14816a33af01bc4cc21d1878cd4c187
\ No newline at end of file
diff --git a/test/data/ui-screenshots/chrome_missing_track_names.test.ts/trace-loaded/trace-loaded.png.sha256 b/test/data/ui-screenshots/chrome_missing_track_names.test.ts/trace-loaded/trace-loaded.png.sha256
index be184fa..6a4301f 100644
--- a/test/data/ui-screenshots/chrome_missing_track_names.test.ts/trace-loaded/trace-loaded.png.sha256
+++ b/test/data/ui-screenshots/chrome_missing_track_names.test.ts/trace-loaded/trace-loaded.png.sha256
@@ -1 +1 @@
-75e9f9fb872e1fe12b1aafa5428abf8695336b9602be66ff84f5b12fc70b5b9c
\ No newline at end of file
+e6ce2901471b1ef54fd2075f8ab9a0713d86766bff6951882ede3e8fbaaa65ad
\ No newline at end of file
diff --git a/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/expand-browser/browser-expanded.png.sha256 b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/expand-browser/browser-expanded.png.sha256
index 4faf095..94e4723 100644
--- a/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/expand-browser/browser-expanded.png.sha256
+++ b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/expand-browser/browser-expanded.png.sha256
@@ -1 +1 @@
-9f9203330bdd5fd395c66dbdf5cb5056e711a243de9c2145db22f48b2c40662a
\ No newline at end of file
+1fb4597938818a3dfc4af7fee836738f42bbf74cc70cc88c7412567ecb93e5ea
\ No newline at end of file
diff --git a/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/load-trace/loaded.png.sha256 b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/load-trace/loaded.png.sha256
index 1d4c10e..7276ab8 100644
--- a/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/load-trace/loaded.png.sha256
+++ b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/load-trace/loaded.png.sha256
@@ -1 +1 @@
-029dccb932965ac1aa292b86953a5ad3716a822f4f07d83d8124759ff106e040
\ No newline at end of file
+6333a4a7df4e5ca0471e3643bffd2b858ee70f618675dde459c704b086fa0e2c
\ No newline at end of file
diff --git a/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/slice-with-flows/slice-with-flows.png.sha256 b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/slice-with-flows/slice-with-flows.png.sha256
index 8053e36..4d73652 100644
--- a/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/slice-with-flows/slice-with-flows.png.sha256
+++ b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/slice-with-flows/slice-with-flows.png.sha256
@@ -1 +1 @@
-30b25f7f8a24ed7375b570ba70ee24902a8d409031ebf74b5227eeec052b55e9
\ No newline at end of file
+6e1b5bcc79a99f97b06b9581dd24f0c2e3eaac36dcfe975680ad5e071ba0f3ca
\ No newline at end of file
diff --git a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks-pivot/debug-track-pivot.png.sha256 b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks-pivot/debug-track-pivot.png.sha256
index fafbb80..372ff6b 100644
--- a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks-pivot/debug-track-pivot.png.sha256
+++ b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks-pivot/debug-track-pivot.png.sha256
@@ -1 +1 @@
-498b44ca27f6ae25bf5cc6c065b314ac685c9cd7d80675f68824ccd9ca939722
\ No newline at end of file
+1a0f90dd580562e442c7fbbfcdad37f9f9aeeb20d88c66b4f4deaa98e7630744
\ No newline at end of file
diff --git a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-slice-clicked.png.sha256 b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-slice-clicked.png.sha256
index 3d047f3..4bb3e55 100644
--- a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-slice-clicked.png.sha256
+++ b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-slice-clicked.png.sha256
@@ -1 +1 @@
-40158e9d8e8b82f161fdedefb888ab998a16dee844894e3c1c8a0540b9d530a8
\ No newline at end of file
+2bbb39c5a1116587f8f511e2d2f346d6052efd796bb446e64f35fa83b34fc631
\ No newline at end of file
diff --git a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-added.png.sha256 b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-added.png.sha256
index 61c9caf..e532e33 100644
--- a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-added.png.sha256
+++ b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-added.png.sha256
@@ -1 +1 @@
-d0b94afc57a69c00487918dd5a08c684b29fb530fe0edf0828d3b39c783dd9fa
\ No newline at end of file
+3907851e2b92ceb523d3ceb03b28003dfe7568d8431bf10a4997ca17ef9a6ba0
\ No newline at end of file
diff --git a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-removed.png.sha256 b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-removed.png.sha256
index 6e60ba0..6d5456b 100644
--- a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-removed.png.sha256
+++ b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-removed.png.sha256
@@ -1 +1 @@
-9164ec2cc6c950abeae247982871a0175959acbb2b75b2b7a5dc715d91763455
\ No newline at end of file
+72287b646e7f912b752c806b84c610aa3188acde481a75b7e779f74825750e23
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tab/ftrace-tab.png.sha256 b/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tab/ftrace-tab.png.sha256
index a05f60c..6248292 100644
--- a/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tab/ftrace-tab.png.sha256
+++ b/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tab/ftrace-tab.png.sha256
@@ -1 +1 @@
-a3f878e50c3e342055a5f1ce3bc56bcc4d9be7852751eef8b301f5864a859dfa
\ No newline at end of file
+cee4518a2575d9cc929b8ba6c0409f7e7139a47da22ca7e7ab6c7658db275942
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256 b/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256
index 38dfbf6..0a0b087 100644
--- a/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256
+++ b/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256
@@ -1 +1 @@
-307d93331ab863a4e92dce150176b92536394d62894d35064c9aa24ac8fb5e1a
\ No newline at end of file
+201b85b1dc910a5956674b9a60b45d2bf96eaf0bd1cf25a2a3380c0ae8d1eee4
\ No newline at end of file
diff --git a/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip-expanded.png.sha256 b/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip-expanded.png.sha256
index f20a2ae..4616708 100644
--- a/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip-expanded.png.sha256
+++ b/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip-expanded.png.sha256
@@ -1 +1 @@
-aa4ff3edb135b7f2210a3b136f8cd82e509a50cfc0617aff8a779f8adc8655ac
\ No newline at end of file
+041f3323e330c1e9b19bdcc306fbc6fc0aaf16288840e7d99e22968b12d57ad6
\ No newline at end of file
diff --git a/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip.png.sha256 b/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip.png.sha256
index c01cf39..7102005 100644
--- a/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip.png.sha256
+++ b/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip.png.sha256
@@ -1 +1 @@
-d25beab0531610669dfa40e5d5c12412d6550f2db758accc386bd995c1d6f7cc
\ No newline at end of file
+8f2d538e455267467d91044a3d1f4e728fef93f91443a4265276bbe9cc3ebc8e
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/info-and-stats/back-to-timeline.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/info-and-stats/back-to-timeline.png.sha256
index 5f63afd..c417ca1 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/info-and-stats/back-to-timeline.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/info-and-stats/back-to-timeline.png.sha256
@@ -1 +1 @@
-9f40fc0b8d54831ba0f0d9b9c39c7442d1e546ad0d51f1c0fafa9ee55c6a1609
\ No newline at end of file
+d34d9a1cdc9f65fafa41b838939ebf13aa408c7bd8d0b2e3bf7f1e4f9f57a9e8
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/load-trace/loaded.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/load-trace/loaded.png.sha256
index 5f63afd..c417ca1 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/load-trace/loaded.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/load-trace/loaded.png.sha256
@@ -1 +1 @@
-9f40fc0b8d54831ba0f0d9b9c39c7442d1e546ad0d51f1c0fafa9ee55c6a1609
\ No newline at end of file
+d34d9a1cdc9f65fafa41b838939ebf13aa408c7bd8d0b2e3bf7f1e4f9f57a9e8
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-0.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-0.png.sha256
index c116e4a..43ac071 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-0.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-0.png.sha256
@@ -1 +1 @@
-b3af6ddd9238a4712ea9ca96b3ed14dab13817b049dd701e6e7f64b5415d6c89
\ No newline at end of file
+44a410d14dcf3197ad29cc59623c8744717a517cf23d4ec85fd2b78926a343a7
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-1.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-1.png.sha256
index c116e4a..43ac071 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-1.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-1.png.sha256
@@ -1 +1 @@
-b3af6ddd9238a4712ea9ca96b3ed14dab13817b049dd701e6e7f64b5415d6c89
\ No newline at end of file
+44a410d14dcf3197ad29cc59623c8744717a517cf23d4ec85fd2b78926a343a7
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-2.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-2.png.sha256
index 5a5853e..0437d0e 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-2.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-2.png.sha256
@@ -1 +1 @@
-2a37e0335d7dca1a5cc91a763e8fc2064e22a4504cfdbc9b05105613c0962333
\ No newline at end of file
+eeb4097bef65291e8741b016a327e2342eaca56a3265765288397b5c4349af78
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-3.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-3.png.sha256
index 620170b..03ad4aa 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-3.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-3.png.sha256
@@ -1 +1 @@
-0562e85510daf4c00a1dafb97f745b282e058989b25d1965627d672df32bb381
\ No newline at end of file
+5fce62f177042f1f16eea2fb83123ef01a53d9320a332a86df99855ea18d7e58
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/process-details.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/process-details.png.sha256
index ab2896e..f95582f 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/process-details.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/process-details.png.sha256
@@ -1 +1 @@
-7f46d1f492962572c6d99829f4e46604da437e3ffcf5c4c871854d2952fb7d8f
\ No newline at end of file
+66c2f109471f01b250328a21880c8d2393553f9b80459eb4e387bcdb36c78743
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/search-slice.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/search-slice.png.sha256
index 80822dc..ec62ebc 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/search-slice.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/search-slice.png.sha256
@@ -1 +1 @@
-dc636b348dee1f9d010ffc03b4eafb8622f19e5df5149559048f7dd81df4a8dc
\ No newline at end of file
+d7f69cde62ff76b61102c4fa029603a4efb0eb43b523585a5babe104e24bba4a
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/one-track-pinned.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/one-track-pinned.png.sha256
index 0b394f0..d784edf 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/one-track-pinned.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/one-track-pinned.png.sha256
@@ -1 +1 @@
-cded4a4c2cfad1f7cd37a548a58fd78c0ac9f40938ec31072e4110001f35b807
\ No newline at end of file
+46dad937c32fc5ac1e2a5a4763f6815c64407caab0534e85d0fd2c684082e9ea
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/two-tracks-pinned.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/two-tracks-pinned.png.sha256
index 7391dd8..5280776 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/two-tracks-pinned.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/two-tracks-pinned.png.sha256
@@ -1 +1 @@
-6091f10c58949550b7ad5bd5f68949dd5317ecbd5e393c48851636a782a943b1
\ No newline at end of file
+5e49f590943df485a954f4954e0cfe92544c937e6d8883f7aed9e65960f5aeac
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-compressed.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-compressed.png.sha256
index 3b7bb8c..c315f69 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-compressed.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-compressed.png.sha256
@@ -1 +1 @@
-9d24bd0ce0b301cba4026055a154e1a71ce30547e75b8db37ccb0c5eb0691207
\ No newline at end of file
+11d70ca250031414507c3275e004ffa99e0e19b303eb3d744d7f3910432aca17
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-expanded.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-expanded.png.sha256
index 3b7bb8c..a8062ac 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-expanded.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-expanded.png.sha256
@@ -1 +1 @@
-9d24bd0ce0b301cba4026055a154e1a71ce30547e75b8db37ccb0c5eb0691207
\ No newline at end of file
+4c3c5c8441cb9c3fd05f7c14cba59354ff2ff95cc8b07cb10ab0c73d438ab1b1
\ No newline at end of file
diff --git a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/back-to-trace-1.png.sha256 b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/back-to-trace-1.png.sha256
index dc4defa..534a03a 100644
--- a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/back-to-trace-1.png.sha256
+++ b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/back-to-trace-1.png.sha256
@@ -1 +1 @@
-09c85f30096c40203d218215cf15288cce65baa648bce78d47433c6d22d70bd0
\ No newline at end of file
+d5d97f3c8f0f5db86f34edb5a94f8e5d49a7b8ddd917797fce1eb1276737b7cf
\ No newline at end of file
diff --git a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/confirmation-dialog.png.sha256 b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/confirmation-dialog.png.sha256
index 7b10b88..8df0a78 100644
--- a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/confirmation-dialog.png.sha256
+++ b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/confirmation-dialog.png.sha256
@@ -1 +1 @@
-d00d65fb1e0f4bb661a71e7e3cb7ae07ceeb1bc5925d49632fa932dd33fe9813
\ No newline at end of file
+e708381bdb43de90525020519a0b6c7f61af9c8ae08c2879e93bc24d44986e5b
\ No newline at end of file
diff --git a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-1.png.sha256 b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-1.png.sha256
index baf52b1..534a03a 100644
--- a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-1.png.sha256
+++ b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-1.png.sha256
@@ -1 +1 @@
-aa5ae8738d00562f3a0bad44ea0bea142c6669fb4d2518b75bf1464b31e442ea
\ No newline at end of file
+d5d97f3c8f0f5db86f34edb5a94f8e5d49a7b8ddd917797fce1eb1276737b7cf
\ No newline at end of file
diff --git a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-2.png.sha256 b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-2.png.sha256
index b44bd50..1bd4993 100644
--- a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-2.png.sha256
+++ b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-2.png.sha256
@@ -1 +1 @@
-fd84ea4faded9b6e777bd2a1498d02378068f9bc44c697617c4520e12a02a864
\ No newline at end of file
+afd50e2132446e616267ac176a1860e3d9e6b2de1077507ecd42f41054949eac
\ No newline at end of file
diff --git a/test/data/ui-screenshots/queries.test.ts/omnibox-query/omnibox-cleared.png.sha256 b/test/data/ui-screenshots/queries.test.ts/omnibox-query/omnibox-cleared.png.sha256
index 20bcf2f..8ea072b 100644
--- a/test/data/ui-screenshots/queries.test.ts/omnibox-query/omnibox-cleared.png.sha256
+++ b/test/data/ui-screenshots/queries.test.ts/omnibox-query/omnibox-cleared.png.sha256
@@ -1 +1 @@
-21cd6ce57f0a7bbb3e1c683b6e3ef965b70eef223c83675a1908c78610d8e1b4
\ No newline at end of file
+9237e4e9027b64f60c6e36d44e6b0a32d6f2d4cc1ca3f7dad55c6b7b26dab0b1
\ No newline at end of file
diff --git a/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png.sha256 b/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png.sha256
index 32aded1..485cbf4 100644
--- a/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png.sha256
+++ b/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png.sha256
@@ -1 +1 @@
-f404c1b3b8feae5ed5c525e0f37fefc77f08721a192426ed75a50c1a811165f8
\ No newline at end of file
+ce0814c13403d7b21211cfc068ba152d54c8da03726e1732087f256a30260d09
\ No newline at end of file
diff --git a/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-1-clicked.png.sha256 b/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-1-clicked.png.sha256
index c42b448..8c556d9 100644
--- a/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-1-clicked.png.sha256
+++ b/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-1-clicked.png.sha256
@@ -1 +1 @@
-3cf361f939b0a2056a81cffb5344e6cb8ad3b0d561ae6f8131164599c36a1a6c
\ No newline at end of file
+ee126bb2252e5d597ea8ccf5fe74d9ff8fef0021b41cd583ce9db66debbab6e5
\ No newline at end of file
diff --git a/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-2-clicked.png.sha256 b/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-2-clicked.png.sha256
index 97f7a63..e578367 100644
--- a/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-2-clicked.png.sha256
+++ b/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-2-clicked.png.sha256
@@ -1 +1 @@
-e6298b572b9069e689af92597af259979d63746c1d68f70cb6f03821cd4f8ffc
\ No newline at end of file
+ae3f2d86686aff7e5085327eae154f301a7f9f6fdd214acf31484985e4de67ba
\ No newline at end of file
diff --git a/test/data/ui-screenshots/sql_table_tab.test.ts/ShowTable-command/slices-table.png.sha256 b/test/data/ui-screenshots/sql_table_tab.test.ts/ShowTable-command/slices-table.png.sha256
index 4b11647..fcc6ea2 100644
--- a/test/data/ui-screenshots/sql_table_tab.test.ts/ShowTable-command/slices-table.png.sha256
+++ b/test/data/ui-screenshots/sql_table_tab.test.ts/ShowTable-command/slices-table.png.sha256
@@ -1 +1 @@
-4edd5cd26a4938b0d510ae4fb4ac62b55825e0d14f094a484bf332e58fb61a8f
\ No newline at end of file
+c39001db22206931e273678382447bb87b5a95e832a54bb861209c93af32a082
\ No newline at end of file
diff --git a/test/data/ui-screenshots/sql_table_tab.test.ts/slices-with-same-name/slices-with-same-name.png.sha256 b/test/data/ui-screenshots/sql_table_tab.test.ts/slices-with-same-name/slices-with-same-name.png.sha256
index ba23b96..9ba0acf 100644
--- a/test/data/ui-screenshots/sql_table_tab.test.ts/slices-with-same-name/slices-with-same-name.png.sha256
+++ b/test/data/ui-screenshots/sql_table_tab.test.ts/slices-with-same-name/slices-with-same-name.png.sha256
@@ -1 +1 @@
-550d2ec336bace4634a02f04b0bf5a5e828a918de0b4cb31dfd9c16ae20b29f6
\ No newline at end of file
+3f706d1008ac3fc3cdff6c900b5cd4b9c9d37ec02237b4575a58aaa277745eb5
\ No newline at end of file
diff --git a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/chronological-order/chronological.png.sha256 b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/chronological-order/chronological.png.sha256
index adfaad5..8081e34 100644
--- a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/chronological-order/chronological.png.sha256
+++ b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/chronological-order/chronological.png.sha256
@@ -1 +1 @@
-e0ee27a824201ec74ecd8d490a4ff593a575f89be473a7c41d1fded7d7b4e30c
\ No newline at end of file
+31116f9523c9c99f936767da66b4d3b58c982cdb9971def21f7a561a2ae6ed73
\ No newline at end of file
diff --git a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/explicit-order/explicit.png.sha256 b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/explicit-order/explicit.png.sha256
index 96d28c3..6336d58 100644
--- a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/explicit-order/explicit.png.sha256
+++ b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/explicit-order/explicit.png.sha256
@@ -1 +1 @@
-f831590f6ecef858fa6f27db6a409c4f6af5bb31912250f36b06e82781981098
\ No newline at end of file
+d5342d1d3eb8de2d95596a94abdabab854befbf77c9d45ebfb2948693ec27620
\ No newline at end of file
diff --git a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/lexicographic-tracks/lexicographic.png.sha256 b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/lexicographic-tracks/lexicographic.png.sha256
index 9f5d054..804115a 100644
--- a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/lexicographic-tracks/lexicographic.png.sha256
+++ b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/lexicographic-tracks/lexicographic.png.sha256
@@ -1 +1 @@
-b53e700573ce7561da8f51ac094c16e8eb80ef43b56f861cd82ab47383f4a1a6
\ No newline at end of file
+c8d7378301534f1ebd1cbdf5da6bd9dca7b6be460bf81657a7a5baf2ecb769cd
\ No newline at end of file
diff --git a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/load-trace/loaded.png.sha256 b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/load-trace/loaded.png.sha256
index 3cbc9eb..95024a1 100644
--- a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/load-trace/loaded.png.sha256
+++ b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/load-trace/loaded.png.sha256
@@ -1 +1 @@
-7d91add11aee7a160090a443e4b26543dbbdb49d9ace7d72a3f32aed61ba4a52
\ No newline at end of file
+ef1f15fba695a997b3eeec9faf6776a7c9b481d77ff3c2a5d7d2ef2ba06be71f
\ No newline at end of file
diff --git a/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-process.png.sha256 b/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-process.png.sha256
index 610f226..42d5c45 100644
--- a/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-process.png.sha256
+++ b/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-process.png.sha256
@@ -1 +1 @@
-bdac97813abf1ef98f780fcf93d3f3b48c40cf807a2530e50d760bb7b9ac15dd
\ No newline at end of file
+84759b08519a616e0be568036aec73a90c603c73992416ef439e6db7d7a0bfc1
\ No newline at end of file
diff --git a/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-thread.png.sha256 b/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-thread.png.sha256
index 13270b5..8f0c982 100644
--- a/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-thread.png.sha256
+++ b/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-thread.png.sha256
@@ -1 +1 @@
-c50394d0808fcb7909e8a491322676e9ae64148dc2c815f6d70250e0041c793d
\ No newline at end of file
+de4c43ba9570aebf4dfe964cde1802445856ab02812781ff368d4fcb259fb1f2
\ No newline at end of file
diff --git a/test/data/ui-screenshots/wattson.test.ts/wattson-aggregations/wattson-estimate-aggr.png.sha256 b/test/data/ui-screenshots/wattson.test.ts/wattson-aggregations/wattson-estimate-aggr.png.sha256
index 09ceb1b..a1ecd5c 100644
--- a/test/data/ui-screenshots/wattson.test.ts/wattson-aggregations/wattson-estimate-aggr.png.sha256
+++ b/test/data/ui-screenshots/wattson.test.ts/wattson-aggregations/wattson-estimate-aggr.png.sha256
@@ -1 +1 @@
-0836aa16d7c78298d9563893e7155f591f19df6bd5cd03dd73a476fb3175293d
\ No newline at end of file
+bf940c29825fd4aa44cecc93bf146ee56f0e1013af4267ff5311da40a2b1adb9
\ No newline at end of file
diff --git a/test/data/ui-screenshots/zoom.test.ts/zoom-in/zoomed-in.png.sha256 b/test/data/ui-screenshots/zoom.test.ts/zoom-in/zoomed-in.png.sha256
index cc5978e..18c7ea1 100644
--- a/test/data/ui-screenshots/zoom.test.ts/zoom-in/zoomed-in.png.sha256
+++ b/test/data/ui-screenshots/zoom.test.ts/zoom-in/zoomed-in.png.sha256
@@ -1 +1 @@
-7b179c70b90fbd8c4410a8c018ce6d53c93a5b7b311fbb42d9e61ba39a3c7090
\ No newline at end of file
+f7aaef8d41d81ee1b034b8788fd2f39abaa057a303769327eeb9c3c6883e4965
\ No newline at end of file
diff --git a/test/data/ui-screenshots/zoom.test.ts/zoom-out/zoomed-out.png.sha256 b/test/data/ui-screenshots/zoom.test.ts/zoom-out/zoomed-out.png.sha256
index 4e405b8..d2c7787 100644
--- a/test/data/ui-screenshots/zoom.test.ts/zoom-out/zoomed-out.png.sha256
+++ b/test/data/ui-screenshots/zoom.test.ts/zoom-out/zoomed-out.png.sha256
@@ -1 +1 @@
-5370b83cf5d9935c3c470ddbe1c4cff10852e634314434a91378172365d6de6f
\ No newline at end of file
+d5532af59943fad497ab4bfb53fb30be76dc830eaddee3fa8484a4c0fd65dfeb
\ No newline at end of file
diff --git a/test/ftrace_integrationtest.cc b/test/ftrace_integrationtest.cc
index 4791b0d..3c14008 100644
--- a/test/ftrace_integrationtest.cc
+++ b/test/ftrace_integrationtest.cc
@@ -209,8 +209,7 @@
// 1. On cuttlefish (x86-kvm). It's too slow when running on GCE (b/171771440).
// We cannot change the length of the production code in
// CanReadKernelSymbolAddresses() to deal with it.
-// 2. On user (i.e. non-userdebug) builds. As that doesn't work there by design.
-// 3. On ARM builds, because they fail on our CI.
+// 2. On ARM builds, because they fail on our CI.
#if (PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD) && defined(__i386__)) || \
defined(__arm__)
#define MAYBE_KernelAddressSymbolization DISABLED_KernelAddressSymbolization
@@ -219,14 +218,10 @@
#endif
TEST_F(PerfettoFtraceIntegrationTest, MAYBE_KernelAddressSymbolization) {
// On Android in-tree builds (TreeHugger): this test must always run to
- // prevent selinux / property-related regressions. However it can run only on
- // userdebug.
+ // prevent selinux / property-related regressions.
// On standalone builds and Linux, this can be optionally skipped because
// there it requires root to lower kptr_restrict.
-#if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
- if (!IsDebuggableBuild())
- GTEST_SKIP();
-#else
+#if !PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
if (geteuid() != 0)
GTEST_SKIP();
#endif
diff --git a/test/synth_common.py b/test/synth_common.py
index c0355c3..cc2ae75 100644
--- a/test/synth_common.py
+++ b/test/synth_common.py
@@ -198,6 +198,9 @@
def add_atrace_instant(self, ts, tid, pid, buf):
self.add_print(ts, tid, 'I|{}|{}'.format(pid, buf))
+ def add_atrace_instant_for_track(self, ts, tid, pid, track_name, buf):
+ self.add_print(ts, tid, 'N|{}|{}|{}'.format(pid, track_name, buf))
+
def add_process(self, pid, ppid, cmdline, uid=None):
process = self.packet.process_tree.processes.add()
process.pid = pid
diff --git a/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_cuj_per_frame_metric.out b/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_cuj_per_frame_metric.out
new file mode 100644
index 0000000..e413979
--- /dev/null
+++ b/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_cuj_per_frame_metric.out
@@ -0,0 +1,45 @@
+android_blocking_calls_cuj_per_frame_metric {
+ cuj {
+ name: "BACK_PANEL_ARROW"
+ process {
+ name: "com.android.systemui"
+ uid: 10001
+ pid: 5000
+ }
+ blocking_calls {
+ name: "animation"
+ max_dur_per_frame_ms: 2
+ max_dur_per_frame_ns: 2000000
+ mean_dur_per_frame_ms: 1
+ mean_dur_per_frame_ns: 1000000
+ max_cnt_per_frame: 1
+ mean_cnt_per_frame: 0.5
+ }
+ blocking_calls {
+ name: "binder transaction"
+ max_dur_per_frame_ms: 2
+ max_dur_per_frame_ns: 2500000
+ mean_dur_per_frame_ms: 0
+ mean_dur_per_frame_ns: 750000
+ max_cnt_per_frame: 1
+ mean_cnt_per_frame: 0.5
+ }
+ }
+ cuj {
+ name: "CUJ_NAME"
+ process {
+ name: "com.google.android.apps.nexuslauncher"
+ uid: 10002
+ pid: 6000
+ }
+ blocking_calls {
+ name: "animation"
+ max_dur_per_frame_ms: 2
+ max_dur_per_frame_ns: 2000000
+ mean_dur_per_frame_ms: 1
+ mean_dur_per_frame_ns: 1500000
+ max_cnt_per_frame: 1
+ mean_cnt_per_frame: 1.0
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_cuj_per_frame_metric.py b/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_cuj_per_frame_metric.py
new file mode 100755
index 0000000..fa091ac
--- /dev/null
+++ b/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_cuj_per_frame_metric.py
@@ -0,0 +1,340 @@
+#!/usr/bin/env python3
+# 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 synth_common
+from os import sys
+
+SYSUI_PID = 5000
+SYSUI_UI_TID = 5020
+LAUNCHER_PID = 6000
+LAUNCHER_UI_TID = 6020
+
+# RenderThread
+SYSUI_RTID = 1555
+LAUNCHER_RTID = 1655
+
+SYSUI_PACKAGE = "com.android.systemui"
+LAUNCHER_PACKAGE = "com.google.android.apps.nexuslauncher"
+
+SYSUI_UID = 10001
+LAUNCHER_UID = 10002
+
+LAYER_1 = "TX - first_layer#0"
+LAYER_2 = "TX - second_layer#1"
+LAYER_3 = "TX - third_layer#2"
+
+FIRST_CUJ = "J<BACK_PANEL_ARROW>"
+SECOND_CUJ = "J<CUJ_NAME>"
+
+
+def add_async_trace(trace, ts, ts_end, buf, pid, tid):
+ trace.add_atrace_async_begin(ts=ts, tid=tid, pid=pid, buf=buf)
+ trace.add_atrace_async_end(ts=ts_end, tid=tid, pid=pid, buf=buf)
+
+def add_ui_thread_atrace(trace, ts, ts_end, buf, tid, pid):
+ trace.add_atrace_begin(ts=ts, tid=tid, pid=pid, buf=buf)
+ trace.add_atrace_end(ts=ts_end, tid=tid, pid=pid)
+
+
+def add_instant_event_in_thread(trace, ts, buf, pid, tid):
+ trace.add_atrace_instant(ts=ts, tid=tid, pid=pid, buf=buf)
+
+def add_render_thread_atrace_begin(trace, ts, buf, rtid, pid):
+ trace.add_atrace_begin(ts=ts, tid=rtid, pid=pid, buf=buf)
+
+
+def add_render_thread_atrace_end(trace, ts_end, rtid, pid):
+ trace.add_atrace_end(ts=ts_end, tid=rtid, pid=pid)
+
+
+def add_frame(trace, vsync, ts_do_frame, ts_end_do_frame, tid, pid):
+ add_ui_thread_atrace(
+ trace,
+ ts=ts_do_frame,
+ ts_end=ts_end_do_frame,
+ buf="Choreographer#doFrame %d" % vsync,
+ tid=tid,
+ pid=pid)
+
+def add_expected_surface_frame_events(ts, dur, token, pid):
+ trace.add_expected_surface_frame_start_event(
+ ts=ts,
+ cookie=100000 + token,
+ token=token,
+ display_frame_token=100 + token,
+ pid=pid,
+ layer_name='')
+ trace.add_frame_end_event(ts=ts + dur, cookie=100000 + token)
+
+def add_actual_surface_frame_events(ts, dur, token, layer, pid):
+ cookie = token + 1
+ trace.add_actual_surface_frame_start_event(
+ ts=ts,
+ cookie=100002 + cookie,
+ token=token,
+ display_frame_token=token + 100,
+ pid=pid,
+ present_type=1,
+ on_time_finish=1,
+ gpu_composition=0,
+ jank_type=1,
+ prediction_type=3,
+ layer_name=layer)
+ trace.add_frame_end_event(ts=ts + dur, cookie=100002 + cookie)
+
+def add_blocking_calls_per_frame_multiple_cuj_instance(trace, cuj_name):
+
+ # add a new CUJ in trace.
+ add_async_trace(trace, ts=25_000_000, ts_end=77_000_000, buf=cuj_name, pid=SYSUI_PID, tid=SYSUI_UI_TID)
+ add_async_trace(trace, ts=83_000_000, ts_end=102_000_000, buf=cuj_name, pid=SYSUI_PID, tid=SYSUI_UI_TID)
+
+ trace.add_atrace_instant_for_track(ts=25_000_001,
+ buf="FT#beginVsync#20",
+ pid=SYSUI_PID,
+ tid=SYSUI_UI_TID,
+ track_name=cuj_name)
+
+ trace.add_atrace_instant_for_track(ts=25_000_010,
+ buf="FT#layerId#0",
+ pid=SYSUI_PID,
+ tid=SYSUI_UI_TID,
+ track_name=cuj_name)
+
+ trace.add_atrace_instant_for_track(ts=76_000_001,
+ buf="FT#endVsync#30",
+ pid=SYSUI_PID,
+ tid=SYSUI_UI_TID,
+ track_name=cuj_name)
+
+
+ trace.add_atrace_instant_for_track(ts=83_000_001,
+ buf="FT#beginVsync#60",
+ pid=SYSUI_PID,
+ tid=SYSUI_UI_TID,
+ track_name=cuj_name)
+
+ trace.add_atrace_instant_for_track(ts=83_000_010,
+ buf="FT#layerId#2",
+ pid=SYSUI_PID,
+ tid=SYSUI_UI_TID,
+ track_name=cuj_name)
+
+ trace.add_atrace_instant_for_track(ts=101_000_001,
+ buf="FT#endVsync#70",
+ pid=SYSUI_PID,
+ tid=SYSUI_UI_TID,
+ track_name=cuj_name)
+
+ # Add Choreographer#doFrame outside CUJ boundary. This frame will not be considered during
+ # metric calculation.
+ add_frame(
+ trace,
+ vsync=15,
+ ts_do_frame=9_000_000,
+ ts_end_do_frame=15_000_000,
+ tid=SYSUI_UI_TID,
+ pid=SYSUI_PID)
+
+ add_render_thread_atrace_begin(trace, ts=10_000_000, buf="DrawFrames 15", rtid=SYSUI_RTID, pid=SYSUI_PID)
+ add_render_thread_atrace_end(trace, ts_end=12_000_000, rtid=SYSUI_RTID, pid=SYSUI_PID)
+
+ # Add Choreographer#doFrame slices within CUJ boundary.
+ add_frame(
+ trace,
+ vsync=20,
+ ts_do_frame=26_000_000,
+ ts_end_do_frame=32_000_000,
+ tid=SYSUI_UI_TID,
+ pid=SYSUI_PID)
+
+ add_render_thread_atrace_begin(trace, ts=27_000_000, buf="DrawFrames 20", rtid=SYSUI_RTID, pid=SYSUI_PID)
+ add_render_thread_atrace_end(trace, ts_end=28_000_000, rtid=SYSUI_RTID, pid=SYSUI_PID)
+
+ add_frame(
+ trace,
+ vsync=22,
+ ts_do_frame=43_000_000,
+ ts_end_do_frame=49_000_000,
+ tid=SYSUI_UI_TID,
+ pid=SYSUI_PID)
+
+ add_render_thread_atrace_begin(trace, ts=44_000_000, buf="DrawFrames 22", rtid=SYSUI_RTID, pid=SYSUI_PID)
+ add_render_thread_atrace_end(trace, ts_end=45_000_000, rtid=SYSUI_RTID, pid=SYSUI_PID)
+
+ add_frame(
+ trace,
+ vsync=24,
+ ts_do_frame=60_000_000,
+ ts_end_do_frame=65_000_000,
+ tid=SYSUI_UI_TID,
+ pid=SYSUI_PID)
+
+ add_render_thread_atrace_begin(trace, ts=61_000_000, buf="DrawFrames 24", rtid=SYSUI_RTID, pid=SYSUI_PID)
+ add_render_thread_atrace_end(trace, ts_end=62_000_000, rtid=SYSUI_RTID, pid=SYSUI_PID)
+
+ add_frame(
+ trace,
+ vsync=65,
+ ts_do_frame=84_000_000,
+ ts_end_do_frame=89_000_000,
+ tid=SYSUI_UI_TID,
+ pid=SYSUI_PID)
+
+ add_render_thread_atrace_begin(trace, ts=85_000_000, buf="DrawFrames 65", rtid=SYSUI_RTID, pid=SYSUI_PID)
+ add_render_thread_atrace_end(trace, ts_end=86_000_000, rtid=SYSUI_RTID, pid=SYSUI_PID)
+
+ trace.add_atrace_begin(
+ ts=28_000_000, buf="binder transaction", tid=SYSUI_UI_TID, pid=SYSUI_PID)
+ trace.add_atrace_end(ts=28_500_000, tid=SYSUI_UI_TID, pid=SYSUI_PID)
+
+ trace.add_atrace_begin(
+ ts=30_000_000, buf="animation", tid=SYSUI_UI_TID, pid=SYSUI_PID)
+ trace.add_atrace_end(ts=32_000_000, tid=SYSUI_UI_TID, pid=SYSUI_PID)
+
+ trace.add_atrace_begin(
+ ts=62_000_000, buf="animation", tid=SYSUI_UI_TID, pid=SYSUI_PID)
+ trace.add_atrace_end(ts=64_000_000, tid=SYSUI_UI_TID, pid=SYSUI_PID)
+
+ trace.add_atrace_begin(
+ ts=86_000_000, buf="binder transaction", tid=SYSUI_UI_TID, pid=SYSUI_PID)
+ trace.add_atrace_end(ts=88_500_000, tid=SYSUI_UI_TID, pid=SYSUI_PID)
+
+ # Add expected and actual frames.
+ add_expected_surface_frame_events(ts=10_000_000, dur=16_000_000, token=15, pid=SYSUI_PID)
+ add_actual_surface_frame_events(ts=10_000_000, dur=16_000_000, token=15, layer=LAYER_1, pid=SYSUI_PID)
+
+ add_expected_surface_frame_events(ts=27_000_000, dur=16_000_000, token=20, pid=SYSUI_PID)
+ add_actual_surface_frame_events(ts=27_000_000, dur=7_000_000, token=20, layer=LAYER_1, pid=SYSUI_PID)
+
+ add_expected_surface_frame_events(ts=44_000_000, dur=16_000_000, token=22, pid=SYSUI_PID)
+ add_actual_surface_frame_events(ts=44_000_000, dur=7_000_000, token=22, layer=LAYER_1, pid=SYSUI_PID)
+
+ add_expected_surface_frame_events(ts=61_000_000, dur=16_000_000, token=24, pid=SYSUI_PID)
+ add_actual_surface_frame_events(ts=61_000_000, dur=6_000_000, token=24, layer=LAYER_1, pid=SYSUI_PID)
+
+ add_expected_surface_frame_events(ts=85_000_000, dur=16_000_000, token=65, pid=SYSUI_PID)
+ add_actual_surface_frame_events(ts=85_000_000, dur=6_000_000, token=65, layer=LAYER_3, pid=SYSUI_PID)
+
+def add_blocking_call_crossing_frame_boundary(trace, cuj_name):
+
+ # add a new CUJ in trace.
+ add_async_trace(trace, ts=120_000_000, ts_end=145_000_000, buf=cuj_name, pid=LAUNCHER_PID, tid=LAUNCHER_UI_TID)
+
+ add_instant_event_in_thread(
+ trace,
+ ts=120_000_001,
+ buf=cuj_name + "#UIThread",
+ pid=LAUNCHER_PID,
+ tid=LAUNCHER_UI_TID)
+
+ trace.add_atrace_instant_for_track(ts=120_000_002,
+ buf="FT#beginVsync#80",
+ pid=LAUNCHER_PID,
+ tid=LAUNCHER_UI_TID,
+ track_name=cuj_name)
+
+ trace.add_atrace_instant_for_track(ts=120_000_010,
+ buf="FT#layerId#1",
+ pid=LAUNCHER_PID,
+ tid=LAUNCHER_UI_TID,
+ track_name=cuj_name)
+
+ trace.add_atrace_instant_for_track(ts=144_000_001,
+ buf="FT#endVsync#90",
+ pid=LAUNCHER_PID,
+ tid=LAUNCHER_UI_TID,
+ track_name=cuj_name)
+
+
+ # Add Choreographer#doFrame outside CUJ boundary. This frame will not be considered during
+ # metric calculation.
+ add_frame(
+ trace,
+ vsync=75,
+ ts_do_frame=103_000_000,
+ ts_end_do_frame=110_000_000,
+ tid=LAUNCHER_UI_TID,
+ pid=LAUNCHER_PID)
+
+ add_render_thread_atrace_begin(trace, ts=104_000_000, buf="DrawFrames 75", rtid=LAUNCHER_RTID, pid=LAUNCHER_PID)
+ add_render_thread_atrace_end(trace, ts_end=105_000_000, rtid=LAUNCHER_RTID, pid=LAUNCHER_PID)
+
+ # Add Choreographer#doFrame slices within CUJ boundary.
+ add_frame(
+ trace,
+ vsync=80,
+ ts_do_frame=120_000_000,
+ ts_end_do_frame=126_000_000,
+ tid=LAUNCHER_UI_TID,
+ pid=LAUNCHER_PID)
+
+ add_render_thread_atrace_begin(trace, ts=121_000_000, buf="DrawFrames 80", rtid=LAUNCHER_RTID, pid=LAUNCHER_PID)
+ add_render_thread_atrace_end(trace, ts_end=122_000_000, rtid=LAUNCHER_RTID, pid=LAUNCHER_PID)
+
+ add_frame(
+ trace,
+ vsync=82,
+ ts_do_frame=141_000_000,
+ ts_end_do_frame=143_000_000,
+ tid=LAUNCHER_UI_TID,
+ pid=LAUNCHER_PID)
+
+ add_render_thread_atrace_begin(trace, ts=142_000_000, buf="DrawFrames 82", rtid=LAUNCHER_RTID, pid=LAUNCHER_PID)
+ add_render_thread_atrace_end(trace, ts_end=142_500_000, rtid=LAUNCHER_RTID, pid=LAUNCHER_PID)
+
+ trace.add_atrace_begin(
+ ts=127_000_000, buf="animation", tid=LAUNCHER_UI_TID, pid=LAUNCHER_PID)
+ trace.add_atrace_end(ts=140_000_000, tid=LAUNCHER_UI_TID, pid=LAUNCHER_PID)
+
+ # Add expected and actual frames.
+ add_expected_surface_frame_events(ts=104_000_000, dur=16_000_000, token=75, pid=LAUNCHER_PID)
+ add_actual_surface_frame_events(ts=104_000_000, dur=16_000_000, token=75, layer=LAYER_2, pid=LAUNCHER_PID)
+
+ add_expected_surface_frame_events(ts=121_000_000, dur=16_000_000, token=80, pid=LAUNCHER_PID)
+ add_actual_surface_frame_events(ts=121_000_000, dur=7_000_000, token=80, layer=LAYER_2, pid=LAUNCHER_PID)
+
+ add_expected_surface_frame_events(ts=138_000_000, dur=16_000_000, token=82, pid=LAUNCHER_PID)
+ add_actual_surface_frame_events(ts=138_000_000, dur=6_000_000, token=82, layer=LAYER_2, pid=LAUNCHER_PID)
+
+def add_process(trace, package_name, uid, pid):
+ trace.add_package_list(ts=0, name=package_name, uid=uid, version_code=1)
+ trace.add_process(pid=pid, ppid=0, cmdline=package_name, uid=uid)
+ trace.add_thread(tid=pid, tgid=pid, cmdline="MainThread", name="MainThread")
+
+def setup_trace():
+ trace = synth_common.create_trace()
+ trace.add_packet()
+ add_process(
+ trace, package_name=SYSUI_PACKAGE, uid=SYSUI_UID, pid=SYSUI_PID)
+ add_process(
+ trace,
+ package_name=LAUNCHER_PACKAGE,
+ uid=LAUNCHER_UID,
+ pid=LAUNCHER_PID)
+
+ trace.add_thread(
+ tid=SYSUI_UI_TID,
+ tgid=SYSUI_PID,
+ cmdline="BackPanelUiThre",
+ name="BackPanelUiThre")
+
+ trace.add_ftrace_packet(cpu=0)
+ return trace
+
+trace = setup_trace()
+add_blocking_calls_per_frame_multiple_cuj_instance(trace, FIRST_CUJ)
+trace.add_ftrace_packet(cpu=0)
+add_blocking_call_crossing_frame_boundary(trace, SECOND_CUJ)
+sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/test/trace_processor/diff_tests/metrics/android/tests.py b/test/trace_processor/diff_tests/metrics/android/tests.py
index 5c1e0ba..15925a1 100644
--- a/test/trace_processor/diff_tests/metrics/android/tests.py
+++ b/test/trace_processor/diff_tests/metrics/android/tests.py
@@ -145,6 +145,12 @@
query=Metric('android_blocking_calls_cuj_metric'),
out=Path('android_blocking_calls_cuj_different_ui_thread.out'))
+ def test_android_blocking_calls_cuj_per_frame(self):
+ return DiffTestBlueprint(
+ trace=Path('android_blocking_calls_cuj_per_frame_metric.py'),
+ query=Metric('android_blocking_calls_cuj_per_frame_metric'),
+ out=Path('android_blocking_calls_cuj_per_frame_metric.out'))
+
def test_sysui_notif_shade_list_builder(self):
return DiffTestBlueprint(
trace=Path('android_sysui_notif_shade_list_builder_metric.py'),
diff --git a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range.textproto b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range.textproto
index d1115cf..63bd6df 100644
--- a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range.textproto
+++ b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range.textproto
@@ -13,7 +13,7 @@
pid: 1
tid: 1
}
- parent_uuid: 0
+ disallow_merging_with_system_tracks: true
}
}
packet {
@@ -26,7 +26,7 @@
pid: 2
tid: 2
}
- parent_uuid: 0
+ disallow_merging_with_system_tracks: true
}
}
packet {
diff --git a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_cropping.textproto b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_cropping.textproto
index f02a3e6..cce038c 100644
--- a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_cropping.textproto
+++ b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_cropping.textproto
@@ -20,7 +20,7 @@
pid: 1
tid: 1
}
- parent_uuid: 0
+ disallow_merging_with_system_tracks: true
}
}
packet {
@@ -33,7 +33,7 @@
pid: 2
tid: 2
}
- parent_uuid: 0
+ disallow_merging_with_system_tracks: true
}
}
packet {
@@ -47,7 +47,7 @@
pid: 3
tid: 3
}
- parent_uuid: 0
+ disallow_merging_with_system_tracks: true
}
}
diff --git a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_browser_main.textproto b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_browser_main.textproto
index b7dac0b..9605a6d 100644
--- a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_browser_main.textproto
+++ b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_browser_main.textproto
@@ -11,19 +11,19 @@
pid: 1
tid: 1
}
- parent_uuid: 0
+ disallow_merging_with_system_tracks: true
}
}
packet {
timestamp: 2
track_descriptor {
- uuid: 2
- process {
- pid: 1
- }
- chrome_process {
- process_type: PROCESS_BROWSER
- }
+ uuid: 2
+ process {
+ pid: 1
+ }
+ chrome_process {
+ process_type: PROCESS_BROWSER
+ }
}
}
diff --git a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_gpu_main.textproto b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_gpu_main.textproto
index 4465a30..933f549 100644
--- a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_gpu_main.textproto
+++ b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_gpu_main.textproto
@@ -11,19 +11,19 @@
pid: 1
tid: 1
}
- parent_uuid: 0
+ disallow_merging_with_system_tracks: true
}
}
packet {
timestamp: 2
track_descriptor {
- uuid: 2
- process {
- pid: 1
- }
- chrome_process {
- process_type: PROCESS_GPU
- }
+ uuid: 2
+ process {
+ pid: 1
+ }
+ chrome_process {
+ process_type: PROCESS_GPU
+ }
}
}
diff --git a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_processes.textproto b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_processes.textproto
index ffc093c..a42f47f 100644
--- a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_processes.textproto
+++ b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_processes.textproto
@@ -24,9 +24,9 @@
tid: 1
}
process {
- pid: 1
+ pid: 1
}
- parent_uuid: 0
+ disallow_merging_with_system_tracks: true
}
}
diff --git a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_renderer_main.textproto b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_renderer_main.textproto
index 42cb741..6cee9c3 100644
--- a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_renderer_main.textproto
+++ b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_renderer_main.textproto
@@ -11,19 +11,19 @@
pid: 1
tid: 1
}
- parent_uuid: 0
+ disallow_merging_with_system_tracks: true
}
}
packet {
timestamp: 2
track_descriptor {
- uuid: 2
- process {
- pid: 1
- }
- chrome_process {
- process_type: PROCESS_RENDERER
- }
+ uuid: 2
+ process {
+ pid: 1
+ }
+ chrome_process {
+ process_type: PROCESS_RENDERER
+ }
}
}
diff --git a/test/trace_processor/diff_tests/parser/etm/iterate_instructions.out b/test/trace_processor/diff_tests/parser/etm/iterate_instructions.out
new file mode 100644
index 0000000..edc1d93
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/etm/iterate_instructions.out
@@ -0,0 +1,18 @@
+"element_index","instruction_index","address","opcode","type","branch_address","is_conditional","is_link","sub_type"
+7,0,434500225096,335544321,"BR",434500225100,0,0,"NONE"
+8,0,434500225100,1384120360,"OTHER","[NULL]",0,0,"NONE"
+8,1,434500225104,923271016,"BR",434500225084,1,0,"NONE"
+9,0,434500225084,2483027979,"BR",434500225128,0,1,"BR_LINK"
+10,0,434500225128,3506471935,"OTHER","[NULL]",0,0,"NONE"
+10,1,434500225132,2835446781,"OTHER","[NULL]",0,0,"NONE"
+10,2,434500225136,2432713725,"OTHER","[NULL]",0,0,"NONE"
+10,3,434500225140,3577471048,"OTHER","[NULL]",0,0,"NONE"
+10,4,434500225144,4177528808,"OTHER","[NULL]",0,0,"NONE"
+10,5,434500225148,4181723105,"OTHER","[NULL]",0,0,"NONE"
+10,6,434500225152,3573751839,"OTHER","[NULL]",0,0,"NONE"
+10,7,434500225156,285076384,"OTHER","[NULL]",0,0,"NONE"
+10,8,434500225160,2483028070,"BR",434500225568,0,1,"BR_LINK"
+11,0,434500225568,2415919152,"OTHER","[NULL]",0,0,"NONE"
+11,1,434500225572,4182138385,"OTHER","[NULL]",0,0,"NONE"
+11,2,434500225576,2436030992,"OTHER","[NULL]",0,0,"NONE"
+11,3,434500225580,3592356384,"BR_INDIRECT",434500225568,0,0,"NONE"
diff --git a/test/trace_processor/diff_tests/parser/etm/tests.py b/test/trace_processor/diff_tests/parser/etm/tests.py
index 8aaac9a..e1b4c77 100644
--- a/test/trace_processor/diff_tests/parser/etm/tests.py
+++ b/test/trace_processor/diff_tests/parser/etm/tests.py
@@ -78,3 +78,17 @@
WHERE trace_id = 0
''',
out=Path('decode_trace.out'))
+
+ def test_iterate_instructions(self):
+ return DiffTestBlueprint(
+ register_files_dir=DataPath('simpleperf/bin'),
+ trace=DataPath('simpleperf/cs_etm_u.perf'),
+ query='''
+ SELECT d.element_index, i.*
+ FROM
+ __intrinsic_etm_decode_trace d,
+ __intrinsic_etm_iterate_instruction_range i
+ USING(instruction_range)
+ WHERE trace_id = 0 AND mapping_id = 1
+ ''',
+ out=Path('iterate_instructions.out'))
diff --git a/test/trace_processor/diff_tests/parser/graphics/actual_frame_timeline_events_test.sql b/test/trace_processor/diff_tests/parser/graphics/actual_frame_timeline_events_test.sql
deleted file mode 100644
index 269523c..0000000
--- a/test/trace_processor/diff_tests/parser/graphics/actual_frame_timeline_events_test.sql
+++ /dev/null
@@ -1,24 +0,0 @@
---
--- Copyright 2020 The Android Open Source Project
---
--- Licensed under the Apache License, Version 2.0 (the "License");
--- you may not use this file except in compliance with the License.
--- You may obtain a copy of the License at
---
--- https://www.apache.org/licenses/LICENSE-2.0
---
--- Unless required by applicable law or agreed to in writing, software
--- distributed under the License is distributed on an "AS IS" BASIS,
--- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
--- See the License for the specific language governing permissions and
--- limitations under the License.
-
-SELECT ts, dur, process.pid, display_frame_token, surface_frame_token, layer_name,
- present_type, on_time_finish, gpu_composition, jank_type, prediction_type, jank_tag, jank_severity_type
-FROM
- (SELECT t.*, process_track.name AS track_name FROM
- process_track LEFT JOIN actual_frame_timeline_slice t
- ON process_track.id = t.track_id) s
-JOIN process USING(upid)
-WHERE s.track_name = 'Actual Timeline'
-ORDER BY ts;
diff --git a/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages.out b/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages.out
deleted file mode 100644
index da72f32..0000000
--- a/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages.out
+++ /dev/null
@@ -1,22 +0,0 @@
-"track_name","track_desc","ts","dur","slice_name","depth","flat_key","string_value","context_id","render_target","render_target_name","render_pass","render_pass_name","command_buffer","command_buffer_name","submission_id","hw_queue_id","render_subpasses"
-"queue 1","queue desc 1",0,5,"render stage(1)",0,"[NULL]","[NULL]",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
-"queue 0","queue desc 0",0,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"queue 1","queue desc 1",10,5,"stage 1",0,"description","stage desc 1",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
-"queue 2","[NULL]",20,5,"stage 2",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,2,"[NULL]"
-"queue 0","queue desc 0",30,5,"stage 3",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"Unknown GPU Queue 3","[NULL]",40,5,"render stage(4)",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,3,"[NULL]"
-"queue 0","queue desc 0",50,5,"stage 0",0,"key1","value1",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"queue 0","queue desc 0",60,5,"stage 0",0,"key1","value1",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"queue 0","queue desc 0",60,5,"stage 0",0,"key2","value2",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"queue 0","queue desc 0",70,5,"stage 0",0,"key1","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"queue 0","queue desc 0",80,5,"stage 2",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"queue 0","queue desc 0",90,5,"stage 0",0,"[NULL]","[NULL]",42,16,"[NULL]",32,"[NULL]",48,"[NULL]",0,0,"[NULL]"
-"queue 0","queue desc 0",100,5,"stage 0",0,"[NULL]","[NULL]",42,16,"[NULL]",16,"[NULL]",16,"command_buffer",0,0,"[NULL]"
-"queue 0","queue desc 0",110,5,"stage 0",0,"[NULL]","[NULL]",42,16,"[NULL]",16,"render_pass",16,"command_buffer",0,0,"[NULL]"
-"queue 0","queue desc 0",120,5,"stage 0",0,"[NULL]","[NULL]",42,16,"framebuffer",16,"render_pass",16,"command_buffer",0,0,"[NULL]"
-"queue 0","queue desc 0",130,5,"stage 0",0,"[NULL]","[NULL]",42,16,"renamed_buffer",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"Unknown GPU Queue ","[NULL]",140,5,"render stage(18446744073709551615)",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1024,"[NULL]"
-"queue 0","queue desc 0",150,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"0"
-"queue 0","queue desc 0",160,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"63,64"
-"queue 0","queue desc 0",170,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"64"
-"queue 0","queue desc 0",180,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"3,68,69,70,71"
diff --git a/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages_interned_spec.out b/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages_interned_spec.out
deleted file mode 100644
index 05afa4d..0000000
--- a/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages_interned_spec.out
+++ /dev/null
@@ -1,4 +0,0 @@
-"track_name","track_desc","ts","dur","slice_name","depth","flat_key","string_value","context_id","render_target","render_target_name","render_pass","render_pass_name","command_buffer","command_buffer_name","submission_id","hw_queue_id","render_subpasses"
-"vertex","vertex queue",100,10,"binning",0,"description","binning graphics",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
-"fragment","fragment queue",200,10,"render",0,"description","render graphics",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,2,"[NULL]"
-"queue2","queue2 description",300,10,"render",0,"description","render graphics",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
diff --git a/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages_test.sql b/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages_test.sql
deleted file mode 100644
index bac5239..0000000
--- a/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages_test.sql
+++ /dev/null
@@ -1,24 +0,0 @@
---
--- Copyright 2019 The Android Open Source Project
---
--- Licensed under the Apache License, Version 2.0 (the "License");
--- you may not use this file except in compliance with the License.
--- You may obtain a copy of the License at
---
--- https://www.apache.org/licenses/LICENSE-2.0
---
--- Unless required by applicable law or agreed to in writing, software
--- distributed under the License is distributed on an "AS IS" BASIS,
--- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
--- See the License for the specific language governing permissions and
--- limitations under the License.
---
-SELECT track.name AS track_name, gpu_track.description AS track_desc, ts, dur,
- gpu_slice.name AS slice_name, depth, flat_key, string_value,
- gpu_slice.context_id, render_target, render_target_name, render_pass, render_pass_name,
- command_buffer, command_buffer_name, submission_id, hw_queue_id, render_subpasses
-FROM gpu_track
-LEFT JOIN track USING (id)
-JOIN gpu_slice ON gpu_track.id = gpu_slice.track_id
-LEFT JOIN args ON gpu_slice.arg_set_id = args.arg_set_id
-ORDER BY ts;
diff --git a/test/trace_processor/diff_tests/parser/graphics/graphics_frame_events.out b/test/trace_processor/diff_tests/parser/graphics/graphics_frame_events.out
deleted file mode 100644
index be781b8..0000000
--- a/test/trace_processor/diff_tests/parser/graphics/graphics_frame_events.out
+++ /dev/null
@@ -1,50 +0,0 @@
-"ts","track_name","dur","slice_name","frame_number","layer_name"
-1,"Buffer: 1 layer1",0,"Dequeue",11,"layer1"
-1,"APP_1 layer1",3,"11",11,"layer1"
-4,"Buffer: 1 layer1",0,"Queue",11,"layer1"
-4,"GPU_1 layer1",2,"11",11,"layer1"
-6,"Buffer: 1 layer1",0,"AcquireFenceSignaled",11,"layer1"
-6,"Buffer: 2 layer2",0,"Dequeue",12,"layer2"
-6,"APP_2 layer2",3,"12",12,"layer2"
-7,"Buffer: 7 layer7",0,"unknown_event",15,"layer7"
-8,"Buffer: 1 layer1",0,"Latch",11,"layer1"
-8,"Buffer: 2 layer2",0,"AcquireFenceSignaled",12,"layer2"
-8,"SF_1 layer1",6,"11",11,"layer1"
-9,"Buffer: 2 layer2",0,"Queue",12,"layer2"
-11,"Buffer: 2 layer2",0,"Latch",12,"layer2"
-11,"SF_2 layer2",5,"12",12,"layer2"
-14,"Buffer: 1 layer1",0,"PresentFenceSignaled",11,"layer1"
-14,"Display_layer1",10,"11",11,"layer1"
-16,"Buffer: 2 layer2",0,"PresentFenceSignaled",12,"layer2"
-16,"Display_layer2",-1,"12",12,"layer2"
-24,"Buffer: 1 layer1",0,"PresentFenceSignaled",13,"layer1"
-24,"Display_layer1",-1,"13",13,"layer1"
-31,"Buffer: 1 layer1",0,"Dequeue",21,"layer1"
-31,"APP_1 layer1",3,"21",21,"layer1"
-34,"Buffer: 1 layer1",0,"Queue",21,"layer1"
-34,"GPU_1 layer1",-1,"21",21,"layer1"
-37,"Buffer: 1 layer1",0,"Dequeue",22,"layer1"
-37,"APP_1 layer1",4,"22",22,"layer1"
-41,"Buffer: 1 layer1",0,"Queue",22,"layer1"
-41,"GPU_1 layer1",5,"22",22,"layer1"
-46,"Buffer: 1 layer1",0,"AcquireFenceSignaled",22,"layer1"
-53,"Buffer: 2 layer2",0,"Dequeue",24,"layer2"
-53,"APP_2 layer2",-1,"0",0,"layer2"
-59,"Buffer: 2 layer2",0,"AcquireFenceSignaled",24,"layer2"
-61,"Buffer: 2 layer2",0,"Latch",24,"layer2"
-61,"SF_2 layer2",-1,"24",24,"layer2"
-63,"Buffer: 1 layer1",0,"Dequeue",25,"layer1"
-63,"APP_1 layer1",-1,"0",0,"layer1"
-73,"Buffer: 1 layer1",0,"Dequeue",26,"layer1"
-73,"APP_1 layer1",2,"26",26,"layer1"
-75,"Buffer: 1 layer1",0,"Queue",26,"layer1"
-75,"GPU_1 layer1",4,"26",26,"layer1"
-79,"Buffer: 1 layer1",0,"AcquireFenceSignaled",26,"layer1"
-81,"Buffer: 1 layer1",0,"Dequeue",30,"layer1"
-81,"APP_1 layer1",2,"30",30,"layer1"
-83,"Buffer: 1 layer1",0,"Queue",30,"layer1"
-83,"GPU_1 layer1",-1,"30",30,"layer1"
-90,"Buffer: 1 layer2",0,"Dequeue",35,"layer2"
-90,"APP_1 layer2",2,"35",35,"layer2"
-92,"Buffer: 1 layer2",0,"Queue",35,"layer2"
-92,"GPU_1 layer2",-1,"35",35,"layer2"
diff --git a/test/trace_processor/diff_tests/parser/graphics/tests.py b/test/trace_processor/diff_tests/parser/graphics/tests.py
index f92264a..7da3eb0 100644
--- a/test/trace_processor/diff_tests/parser/graphics/tests.py
+++ b/test/trace_processor/diff_tests/parser/graphics/tests.py
@@ -33,7 +33,58 @@
WHERE scope = 'graphics_frame_event'
ORDER BY ts;
""",
- out=Path('graphics_frame_events.out'))
+ out=Csv('''
+ "ts","track_name","dur","slice_name","frame_number","layer_name"
+ 1,"Buffer: 1 layer1",0,"Dequeue",11,"layer1"
+ 1,"APP_1 layer1",3,"11",11,"layer1"
+ 4,"Buffer: 1 layer1",0,"Queue",11,"layer1"
+ 4,"GPU_1 layer1",2,"11",11,"layer1"
+ 6,"Buffer: 1 layer1",0,"AcquireFenceSignaled",11,"layer1"
+ 6,"Buffer: 2 layer2",0,"Dequeue",12,"layer2"
+ 6,"APP_2 layer2",3,"12",12,"layer2"
+ 7,"Buffer: 7 layer7",0,"unknown_event",15,"layer7"
+ 8,"Buffer: 1 layer1",0,"Latch",11,"layer1"
+ 8,"Buffer: 2 layer2",0,"AcquireFenceSignaled",12,"layer2"
+ 8,"SF_1 layer1",6,"11",11,"layer1"
+ 9,"Buffer: 2 layer2",0,"Queue",12,"layer2"
+ 11,"Buffer: 2 layer2",0,"Latch",12,"layer2"
+ 11,"SF_2 layer2",5,"12",12,"layer2"
+ 14,"Buffer: 1 layer1",0,"PresentFenceSignaled",11,"layer1"
+ 14,"Display_layer1",10,"11",11,"layer1"
+ 16,"Buffer: 2 layer2",0,"PresentFenceSignaled",12,"layer2"
+ 16,"Display_layer2",-1,"12",12,"layer2"
+ 24,"Buffer: 1 layer1",0,"PresentFenceSignaled",13,"layer1"
+ 24,"Display_layer1",-1,"13",13,"layer1"
+ 31,"Buffer: 1 layer1",0,"Dequeue",21,"layer1"
+ 31,"APP_1 layer1",3,"21",21,"layer1"
+ 34,"Buffer: 1 layer1",0,"Queue",21,"layer1"
+ 34,"GPU_1 layer1",-1,"21",21,"layer1"
+ 37,"Buffer: 1 layer1",0,"Dequeue",22,"layer1"
+ 37,"APP_1 layer1",4,"22",22,"layer1"
+ 41,"Buffer: 1 layer1",0,"Queue",22,"layer1"
+ 41,"GPU_1 layer1",5,"22",22,"layer1"
+ 46,"Buffer: 1 layer1",0,"AcquireFenceSignaled",22,"layer1"
+ 53,"Buffer: 2 layer2",0,"Dequeue",24,"layer2"
+ 53,"APP_2 layer2",-1,"0",0,"layer2"
+ 59,"Buffer: 2 layer2",0,"AcquireFenceSignaled",24,"layer2"
+ 61,"Buffer: 2 layer2",0,"Latch",24,"layer2"
+ 61,"SF_2 layer2",-1,"24",24,"layer2"
+ 63,"Buffer: 1 layer1",0,"Dequeue",25,"layer1"
+ 63,"APP_1 layer1",-1,"0",0,"layer1"
+ 73,"Buffer: 1 layer1",0,"Dequeue",26,"layer1"
+ 73,"APP_1 layer1",2,"26",26,"layer1"
+ 75,"Buffer: 1 layer1",0,"Queue",26,"layer1"
+ 75,"GPU_1 layer1",4,"26",26,"layer1"
+ 79,"Buffer: 1 layer1",0,"AcquireFenceSignaled",26,"layer1"
+ 81,"Buffer: 1 layer1",0,"Dequeue",30,"layer1"
+ 81,"APP_1 layer1",2,"30",30,"layer1"
+ 83,"Buffer: 1 layer1",0,"Queue",30,"layer1"
+ 83,"GPU_1 layer1",-1,"30",30,"layer1"
+ 90,"Buffer: 1 layer2",0,"Dequeue",35,"layer2"
+ 90,"APP_1 layer2",2,"35",35,"layer2"
+ 92,"Buffer: 1 layer2",0,"Queue",35,"layer2"
+ 92,"GPU_1 layer2",-1,"35",35,"layer2"
+ '''))
# GPU Memory ftrace packets
def test_gpu_mem_total(self):
@@ -104,42 +155,52 @@
query=Path('expected_frame_timeline_events_test.sql'),
out=Csv("""
"ts","dur","pid","display_frame_token","surface_frame_token","layer_name"
- 20,6,666,2,0,"[NULL]"
+ 20,6,666,2,"[NULL]","[NULL]"
21,15,1000,4,1,"Layer1"
- 40,6,666,4,0,"[NULL]"
+ 40,6,666,4,"[NULL]","[NULL]"
41,15,1000,6,5,"Layer1"
- 80,6,666,6,0,"[NULL]"
+ 80,6,666,6,"[NULL]","[NULL]"
90,16,1000,8,7,"Layer1"
- 120,6,666,8,0,"[NULL]"
- 140,6,666,12,0,"[NULL]"
+ 120,6,666,8,"[NULL]","[NULL]"
+ 140,6,666,12,"[NULL]","[NULL]"
150,20,1000,15,14,"Layer1"
- 170,6,666,15,0,"[NULL]"
- 200,6,666,17,0,"[NULL]"
- 220,-1,666,18,0,"[NULL]"
- 220,10,666,18,0,"[NULL]"
+ 170,6,666,15,"[NULL]","[NULL]"
+ 200,6,666,17,"[NULL]","[NULL]"
+ 220,-1,666,18,"[NULL]","[NULL]"
+ 220,10,666,18,"[NULL]","[NULL]"
"""))
def test_actual_frame_timeline_events(self):
return DiffTestBlueprint(
trace=Path('frame_timeline_events.py'),
- query=Path('actual_frame_timeline_events_test.sql'),
+ query='''
+ SELECT ts, dur, process.pid, display_frame_token, surface_frame_token, layer_name,
+ present_type, on_time_finish, gpu_composition, jank_type, prediction_type, jank_tag, jank_severity_type
+ FROM
+ (SELECT t.*, process_track.name AS track_name FROM
+ process_track LEFT JOIN actual_frame_timeline_slice t
+ ON process_track.id = t.track_id) s
+ JOIN process USING(upid)
+ WHERE s.track_name = 'Actual Timeline'
+ ORDER BY ts;
+ ''',
out=Csv("""
- "ts","dur","pid","display_frame_token","surface_frame_token","layer_name","present_type","on_time_finish","gpu_composition","jank_type","prediction_type","jank_tag","jank_severity_type"
- 20,6,666,2,0,"[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
- 21,16,1000,4,1,"Layer1","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
- 41,33,1000,6,5,"Layer1","Late Present",0,0,"App Deadline Missed","Valid Prediction","Self Jank","Full"
- 42,5,666,4,0,"[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
- 80,110,1000,17,16,"Layer1","Unknown Present",0,0,"Unknown Jank","Expired Prediction","Self Jank","Partial"
- 81,7,666,6,0,"[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
- 90,16,1000,8,7,"Layer1","Early Present",1,0,"SurfaceFlinger Scheduling","Valid Prediction","Other Jank","Unknown"
- 108,4,666,8,0,"[NULL]","Early Present",1,0,"SurfaceFlinger Scheduling","Valid Prediction","Self Jank","Unknown"
- 148,8,666,12,0,"[NULL]","Late Present",0,0,"SurfaceFlinger Scheduling, SurfaceFlinger CPU Deadline Missed","Valid Prediction","Self Jank","Unknown"
- 150,17,1000,15,14,"Layer1","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
- 150,17,1000,15,14,"Layer2","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
- 170,6,666,15,0,"[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
- 200,6,666,17,0,"[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
- 245,-1,666,18,0,"[NULL]","Late Present",0,0,"SurfaceFlinger Stuffing","Valid Prediction","SurfaceFlinger Stuffing","Unknown"
- 245,15,666,18,0,"[NULL]","Dropped Frame",0,0,"Dropped Frame","Unspecified Prediction","Dropped Frame","Unknown"
+ "ts","dur","pid","display_frame_token","surface_frame_token","layer_name","present_type","on_time_finish","gpu_composition","jank_type","prediction_type","jank_tag","jank_severity_type"
+ 20,6,666,2,"[NULL]","[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+ 21,16,1000,4,1,"Layer1","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+ 41,33,1000,6,5,"Layer1","Late Present",0,0,"App Deadline Missed","Valid Prediction","Self Jank","Full"
+ 42,5,666,4,"[NULL]","[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+ 80,110,1000,17,16,"Layer1","Unknown Present",0,0,"Unknown Jank","Expired Prediction","Self Jank","Partial"
+ 81,7,666,6,"[NULL]","[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+ 90,16,1000,8,7,"Layer1","Early Present",1,0,"SurfaceFlinger Scheduling","Valid Prediction","Other Jank","Unknown"
+ 108,4,666,8,"[NULL]","[NULL]","Early Present",1,0,"SurfaceFlinger Scheduling","Valid Prediction","Self Jank","Unknown"
+ 148,8,666,12,"[NULL]","[NULL]","Late Present",0,0,"SurfaceFlinger Scheduling, SurfaceFlinger CPU Deadline Missed","Valid Prediction","Self Jank","Unknown"
+ 150,17,1000,15,14,"Layer1","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+ 150,17,1000,15,14,"Layer2","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+ 170,6,666,15,"[NULL]","[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+ 200,6,666,17,"[NULL]","[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+ 245,-1,666,18,"[NULL]","[NULL]","Late Present",0,0,"SurfaceFlinger Stuffing","Valid Prediction","SurfaceFlinger Stuffing","Unknown"
+ 245,15,666,18,"[NULL]","[NULL]","Dropped Frame",0,0,"Dropped Frame","Unspecified Prediction","Dropped Frame","Unknown"
"""))
# Video 4 Linux 2 related tests
@@ -207,110 +268,6 @@
1345090746311,1167135,"CTX_DETACH_RESOURCE"
"""))
- # TODO(b/294866695): Reenable
- # mali GPU events
- #def test_mali(self):
- # return DiffTestBlueprint(
- # trace=TextProto(r"""
- # packet {
- # ftrace_events {
- # cpu: 2
- # event {
- # timestamp: 751796307210
- # pid: 2857
- # mali_mali_KCPU_CQS_WAIT_START {
- # info_val1: 1
- # info_val2: 0
- # kctx_tgid: 2201
- # kctx_id: 10
- # id: 0
- # }
- # }
- # event {
- # timestamp: 751800621175
- # pid: 2857
- # mali_mali_KCPU_CQS_WAIT_END {
- # info_val1: 412313493488
- # info_val2: 0
- # kctx_tgid: 2201
- # kctx_id: 10
- # id: 0
- # }
- # }
- # event {
- # timestamp: 751800638997
- # pid: 2857
- # mali_mali_KCPU_CQS_SET {
- # info_val1: 412313493480
- # info_val2: 0
- # kctx_tgid: 2201
- # kctx_id: 10
- # id: 0
- # }
- # }
- # }
- # }
- # """),
- # query="""
- # SELECT ts, dur, name FROM slice WHERE name GLOB "mali_KCPU_CQS*";
- # """,
- # out=Csv("""
- # "ts","dur","name"
- # 751796307210,4313965,"mali_KCPU_CQS_WAIT"
- # 751800638997,0,"mali_KCPU_CQS_SET"
- # """))
-
- #def test_mali_fence(self):
- # return DiffTestBlueprint(
- # trace=TextProto(r"""
- # packet {
- # ftrace_events {
- # cpu: 2
- # event {
- # timestamp: 751796307210
- # pid: 2857
- # mali_mali_KCPU_FENCE_WAIT_START {
- # info_val1: 1
- # info_val2: 0
- # kctx_tgid: 2201
- # kctx_id: 10
- # id: 0
- # }
- # }
- # event {
- # timestamp: 751800621175
- # pid: 2857
- # mali_mali_KCPU_FENCE_WAIT_END {
- # info_val1: 412313493488
- # info_val2: 0
- # kctx_tgid: 2201
- # kctx_id: 10
- # id: 0
- # }
- # }
- # event {
- # timestamp: 751800638997
- # pid: 2857
- # mali_mali_KCPU_FENCE_SIGNAL {
- # info_val1: 412313493480
- # info_val2: 0
- # kctx_tgid: 2201
- # kctx_id: 10
- # id: 0
- # }
- # }
- # }
- # }
- # """),
- # query="""
- # SELECT ts, dur, name FROM slice WHERE name GLOB "mali_KCPU_FENCE*";
- # """,
- # out=Csv("""
- # "ts","dur","name"
- # 751796307210,4313965,"mali_KCPU_FENCE_WAIT"
- # 751800638997,0,"mali_KCPU_FENCE_SIGNAL"
- # """))
-
# Tests gpu_track with machine_id ID.
def test_graphics_frame_events_machine_id(self):
return DiffTestBlueprint(
@@ -326,4 +283,55 @@
AND gpu_track.machine_id IS NOT NULL
ORDER BY ts;
""",
- out=Path('graphics_frame_events.out'))
+ out=Csv('''
+ "ts","track_name","dur","slice_name","frame_number","layer_name"
+ 1,"Buffer: 1 layer1",0,"Dequeue",11,"layer1"
+ 1,"APP_1 layer1",3,"11",11,"layer1"
+ 4,"Buffer: 1 layer1",0,"Queue",11,"layer1"
+ 4,"GPU_1 layer1",2,"11",11,"layer1"
+ 6,"Buffer: 1 layer1",0,"AcquireFenceSignaled",11,"layer1"
+ 6,"Buffer: 2 layer2",0,"Dequeue",12,"layer2"
+ 6,"APP_2 layer2",3,"12",12,"layer2"
+ 7,"Buffer: 7 layer7",0,"unknown_event",15,"layer7"
+ 8,"Buffer: 1 layer1",0,"Latch",11,"layer1"
+ 8,"Buffer: 2 layer2",0,"AcquireFenceSignaled",12,"layer2"
+ 8,"SF_1 layer1",6,"11",11,"layer1"
+ 9,"Buffer: 2 layer2",0,"Queue",12,"layer2"
+ 11,"Buffer: 2 layer2",0,"Latch",12,"layer2"
+ 11,"SF_2 layer2",5,"12",12,"layer2"
+ 14,"Buffer: 1 layer1",0,"PresentFenceSignaled",11,"layer1"
+ 14,"Display_layer1",10,"11",11,"layer1"
+ 16,"Buffer: 2 layer2",0,"PresentFenceSignaled",12,"layer2"
+ 16,"Display_layer2",-1,"12",12,"layer2"
+ 24,"Buffer: 1 layer1",0,"PresentFenceSignaled",13,"layer1"
+ 24,"Display_layer1",-1,"13",13,"layer1"
+ 31,"Buffer: 1 layer1",0,"Dequeue",21,"layer1"
+ 31,"APP_1 layer1",3,"21",21,"layer1"
+ 34,"Buffer: 1 layer1",0,"Queue",21,"layer1"
+ 34,"GPU_1 layer1",-1,"21",21,"layer1"
+ 37,"Buffer: 1 layer1",0,"Dequeue",22,"layer1"
+ 37,"APP_1 layer1",4,"22",22,"layer1"
+ 41,"Buffer: 1 layer1",0,"Queue",22,"layer1"
+ 41,"GPU_1 layer1",5,"22",22,"layer1"
+ 46,"Buffer: 1 layer1",0,"AcquireFenceSignaled",22,"layer1"
+ 53,"Buffer: 2 layer2",0,"Dequeue",24,"layer2"
+ 53,"APP_2 layer2",-1,"0",0,"layer2"
+ 59,"Buffer: 2 layer2",0,"AcquireFenceSignaled",24,"layer2"
+ 61,"Buffer: 2 layer2",0,"Latch",24,"layer2"
+ 61,"SF_2 layer2",-1,"24",24,"layer2"
+ 63,"Buffer: 1 layer1",0,"Dequeue",25,"layer1"
+ 63,"APP_1 layer1",-1,"0",0,"layer1"
+ 73,"Buffer: 1 layer1",0,"Dequeue",26,"layer1"
+ 73,"APP_1 layer1",2,"26",26,"layer1"
+ 75,"Buffer: 1 layer1",0,"Queue",26,"layer1"
+ 75,"GPU_1 layer1",4,"26",26,"layer1"
+ 79,"Buffer: 1 layer1",0,"AcquireFenceSignaled",26,"layer1"
+ 81,"Buffer: 1 layer1",0,"Dequeue",30,"layer1"
+ 81,"APP_1 layer1",2,"30",30,"layer1"
+ 83,"Buffer: 1 layer1",0,"Queue",30,"layer1"
+ 83,"GPU_1 layer1",-1,"30",30,"layer1"
+ 90,"Buffer: 1 layer2",0,"Dequeue",35,"layer2"
+ 90,"APP_1 layer2",2,"35",35,"layer2"
+ 92,"Buffer: 1 layer2",0,"Queue",35,"layer2"
+ 92,"GPU_1 layer2",-1,"35",35,"layer2"
+ '''))
diff --git a/test/trace_processor/diff_tests/parser/graphics/tests_gpu_trace.py b/test/trace_processor/diff_tests/parser/graphics/tests_gpu_trace.py
index ead5000..b79e124 100644
--- a/test/trace_processor/diff_tests/parser/graphics/tests_gpu_trace.py
+++ b/test/trace_processor/diff_tests/parser/graphics/tests_gpu_trace.py
@@ -65,29 +65,148 @@
def test_gpu_render_stages(self):
return DiffTestBlueprint(
trace=Path('gpu_render_stages.py'),
- query=Path('gpu_render_stages_test.sql'),
- out=Path('gpu_render_stages.out'))
+ query='''
+ SELECT
+ g.name AS track_name,
+ g.description AS track_desc,
+ ts,
+ dur,
+ s.name AS slice_name,
+ depth,
+ args.flat_key,
+ args.string_value,
+ s.context_id,
+ render_target,
+ render_target_name,
+ render_pass,
+ render_pass_name,
+ command_buffer,
+ command_buffer_name,
+ submission_id,
+ hw_queue_id,
+ render_subpasses
+ FROM gpu_track g
+ JOIN gpu_slice s ON g.id = s.track_id
+ LEFT JOIN (
+ SELECT arg_set_id, flat_key, string_value
+ FROM args
+ WHERE args.key IS NULL OR args.key NOT IN (
+ 'context_id',
+ 'render_target',
+ 'render_target_name',
+ 'render_pass',
+ 'render_pass_name',
+ 'command_buffer',
+ 'command_buffer_name',
+ 'submission_id',
+ 'hw_queue_id',
+ 'render_subpasses',
+ 'upid'
+ )
+ ) args USING (arg_set_id)
+ ORDER BY ts;
+ ''',
+ out=Csv('''
+ "track_name","track_desc","ts","dur","slice_name","depth","flat_key","string_value","context_id","render_target","render_target_name","render_pass","render_pass_name","command_buffer","command_buffer_name","submission_id","hw_queue_id","render_subpasses"
+ "queue 1","queue desc 1",0,5,"render stage(1)",0,"[NULL]","[NULL]",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
+ "queue 0","queue desc 0",0,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+ "queue 1","queue desc 1",10,5,"stage 1",0,"description","stage desc 1",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
+ "queue 2","[NULL]",20,5,"stage 2",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,2,"[NULL]"
+ "queue 0","queue desc 0",30,5,"stage 3",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+ "Unknown GPU Queue 3","[NULL]",40,5,"render stage(4)",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,3,"[NULL]"
+ "queue 0","queue desc 0",50,5,"stage 0",0,"key1","value1",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+ "queue 0","queue desc 0",60,5,"stage 0",0,"key1","value1",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+ "queue 0","queue desc 0",60,5,"stage 0",0,"key2","value2",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+ "queue 0","queue desc 0",70,5,"stage 0",0,"key1","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+ "queue 0","queue desc 0",80,5,"stage 2",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+ "queue 0","queue desc 0",90,5,"stage 0",0,"[NULL]","[NULL]",42,16,"[NULL]",32,"[NULL]",48,"[NULL]",0,0,"[NULL]"
+ "queue 0","queue desc 0",100,5,"stage 0",0,"[NULL]","[NULL]",42,16,"[NULL]",16,"[NULL]",16,"command_buffer",0,0,"[NULL]"
+ "queue 0","queue desc 0",110,5,"stage 0",0,"[NULL]","[NULL]",42,16,"[NULL]",16,"render_pass",16,"command_buffer",0,0,"[NULL]"
+ "queue 0","queue desc 0",120,5,"stage 0",0,"[NULL]","[NULL]",42,16,"framebuffer",16,"render_pass",16,"command_buffer",0,0,"[NULL]"
+ "queue 0","queue desc 0",130,5,"stage 0",0,"[NULL]","[NULL]",42,16,"renamed_buffer",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+ "Unknown GPU Queue ","[NULL]",140,5,"render stage(18446744073709551615)",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1024,"[NULL]"
+ "queue 0","queue desc 0",150,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"0"
+ "queue 0","queue desc 0",160,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"63,64"
+ "queue 0","queue desc 0",170,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"64"
+ "queue 0","queue desc 0",180,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"3,68,69,70,71"
+ '''))
def test_gpu_render_stages_interned_spec(self):
return DiffTestBlueprint(
trace=Path('gpu_render_stages_interned_spec.textproto'),
- query=Path('gpu_render_stages_test.sql'),
- out=Path('gpu_render_stages_interned_spec.out'))
+ query='''
+ SELECT
+ g.name AS track_name,
+ g.description AS track_desc,
+ ts,
+ dur,
+ s.name AS slice_name,
+ depth,
+ args.flat_key,
+ args.string_value,
+ s.context_id,
+ render_target,
+ render_target_name,
+ render_pass,
+ render_pass_name,
+ command_buffer,
+ command_buffer_name,
+ submission_id,
+ hw_queue_id,
+ render_subpasses
+ FROM gpu_track g
+ JOIN gpu_slice s ON g.id = s.track_id
+ LEFT JOIN (
+ SELECT arg_set_id, flat_key, string_value
+ FROM args
+ WHERE args.key IS NULL OR args.key NOT IN (
+ 'context_id',
+ 'render_target',
+ 'render_target_name',
+ 'render_pass',
+ 'render_pass_name',
+ 'command_buffer',
+ 'command_buffer_name',
+ 'submission_id',
+ 'hw_queue_id',
+ 'render_subpasses',
+ 'upid'
+ )
+ ) args USING (arg_set_id)
+ ORDER BY ts;
+ ''',
+ out=Csv('''
+ "track_name","track_desc","ts","dur","slice_name","depth","flat_key","string_value","context_id","render_target","render_target_name","render_pass","render_pass_name","command_buffer","command_buffer_name","submission_id","hw_queue_id","render_subpasses"
+ "vertex","vertex queue",100,10,"binning",0,"description","binning graphics",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
+ "fragment","fragment queue",200,10,"render",0,"description","render graphics",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,2,"[NULL]"
+ "queue2","queue2 description",300,10,"render",0,"description","render graphics",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
+ '''))
def test_vulkan_api_events(self):
return DiffTestBlueprint(
trace=Path('vulkan_api_events.py'),
query="""
- SELECT track.name AS track_name, gpu_track.description AS track_desc, ts, dur,
- gpu_slice.name AS slice_name, depth, flat_key, int_value,
- gpu_slice.context_id, command_buffer, submission_id
- FROM gpu_track
- LEFT JOIN track USING (id)
- JOIN gpu_slice ON gpu_track.id = gpu_slice.track_id
- LEFT JOIN args ON gpu_slice.arg_set_id = args.arg_set_id
+ SELECT
+ g.name AS track_name,
+ g.description AS track_desc,
+ ts,
+ dur,
+ s.name AS slice_name,
+ depth,
+ s.context_id,
+ command_buffer,
+ submission_id,
+ extract_arg(s.arg_set_id, 'tid') as tid,
+ extract_arg(s.arg_set_id, 'pid') as pid
+ FROM gpu_track g
+ JOIN gpu_slice s ON g.id = s.track_id
ORDER BY ts;
""",
- out=Path('vulkan_api_events.out'))
+ out=Csv('''
+ "track_name","track_desc","ts","dur","slice_name","depth","context_id","command_buffer","submission_id","tid","pid"
+ "Vulkan Events","[NULL]",10,2,"vkQueueSubmit",0,"[NULL]",100,1,43,42
+ "Vulkan Events","[NULL]",20,2,"vkQueueSubmit",0,"[NULL]",200,2,45,44
+ '''))
def test_gpu_log(self):
return DiffTestBlueprint(
diff --git a/test/trace_processor/diff_tests/parser/graphics/vulkan_api_events.out b/test/trace_processor/diff_tests/parser/graphics/vulkan_api_events.out
deleted file mode 100644
index de409f9..0000000
--- a/test/trace_processor/diff_tests/parser/graphics/vulkan_api_events.out
+++ /dev/null
@@ -1,5 +0,0 @@
-"track_name","track_desc","ts","dur","slice_name","depth","flat_key","int_value","context_id","command_buffer","submission_id"
-"Vulkan Events","[NULL]",10,2,"vkQueueSubmit",0,"pid",42,"[NULL]",100,1
-"Vulkan Events","[NULL]",10,2,"vkQueueSubmit",0,"tid",43,"[NULL]",100,1
-"Vulkan Events","[NULL]",20,2,"vkQueueSubmit",0,"pid",44,"[NULL]",200,2
-"Vulkan Events","[NULL]",20,2,"vkQueueSubmit",0,"tid",45,"[NULL]",200,2
diff --git a/test/trace_processor/diff_tests/parser/parsing/tests.py b/test/trace_processor/diff_tests/parser/parsing/tests.py
index dba762d..25017ca 100644
--- a/test/trace_processor/diff_tests/parser/parsing/tests.py
+++ b/test/trace_processor/diff_tests/parser/parsing/tests.py
@@ -618,6 +618,102 @@
101000004,"test2","producer2",4
"""))
+ def test_triggers_packets_clone_snapshot_trigger_packet(self):
+ return DiffTestBlueprint(
+ trace=TextProto(r"""
+ packet {
+ clone_snapshot_trigger {
+ trigger_name: "test1"
+ trusted_producer_uid: 3
+ producer_name: "producer1"
+ }
+ timestamp: 101000002
+ }
+ """),
+ query=Path('triggers_packets_test.sql'),
+ out=Csv("""
+ "ts","name","string_value","int_value"
+ 101000002,"test1","producer1",3
+ """))
+
+ def test_trigger_metadata_test_clone_snapshot_trigger_packet(self):
+ return DiffTestBlueprint(
+ trace=TextProto(r"""
+ packet {
+ clone_snapshot_trigger {
+ trigger_name: "test1"
+ trusted_producer_uid: 3
+ producer_name: "producer1"
+ }
+ timestamp: 101000002
+ }
+ """),
+ query=Path('trigger_metadata_test.sql'),
+ out=Csv("""
+ "str_value"
+ "test1"
+ """))
+
+ def test_trigger_metadata_trigger_packet(self):
+ return DiffTestBlueprint(
+ trace=TextProto(r"""
+ packet {
+ trigger {
+ trigger_name: "test1"
+ trusted_producer_uid: 1
+ producer_name: "producer1"
+ }
+ timestamp: 101000002
+ }
+ packet {
+ trigger {
+ trigger_name: "test2"
+ trusted_producer_uid: 2
+ producer_name: "producer2"
+ }
+ timestamp: 101000001
+ }
+ """),
+ query=Path('trigger_metadata_test.sql'),
+ out=Csv("""
+ "str_value"
+ "test2"
+ """))
+
+ def test_trigger_metadata_trigger_packet_and_clone_snapshot_packet(self):
+ return DiffTestBlueprint(
+ trace=TextProto(r"""
+ packet {
+ trigger {
+ trigger_name: "test1"
+ trusted_producer_uid: 1
+ producer_name: "producer1"
+ }
+ timestamp: 101000004
+ }
+ packet {
+ trigger {
+ trigger_name: "test2"
+ trusted_producer_uid: 2
+ producer_name: "producer2"
+ }
+ timestamp: 101000002
+ }
+ packet {
+ clone_snapshot_trigger {
+ trigger_name: "testClone"
+ trusted_producer_uid: 3
+ producer_name: "producer3"
+ }
+ timestamp: 101000003
+ }
+ """),
+ query=Path('trigger_metadata_test.sql'),
+ out=Csv("""
+ "str_value"
+ "testClone"
+ """))
+
def test_chrome_metadata(self):
return DiffTestBlueprint(
trace=TextProto(r"""
diff --git a/test/trace_processor/diff_tests/parser/parsing/trigger_metadata_test.sql b/test/trace_processor/diff_tests/parser/parsing/trigger_metadata_test.sql
new file mode 100644
index 0000000..45f85e8
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/parsing/trigger_metadata_test.sql
@@ -0,0 +1,16 @@
+--
+-- Copyright 2019 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+SELECT str_value FROM metadata WHERE name = 'trace_trigger';
diff --git a/test/trace_processor/diff_tests/parser/track_event/tests.py b/test/trace_processor/diff_tests/parser/track_event/tests.py
index 419e171..fef7df6 100644
--- a/test/trace_processor/diff_tests/parser/track_event/tests.py
+++ b/test/trace_processor/diff_tests/parser/track_event/tests.py
@@ -300,13 +300,13 @@
"async2","process=p1",1
"async3","thread=t2",1
"event_and_track_async3","process=p1",1
- "process=p1","[NULL]","[NULL]"
+ "process=p1","[NULL]",1
"process=p2","[NULL]","[NULL]"
"process=p2","[NULL]","[NULL]"
- "thread=t1","process=p1",1
- "thread=t2","process=p1",1
- "thread=t3","process=p1",1
- "thread=t4","process=p2","[NULL]"
+ "thread=t1","[NULL]",1
+ "thread=t2","[NULL]",1
+ "thread=t3","[NULL]",1
+ "thread=t4","[NULL]","[NULL]"
"tid=1","[NULL]","[NULL]"
"""))
@@ -532,13 +532,10 @@
"event.category","event.category","[NULL]","cat"
"event.name","event.name","[NULL]","[NULL]"
"event.name","event.name","[NULL]","name1"
- "is_root_in_scope","is_root_in_scope",1,"[NULL]"
"legacy_event.passthrough_utid","legacy_event.passthrough_utid",1,"[NULL]"
"scope","scope","[NULL]","cat"
"source","source","[NULL]","chrome"
- "source","source","[NULL]","descriptor"
"source_scope","source_scope","[NULL]","cat"
- "trace_id","trace_id",1,"[NULL]"
"trace_id","trace_id",1234,"[NULL]"
"trace_id_is_process_scoped","trace_id_is_process_scoped",0,"[NULL]"
"upid","upid",1,"[NULL]"
@@ -856,10 +853,6 @@
6,0,"[NULL]","[NULL]"
7,"[NULL]","[NULL]","[NULL]"
8,7,"[NULL]","[NULL]"
- 9,"[NULL]","[NULL]","[NULL]"
- 10,"[NULL]","[NULL]","[NULL]"
- 11,"[NULL]","[NULL]","[NULL]"
- 12,0,"[NULL]","[NULL]"
"""))
def test_track_event_tracks_machine_id(self):
@@ -900,13 +893,13 @@
"async2","process=p1",1
"async3","thread=t2",1
"event_and_track_async3","process=p1",1
- "process=p1","[NULL]","[NULL]"
+ "process=p1","[NULL]",1
"process=p2","[NULL]","[NULL]"
"process=p2","[NULL]","[NULL]"
- "thread=t1","process=p1",1
- "thread=t2","process=p1",1
- "thread=t3","process=p1",1
- "thread=t4","process=p2","[NULL]"
+ "thread=t1","[NULL]",1
+ "thread=t2","[NULL]",1
+ "thread=t3","[NULL]",1
+ "thread=t4","[NULL]","[NULL]"
"tid=1","[NULL]","[NULL]"
"""))
diff --git a/test/trace_processor/diff_tests/parser/track_event/track_event_tracks.textproto b/test/trace_processor/diff_tests/parser/track_event/track_event_tracks.textproto
index 779998b..7de8dcc 100644
--- a/test/trace_processor/diff_tests/parser/track_event/track_event_tracks.textproto
+++ b/test/trace_processor/diff_tests/parser/track_event/track_event_tracks.textproto
@@ -6,12 +6,12 @@
first_packet_on_sequence: true
track_descriptor {
uuid: 1
- parent_uuid: 10
thread {
pid: 5
tid: 1
thread_name: "t1"
}
+ disallow_merging_with_system_tracks: true
}
trace_packet_defaults {
track_event_defaults {
@@ -27,12 +27,12 @@
first_packet_on_sequence: true
track_descriptor {
uuid: 2
- parent_uuid: 10
thread {
pid: 5
tid: 2
thread_name: "t2"
}
+ disallow_merging_with_system_tracks: true
}
trace_packet_defaults {
track_event_defaults {
@@ -179,12 +179,12 @@
first_packet_on_sequence: true
track_descriptor {
uuid: 3
- parent_uuid: 10
thread {
pid: 5
tid: 1
thread_name: "t3"
}
+ disallow_merging_with_system_tracks: true
}
}
# Should appear on t3.
@@ -231,7 +231,6 @@
incremental_state_cleared: true
track_descriptor {
uuid: 21
- parent_uuid: 20
thread {
pid: 5
tid: 4
diff --git a/test/trace_processor/diff_tests/parser/track_event/track_event_tracks_ordering.textproto b/test/trace_processor/diff_tests/parser/track_event/track_event_tracks_ordering.textproto
index 892266b..460367a 100644
--- a/test/trace_processor/diff_tests/parser/track_event/track_event_tracks_ordering.textproto
+++ b/test/trace_processor/diff_tests/parser/track_event/track_event_tracks_ordering.textproto
@@ -7,11 +7,6 @@
track_descriptor {
uuid: 1
parent_uuid: 10
- thread {
- pid: 5
- tid: 1
- thread_name: "t1"
- }
sibling_order_rank: -10
}
trace_packet_defaults {
@@ -29,11 +24,6 @@
track_descriptor {
uuid: 2
parent_uuid: 10
- thread {
- pid: 5
- tid: 2
- thread_name: "t2"
- }
sibling_order_rank: -2
}
trace_packet_defaults {
@@ -99,82 +89,6 @@
name: "async3"
}
}
-
-# Should appear on default track "t1".
-packet {
- trusted_packet_sequence_id: 1
- timestamp: 1000
- track_event {
- categories: "cat"
- name: "event1_on_t1"
- type: 3
- }
-}
-# Should appear on default track "t2".
-packet {
- trusted_packet_sequence_id: 2
- timestamp: 2000
- track_event {
- categories: "cat"
- name: "event1_on_t2"
- type: 3
- }
-}
-# Should appear on overridden track "t2".
-packet {
- trusted_packet_sequence_id: 1
- timestamp: 3000
- track_event {
- track_uuid: 2
- categories: "cat"
- name: "event2_on_t2"
- type: 3
- }
-}
-# Should appear on process track.
-packet {
- trusted_packet_sequence_id: 1
- timestamp: 4000
- track_event {
- track_uuid: 10
- categories: "cat"
- name: "event1_on_p1"
- type: 3
- }
-}
-# Should appear on async track.
-packet {
- trusted_packet_sequence_id: 1
- timestamp: 5000
- track_event {
- track_uuid: 11
- categories: "cat"
- name: "event1_on_async"
- type: 3
- }
-}
-# Event for the "async2" track starting on one thread and ending on another.
-packet {
- trusted_packet_sequence_id: 1
- timestamp: 5100
- track_event {
- track_uuid: 12
- categories: "cat"
- name: "event1_on_async2"
- type: 1
- }
-}
-packet {
- trusted_packet_sequence_id: 2
- timestamp: 5200
- track_event {
- track_uuid: 12
- categories: "cat"
- name: "event1_on_async2"
- type: 2
- }
-}
-
# If we later see another track descriptor for tid 1, but with a different uuid,
# we should detect tid reuse and start a new thread.
packet {
@@ -185,11 +99,6 @@
track_descriptor {
uuid: 3
parent_uuid: 10
- thread {
- pid: 5
- tid: 1
- thread_name: "t3"
- }
}
}
# Should appear on t3.
@@ -203,7 +112,6 @@
type: 3
}
}
-
# If we later see another track descriptor for pid 5, but with a different uuid,
# we should detect pid reuse and start a new process.
packet {
@@ -218,18 +126,6 @@
}
}
}
-# Should appear on p2.
-packet {
- trusted_packet_sequence_id: 4
- timestamp: 21000
- track_event {
- track_uuid: 20
- categories: "cat"
- name: "event1_on_p2"
- type: 3
- }
-}
-# Another thread t4 in the new process.
packet {
trusted_packet_sequence_id: 4
timestamp: 22000
@@ -237,102 +133,5 @@
track_descriptor {
uuid: 21
parent_uuid: 20
- thread {
- pid: 5
- tid: 4
- thread_name: "t4"
- }
- }
-}
-# Should appear on t4.
-packet {
- trusted_packet_sequence_id: 4
- timestamp: 22000
- track_event {
- track_uuid: 21
- categories: "cat"
- name: "event1_on_t4"
- type: 3
- }
-}
-
-# Another packet for a thread track in the old process, badly sorted.
-packet {
- trusted_packet_sequence_id: 2
- timestamp: 6000
- track_event {
- track_uuid: 1
- categories: "cat"
- name: "event3_on_t1"
- type: 3
- }
-}
-
-# Override the track to the default descriptor track for an event with a
-# TrackEvent type. Should appear on the default descriptor track instead of
-# "t1".
-packet {
- trusted_packet_sequence_id: 1
- timestamp: 30000
- track_event {
- track_uuid: 0
- categories: "cat"
- name: "event1_on_t1"
- type: 3
- }
-}
-
-# But a legacy event without TrackEvent type falls back to legacy tracks (based
-# on ThreadDescriptor / async IDs / legacy instant scopes). This instant event
-# should appear on the process track "p2".
-packet {
- trusted_packet_sequence_id: 1
- timestamp: 31000
- track_event {
- track_uuid: 0
- categories: "cat"
- name: "event2_on_p2"
- legacy_event {
- phase: 73 # 'I'
- instant_event_scope: 2 # Process scope
- }
- }
-}
-
-# And pid/tid overrides take effect even for TrackEvent type events.
-packet {
- trusted_packet_sequence_id: 1
- timestamp: 32000
- track_event {
- track_uuid: 0
- categories: "cat"
- name: "event2_on_t4"
- type: 3
- legacy_event {
- pid_override: 5
- tid_override: 4
- }
- }
-}
-
-# Track descriptor without name and process/thread association derives its
-# name from the first event on the track.
-packet {
- trusted_packet_sequence_id: 1
- timestamp: 40000
- track_descriptor {
- uuid: 13
- parent_uuid: 10
- }
-}
-
-packet {
- trusted_packet_sequence_id: 1
- timestamp: 40000
- track_event {
- track_uuid: 13
- categories: "cat"
- name: "event_and_track_async3"
- type: 3
}
}
diff --git a/test/trace_processor/diff_tests/parser/track_event/track_event_typed_args_args.out b/test/trace_processor/diff_tests/parser/track_event/track_event_typed_args_args.out
index 2e88593..75ed889 100644
--- a/test/trace_processor/diff_tests/parser/track_event/track_event_typed_args_args.out
+++ b/test/trace_processor/diff_tests/parser/track_event/track_event_typed_args_args.out
@@ -26,16 +26,13 @@
"event.name","event.name","[NULL]","name6"
"int_extension_for_testing","int_extension_for_testing[0]",42,"[NULL]"
"int_extension_for_testing","int_extension_for_testing[1]",1337,"[NULL]"
-"is_root_in_scope","is_root_in_scope",1,"[NULL]"
"nested_message_extension_for_testing.arg1","nested_message_extension_for_testing.arg1","[NULL]","value"
"nested_message_extension_for_testing.arg2.key","nested_message_extension_for_testing.arg2.key","[NULL]","value"
"nested_message_extension_for_testing.child_field_for_testing","nested_message_extension_for_testing.child_field_for_testing","[NULL]","nesting test"
-"source","source","[NULL]","descriptor"
"source.file_name","source.file_name","[NULL]","source.cc"
"source.function_name","source.function_name","[NULL]","SourceFunction"
"source.line_number","source.line_number",0,"[NULL]"
"source_location_iid","source_location_iid",1,"[NULL]"
"string_extension_for_testing","string_extension_for_testing","[NULL]","an extension string!"
"string_extension_for_testing2","string_extension_for_testing2","[NULL]","a second extension string!"
-"trace_id","trace_id",1,"[NULL]"
"utid","utid",1,"[NULL]"
diff --git a/test/trace_processor/diff_tests/parser/translated_args/chrome_histogram.out b/test/trace_processor/diff_tests/parser/translated_args/chrome_histogram.out
index efbb466..cacf4eb 100644
--- a/test/trace_processor/diff_tests/parser/translated_args/chrome_histogram.out
+++ b/test/trace_processor/diff_tests/parser/translated_args/chrome_histogram.out
@@ -11,7 +11,4 @@
"event.name","event.name","[NULL]","slice1"
"event.name","event.name","[NULL]","slice2"
"event.name","event.name","[NULL]","slice3"
-"is_root_in_scope","is_root_in_scope",1,"[NULL]"
-"source","source","[NULL]","descriptor"
-"trace_id","trace_id",12345,"[NULL]"
"utid","utid",1,"[NULL]"
diff --git a/test/trace_processor/diff_tests/parser/translated_args/chrome_performance_mark.out b/test/trace_processor/diff_tests/parser/translated_args/chrome_performance_mark.out
index fd17777ee..1d27656 100644
--- a/test/trace_processor/diff_tests/parser/translated_args/chrome_performance_mark.out
+++ b/test/trace_processor/diff_tests/parser/translated_args/chrome_performance_mark.out
@@ -5,7 +5,4 @@
"chrome_hashed_performance_mark.site_hash","chrome_hashed_performance_mark.site_hash",10,"[NULL]"
"event.category","event.category","[NULL]","cat1"
"event.name","event.name","[NULL]","slice1"
-"is_root_in_scope","is_root_in_scope",1,"[NULL]"
-"source","source","[NULL]","descriptor"
-"trace_id","trace_id",12345,"[NULL]"
"utid","utid",1,"[NULL]"
diff --git a/test/trace_processor/diff_tests/parser/translated_args/chrome_user_event.out b/test/trace_processor/diff_tests/parser/translated_args/chrome_user_event.out
index ace9de6..7ea9784 100644
--- a/test/trace_processor/diff_tests/parser/translated_args/chrome_user_event.out
+++ b/test/trace_processor/diff_tests/parser/translated_args/chrome_user_event.out
@@ -10,7 +10,4 @@
"event.name","event.name","[NULL]","slice1"
"event.name","event.name","[NULL]","slice2"
"event.name","event.name","[NULL]","slice3"
-"is_root_in_scope","is_root_in_scope",1,"[NULL]"
-"source","source","[NULL]","descriptor"
-"trace_id","trace_id",12345,"[NULL]"
"utid","utid",1,"[NULL]"
diff --git a/test/trace_processor/diff_tests/parser/translated_args/native_symbol_arg.out b/test/trace_processor/diff_tests/parser/translated_args/native_symbol_arg.out
index ea05157..9fac5d8 100644
--- a/test/trace_processor/diff_tests/parser/translated_args/native_symbol_arg.out
+++ b/test/trace_processor/diff_tests/parser/translated_args/native_symbol_arg.out
@@ -15,7 +15,4 @@
"event.name","event.name","[NULL]","slice2"
"event.name","event.name","[NULL]","slice3"
"event.name","event.name","[NULL]","slice4"
-"is_root_in_scope","is_root_in_scope",1,"[NULL]"
-"source","source","[NULL]","descriptor"
-"trace_id","trace_id",12345,"[NULL]"
"utid","utid",1,"[NULL]"
diff --git a/test/trace_processor/diff_tests/stdlib/chrome/tests_scroll_jank.py b/test/trace_processor/diff_tests/stdlib/chrome/tests_scroll_jank.py
index 64ae4d59..2411809 100755
--- a/test/trace_processor/diff_tests/stdlib/chrome/tests_scroll_jank.py
+++ b/test/trace_processor/diff_tests/stdlib/chrome/tests_scroll_jank.py
@@ -597,28 +597,28 @@
""",
out=Csv("""
"id","presented_in_frame_id","is_presented","is_janky","is_inertial","is_first_scroll_update_in_scroll","is_first_scroll_update_in_frame","generation_ts","generation_to_browser_main_dur","browser_utid","touch_move_received_slice_id","touch_move_received_ts","touch_move_processing_dur","scroll_update_created_slice_id","scroll_update_created_ts","scroll_update_processing_dur","scroll_update_created_end_ts","browser_to_compositor_delay_dur","compositor_utid","compositor_dispatch_slice_id","compositor_dispatch_ts","compositor_dispatch_dur","compositor_dispatch_end_ts","compositor_dispatch_to_coalesced_input_handled_dur","compositor_coalesced_input_handled_slice_id","compositor_coalesced_input_handled_ts","compositor_coalesced_input_handled_dur","compositor_coalesced_input_handled_end_ts"
- -2143831735395280256,-2143831735395280256,1,0,1,0,1,1292554141489270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10781,1292554142167257,363000,1292554142530257,472953,4,10796,1292554143003210,108000,1292554143111210,10912000,10827,1292554154023210,83000,1292554154106210
- -2143831735395280254,-2143831735395280254,1,0,1,0,1,1292554152575270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10830,1292554154230257,259000,1292554154489257,698953,4,10845,1292554155188210,120000,1292554155308210,9637000,10869,1292554164945210,223000,1292554165168210
- -2143831735395280250,-2143831735395280250,1,0,1,0,1,1292554130385270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10742,1292554131192257,279000,1292554131471257,393953,4,10757,1292554131865210,98000,1292554131963210,10636000,10790,1292554142599210,191000,1292554142790210
- -2143831735395280248,-2143831735395280248,1,0,1,0,1,1292554185877270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10939,1292554186628257,398000,1292554187026257,217953,4,10950,1292554187244210,107000,1292554187351210,10849000,10988,1292554198200210,82000,1292554198282210
- -2143831735395280246,-2143831735395280246,1,0,1,0,1,1292554196968270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10980,1292554198042257,362000,1292554198404257,890953,4,11000,1292554199295210,110000,1292554199405210,9963000,11025,1292554209368210,90000,1292554209458210
- -2143831735395280244,-2143831735395280244,1,0,1,0,1,1292554163682270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10860,1292554164468257,393000,1292554164861257,513953,4,10876,1292554165375210,127000,1292554165502210,10798000,10908,1292554176300210,226000,1292554176526210
- -2143831735395280242,-2143831735395280242,1,0,1,0,1,1292554174786270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10899,1292554175708257,321000,1292554176029257,697953,4,10915,1292554176727210,107000,1292554176834210,10177000,10947,1292554187011210,88000,1292554187099210
- -2143831735395280239,-2143831735395280239,1,0,1,0,1,1292554086893270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10555,1292554086897257,128000,1292554087025257,1290953,4,10586,1292554088316210,79000,1292554088395210,9853000,10620,1292554098248210,177000,1292554098425210
- -2143831735395280229,-2143831735395280229,1,0,1,0,1,1292554119302270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10699,1292554120042257,327000,1292554120369257,167953,4,10714,1292554120537210,94000,1292554120631210,10935000,10750,1292554131566210,158000,1292554131724210
- -2143831735395280227,-2143831735395280227,1,0,1,0,1,1292554097138270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10611,1292554097987257,189000,1292554098176257,366953,4,10626,1292554098543210,76000,1292554098619210,10479000,10662,1292554109098210,151000,1292554109249210
- -2143831735395280226,-2143831735395280226,1,0,1,0,1,1292554108216270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10657,1292554108988257,322000,1292554109310257,80953,4,10666,1292554109391210,100000,1292554109491210,10760000,10706,1292554120251210,138000,1292554120389210
- -2143831735395280208,-2143831735395280208,1,0,1,0,1,1292554230251270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",11096,1292554231054257,408000,1292554231462257,145953,4,11106,1292554231608210,103000,1292554231711210,11015000,11142,1292554242726210,128000,1292554242854210
- -2143831735395280206,-2143831735395280206,1,0,1,0,1,1292554241443270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",11134,1292554242336257,324000,1292554242660257,335953,4,11148,1292554242996210,120000,1292554243116210,11070000,11184,1292554254186210,138000,1292554254324210
- -2143831735395280204,-2143831735395280204,1,0,1,0,1,1292554208072270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",11017,1292554208931257,257000,1292554209188257,423953,4,11031,1292554209612210,110000,1292554209722210,10857000,11064,1292554220579210,107000,1292554220686210
- -2143831735395280202,-2143831735395280202,1,0,1,0,1,1292554219159270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",11057,1292554220303257,375000,1292554220678257,1225953,4,11078,1292554221904210,150000,1292554222054210,9337000,11103,1292554231391210,77000,1292554231468210
- -2143831735395280200,-2143831735395280200,0,0,1,0,1,1292554274773270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",11250,1292554275745257,304000,1292554276049257,837953,4,11266,1292554276887210,144000,1292554277031210,9856000,11290,1292554286887210,242000,1292554287129210
- -2143831735395280196,-2143831735395280196,1,0,1,0,1,1292554252553270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",11172,1292554253301257,345000,1292554253646257,819953,4,11187,1292554254466210,119000,1292554254585210,11932000,11223,1292554266517210,117000,1292554266634210
- -2143831735395280194,-2143831735395280194,0,0,1,0,1,1292554263653270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",11211,1292554264600257,279000,1292554264879257,1915953,4,11227,1292554266795210,193000,1292554266988210,9556000,11259,1292554276544210,133000,1292554276677210
+ -2143831735395280256,-2143831735395280256,1,0,1,0,1,1292554141489270,677987,1,"[NULL]","[NULL]","[NULL]",10781,1292554142167257,363000,1292554142530257,472953,4,10796,1292554143003210,108000,1292554143111210,10912000,10827,1292554154023210,83000,1292554154106210
+ -2143831735395280254,-2143831735395280254,1,0,1,0,1,1292554152575270,1654987,1,"[NULL]","[NULL]","[NULL]",10830,1292554154230257,259000,1292554154489257,698953,4,10845,1292554155188210,120000,1292554155308210,9637000,10869,1292554164945210,223000,1292554165168210
+ -2143831735395280250,-2143831735395280250,1,0,1,0,1,1292554130385270,806987,1,"[NULL]","[NULL]","[NULL]",10742,1292554131192257,279000,1292554131471257,393953,4,10757,1292554131865210,98000,1292554131963210,10636000,10790,1292554142599210,191000,1292554142790210
+ -2143831735395280248,-2143831735395280248,1,0,1,0,1,1292554185877270,750987,1,"[NULL]","[NULL]","[NULL]",10939,1292554186628257,398000,1292554187026257,217953,4,10950,1292554187244210,107000,1292554187351210,10849000,10988,1292554198200210,82000,1292554198282210
+ -2143831735395280246,-2143831735395280246,1,0,1,0,1,1292554196968270,1073987,1,"[NULL]","[NULL]","[NULL]",10980,1292554198042257,362000,1292554198404257,890953,4,11000,1292554199295210,110000,1292554199405210,9963000,11025,1292554209368210,90000,1292554209458210
+ -2143831735395280244,-2143831735395280244,1,0,1,0,1,1292554163682270,785987,1,"[NULL]","[NULL]","[NULL]",10860,1292554164468257,393000,1292554164861257,513953,4,10876,1292554165375210,127000,1292554165502210,10798000,10908,1292554176300210,226000,1292554176526210
+ -2143831735395280242,-2143831735395280242,1,0,1,0,1,1292554174786270,921987,1,"[NULL]","[NULL]","[NULL]",10899,1292554175708257,321000,1292554176029257,697953,4,10915,1292554176727210,107000,1292554176834210,10177000,10947,1292554187011210,88000,1292554187099210
+ -2143831735395280239,-2143831735395280239,1,0,1,0,1,1292554086893270,3987,1,"[NULL]","[NULL]","[NULL]",10555,1292554086897257,128000,1292554087025257,1290953,4,10586,1292554088316210,79000,1292554088395210,9853000,10620,1292554098248210,177000,1292554098425210
+ -2143831735395280229,-2143831735395280229,1,0,1,0,1,1292554119302270,739987,1,"[NULL]","[NULL]","[NULL]",10699,1292554120042257,327000,1292554120369257,167953,4,10714,1292554120537210,94000,1292554120631210,10935000,10750,1292554131566210,158000,1292554131724210
+ -2143831735395280227,-2143831735395280227,1,0,1,0,1,1292554097138270,848987,1,"[NULL]","[NULL]","[NULL]",10611,1292554097987257,189000,1292554098176257,366953,4,10626,1292554098543210,76000,1292554098619210,10479000,10662,1292554109098210,151000,1292554109249210
+ -2143831735395280226,-2143831735395280226,1,0,1,0,1,1292554108216270,771987,1,"[NULL]","[NULL]","[NULL]",10657,1292554108988257,322000,1292554109310257,80953,4,10666,1292554109391210,100000,1292554109491210,10760000,10706,1292554120251210,138000,1292554120389210
+ -2143831735395280208,-2143831735395280208,1,0,1,0,1,1292554230251270,802987,1,"[NULL]","[NULL]","[NULL]",11096,1292554231054257,408000,1292554231462257,145953,4,11106,1292554231608210,103000,1292554231711210,11015000,11142,1292554242726210,128000,1292554242854210
+ -2143831735395280206,-2143831735395280206,1,0,1,0,1,1292554241443270,892987,1,"[NULL]","[NULL]","[NULL]",11134,1292554242336257,324000,1292554242660257,335953,4,11148,1292554242996210,120000,1292554243116210,11070000,11184,1292554254186210,138000,1292554254324210
+ -2143831735395280204,-2143831735395280204,1,0,1,0,1,1292554208072270,858987,1,"[NULL]","[NULL]","[NULL]",11017,1292554208931257,257000,1292554209188257,423953,4,11031,1292554209612210,110000,1292554209722210,10857000,11064,1292554220579210,107000,1292554220686210
+ -2143831735395280202,-2143831735395280202,1,0,1,0,1,1292554219159270,1143987,1,"[NULL]","[NULL]","[NULL]",11057,1292554220303257,375000,1292554220678257,1225953,4,11078,1292554221904210,150000,1292554222054210,9337000,11103,1292554231391210,77000,1292554231468210
+ -2143831735395280200,-2143831735395280200,0,0,1,0,1,1292554274773270,971987,1,"[NULL]","[NULL]","[NULL]",11250,1292554275745257,304000,1292554276049257,837953,4,11266,1292554276887210,144000,1292554277031210,9856000,11290,1292554286887210,242000,1292554287129210
+ -2143831735395280196,-2143831735395280196,1,0,1,0,1,1292554252553270,747987,1,"[NULL]","[NULL]","[NULL]",11172,1292554253301257,345000,1292554253646257,819953,4,11187,1292554254466210,119000,1292554254585210,11932000,11223,1292554266517210,117000,1292554266634210
+ -2143831735395280194,-2143831735395280194,0,0,1,0,1,1292554263653270,946987,1,"[NULL]","[NULL]","[NULL]",11211,1292554264600257,279000,1292554264879257,1915953,4,11227,1292554266795210,193000,1292554266988210,9556000,11259,1292554276544210,133000,1292554276677210
-2143831735395280183,-2143831735395280179,0,0,0,0,0,1292554034979270,3955987,1,10192,1292554038935257,286000,10197,1292554039221257,141000,1292554039362257,17953,4,10210,1292554039380210,124000,1292554039504210,3940000,10230,1292554043444210,101000,1292554043545210
-2143831735395280179,-2143831735395280179,1,0,0,0,1,1292554029441270,7839987,1,10172,1292554037281257,337000,10177,1292554037618257,167000,1292554037785257,451953,4,10189,1292554038237210,89000,1292554038326210,4800000,10229,1292554043126210,303000,1292554043429210
-2143831735395280166,-2143831735395280166,1,0,0,1,1,1292554023976270,3704987,1,10071,1292554027681257,2166000,10102,1292554029847257,236000,1292554030083257,276953,4,10123,1292554030360210,377000,1292554030737210,-68000,10128,1292554030669210,56000,1292554030725210
- """))
+ """))
def test_chrome_scroll_update_frame_info(self):
return DiffTestBlueprint(
@@ -658,38 +658,36 @@
viz_swap_buffers_end_ts,
viz_swap_buffers_to_latch_dur,
latch_timestamp,
- viz_latch_to_swap_end_dur,
- swap_end_timestamp,
- swap_end_to_presentation_dur,
+ viz_latch_to_presentation_dur,
presentation_timestamp
FROM chrome_scroll_update_frame_info
ORDER BY id
LIMIT 21
""",
out=Csv("""
- "id","vsync_interval_ms","compositor_resample_slice_id","compositor_resample_ts","compositor_generate_compositor_frame_slice_id","compositor_generate_compositor_frame_ts","compositor_generate_frame_to_submit_frame_dur","compositor_submit_compositor_frame_slice_id","compositor_submit_compositor_frame_ts","compositor_submit_frame_dur","compositor_submit_compositor_frame_end_ts","compositor_to_viz_delay_dur","viz_compositor_utid","viz_receive_compositor_frame_slice_id","viz_receive_compositor_frame_ts","viz_receive_compositor_frame_dur","viz_receive_compositor_frame_end_ts","viz_wait_for_draw_dur","viz_draw_and_swap_slice_id","viz_draw_and_swap_ts","viz_draw_and_swap_dur","viz_send_buffer_swap_slice_id","viz_send_buffer_swap_end_ts","viz_to_gpu_delay_dur","viz_gpu_thread_utid","viz_swap_buffers_slice_id","viz_swap_buffers_ts","viz_swap_buffers_dur","viz_swap_buffers_end_ts","viz_swap_buffers_to_latch_dur","latch_timestamp","viz_latch_to_swap_end_dur","swap_end_timestamp","swap_end_to_presentation_dur","presentation_timestamp"
- -2143831735395280256,11.111000,"[NULL]","[NULL]",10834,1292554154282210,337000,10838,1292554154619210,337000,1292554154956210,139423,6,10840,1292554155095633,126000,1292554155221633,65000,10846,1292554155286633,1295000,10849,1292554156581633,1620498,7,10850,1292554158202131,536000,1292554158738131,10898139,1292554169636270,6818000,1292554176454270,9345000,1292554185799270
- -2143831735395280254,11.111000,"[NULL]","[NULL]",10878,1292554165562210,387000,10881,1292554165949210,363000,1292554166312210,148423,6,10882,1292554166460633,129000,1292554166589633,101000,10885,1292554166690633,1134000,10888,1292554167824633,1573498,7,10889,1292554169398131,545000,1292554169943131,10941139,1292554180884270,6702000,1292554187586270,9405000,1292554196991270
- -2143831735395280250,11.111000,"[NULL]","[NULL]",10799,1292554143168210,382000,10802,1292554143550210,355000,1292554143905210,122423,6,10803,1292554144027633,99000,1292554144126633,46000,10806,1292554144172633,1016000,10809,1292554145188633,1514498,7,10810,1292554146703131,483000,1292554147186131,11323139,1292554158509270,6698000,1292554165207270,9484000,1292554174691270
- -2143831735395280248,11.111000,"[NULL]","[NULL]",10991,1292554198448210,361000,10995,1292554198809210,317000,1292554199126210,167423,6,10996,1292554199293633,123000,1292554199416633,66000,11002,1292554199482633,1058000,11005,1292554200540633,1691498,7,11006,1292554202232131,543000,1292554202775131,11459139,1292554214234270,6958000,1292554221192270,9043000,1292554230235270
- -2143831735395280246,11.111000,"[NULL]","[NULL]",11033,1292554209783210,326000,11037,1292554210109210,338000,1292554210447210,139423,6,11038,1292554210586633,158000,1292554210744633,61000,11041,1292554210805633,1109000,11044,1292554211914633,763498,7,11045,1292554212678131,458000,1292554213136131,12006139,1292554225142270,6727000,1292554231869270,9462000,1292554241331270
- -2143831735395280244,11.111000,"[NULL]","[NULL]",10917,1292554176906210,371000,10920,1292554177277210,302000,1292554177579210,135423,6,10921,1292554177714633,105000,1292554177819633,47000,10924,1292554177866633,1033000,10927,1292554178899633,1364498,7,10928,1292554180264131,468000,1292554180732131,11126139,1292554191858270,6255000,1292554198113270,9900000,1292554208013270
- -2143831735395280242,11.111000,"[NULL]","[NULL]",10953,1292554187399210,383000,10959,1292554187782210,302000,1292554188084210,153423,6,10960,1292554188237633,149000,1292554188386633,57000,10963,1292554188443633,1080000,10966,1292554189523633,1628498,7,10967,1292554191152131,537000,1292554191689131,11488139,1292554203177270,6351000,1292554209528270,9588000,1292554219116270
- -2143831735395280239,11.111000,10616,1292554097735210,10629,1292554098654210,282000,10632,1292554098936210,237000,1292554099173210,121423,6,10633,1292554099294633,113000,1292554099407633,62000,10636,1292554099469633,953000,10641,1292554100422633,1211498,7,10643,1292554101634131,364000,1292554101998131,12589139,1292554114587270,5702000,1292554120289270,10025000,1292554130314270
- -2143831735395280229,11.111000,"[NULL]","[NULL]",10759,1292554132003210,421000,10762,1292554132424210,509000,1292554132933210,113423,6,10763,1292554133046633,120000,1292554133166633,96000,10766,1292554133262633,1095000,10769,1292554134357633,1469498,7,10770,1292554135827131,499000,1292554136326131,11256139,1292554147582270,6606000,1292554154188270,9466000,1292554163654270
- -2143831735395280227,11.111000,"[NULL]","[NULL]",10669,1292554109537210,306000,10677,1292554109843210,243000,1292554110086210,214423,6,10679,1292554110300633,119000,1292554110419633,42000,10682,1292554110461633,953000,10685,1292554111414633,731498,7,10686,1292554112146131,440000,1292554112586131,12791139,1292554125377270,6239000,1292554131616270,9725000,1292554141341270
- -2143831735395280226,11.111000,"[NULL]","[NULL]",10716,1292554120708210,290000,10721,1292554120998210,195000,1292554121193210,190423,6,10724,1292554121383633,89000,1292554121472633,42000,10727,1292554121514633,923000,10730,1292554122437633,1542498,7,10731,1292554123980131,426000,1292554124406131,12351139,1292554136757270,6289000,1292554143046270,9504000,1292554152550270
- -2143831735395280208,11.111000,"[NULL]","[NULL]",11150,1292554243173210,360000,11153,1292554243533210,219000,1292554243752210,267423,6,11154,1292554244019633,158000,1292554244177633,61000,11157,1292554244238633,1109000,11161,1292554245347633,1467498,7,11162,1292554246815131,558000,1292554247373131,11123139,1292554258496270,6595000,1292554265091270,9589000,1292554274680270
- -2143831735395280206,11.111000,"[NULL]","[NULL]",11189,1292554254651210,430000,11192,1292554255081210,201000,1292554255282210,299423,6,11193,1292554255581633,129000,1292554255710633,66000,11197,1292554255776633,1110000,11200,1292554256886633,1522498,7,11201,1292554258409131,591000,1292554259000131,10967139,1292554269967270,6691000,1292554276658270,9116000,1292554285774270
- -2143831735395280204,11.111000,"[NULL]","[NULL]",11066,1292554220890210,434000,11073,1292554221324210,355000,1292554221679210,197423,6,11074,1292554221876633,144000,1292554222020633,67000,11080,1292554222087633,1470000,11083,1292554223557633,1731498,7,11085,1292554225289131,658000,1292554225947131,10096139,1292554236043270,6959000,1292554243002270,9447000,1292554252449270
- -2143831735395280202,11.111000,"[NULL]","[NULL]",11109,1292554231758210,340000,11115,1292554232098210,296000,1292554232394210,127423,6,11116,1292554232521633,154000,1292554232675633,62000,11119,1292554232737633,1042000,11122,1292554233779633,1446498,7,11123,1292554235226131,479000,1292554235705131,11595139,1292554247300270,6465000,1292554253765270,9784000,1292554263549270
- -2143831735395280200,0.000000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
- -2143831735395280196,11.111000,"[NULL]","[NULL]",11229,1292554267072210,466000,11232,1292554267538210,202000,1292554267740210,469423,6,11233,1292554268209633,196000,1292554268405633,61000,11236,1292554268466633,1147000,11239,1292554269613633,2276498,7,11241,1292554271890131,695000,1292554272585131,8276139,1292554280861270,6143000,1292554287004270,9906000,1292554296910270
- -2143831735395280194,0.000000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
- -2143831735395280179,11.111000,10223,1292554042749210,10233,1292554043721210,451000,10239,1292554044172210,315000,1292554044487210,169423,6,10245,1292554044656633,670000,1292554045326633,1785000,10266,1292554047111633,1048000,10271,1292554048159633,1540498,7,10272,1292554049700131,458000,1292554050158131,8651139,1292554058809270,6861000,1292554065670270,9035000,1292554074705270
- -2143831735395280166,11.111000,10124,1292554030360210,10130,1292554030873210,568000,10135,1292554031441210,227000,1292554031668210,919423,6,10141,1292554032587633,754000,1292554033341633,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",1292554048008270,5822000,1292554053830270,9774000,1292554063604270
- -2143831735395280153,11.111000,10469,1292554075561210,10481,1292554076592210,334000,10488,1292554076926210,301000,1292554077227210,177423,6,10494,1292554077404633,138000,1292554077542633,280000,10506,1292554077822633,988000,10509,1292554078810633,1377498,7,10516,1292554080188131,494000,1292554080682131,11265139,1292554091947270,7535000,1292554099482270,8561000,1292554108043270
- """))
+ "id","vsync_interval_ms","compositor_resample_slice_id","compositor_resample_ts","compositor_generate_compositor_frame_slice_id","compositor_generate_compositor_frame_ts","compositor_generate_frame_to_submit_frame_dur","compositor_submit_compositor_frame_slice_id","compositor_submit_compositor_frame_ts","compositor_submit_frame_dur","compositor_submit_compositor_frame_end_ts","compositor_to_viz_delay_dur","viz_compositor_utid","viz_receive_compositor_frame_slice_id","viz_receive_compositor_frame_ts","viz_receive_compositor_frame_dur","viz_receive_compositor_frame_end_ts","viz_wait_for_draw_dur","viz_draw_and_swap_slice_id","viz_draw_and_swap_ts","viz_draw_and_swap_dur","viz_send_buffer_swap_slice_id","viz_send_buffer_swap_end_ts","viz_to_gpu_delay_dur","viz_gpu_thread_utid","viz_swap_buffers_slice_id","viz_swap_buffers_ts","viz_swap_buffers_dur","viz_swap_buffers_end_ts","viz_swap_buffers_to_latch_dur","latch_timestamp","viz_latch_to_presentation_dur","presentation_timestamp"
+ -2143831735395280256,11.111000,"[NULL]","[NULL]",10834,1292554154282210,337000,10838,1292554154619210,337000,1292554154956210,139423,6,10840,1292554155095633,126000,1292554155221633,65000,10846,1292554155286633,1295000,10849,1292554156581633,1620498,7,10850,1292554158202131,536000,1292554158738131,10898139,1292554169636270,16163000,1292554185799270
+ -2143831735395280254,11.111000,"[NULL]","[NULL]",10878,1292554165562210,387000,10881,1292554165949210,363000,1292554166312210,148423,6,10882,1292554166460633,129000,1292554166589633,101000,10885,1292554166690633,1134000,10888,1292554167824633,1573498,7,10889,1292554169398131,545000,1292554169943131,10941139,1292554180884270,16107000,1292554196991270
+ -2143831735395280250,11.111000,"[NULL]","[NULL]",10799,1292554143168210,382000,10802,1292554143550210,355000,1292554143905210,122423,6,10803,1292554144027633,99000,1292554144126633,46000,10806,1292554144172633,1016000,10809,1292554145188633,1514498,7,10810,1292554146703131,483000,1292554147186131,11323139,1292554158509270,16182000,1292554174691270
+ -2143831735395280248,11.111000,"[NULL]","[NULL]",10991,1292554198448210,361000,10995,1292554198809210,317000,1292554199126210,167423,6,10996,1292554199293633,123000,1292554199416633,66000,11002,1292554199482633,1058000,11005,1292554200540633,1691498,7,11006,1292554202232131,543000,1292554202775131,11459139,1292554214234270,16001000,1292554230235270
+ -2143831735395280246,11.111000,"[NULL]","[NULL]",11033,1292554209783210,326000,11037,1292554210109210,338000,1292554210447210,139423,6,11038,1292554210586633,158000,1292554210744633,61000,11041,1292554210805633,1109000,11044,1292554211914633,763498,7,11045,1292554212678131,458000,1292554213136131,12006139,1292554225142270,16189000,1292554241331270
+ -2143831735395280244,11.111000,"[NULL]","[NULL]",10917,1292554176906210,371000,10920,1292554177277210,302000,1292554177579210,135423,6,10921,1292554177714633,105000,1292554177819633,47000,10924,1292554177866633,1033000,10927,1292554178899633,1364498,7,10928,1292554180264131,468000,1292554180732131,11126139,1292554191858270,16155000,1292554208013270
+ -2143831735395280242,11.111000,"[NULL]","[NULL]",10953,1292554187399210,383000,10959,1292554187782210,302000,1292554188084210,153423,6,10960,1292554188237633,149000,1292554188386633,57000,10963,1292554188443633,1080000,10966,1292554189523633,1628498,7,10967,1292554191152131,537000,1292554191689131,11488139,1292554203177270,15939000,1292554219116270
+ -2143831735395280239,11.111000,10616,1292554097735210,10629,1292554098654210,282000,10632,1292554098936210,237000,1292554099173210,121423,6,10633,1292554099294633,113000,1292554099407633,62000,10636,1292554099469633,953000,10641,1292554100422633,1211498,7,10643,1292554101634131,364000,1292554101998131,12589139,1292554114587270,15727000,1292554130314270
+ -2143831735395280229,11.111000,"[NULL]","[NULL]",10759,1292554132003210,421000,10762,1292554132424210,509000,1292554132933210,113423,6,10763,1292554133046633,120000,1292554133166633,96000,10766,1292554133262633,1095000,10769,1292554134357633,1469498,7,10770,1292554135827131,499000,1292554136326131,11256139,1292554147582270,16072000,1292554163654270
+ -2143831735395280227,11.111000,"[NULL]","[NULL]",10669,1292554109537210,306000,10677,1292554109843210,243000,1292554110086210,214423,6,10679,1292554110300633,119000,1292554110419633,42000,10682,1292554110461633,953000,10685,1292554111414633,731498,7,10686,1292554112146131,440000,1292554112586131,12791139,1292554125377270,15964000,1292554141341270
+ -2143831735395280226,11.111000,"[NULL]","[NULL]",10716,1292554120708210,290000,10721,1292554120998210,195000,1292554121193210,190423,6,10724,1292554121383633,89000,1292554121472633,42000,10727,1292554121514633,923000,10730,1292554122437633,1542498,7,10731,1292554123980131,426000,1292554124406131,12351139,1292554136757270,15793000,1292554152550270
+ -2143831735395280208,11.111000,"[NULL]","[NULL]",11150,1292554243173210,360000,11153,1292554243533210,219000,1292554243752210,267423,6,11154,1292554244019633,158000,1292554244177633,61000,11157,1292554244238633,1109000,11161,1292554245347633,1467498,7,11162,1292554246815131,558000,1292554247373131,11123139,1292554258496270,16184000,1292554274680270
+ -2143831735395280206,11.111000,"[NULL]","[NULL]",11189,1292554254651210,430000,11192,1292554255081210,201000,1292554255282210,299423,6,11193,1292554255581633,129000,1292554255710633,66000,11197,1292554255776633,1110000,11200,1292554256886633,1522498,7,11201,1292554258409131,591000,1292554259000131,10967139,1292554269967270,15807000,1292554285774270
+ -2143831735395280204,11.111000,"[NULL]","[NULL]",11066,1292554220890210,434000,11073,1292554221324210,355000,1292554221679210,197423,6,11074,1292554221876633,144000,1292554222020633,67000,11080,1292554222087633,1470000,11083,1292554223557633,1731498,7,11085,1292554225289131,658000,1292554225947131,10096139,1292554236043270,16406000,1292554252449270
+ -2143831735395280202,11.111000,"[NULL]","[NULL]",11109,1292554231758210,340000,11115,1292554232098210,296000,1292554232394210,127423,6,11116,1292554232521633,154000,1292554232675633,62000,11119,1292554232737633,1042000,11122,1292554233779633,1446498,7,11123,1292554235226131,479000,1292554235705131,11595139,1292554247300270,16249000,1292554263549270
+ -2143831735395280200,0.000000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
+ -2143831735395280196,11.111000,"[NULL]","[NULL]",11229,1292554267072210,466000,11232,1292554267538210,202000,1292554267740210,469423,6,11233,1292554268209633,196000,1292554268405633,61000,11236,1292554268466633,1147000,11239,1292554269613633,2276498,7,11241,1292554271890131,695000,1292554272585131,8276139,1292554280861270,16049000,1292554296910270
+ -2143831735395280194,0.000000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
+ -2143831735395280179,11.111000,10223,1292554042749210,10233,1292554043721210,451000,10239,1292554044172210,315000,1292554044487210,169423,6,10245,1292554044656633,670000,1292554045326633,1785000,10266,1292554047111633,1048000,10271,1292554048159633,1540498,7,10272,1292554049700131,458000,1292554050158131,8651139,1292554058809270,15896000,1292554074705270
+ -2143831735395280166,11.111000,10124,1292554030360210,10130,1292554030873210,568000,10135,1292554031441210,227000,1292554031668210,919423,6,10141,1292554032587633,754000,1292554033341633,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",1292554048008270,15596000,1292554063604270
+ -2143831735395280153,11.111000,10469,1292554075561210,10481,1292554076592210,334000,10488,1292554076926210,301000,1292554077227210,177423,6,10494,1292554077404633,138000,1292554077542633,280000,10506,1292554077822633,988000,10509,1292554078810633,1377498,7,10516,1292554080188131,494000,1292554080682131,11265139,1292554091947270,16096000,1292554108043270
+ """))
def test_chrome_scroll_update_info(self):
return DiffTestBlueprint(
@@ -739,39 +737,82 @@
viz_to_gpu_delay_dur,
viz_swap_buffers_dur,
latch_timestamp,
- swap_end_timestamp,
presentation_timestamp,
viz_swap_buffers_to_latch_dur,
- viz_latch_to_swap_end_dur,
- swap_end_to_presentation_dur
+ viz_latch_to_presentation_dur
FROM chrome_scroll_update_info
ORDER BY id
LIMIT 21
""",
out=Csv("""
- "id","vsync_interval_ms","is_presented","is_janky","is_inertial","is_first_scroll_update_in_scroll","is_first_scroll_update_in_frame","generation_ts","touch_move_received_ts","generation_to_browser_main_dur","scroll_update_created_ts","scroll_update_created_end_ts","touch_move_processing_dur","scroll_update_processing_dur","compositor_dispatch_ts","compositor_dispatch_end_ts","browser_to_compositor_delay_dur","compositor_dispatch_dur","compositor_on_begin_frame_ts","compositor_on_begin_frame_end_ts","compositor_dispatch_to_on_begin_frame_delay_dur","compositor_on_begin_frame_dur","compositor_generate_compositor_frame_ts","compositor_on_begin_frame_to_generation_delay_dur","compositor_submit_compositor_frame_ts","compositor_submit_compositor_frame_end_ts","compositor_generate_frame_to_submit_frame_dur","compositor_submit_frame_dur","viz_receive_compositor_frame_ts","viz_receive_compositor_frame_end_ts","compositor_to_viz_delay_dur","viz_receive_compositor_frame_dur","viz_draw_and_swap_ts","viz_wait_for_draw_dur","viz_send_buffer_swap_end_ts","viz_draw_and_swap_dur","viz_swap_buffers_ts","viz_swap_buffers_end_ts","viz_to_gpu_delay_dur","viz_swap_buffers_dur","latch_timestamp","swap_end_timestamp","presentation_timestamp","viz_swap_buffers_to_latch_dur","viz_latch_to_swap_end_dur","swap_end_to_presentation_dur"
- -2143831735395280256,11.111000,1,0,1,0,1,1292554141489270,"[NULL]","[NULL]",1292554142167257,1292554142530257,"[NULL]",363000,1292554143003210,1292554143111210,472953,108000,1292554154023210,1292554154106210,10912000,83000,1292554154282210,176000,1292554154619210,1292554154956210,337000,337000,1292554155095633,1292554155221633,139423,126000,1292554155286633,65000,1292554156581633,1295000,1292554158202131,1292554158738131,1620498,536000,1292554169636270,1292554176454270,1292554185799270,10898139,6818000,9345000
- -2143831735395280254,11.111000,1,0,1,0,1,1292554152575270,"[NULL]","[NULL]",1292554154230257,1292554154489257,"[NULL]",259000,1292554155188210,1292554155308210,698953,120000,1292554164945210,1292554165168210,9637000,223000,1292554165562210,394000,1292554165949210,1292554166312210,387000,363000,1292554166460633,1292554166589633,148423,129000,1292554166690633,101000,1292554167824633,1134000,1292554169398131,1292554169943131,1573498,545000,1292554180884270,1292554187586270,1292554196991270,10941139,6702000,9405000
- -2143831735395280250,11.111000,1,0,1,0,1,1292554130385270,"[NULL]","[NULL]",1292554131192257,1292554131471257,"[NULL]",279000,1292554131865210,1292554131963210,393953,98000,1292554142599210,1292554142790210,10636000,191000,1292554143168210,378000,1292554143550210,1292554143905210,382000,355000,1292554144027633,1292554144126633,122423,99000,1292554144172633,46000,1292554145188633,1016000,1292554146703131,1292554147186131,1514498,483000,1292554158509270,1292554165207270,1292554174691270,11323139,6698000,9484000
- -2143831735395280248,11.111000,1,0,1,0,1,1292554185877270,"[NULL]","[NULL]",1292554186628257,1292554187026257,"[NULL]",398000,1292554187244210,1292554187351210,217953,107000,1292554198200210,1292554198282210,10849000,82000,1292554198448210,166000,1292554198809210,1292554199126210,361000,317000,1292554199293633,1292554199416633,167423,123000,1292554199482633,66000,1292554200540633,1058000,1292554202232131,1292554202775131,1691498,543000,1292554214234270,1292554221192270,1292554230235270,11459139,6958000,9043000
- -2143831735395280246,11.111000,1,0,1,0,1,1292554196968270,"[NULL]","[NULL]",1292554198042257,1292554198404257,"[NULL]",362000,1292554199295210,1292554199405210,890953,110000,1292554209368210,1292554209458210,9963000,90000,1292554209783210,325000,1292554210109210,1292554210447210,326000,338000,1292554210586633,1292554210744633,139423,158000,1292554210805633,61000,1292554211914633,1109000,1292554212678131,1292554213136131,763498,458000,1292554225142270,1292554231869270,1292554241331270,12006139,6727000,9462000
- -2143831735395280244,11.111000,1,0,1,0,1,1292554163682270,"[NULL]","[NULL]",1292554164468257,1292554164861257,"[NULL]",393000,1292554165375210,1292554165502210,513953,127000,1292554176300210,1292554176526210,10798000,226000,1292554176906210,380000,1292554177277210,1292554177579210,371000,302000,1292554177714633,1292554177819633,135423,105000,1292554177866633,47000,1292554178899633,1033000,1292554180264131,1292554180732131,1364498,468000,1292554191858270,1292554198113270,1292554208013270,11126139,6255000,9900000
- -2143831735395280242,11.111000,1,0,1,0,1,1292554174786270,"[NULL]","[NULL]",1292554175708257,1292554176029257,"[NULL]",321000,1292554176727210,1292554176834210,697953,107000,1292554187011210,1292554187099210,10177000,88000,1292554187399210,300000,1292554187782210,1292554188084210,383000,302000,1292554188237633,1292554188386633,153423,149000,1292554188443633,57000,1292554189523633,1080000,1292554191152131,1292554191689131,1628498,537000,1292554203177270,1292554209528270,1292554219116270,11488139,6351000,9588000
- -2143831735395280239,11.111000,1,0,1,0,1,1292554086893270,"[NULL]","[NULL]",1292554086897257,1292554087025257,"[NULL]",128000,1292554088316210,1292554088395210,1290953,79000,1292554097735210,1292554098425210,9340000,690000,1292554098654210,229000,1292554098936210,1292554099173210,282000,237000,1292554099294633,1292554099407633,121423,113000,1292554099469633,62000,1292554100422633,953000,1292554101634131,1292554101998131,1211498,364000,1292554114587270,1292554120289270,1292554130314270,12589139,5702000,10025000
- -2143831735395280229,11.111000,1,0,1,0,1,1292554119302270,"[NULL]","[NULL]",1292554120042257,1292554120369257,"[NULL]",327000,1292554120537210,1292554120631210,167953,94000,1292554131566210,1292554131724210,10935000,158000,1292554132003210,279000,1292554132424210,1292554132933210,421000,509000,1292554133046633,1292554133166633,113423,120000,1292554133262633,96000,1292554134357633,1095000,1292554135827131,1292554136326131,1469498,499000,1292554147582270,1292554154188270,1292554163654270,11256139,6606000,9466000
- -2143831735395280227,11.111000,1,0,1,0,1,1292554097138270,"[NULL]","[NULL]",1292554097987257,1292554098176257,"[NULL]",189000,1292554098543210,1292554098619210,366953,76000,1292554109098210,1292554109249210,10479000,151000,1292554109537210,288000,1292554109843210,1292554110086210,306000,243000,1292554110300633,1292554110419633,214423,119000,1292554110461633,42000,1292554111414633,953000,1292554112146131,1292554112586131,731498,440000,1292554125377270,1292554131616270,1292554141341270,12791139,6239000,9725000
- -2143831735395280226,11.111000,1,0,1,0,1,1292554108216270,"[NULL]","[NULL]",1292554108988257,1292554109310257,"[NULL]",322000,1292554109391210,1292554109491210,80953,100000,1292554120251210,1292554120389210,10760000,138000,1292554120708210,319000,1292554120998210,1292554121193210,290000,195000,1292554121383633,1292554121472633,190423,89000,1292554121514633,42000,1292554122437633,923000,1292554123980131,1292554124406131,1542498,426000,1292554136757270,1292554143046270,1292554152550270,12351139,6289000,9504000
- -2143831735395280208,11.111000,1,0,1,0,1,1292554230251270,"[NULL]","[NULL]",1292554231054257,1292554231462257,"[NULL]",408000,1292554231608210,1292554231711210,145953,103000,1292554242726210,1292554242854210,11015000,128000,1292554243173210,319000,1292554243533210,1292554243752210,360000,219000,1292554244019633,1292554244177633,267423,158000,1292554244238633,61000,1292554245347633,1109000,1292554246815131,1292554247373131,1467498,558000,1292554258496270,1292554265091270,1292554274680270,11123139,6595000,9589000
- -2143831735395280206,11.111000,1,0,1,0,1,1292554241443270,"[NULL]","[NULL]",1292554242336257,1292554242660257,"[NULL]",324000,1292554242996210,1292554243116210,335953,120000,1292554254186210,1292554254324210,11070000,138000,1292554254651210,327000,1292554255081210,1292554255282210,430000,201000,1292554255581633,1292554255710633,299423,129000,1292554255776633,66000,1292554256886633,1110000,1292554258409131,1292554259000131,1522498,591000,1292554269967270,1292554276658270,1292554285774270,10967139,6691000,9116000
- -2143831735395280204,11.111000,1,0,1,0,1,1292554208072270,"[NULL]","[NULL]",1292554208931257,1292554209188257,"[NULL]",257000,1292554209612210,1292554209722210,423953,110000,1292554220579210,1292554220686210,10857000,107000,1292554220890210,204000,1292554221324210,1292554221679210,434000,355000,1292554221876633,1292554222020633,197423,144000,1292554222087633,67000,1292554223557633,1470000,1292554225289131,1292554225947131,1731498,658000,1292554236043270,1292554243002270,1292554252449270,10096139,6959000,9447000
- -2143831735395280202,11.111000,1,0,1,0,1,1292554219159270,"[NULL]","[NULL]",1292554220303257,1292554220678257,"[NULL]",375000,1292554221904210,1292554222054210,1225953,150000,1292554231391210,1292554231468210,9337000,77000,1292554231758210,290000,1292554232098210,1292554232394210,340000,296000,1292554232521633,1292554232675633,127423,154000,1292554232737633,62000,1292554233779633,1042000,1292554235226131,1292554235705131,1446498,479000,1292554247300270,1292554253765270,1292554263549270,11595139,6465000,9784000
- -2143831735395280200,0.000000,0,0,1,0,1,1292554274773270,"[NULL]","[NULL]",1292554275745257,1292554276049257,"[NULL]",304000,1292554276887210,1292554277031210,837953,144000,1292554286887210,1292554287129210,9856000,242000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
- -2143831735395280196,11.111000,1,0,1,0,1,1292554252553270,"[NULL]","[NULL]",1292554253301257,1292554253646257,"[NULL]",345000,1292554254466210,1292554254585210,819953,119000,1292554266517210,1292554266634210,11932000,117000,1292554267072210,438000,1292554267538210,1292554267740210,466000,202000,1292554268209633,1292554268405633,469423,196000,1292554268466633,61000,1292554269613633,1147000,1292554271890131,1292554272585131,2276498,695000,1292554280861270,1292554287004270,1292554296910270,8276139,6143000,9906000
- -2143831735395280194,0.000000,0,0,1,0,1,1292554263653270,"[NULL]","[NULL]",1292554264600257,1292554264879257,"[NULL]",279000,1292554266795210,1292554266988210,1915953,193000,1292554276544210,1292554276677210,9556000,133000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
- -2143831735395280183,11.111000,0,0,0,0,0,1292554034979270,1292554038935257,3955987,1292554039221257,1292554039362257,286000,141000,1292554039380210,1292554039504210,17953,124000,1292554042749210,1292554043545210,3245000,796000,1292554043721210,176000,1292554044172210,1292554044487210,451000,315000,1292554044656633,1292554045326633,169423,670000,1292554047111633,1785000,1292554048159633,1048000,1292554049700131,1292554050158131,1540498,458000,1292554058809270,1292554065670270,1292554074705270,8651139,6861000,9035000
- -2143831735395280179,11.111000,1,0,0,0,1,1292554029441270,1292554037281257,7839987,1292554037618257,1292554037785257,337000,167000,1292554038237210,1292554038326210,451953,89000,1292554042749210,1292554043429210,4423000,680000,1292554043721210,292000,1292554044172210,1292554044487210,451000,315000,1292554044656633,1292554045326633,169423,670000,1292554047111633,1785000,1292554048159633,1048000,1292554049700131,1292554050158131,1540498,458000,1292554058809270,1292554065670270,1292554074705270,8651139,6861000,9035000
- -2143831735395280166,11.111000,1,0,0,1,1,1292554023976270,1292554027681257,3704987,1292554029847257,1292554030083257,2166000,236000,1292554030360210,1292554030737210,276953,377000,1292554030360210,1292554030725210,-377000,365000,1292554030873210,148000,1292554031441210,1292554031668210,568000,227000,1292554032587633,1292554033341633,919423,754000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",1292554048008270,1292554053830270,1292554063604270,"[NULL]",5822000,9774000
- """))
+ "id","vsync_interval_ms","is_presented","is_janky","is_inertial","is_first_scroll_update_in_scroll","is_first_scroll_update_in_frame","generation_ts","touch_move_received_ts","generation_to_browser_main_dur","scroll_update_created_ts","scroll_update_created_end_ts","touch_move_processing_dur","scroll_update_processing_dur","compositor_dispatch_ts","compositor_dispatch_end_ts","browser_to_compositor_delay_dur","compositor_dispatch_dur","compositor_on_begin_frame_ts","compositor_on_begin_frame_end_ts","compositor_dispatch_to_on_begin_frame_delay_dur","compositor_on_begin_frame_dur","compositor_generate_compositor_frame_ts","compositor_on_begin_frame_to_generation_delay_dur","compositor_submit_compositor_frame_ts","compositor_submit_compositor_frame_end_ts","compositor_generate_frame_to_submit_frame_dur","compositor_submit_frame_dur","viz_receive_compositor_frame_ts","viz_receive_compositor_frame_end_ts","compositor_to_viz_delay_dur","viz_receive_compositor_frame_dur","viz_draw_and_swap_ts","viz_wait_for_draw_dur","viz_send_buffer_swap_end_ts","viz_draw_and_swap_dur","viz_swap_buffers_ts","viz_swap_buffers_end_ts","viz_to_gpu_delay_dur","viz_swap_buffers_dur","latch_timestamp","presentation_timestamp","viz_swap_buffers_to_latch_dur","viz_latch_to_presentation_dur"
+ -2143831735395280256,11.111000,1,0,1,0,1,1292554141489270,"[NULL]",677987,1292554142167257,1292554142530257,"[NULL]",363000,1292554143003210,1292554143111210,472953,108000,1292554154023210,1292554154106210,10912000,83000,1292554154282210,176000,1292554154619210,1292554154956210,337000,337000,1292554155095633,1292554155221633,139423,126000,1292554155286633,65000,1292554156581633,1295000,1292554158202131,1292554158738131,1620498,536000,1292554169636270,1292554185799270,10898139,16163000
+ -2143831735395280254,11.111000,1,0,1,0,1,1292554152575270,"[NULL]",1654987,1292554154230257,1292554154489257,"[NULL]",259000,1292554155188210,1292554155308210,698953,120000,1292554164945210,1292554165168210,9637000,223000,1292554165562210,394000,1292554165949210,1292554166312210,387000,363000,1292554166460633,1292554166589633,148423,129000,1292554166690633,101000,1292554167824633,1134000,1292554169398131,1292554169943131,1573498,545000,1292554180884270,1292554196991270,10941139,16107000
+ -2143831735395280250,11.111000,1,0,1,0,1,1292554130385270,"[NULL]",806987,1292554131192257,1292554131471257,"[NULL]",279000,1292554131865210,1292554131963210,393953,98000,1292554142599210,1292554142790210,10636000,191000,1292554143168210,378000,1292554143550210,1292554143905210,382000,355000,1292554144027633,1292554144126633,122423,99000,1292554144172633,46000,1292554145188633,1016000,1292554146703131,1292554147186131,1514498,483000,1292554158509270,1292554174691270,11323139,16182000
+ -2143831735395280248,11.111000,1,0,1,0,1,1292554185877270,"[NULL]",750987,1292554186628257,1292554187026257,"[NULL]",398000,1292554187244210,1292554187351210,217953,107000,1292554198200210,1292554198282210,10849000,82000,1292554198448210,166000,1292554198809210,1292554199126210,361000,317000,1292554199293633,1292554199416633,167423,123000,1292554199482633,66000,1292554200540633,1058000,1292554202232131,1292554202775131,1691498,543000,1292554214234270,1292554230235270,11459139,16001000
+ -2143831735395280246,11.111000,1,0,1,0,1,1292554196968270,"[NULL]",1073987,1292554198042257,1292554198404257,"[NULL]",362000,1292554199295210,1292554199405210,890953,110000,1292554209368210,1292554209458210,9963000,90000,1292554209783210,325000,1292554210109210,1292554210447210,326000,338000,1292554210586633,1292554210744633,139423,158000,1292554210805633,61000,1292554211914633,1109000,1292554212678131,1292554213136131,763498,458000,1292554225142270,1292554241331270,12006139,16189000
+ -2143831735395280244,11.111000,1,0,1,0,1,1292554163682270,"[NULL]",785987,1292554164468257,1292554164861257,"[NULL]",393000,1292554165375210,1292554165502210,513953,127000,1292554176300210,1292554176526210,10798000,226000,1292554176906210,380000,1292554177277210,1292554177579210,371000,302000,1292554177714633,1292554177819633,135423,105000,1292554177866633,47000,1292554178899633,1033000,1292554180264131,1292554180732131,1364498,468000,1292554191858270,1292554208013270,11126139,16155000
+ -2143831735395280242,11.111000,1,0,1,0,1,1292554174786270,"[NULL]",921987,1292554175708257,1292554176029257,"[NULL]",321000,1292554176727210,1292554176834210,697953,107000,1292554187011210,1292554187099210,10177000,88000,1292554187399210,300000,1292554187782210,1292554188084210,383000,302000,1292554188237633,1292554188386633,153423,149000,1292554188443633,57000,1292554189523633,1080000,1292554191152131,1292554191689131,1628498,537000,1292554203177270,1292554219116270,11488139,15939000
+ -2143831735395280239,11.111000,1,0,1,0,1,1292554086893270,"[NULL]",3987,1292554086897257,1292554087025257,"[NULL]",128000,1292554088316210,1292554088395210,1290953,79000,1292554097735210,1292554098425210,9340000,690000,1292554098654210,229000,1292554098936210,1292554099173210,282000,237000,1292554099294633,1292554099407633,121423,113000,1292554099469633,62000,1292554100422633,953000,1292554101634131,1292554101998131,1211498,364000,1292554114587270,1292554130314270,12589139,15727000
+ -2143831735395280229,11.111000,1,0,1,0,1,1292554119302270,"[NULL]",739987,1292554120042257,1292554120369257,"[NULL]",327000,1292554120537210,1292554120631210,167953,94000,1292554131566210,1292554131724210,10935000,158000,1292554132003210,279000,1292554132424210,1292554132933210,421000,509000,1292554133046633,1292554133166633,113423,120000,1292554133262633,96000,1292554134357633,1095000,1292554135827131,1292554136326131,1469498,499000,1292554147582270,1292554163654270,11256139,16072000
+ -2143831735395280227,11.111000,1,0,1,0,1,1292554097138270,"[NULL]",848987,1292554097987257,1292554098176257,"[NULL]",189000,1292554098543210,1292554098619210,366953,76000,1292554109098210,1292554109249210,10479000,151000,1292554109537210,288000,1292554109843210,1292554110086210,306000,243000,1292554110300633,1292554110419633,214423,119000,1292554110461633,42000,1292554111414633,953000,1292554112146131,1292554112586131,731498,440000,1292554125377270,1292554141341270,12791139,15964000
+ -2143831735395280226,11.111000,1,0,1,0,1,1292554108216270,"[NULL]",771987,1292554108988257,1292554109310257,"[NULL]",322000,1292554109391210,1292554109491210,80953,100000,1292554120251210,1292554120389210,10760000,138000,1292554120708210,319000,1292554120998210,1292554121193210,290000,195000,1292554121383633,1292554121472633,190423,89000,1292554121514633,42000,1292554122437633,923000,1292554123980131,1292554124406131,1542498,426000,1292554136757270,1292554152550270,12351139,15793000
+ -2143831735395280208,11.111000,1,0,1,0,1,1292554230251270,"[NULL]",802987,1292554231054257,1292554231462257,"[NULL]",408000,1292554231608210,1292554231711210,145953,103000,1292554242726210,1292554242854210,11015000,128000,1292554243173210,319000,1292554243533210,1292554243752210,360000,219000,1292554244019633,1292554244177633,267423,158000,1292554244238633,61000,1292554245347633,1109000,1292554246815131,1292554247373131,1467498,558000,1292554258496270,1292554274680270,11123139,16184000
+ -2143831735395280206,11.111000,1,0,1,0,1,1292554241443270,"[NULL]",892987,1292554242336257,1292554242660257,"[NULL]",324000,1292554242996210,1292554243116210,335953,120000,1292554254186210,1292554254324210,11070000,138000,1292554254651210,327000,1292554255081210,1292554255282210,430000,201000,1292554255581633,1292554255710633,299423,129000,1292554255776633,66000,1292554256886633,1110000,1292554258409131,1292554259000131,1522498,591000,1292554269967270,1292554285774270,10967139,15807000
+ -2143831735395280204,11.111000,1,0,1,0,1,1292554208072270,"[NULL]",858987,1292554208931257,1292554209188257,"[NULL]",257000,1292554209612210,1292554209722210,423953,110000,1292554220579210,1292554220686210,10857000,107000,1292554220890210,204000,1292554221324210,1292554221679210,434000,355000,1292554221876633,1292554222020633,197423,144000,1292554222087633,67000,1292554223557633,1470000,1292554225289131,1292554225947131,1731498,658000,1292554236043270,1292554252449270,10096139,16406000
+ -2143831735395280202,11.111000,1,0,1,0,1,1292554219159270,"[NULL]",1143987,1292554220303257,1292554220678257,"[NULL]",375000,1292554221904210,1292554222054210,1225953,150000,1292554231391210,1292554231468210,9337000,77000,1292554231758210,290000,1292554232098210,1292554232394210,340000,296000,1292554232521633,1292554232675633,127423,154000,1292554232737633,62000,1292554233779633,1042000,1292554235226131,1292554235705131,1446498,479000,1292554247300270,1292554263549270,11595139,16249000
+ -2143831735395280200,0.000000,0,0,1,0,1,1292554274773270,"[NULL]",971987,1292554275745257,1292554276049257,"[NULL]",304000,1292554276887210,1292554277031210,837953,144000,1292554286887210,1292554287129210,9856000,242000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
+ -2143831735395280196,11.111000,1,0,1,0,1,1292554252553270,"[NULL]",747987,1292554253301257,1292554253646257,"[NULL]",345000,1292554254466210,1292554254585210,819953,119000,1292554266517210,1292554266634210,11932000,117000,1292554267072210,438000,1292554267538210,1292554267740210,466000,202000,1292554268209633,1292554268405633,469423,196000,1292554268466633,61000,1292554269613633,1147000,1292554271890131,1292554272585131,2276498,695000,1292554280861270,1292554296910270,8276139,16049000
+ -2143831735395280194,0.000000,0,0,1,0,1,1292554263653270,"[NULL]",946987,1292554264600257,1292554264879257,"[NULL]",279000,1292554266795210,1292554266988210,1915953,193000,1292554276544210,1292554276677210,9556000,133000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
+ -2143831735395280183,11.111000,0,0,0,0,0,1292554034979270,1292554038935257,3955987,1292554039221257,1292554039362257,286000,141000,1292554039380210,1292554039504210,17953,124000,1292554042749210,1292554043545210,3245000,796000,1292554043721210,176000,1292554044172210,1292554044487210,451000,315000,1292554044656633,1292554045326633,169423,670000,1292554047111633,1785000,1292554048159633,1048000,1292554049700131,1292554050158131,1540498,458000,1292554058809270,1292554074705270,8651139,15896000
+ -2143831735395280179,11.111000,1,0,0,0,1,1292554029441270,1292554037281257,7839987,1292554037618257,1292554037785257,337000,167000,1292554038237210,1292554038326210,451953,89000,1292554042749210,1292554043429210,4423000,680000,1292554043721210,292000,1292554044172210,1292554044487210,451000,315000,1292554044656633,1292554045326633,169423,670000,1292554047111633,1785000,1292554048159633,1048000,1292554049700131,1292554050158131,1540498,458000,1292554058809270,1292554074705270,8651139,15896000
+ -2143831735395280166,11.111000,1,0,0,1,1,1292554023976270,1292554027681257,3704987,1292554029847257,1292554030083257,2166000,236000,1292554030360210,1292554030737210,276953,377000,1292554030360210,1292554030725210,-377000,365000,1292554030873210,148000,1292554031441210,1292554031668210,568000,227000,1292554032587633,1292554033341633,919423,754000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",1292554048008270,1292554063604270,"[NULL]",15596000
+ """))
+
+ def test_chrome_scroll_update_info_step_templates(self):
+ # Verify that chrome_scroll_update_info_step_templates references at
+ # least one valid column name and no invalid column names in
+ # chrome_scroll_update_info.
+ return DiffTestBlueprint(
+ trace=DataPath('scroll_m131.pftrace'),
+ query="""
+ INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
+
+ WITH referenced_column_names AS (
+ SELECT
+ ts_column_name AS column_name
+ FROM chrome_scroll_update_info_step_templates
+ WHERE column_name IS NOT NULL
+ UNION ALL
+ SELECT
+ dur_column_name AS column_name
+ FROM chrome_scroll_update_info_step_templates
+ WHERE column_name IS NOT NULL
+ ),
+ valid_column_names AS (
+ SELECT name AS column_name
+ FROM pragma_table_info('chrome_scroll_update_info')
+ )
+ SELECT
+ "valid" AS validity,
+ EXISTS (
+ SELECT column_name FROM referenced_column_names
+ WHERE column_name IN valid_column_names
+ ) AS existence
+ UNION ALL
+ SELECT
+ "invalid" AS validity,
+ EXISTS (
+ SELECT column_name FROM referenced_column_names
+ WHERE column_name NOT IN valid_column_names
+ ) AS existence
+ ORDER BY validity DESC;
+ """,
+ out=Csv("""
+ "validity","existence"
+ "valid",1
+ "invalid",0
+ """))
# A trace from M132 (ToT as of adding this test) has the necessary
# events/arguments (including the ones from the 'view' atrace category).
diff --git a/test/trace_processor/diff_tests/stdlib/linux/tests.py b/test/trace_processor/diff_tests/stdlib/linux/tests.py
index 783625f..4093c2a 100644
--- a/test/trace_processor/diff_tests/stdlib/linux/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/linux/tests.py
@@ -28,23 +28,23 @@
query="""
INCLUDE PERFETTO MODULE linux.threads;
- SELECT upid, utid, pid, tid, process_name, thread_name
+ SELECT pid, tid, process_name, thread_name
FROM linux_kernel_threads
- ORDER by utid LIMIT 10;
+ ORDER by tid LIMIT 10;
""",
out=Csv("""
- "upid","utid","pid","tid","process_name","thread_name"
- 7,14,510,510,"sugov:0","sugov:0"
- 89,23,1365,1365,"com.google.usf.","com.google.usf."
- 87,37,1249,1249,"irq/357-dwc3","irq/357-dwc3"
- 31,38,6,6,"kworker/u16:0","kworker/u16:0"
- 11,42,511,511,"sugov:4","sugov:4"
- 83,43,1152,1152,"irq/502-fts_ts","irq/502-fts_ts"
- 93,44,2374,2374,"csf_sync_update","csf_sync_update"
- 18,45,2379,2379,"csf_kcpu_0","csf_kcpu_0"
- 12,47,247,247,"decon0_kthread","decon0_kthread"
- 65,48,159,159,"spi0","spi0"
- """))
+ "pid","tid","process_name","thread_name"
+ 2,2,"kthreadd","kthreadd"
+ 5,5,"kworker/0:0H","kworker/0:0H"
+ 6,6,"kworker/u16:0","kworker/u16:0"
+ 8,8,"kworker/u16:1","kworker/u16:1"
+ 11,11,"ksoftirqd/0","ksoftirqd/0"
+ 12,12,"rcu_preempt","rcu_preempt"
+ 13,13,"rcuog/0","rcuog/0"
+ 14,14,"rcuop/0","rcuop/0"
+ 15,15,"rcub/0","rcub/0"
+ 17,17,"rcu_exp_gp_kthr","rcu_exp_gp_kthr"
+ """))
# Tests that DSU devfreq counters are working properly
def test_dsu_devfreq(self):
diff --git a/test/trace_processor/diff_tests/stdlib/viz/tests.py b/test/trace_processor/diff_tests/stdlib/viz/tests.py
index 0b986bb..ae1bf05 100644
--- a/test/trace_processor/diff_tests/stdlib/viz/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/viz/tests.py
@@ -271,13 +271,14 @@
return DiffTestBlueprint(
trace=self.chronological_trace,
query="""
- INCLUDE PERFETTO MODULE viz.summary.tracks;
- SELECT id, order_id
- FROM _track_event_tracks_ordered
- ORDER BY id;
+ INCLUDE PERFETTO MODULE viz.summary.track_event;
+ SELECT cast_int!(track_ids) as id, order_id
+ FROM _track_event_tracks_ordered_groups
+ ORDER BY track_ids;
""",
out=Csv("""
"id","order_id"
+ 0,1
1,2
2,1
"""))
@@ -305,13 +306,14 @@
return DiffTestBlueprint(
trace=self.explicit_trace,
query="""
- INCLUDE PERFETTO MODULE viz.summary.tracks;
- SELECT id, order_id
- FROM _track_event_tracks_ordered
+ INCLUDE PERFETTO MODULE viz.summary.track_event;
+ SELECT cast_int!(track_ids) as id, order_id
+ FROM _track_event_tracks_ordered_groups
ORDER BY id;
""",
out=Csv("""
"id","order_id"
+ 0,1
1,2
2,3
3,1
@@ -341,13 +343,14 @@
return DiffTestBlueprint(
trace=self.lexicographic_trace,
query="""
- INCLUDE PERFETTO MODULE viz.summary.tracks;
- SELECT id, order_id
- FROM _track_event_tracks_ordered
+ INCLUDE PERFETTO MODULE viz.summary.track_event;
+ SELECT cast_int!(track_ids) as id, order_id
+ FROM _track_event_tracks_ordered_groups
ORDER BY id;
""",
out=Csv("""
"id","order_id"
+ 0,1
1,2
2,1
3,3
@@ -385,14 +388,16 @@
return DiffTestBlueprint(
trace=self.all_ordering_trace,
query="""
- INCLUDE PERFETTO MODULE viz.summary.tracks;
- SELECT id, parent_id, order_id
- FROM _track_event_tracks_ordered
- JOIN track USING (id)
+ INCLUDE PERFETTO MODULE viz.summary.track_event;
+ SELECT cast_int!(track_ids) as id, parent_id, order_id
+ FROM _track_event_tracks_ordered_groups
ORDER BY parent_id, id
""",
out=Csv("""
"id","parent_id","order_id"
+ 0,"[NULL]",1
+ 3,"[NULL]",3
+ 5,"[NULL]",2
1,0,2
2,0,1
4,3,1
@@ -438,20 +443,22 @@
return DiffTestBlueprint(
trace=Path('track_event_tracks_ordering.textproto'),
query="""
- INCLUDE PERFETTO MODULE viz.summary.tracks;
- SELECT id, order_id
- FROM _track_event_tracks_ordered
+ INCLUDE PERFETTO MODULE viz.summary.track_event;
+ SELECT cast_int!(track_ids) as id, order_id
+ FROM _track_event_tracks_ordered_groups
ORDER BY id;
""",
out=Csv("""
"id","order_id"
+ 0,2
1,3
2,4
3,1
+ 4,1
5,1
6,2
7,3
- 8,2
+ 9,3
10,1
11,2
12,4
diff --git a/test/trace_processor/diff_tests/stdlib/wattson/tests.py b/test/trace_processor/diff_tests/stdlib/wattson/tests.py
index 1f86878..b13a767 100644
--- a/test/trace_processor/diff_tests/stdlib/wattson/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/wattson/tests.py
@@ -161,7 +161,7 @@
out=Csv("""
"duration","freq_0","idle_0","freq_1","idle_1","freq_2","idle_2","freq_3","idle_3","suspended"
16606175990,614400,1,614400,1,614400,1,614400,1,0
- 10648392546,1708800,-1,1708800,-1,1708800,-1,1708800,-1,1
+ 10648392546,1708800,1,1708800,1,1708800,1,1708800,1,1
6972220533,1708800,-1,1708800,-1,1708800,-1,1708800,-1,0
1649400745,614400,0,614400,0,614400,0,614400,0,0
1206977074,614400,-1,614400,1,614400,1,614400,1,0
@@ -451,10 +451,10 @@
"""),
out=Csv("""
"ts","dur","cpu0_id","cpu1_id","cpu2_id","cpu3_id","suspended"
- 385019771468,61975407053,12041,12218,10488,8910,1
- 448320364476,3674872885,13005,12954,11166,9272,1
- 452415394221,69579176303,13654,13361,11651,9609,1
- 564873995228,135118729231,45223,37594,22798,20132,1
+ 385019771468,61975407053,12042,12219,10489,8911,1
+ 448320364476,3674872885,13008,12957,11169,9275,1
+ 452415394221,69579176303,13659,13366,11656,9614,1
+ 564873995228,135118729231,45230,37601,22805,20139,1
"""))
# Tests traces from VM that have incomplete CPU tracks
@@ -485,7 +485,7 @@
"""))
# Tests suspend path with devfreq code path
- def test_wattson_devfreq_suspend(self):
+ def test_wattson_devfreq_hotplug_and_suspend(self):
return DiffTestBlueprint(
trace=DataPath('wattson_cpuhp_devfreq_suspend.pb'),
query=("""
@@ -494,14 +494,16 @@
ts, dur, cpu0_mw, cpu1_mw, cpu2_mw, cpu3_mw, cpu4_mw, cpu5_mw,
cpu6_mw, cpu7_mw, dsu_scu_mw
FROM _system_state_mw
- WHERE ts > 165725472126
- LIMIT 4
+ WHERE ts > 165725449108
+ LIMIT 6
"""),
out=Csv("""
"ts","dur","cpu0_mw","cpu1_mw","cpu2_mw","cpu3_mw","cpu4_mw","cpu5_mw","cpu6_mw","cpu7_mw","dsu_scu_mw"
- 165725475055,6999,0.000000,111.020000,111.020000,111.020000,267.180000,267.180000,267.180000,375.490000,14.560000
- 165725482054,1546,111.020000,111.020000,111.020000,111.020000,267.180000,267.180000,267.180000,375.490000,14.560000
- 165725483600,4468465,111.020000,111.020000,111.020000,111.020000,267.180000,267.180000,267.180000,375.490000,14.560000
+ 165725450194,7527,111.020000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,375.490000,14.560000
+ 165725457721,17334,111.020000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,14.560000
+ 165725475055,6999,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000
+ 165725482054,1546,111.020000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,14.560000
+ 165725483600,4468465,111.020000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,14.560000
165729952065,73480460119,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000
"""))
@@ -529,3 +531,28 @@
1450347293559,715573,3,0
1450348009132,82292,3,-1
"""))
+
+ # Tests that hotplug slices that defined CPU off region are correct
+ def test_wattson_hotplug_tk(self):
+ return DiffTestBlueprint(
+ trace=DataPath('wattson_cpuhp_devfreq_suspend.pb'),
+ query=("""
+ INCLUDE PERFETTO MODULE wattson.cpu_hotplug;
+ SELECT cpu, ts, dur
+ FROM _gapless_hotplug_slices
+ WHERE cpu < 2
+ """),
+ out=Csv("""
+ "cpu","ts","dur"
+ 0,86747008512,302795933205
+ 1,86747008512,3769632400
+ 1,90516640912,4341919
+ 1,90520982831,73692291133
+ 1,164213273964,1478796428
+ 1,165692070392,73525895666
+ 1,239217966058,10896074956
+ 1,250114041014,95948
+ 1,250114136962,4705159
+ 1,250118842121,137102890041
+ 1,387221732162,2321209555
+ """))
diff --git a/test/trace_processor/diff_tests/tables/counter_dur_test.sql b/test/trace_processor/diff_tests/tables/counter_dur_test.sql
deleted file mode 100644
index 67a60b9..0000000
--- a/test/trace_processor/diff_tests/tables/counter_dur_test.sql
+++ /dev/null
@@ -1 +0,0 @@
-SELECT ts, dur FROM experimental_counter_dur WHERE track_id IN (1, 2, 3) ORDER BY dur LIMIT 10;
diff --git a/test/trace_processor/diff_tests/tables/tests.py b/test/trace_processor/diff_tests/tables/tests.py
index c5454dd..7d6b150 100644
--- a/test/trace_processor/diff_tests/tables/tests.py
+++ b/test/trace_processor/diff_tests/tables/tests.py
@@ -402,8 +402,8 @@
query="SELECT id, slice_out, slice_in, trace_id, arg_set_id FROM flow;",
out=Csv("""
"id","slice_out","slice_in","trace_id","arg_set_id"
- 0,0,1,57,0
- 1,1,2,57,0
+ 0,0,1,57,"[NULL]"
+ 1,1,2,57,"[NULL]"
"""))
def test_clock_snapshot_table_multiplier(self):
diff --git a/test/trace_processor/diff_tests/tables/tests_counters.py b/test/trace_processor/diff_tests/tables/tests_counters.py
index fe3f9fb..ace4cf9 100644
--- a/test/trace_processor/diff_tests/tables/tests_counters.py
+++ b/test/trace_processor/diff_tests/tables/tests_counters.py
@@ -108,52 +108,6 @@
""",
out=Path('filter_row_vector_example_android_trace_30s.out'))
- def test_counter_dur_example_android_trace_30s(self):
- return DiffTestBlueprint(
- trace=DataPath('example_android_trace_30s.pb'),
- query=Path('counter_dur_test.sql'),
- out=Csv("""
- "ts","dur"
- 100351738640,-1
- 100351738640,-1
- 100351738640,-1
- 70731059648,19510835
- 70731059648,19510835
- 70731059648,19510835
- 73727335051,23522762
- 73727335051,23522762
- 73727335051,23522762
- 86726132752,24487554
- """))
-
- def test_counter_dur_example_android_trace_30s_machine_id(self):
- return DiffTestBlueprint(
- trace=DataPath('example_android_trace_30s.pb'),
- trace_modifier=TraceInjector(
- ['ftrace_events', 'sys_stats', 'process_stats', 'process_tree'],
- {'machine_id': 1001}),
- query="""
- SELECT ts, dur, m.raw_id as raw_machine_id
- FROM experimental_counter_dur c
- JOIN counter_track t on c.track_id = t.id
- JOIN machine m on t.machine_id = m.id
- WHERE track_id IN (1, 2, 3)
- ORDER BY dur LIMIT 10;
- """,
- out=Csv("""
- "ts","dur","raw_machine_id"
- 100351738640,-1,1001
- 100351738640,-1,1001
- 100351738640,-1,1001
- 70731059648,19510835,1001
- 70731059648,19510835,1001
- 70731059648,19510835,1001
- 73727335051,23522762,1001
- 73727335051,23522762,1001
- 73727335051,23522762,1001
- 86726132752,24487554,1001
- """))
-
# Tests counter.machine_id and process_counter_track.machine.
def test_filter_row_vector_example_android_trace_30s_machine_id(self):
return DiffTestBlueprint(
@@ -287,8 +241,8 @@
""",
out=Csv("""
"id","arg_set_id"
- 1,1
- 15,8
+ 1,0
+ 15,14
"""))
def test_cpu_counter_track_multi_machine(self):
diff --git a/test/trace_processor/diff_tests/tables/tests_sched.py b/test/trace_processor/diff_tests/tables/tests_sched.py
index 82ec0bd..c1416d1 100644
--- a/test/trace_processor/diff_tests/tables/tests_sched.py
+++ b/test/trace_processor/diff_tests/tables/tests_sched.py
@@ -120,27 +120,27 @@
15748
"""))
- def test_raw_common_flags(self):
+ def test_ftrace_event_common_flags(self):
return DiffTestBlueprint(
trace=DataPath('sched_wakeup_trace.atr'),
query="""
SELECT id, ts, name, cpu, utid, arg_set_id, common_flags
- FROM raw
+ FROM ftrace_event
WHERE common_flags != 0
ORDER BY ts LIMIT 10
""",
out=Csv("""
- "id","ts","name","cpu","utid","arg_set_id","common_flags"
- 3,1735489788930,"sched_waking",0,300,4,1
- 4,1735489812571,"sched_waking",0,300,5,1
- 5,1735489833977,"sched_waking",1,305,6,1
- 8,1735489876788,"sched_waking",1,297,9,1
- 9,1735489879097,"sched_waking",0,304,10,1
- 12,1735489933912,"sched_waking",0,428,13,1
- 14,1735489972385,"sched_waking",1,232,15,1
- 17,1735489999987,"sched_waking",1,232,15,1
- 19,1735490039439,"sched_waking",1,298,18,1
- 20,1735490042084,"sched_waking",1,298,19,1
+ "id","ts","name","cpu","utid","arg_set_id","common_flags"
+ 3,1735489788930,"sched_waking",0,300,21,1
+ 4,1735489812571,"sched_waking",0,300,25,1
+ 5,1735489833977,"sched_waking",1,305,29,1
+ 8,1735489876788,"sched_waking",1,297,47,1
+ 9,1735489879097,"sched_waking",0,304,51,1
+ 12,1735489933912,"sched_waking",0,428,69,1
+ 14,1735489972385,"sched_waking",1,232,80,1
+ 17,1735489999987,"sched_waking",1,232,80,1
+ 19,1735490039439,"sched_waking",1,298,98,1
+ 20,1735490042084,"sched_waking",1,298,102,1
"""))
def test_thread_executing_span_graph(self):
@@ -677,13 +677,13 @@
"""))
# Test the support of machine_id ID of the raw table.
- def test_raw_machine_id(self):
+ def test_ftrace_event_machine_id(self):
return DiffTestBlueprint(
trace=DataPath('android_sched_and_ps.pb'),
trace_modifier=TraceInjector(['ftrace_events'], {'machine_id': 1001}),
query="""
SELECT count(*)
- FROM raw LEFT JOIN cpu USING (ucpu)
+ FROM ftrace_event LEFT JOIN cpu USING (ucpu)
WHERE machine_id is NULL;
""",
out=Csv("""
diff --git a/tools/bazel b/tools/bazel
new file mode 100755
index 0000000..5c618c7
--- /dev/null
+++ b/tools/bazel
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+# 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 os
+import sys
+
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.append(ROOT_DIR)
+__package__ = 'tools'
+from .run_buildtools_binary import run_buildtools_binary
+run_buildtools_binary(['bazel'] + sys.argv[1:])
diff --git a/tools/check_sql_metrics.py b/tools/check_sql_metrics.py
index 4d9810f..e56336c 100755
--- a/tools/check_sql_metrics.py
+++ b/tools/check_sql_metrics.py
@@ -66,6 +66,9 @@
'android_blocking_calls_cuj_calls'
],
('/android'
+ '/android_blocking_calls_cuj_per_frame_metric.sql'): [
+ 'android_cujs'],
+ ('/android'
'/android_blocking_calls_unagg.sql'): [
'filtered_processes_with_non_zero_blocking_calls', 'process_info',
'android_blocking_calls_unagg_calls'
diff --git a/tools/cpu_profile b/tools/cpu_profile
index 5339b16..e9e28c5 100755
--- a/tools/cpu_profile
+++ b/tools/cpu_profile
@@ -37,18 +37,18 @@
# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
TRACECONV_MANIFEST = [{
'arch':
'mac-amd64',
'file_name':
'traceconv',
'file_size':
- 9041560,
+ 9599720,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/traceconv',
'sha256':
- 'cec2da5cb771a4812d0b2d15604d5023954d28e0af12e87313da2ab70d26b970',
+ '5e583da4ee716b077a649f366049fbe1eed8ff8f469db92d841307eb817e06c7',
'platform':
'darwin',
'machine': ['x86_64']
@@ -58,11 +58,11 @@
'file_name':
'traceconv',
'file_size':
- 8375512,
+ 8920424,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/traceconv',
'sha256':
- '64e200a58ea9c9f366e1071dd274d0023d1fd14043f75dbba3fe0cc138ff5fc7',
+ '794f45213cb81511c6e2594c47d917ce407650d81c16e2ff1442685e5da3a533',
'platform':
'darwin',
'machine': ['arm64']
@@ -72,11 +72,11 @@
'file_name':
'traceconv',
'file_size':
- 9134136,
+ 9920848,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/traceconv',
'sha256':
- '87b87e1778367c1e3b99fc77439a28b4911125d2751f9909fd1b51f6bd60b6f4',
+ '5f0b86cfb8d75fd574aaabc36c97d229d5234511d3bf77ddcc2a180b96cbd014',
'platform':
'linux',
'machine': ['x86_64']
@@ -86,11 +86,11 @@
'file_name':
'traceconv',
'file_size':
- 6753020,
+ 7430084,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/traceconv',
'sha256':
- '804c4e13aca5798731056952d9cb0c6ee58795c03477c69514ccd39703060812',
+ '1561f9bbbd2b192b834132bd8c515cfde6f6afe2117bf68ac0aeb3caedfeb3fd',
'platform':
'linux',
'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -100,11 +100,11 @@
'file_name':
'traceconv',
'file_size':
- 8740064,
+ 9479552,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/traceconv',
'sha256':
- '0d781886531d11e1d573a1ec5e06376ef139bb479eec38c16c8735821c35b895',
+ '4cb56805a5d1baf5756f459d5fa4a05c982faffc8fc96d9760ca3e86c6ced279',
'platform':
'linux',
'machine': ['aarch64']
@@ -114,55 +114,55 @@
'file_name':
'traceconv',
'file_size':
- 6792280,
+ 7329320,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/traceconv',
'sha256':
- '7d91e4133184a3722a25488edd3692c5a195148eba56621014311d3f85d3fc15'
+ '719ac44e87c45a58d1ba3a6264518ed7384f738cdc293703b7e9a29ebcde6788'
}, {
'arch':
'android-arm64',
'file_name':
'traceconv',
'file_size':
- 8677992,
+ 9232824,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/traceconv',
'sha256':
- 'c03c4a901ed23f1e20a12c98ce4556353a62bddcd260fb4d797cd29ff6c49a05'
+ 'a76f954e8b6bba1e302ee136745ae5a478ba4737bf97bde1f8eeeec1b5238de2'
}, {
'arch':
'android-x86',
'file_name':
'traceconv',
'file_size':
- 9503704,
+ 10121840,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/traceconv',
'sha256':
- '704e58a7249de56aadec64d4c0d83bab0821d2c4fd77114a9b71705ff4224539'
+ '7dcbe7ce3962155a156cb3e85e7fe17389973f93bf22b14ce13e45173f263ea4'
}, {
'arch':
'android-x64',
'file_name':
'traceconv',
'file_size':
- 8964488,
+ 9554408,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/traceconv',
'sha256':
- 'e4f07836fc2a5fb7cd997a9acc4183af7a06997d1e73aac71021af5114b921bc'
+ 'e44bc63def32674c99c67e5525ea36b66b6c1714e8fffe7606957813aaf212fa'
}, {
'arch':
'windows-amd64',
'file_name':
'traceconv.exe',
'file_size':
- 8763904,
+ 9316864,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/traceconv.exe',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/windows-amd64/traceconv.exe',
'sha256':
- '084670ac28ed59a9642782a30e051735c1b7474b8cd569b9bc94c305af68290e',
+ '937df755c7a54484c1a1aa1bbbaf392d978c300d6ca631bd9d9a20fe2b974deb',
'platform':
'win32',
'machine': ['amd64']
diff --git a/tools/gen_stdlib_docs_json.py b/tools/gen_stdlib_docs_json.py
index 27b2c5c..e08d31e 100755
--- a/tools/gen_stdlib_docs_json.py
+++ b/tools/gen_stdlib_docs_json.py
@@ -17,6 +17,7 @@
import os
import sys
import json
+import re
from collections import defaultdict
from typing import Dict
@@ -29,6 +30,13 @@
def _summary_desc(s: str) -> str:
return s.split('. ')[0].replace('\n', ' ')
+def _long_type_to_table(s: str):
+ pattern = '(?:[A-Z]*)\(([a-z_]*).([a-z_]*)\)'
+ m = re.match(pattern, s)
+ if not m:
+ return (None, None)
+ g = m.groups()
+ return (g[0], g[1])
def main():
parser = argparse.ArgumentParser()
@@ -101,7 +109,9 @@
'cols': [{
'name': col_name,
'type': col.long_type,
- 'desc': col.description
+ 'desc': col.description,
+ 'table': _long_type_to_table(col.long_type)[0],
+ 'column': _long_type_to_table(col.long_type)[1],
} for (col_name, col) in table.cols.items()]
} for table in docs.table_views],
'functions': [{
@@ -112,6 +122,8 @@
'name': arg_name,
'type': arg.long_type,
'desc': arg.description,
+ 'table': _long_type_to_table(arg.long_type)[0],
+ 'column': _long_type_to_table(arg.long_type)[1],
} for (arg_name, arg) in function.args.items()],
'return_type': function.return_type,
'return_desc': function.return_desc,
@@ -127,10 +139,14 @@
'name': arg_name,
'type': arg.long_type,
'desc': arg.description,
+ 'table': _long_type_to_table(arg.long_type)[0],
+ 'column': _long_type_to_table(arg.long_type)[1],
} for (arg_name, arg) in function.args.items()],
'cols': [{
'name': col_name,
'type': col.long_type,
+ 'table': _long_type_to_table(col.long_type)[0],
+ 'column': _long_type_to_table(col.long_type)[1],
'desc': col.description
} for (col_name, col) in function.cols.items()]
} for function in docs.table_functions],
@@ -149,6 +165,8 @@
'name': arg_name,
'type': arg.long_type,
'desc': arg.description,
+ 'table': _long_type_to_table(arg.long_type)[0],
+ 'column': _long_type_to_table(arg.long_type)[1],
} for (arg_name, arg) in macro.args.items()],
} for macro in docs.macros],
}
diff --git a/tools/gen_tp_table_docs.py b/tools/gen_tp_table_docs.py
index c69ab58..39f016a 100755
--- a/tools/gen_tp_table_docs.py
+++ b/tools/gen_tp_table_docs.py
@@ -40,7 +40,7 @@
assert table.table.tabledoc
# id and type columns should be skipped if the table specifies so.
- is_skippable_col = col.is_implicit_id or col.is_implicit_type
+ is_skippable_col = col.is_implicit_id
if table.table.tabledoc.skip_id_and_type and is_skippable_col:
return None
diff --git a/tools/heap_profile b/tools/heap_profile
index df14a90..150bad2 100755
--- a/tools/heap_profile
+++ b/tools/heap_profile
@@ -34,18 +34,18 @@
# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
TRACECONV_MANIFEST = [{
'arch':
'mac-amd64',
'file_name':
'traceconv',
'file_size':
- 9041560,
+ 9599720,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/traceconv',
'sha256':
- 'cec2da5cb771a4812d0b2d15604d5023954d28e0af12e87313da2ab70d26b970',
+ '5e583da4ee716b077a649f366049fbe1eed8ff8f469db92d841307eb817e06c7',
'platform':
'darwin',
'machine': ['x86_64']
@@ -55,11 +55,11 @@
'file_name':
'traceconv',
'file_size':
- 8375512,
+ 8920424,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/traceconv',
'sha256':
- '64e200a58ea9c9f366e1071dd274d0023d1fd14043f75dbba3fe0cc138ff5fc7',
+ '794f45213cb81511c6e2594c47d917ce407650d81c16e2ff1442685e5da3a533',
'platform':
'darwin',
'machine': ['arm64']
@@ -69,11 +69,11 @@
'file_name':
'traceconv',
'file_size':
- 9134136,
+ 9920848,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/traceconv',
'sha256':
- '87b87e1778367c1e3b99fc77439a28b4911125d2751f9909fd1b51f6bd60b6f4',
+ '5f0b86cfb8d75fd574aaabc36c97d229d5234511d3bf77ddcc2a180b96cbd014',
'platform':
'linux',
'machine': ['x86_64']
@@ -83,11 +83,11 @@
'file_name':
'traceconv',
'file_size':
- 6753020,
+ 7430084,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/traceconv',
'sha256':
- '804c4e13aca5798731056952d9cb0c6ee58795c03477c69514ccd39703060812',
+ '1561f9bbbd2b192b834132bd8c515cfde6f6afe2117bf68ac0aeb3caedfeb3fd',
'platform':
'linux',
'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -97,11 +97,11 @@
'file_name':
'traceconv',
'file_size':
- 8740064,
+ 9479552,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/traceconv',
'sha256':
- '0d781886531d11e1d573a1ec5e06376ef139bb479eec38c16c8735821c35b895',
+ '4cb56805a5d1baf5756f459d5fa4a05c982faffc8fc96d9760ca3e86c6ced279',
'platform':
'linux',
'machine': ['aarch64']
@@ -111,55 +111,55 @@
'file_name':
'traceconv',
'file_size':
- 6792280,
+ 7329320,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/traceconv',
'sha256':
- '7d91e4133184a3722a25488edd3692c5a195148eba56621014311d3f85d3fc15'
+ '719ac44e87c45a58d1ba3a6264518ed7384f738cdc293703b7e9a29ebcde6788'
}, {
'arch':
'android-arm64',
'file_name':
'traceconv',
'file_size':
- 8677992,
+ 9232824,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/traceconv',
'sha256':
- 'c03c4a901ed23f1e20a12c98ce4556353a62bddcd260fb4d797cd29ff6c49a05'
+ 'a76f954e8b6bba1e302ee136745ae5a478ba4737bf97bde1f8eeeec1b5238de2'
}, {
'arch':
'android-x86',
'file_name':
'traceconv',
'file_size':
- 9503704,
+ 10121840,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/traceconv',
'sha256':
- '704e58a7249de56aadec64d4c0d83bab0821d2c4fd77114a9b71705ff4224539'
+ '7dcbe7ce3962155a156cb3e85e7fe17389973f93bf22b14ce13e45173f263ea4'
}, {
'arch':
'android-x64',
'file_name':
'traceconv',
'file_size':
- 8964488,
+ 9554408,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/traceconv',
'sha256':
- 'e4f07836fc2a5fb7cd997a9acc4183af7a06997d1e73aac71021af5114b921bc'
+ 'e44bc63def32674c99c67e5525ea36b66b6c1714e8fffe7606957813aaf212fa'
}, {
'arch':
'windows-amd64',
'file_name':
'traceconv.exe',
'file_size':
- 8763904,
+ 9316864,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/traceconv.exe',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/windows-amd64/traceconv.exe',
'sha256':
- '084670ac28ed59a9642782a30e051735c1b7474b8cd569b9bc94c305af68290e',
+ '937df755c7a54484c1a1aa1bbbaf392d978c300d6ca631bd9d9a20fe2b974deb',
'platform':
'win32',
'machine': ['amd64']
diff --git a/tools/install-build-deps b/tools/install-build-deps
index a555d56..a29618b 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -330,6 +330,31 @@
'0ce01e934f95efb6a216a6efa35af1245151c779', 'all', 'all'),
]
+# Dependencies required to build code on the host using Bazel build system.
+# Only macOS and Linux.
+BUILD_DEPS_BAZEL = [
+ Dependency(
+ 'buildtools/mac/bazel',
+ 'https://github.com/bazelbuild/bazel/releases/download/7.4.1/bazel-7.4.1-darwin-x86_64',
+ '52dd34c17cc97b3aa5bdfe3d45c4e3938226f23dd0bfb47beedd625a953f1f05',
+ 'darwin', 'x64'),
+ Dependency(
+ 'buildtools/mac/bazel',
+ 'https://github.com/bazelbuild/bazel/releases/download/7.4.1/bazel-7.4.1-darwin-arm64',
+ '02b117b97d0921ae4d4f4e11d27e2c0930381df416e373435d5d0419c6a26f24',
+ 'darwin', 'arm64'),
+ Dependency(
+ 'buildtools/linux64/bazel',
+ 'https://github.com/bazelbuild/bazel/releases/download/7.4.1/bazel-7.4.1-linux-x86_64',
+ 'c97f02133adce63f0c28678ac1f21d65fa8255c80429b588aeeba8a1fac6202b',
+ 'linux', 'x64'),
+ Dependency(
+ 'buildtools/linux64/bazel',
+ 'https://github.com/bazelbuild/bazel/releases/download/7.4.1/bazel-7.4.1-linux-arm64',
+ 'd7aedc8565ed47b6231badb80b09f034e389c5f2b1c2ac2c55406f7c661d8b88',
+ 'linux', 'arm64'),
+]
+
# Dependencies required to build Android code.
# URLs and SHA1s taken from:
# - https://dl.google.com/android/repository/repository-11.xml
@@ -468,8 +493,8 @@
]
ALL_DEPS = (
- BUILD_DEPS_HOST + BUILD_DEPS_ANDROID + BUILD_DEPS_LINUX_CROSS_SYSROOTS +
- TEST_DEPS_ANDROID + UI_DEPS)
+ BUILD_DEPS_HOST + BUILD_DEPS_BAZEL + BUILD_DEPS_ANDROID +
+ BUILD_DEPS_LINUX_CROSS_SYSROOTS + TEST_DEPS_ANDROID + UI_DEPS)
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
UI_DIR = os.path.join(ROOT_DIR, 'ui')
@@ -676,6 +701,10 @@
def Main():
parser = argparse.ArgumentParser()
parser.add_argument(
+ '--bazel',
+ action='store_true',
+ help='Bazel build tool executable to build the project using Bazel')
+ parser.add_argument(
'--android',
action='store_true',
help='NDK and emulator images target_os="android"')
@@ -720,6 +749,8 @@
deps = BUILD_DEPS_HOST
if not args.no_toolchain:
deps += BUILD_DEPS_TOOLCHAIN_HOST
+ if args.bazel:
+ deps += BUILD_DEPS_BAZEL
if args.android:
deps += BUILD_DEPS_ANDROID + TEST_DEPS_ANDROID
if args.linux_arm:
diff --git a/tools/record_android_trace b/tools/record_android_trace
index 203b636..322fce8 100755
--- a/tools/record_android_trace
+++ b/tools/record_android_trace
@@ -34,18 +34,18 @@
# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
TRACEBOX_MANIFEST = [{
'arch':
'mac-amd64',
'file_name':
'tracebox',
'file_size':
- 1613864,
+ 1646808,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/tracebox',
'sha256':
- 'dfb1a3affe905d2e7d1f82bc4dda46b1fda6db054d60ae87c3215dd529b77fee',
+ '85b3060ed4d49e2c8d69dbb4d6ff26ab662f9b28c0032791674c90683dd33d39',
'platform':
'darwin',
'machine': ['x86_64']
@@ -55,11 +55,11 @@
'file_name':
'tracebox',
'file_size':
- 1492184,
+ 1508856,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/tracebox',
'sha256':
- '4a492a629dd1f13f3146c4b8267c0b163afba8cef1d49e0c00c48bb727496066',
+ 'ea2cce845daf0eba469ff356b3bcefc8e9a384084569271a470b58a9dcbf8def',
'platform':
'darwin',
'machine': ['arm64']
@@ -69,11 +69,11 @@
'file_name':
'tracebox',
'file_size':
- 2380040,
+ 2415168,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/tracebox',
'sha256':
- 'd70b284e8c28858fd539ae61ca59764d7f9fd6232073c304926e892fe75e692a',
+ '5361676fb3c2490ae2136ab7a37dcd9e4ee5a2a6c0ba722facf3215a23a8c633',
'platform':
'linux',
'machine': ['x86_64']
@@ -83,11 +83,11 @@
'file_name':
'tracebox',
'file_size':
- 1450708,
+ 1478024,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/tracebox',
'sha256':
- '178fa6a1a9bc80f72d81938d40fe201c25c595ffaff7e030d59c2af09dfcc06c',
+ '18db321576be555d8c9281df9fc03aa6b3b4358ae2424ffbd65fc120cd650b8b',
'platform':
'linux',
'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -97,11 +97,11 @@
'file_name':
'tracebox',
'file_size':
- 2269816,
+ 2304384,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/tracebox',
'sha256':
- '42c64f9807756aaa08a2bfa13e9e4828c193a6b90ba1329408873c3ebf5adf3f',
+ '70c9e2b63eb92a82db65916c346b09867bfedc0c4593974c019102f485c0dc9d',
'platform':
'linux',
'machine': ['aarch64']
@@ -111,44 +111,44 @@
'file_name':
'tracebox',
'file_size':
- 1333336,
+ 1354916,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/tracebox',
'sha256':
- '93a78d2c42e3c00f117e2f155326383f69c891281ed693a39d87b8cb54ca4e19'
+ '724a1cb4774bdf8a64beb37194f7394df5a052c36369ea52f64fe519fcb40117'
}, {
'arch':
'android-arm64',
'file_name':
'tracebox',
'file_size':
- 2115984,
+ 2142008,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/tracebox',
'sha256':
- '508248a9e47ab605fd742efb700391d7267b68b586199a93e13e6ca14b72fe3d'
+ '7616bfc3be1269c3ac1eec5a1f868fb65c2830ed001b5fbcc3800c909c676848'
}, {
'arch':
'android-x86',
'file_name':
'tracebox',
'file_size':
- 2302960,
+ 2341884,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/tracebox',
'sha256':
- '63d20a69c4e0c291329d7917e640fa0d4f146c344e79988e87393b1431d594b1'
+ '29124bee9bf4e2e296b0c96071b8c9706b57d963cbf0359d6afd95a9049b2b82'
}, {
'arch':
'android-x64',
'file_name':
'tracebox',
'file_size':
- 2147880,
+ 2178416,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/tracebox',
'sha256':
- 'c0ea1d5fd6d020e4c2b45d4d45cdd0c44ae63cd755d69260a3e5d2bacd3cbd6a'
+ '826fffce1e138c1d5ac107492ee696c09ad83f9ae9aa647c810d71084f797509'
}]
# ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py
diff --git a/tools/trace_processor b/tools/trace_processor
index 29e6186..999589a 100755
--- a/tools/trace_processor
+++ b/tools/trace_processor
@@ -30,18 +30,18 @@
# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/trace_processor_shell.py
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
TRACE_PROCESSOR_SHELL_MANIFEST = [{
'arch':
'mac-amd64',
'file_name':
'trace_processor_shell',
'file_size':
- 9949656,
+ 10524008,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/trace_processor_shell',
'sha256':
- 'e9dcf95aaa02f8c00a724f0ff34ba3a454c717beb9900cf9fd97ab142b362452',
+ '867c70800cfe81c2640f2aae8bb58eca68fa1389a3258a25c285ee5510edbbe3',
'platform':
'darwin',
'machine': ['x86_64']
@@ -51,11 +51,11 @@
'file_name':
'trace_processor_shell',
'file_size':
- 9223224,
+ 9767976,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/trace_processor_shell',
'sha256':
- '9a0541a0f52f95bfcb8dc88d94bc4494c660d95eefc40fc946ab43d995051ff7',
+ '9c325030078bc4de8693083c9e4e2b72c83ca694c3a4ef8cc1bd9c29fb421815',
'platform':
'darwin',
'machine': ['arm64']
@@ -65,11 +65,11 @@
'file_name':
'trace_processor_shell',
'file_size':
- 10142800,
+ 10935344,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/trace_processor_shell',
'sha256':
- '18c8730b52f8ee1d9e202031527435b6b2e3149fbd9b1046b2e77d18f06aa337',
+ '6af6f87e6521eec186e74c68c0c6eeeeb557556e368d0e4f563be5ce5d9d936b',
'platform':
'linux',
'machine': ['x86_64']
@@ -79,11 +79,11 @@
'file_name':
'trace_processor_shell',
'file_size':
- 7329432,
+ 8010576,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/trace_processor_shell',
'sha256':
- '0558040998666576e1063d6d626b8aa9e354f18d73d225240f043b3c9236befa',
+ 'da0c361d4a2c8d8b2d1ffd45cd388d964cc58b09e8e41f48aa045ed357510755',
'platform':
'linux',
'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
'file_name':
'trace_processor_shell',
'file_size':
- 9703384,
+ 10448648,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/trace_processor_shell',
'sha256':
- 'eeb95cc54358df08375ffae4862c043a6737902179ce8e0408984004c32cf93c',
+ 'ecb6a1a073eb4bbfe36af56ab4406671e8febe02fb4c6dcef73fb1fe5d817fad',
'platform':
'linux',
'machine': ['aarch64']
@@ -107,55 +107,55 @@
'file_name':
'trace_processor_shell',
'file_size':
- 7367412,
+ 7905948,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/trace_processor_shell',
'sha256':
- 'd29b1e6aee52ceff24c072f56c7be7795d0fa29f3596e2633fafa60782384718'
+ 'efeed53819e11f5f82d1ec9c3edce00590b3db1e3e4f0e64acddafba2c35a52e'
}, {
'arch':
'android-arm64',
'file_name':
'trace_processor_shell',
'file_size':
- 9598784,
+ 10154712,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/trace_processor_shell',
'sha256':
- '06e80c562c0043cca9225ade3c961a081bcc7435660117d5a6db26b815d0b9ca'
+ 'c6b0bb85228e4a3785030bbaf718bb428580f7d31de127e73042a30b9a67128a'
}, {
'arch':
'android-x86',
'file_name':
'trace_processor_shell',
'file_size':
- 10625488,
+ 11244904,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/trace_processor_shell',
'sha256':
- '2a576fb397da14d0dabcfa97f5eeec15b4dc55df009308f75a5fdf9de8a9b0dd'
+ 'a113d0f68b89c63da4faefebc094a5be15620afdbe862a23127d55d7ff44ed43'
}, {
'arch':
'android-x64',
'file_name':
'trace_processor_shell',
'file_size':
- 9915664,
+ 10506544,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/trace_processor_shell',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/trace_processor_shell',
'sha256':
- 'a30be9f09b53110394e87af4d6b41ae24cd74d9a3f97ac1cc4d6ae2057ac6977'
+ 'f062e38fd28ab94b0232df8f4eeac70506bb7d304b82100e288d5acb603f84c1'
}, {
'arch':
'windows-amd64',
'file_name':
'trace_processor_shell.exe',
'file_size':
- 9922560,
+ 10479616,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/trace_processor_shell.exe',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/windows-amd64/trace_processor_shell.exe',
'sha256':
- 'd41639844a6c36dbaa195d91e9c356f2172d924c70a1bfed5432c407f857f009',
+ 'a881f3e2d4c6131493e85bfd1f36d1efe58e1478e2991825418d5d21614c1e48',
'platform':
'win32',
'machine': ['amd64']
diff --git a/tools/tracebox b/tools/tracebox
index 9389051..ee19bf8 100755
--- a/tools/tracebox
+++ b/tools/tracebox
@@ -30,18 +30,18 @@
# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
TRACEBOX_MANIFEST = [{
'arch':
'mac-amd64',
'file_name':
'tracebox',
'file_size':
- 1613864,
+ 1646808,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/tracebox',
'sha256':
- 'dfb1a3affe905d2e7d1f82bc4dda46b1fda6db054d60ae87c3215dd529b77fee',
+ '85b3060ed4d49e2c8d69dbb4d6ff26ab662f9b28c0032791674c90683dd33d39',
'platform':
'darwin',
'machine': ['x86_64']
@@ -51,11 +51,11 @@
'file_name':
'tracebox',
'file_size':
- 1492184,
+ 1508856,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/tracebox',
'sha256':
- '4a492a629dd1f13f3146c4b8267c0b163afba8cef1d49e0c00c48bb727496066',
+ 'ea2cce845daf0eba469ff356b3bcefc8e9a384084569271a470b58a9dcbf8def',
'platform':
'darwin',
'machine': ['arm64']
@@ -65,11 +65,11 @@
'file_name':
'tracebox',
'file_size':
- 2380040,
+ 2415168,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/tracebox',
'sha256':
- 'd70b284e8c28858fd539ae61ca59764d7f9fd6232073c304926e892fe75e692a',
+ '5361676fb3c2490ae2136ab7a37dcd9e4ee5a2a6c0ba722facf3215a23a8c633',
'platform':
'linux',
'machine': ['x86_64']
@@ -79,11 +79,11 @@
'file_name':
'tracebox',
'file_size':
- 1450708,
+ 1478024,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/tracebox',
'sha256':
- '178fa6a1a9bc80f72d81938d40fe201c25c595ffaff7e030d59c2af09dfcc06c',
+ '18db321576be555d8c9281df9fc03aa6b3b4358ae2424ffbd65fc120cd650b8b',
'platform':
'linux',
'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
'file_name':
'tracebox',
'file_size':
- 2269816,
+ 2304384,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/tracebox',
'sha256':
- '42c64f9807756aaa08a2bfa13e9e4828c193a6b90ba1329408873c3ebf5adf3f',
+ '70c9e2b63eb92a82db65916c346b09867bfedc0c4593974c019102f485c0dc9d',
'platform':
'linux',
'machine': ['aarch64']
@@ -107,44 +107,44 @@
'file_name':
'tracebox',
'file_size':
- 1333336,
+ 1354916,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/tracebox',
'sha256':
- '93a78d2c42e3c00f117e2f155326383f69c891281ed693a39d87b8cb54ca4e19'
+ '724a1cb4774bdf8a64beb37194f7394df5a052c36369ea52f64fe519fcb40117'
}, {
'arch':
'android-arm64',
'file_name':
'tracebox',
'file_size':
- 2115984,
+ 2142008,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/tracebox',
'sha256':
- '508248a9e47ab605fd742efb700391d7267b68b586199a93e13e6ca14b72fe3d'
+ '7616bfc3be1269c3ac1eec5a1f868fb65c2830ed001b5fbcc3800c909c676848'
}, {
'arch':
'android-x86',
'file_name':
'tracebox',
'file_size':
- 2302960,
+ 2341884,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/tracebox',
'sha256':
- '63d20a69c4e0c291329d7917e640fa0d4f146c344e79988e87393b1431d594b1'
+ '29124bee9bf4e2e296b0c96071b8c9706b57d963cbf0359d6afd95a9049b2b82'
}, {
'arch':
'android-x64',
'file_name':
'tracebox',
'file_size':
- 2147880,
+ 2178416,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/tracebox',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/tracebox',
'sha256':
- 'c0ea1d5fd6d020e4c2b45d4d45cdd0c44ae63cd755d69260a3e5d2bacd3cbd6a'
+ '826fffce1e138c1d5ac107492ee696c09ad83f9ae9aa647c810d71084f797509'
}]
# ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py
diff --git a/tools/traceconv b/tools/traceconv
index 55805b0..c69d6e4 100755
--- a/tools/traceconv
+++ b/tools/traceconv
@@ -30,18 +30,18 @@
# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
TRACECONV_MANIFEST = [{
'arch':
'mac-amd64',
'file_name':
'traceconv',
'file_size':
- 9041560,
+ 9599720,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/traceconv',
'sha256':
- 'cec2da5cb771a4812d0b2d15604d5023954d28e0af12e87313da2ab70d26b970',
+ '5e583da4ee716b077a649f366049fbe1eed8ff8f469db92d841307eb817e06c7',
'platform':
'darwin',
'machine': ['x86_64']
@@ -51,11 +51,11 @@
'file_name':
'traceconv',
'file_size':
- 8375512,
+ 8920424,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/traceconv',
'sha256':
- '64e200a58ea9c9f366e1071dd274d0023d1fd14043f75dbba3fe0cc138ff5fc7',
+ '794f45213cb81511c6e2594c47d917ce407650d81c16e2ff1442685e5da3a533',
'platform':
'darwin',
'machine': ['arm64']
@@ -65,11 +65,11 @@
'file_name':
'traceconv',
'file_size':
- 9134136,
+ 9920848,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/traceconv',
'sha256':
- '87b87e1778367c1e3b99fc77439a28b4911125d2751f9909fd1b51f6bd60b6f4',
+ '5f0b86cfb8d75fd574aaabc36c97d229d5234511d3bf77ddcc2a180b96cbd014',
'platform':
'linux',
'machine': ['x86_64']
@@ -79,11 +79,11 @@
'file_name':
'traceconv',
'file_size':
- 6753020,
+ 7430084,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/traceconv',
'sha256':
- '804c4e13aca5798731056952d9cb0c6ee58795c03477c69514ccd39703060812',
+ '1561f9bbbd2b192b834132bd8c515cfde6f6afe2117bf68ac0aeb3caedfeb3fd',
'platform':
'linux',
'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
'file_name':
'traceconv',
'file_size':
- 8740064,
+ 9479552,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/traceconv',
'sha256':
- '0d781886531d11e1d573a1ec5e06376ef139bb479eec38c16c8735821c35b895',
+ '4cb56805a5d1baf5756f459d5fa4a05c982faffc8fc96d9760ca3e86c6ced279',
'platform':
'linux',
'machine': ['aarch64']
@@ -107,55 +107,55 @@
'file_name':
'traceconv',
'file_size':
- 6792280,
+ 7329320,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/traceconv',
'sha256':
- '7d91e4133184a3722a25488edd3692c5a195148eba56621014311d3f85d3fc15'
+ '719ac44e87c45a58d1ba3a6264518ed7384f738cdc293703b7e9a29ebcde6788'
}, {
'arch':
'android-arm64',
'file_name':
'traceconv',
'file_size':
- 8677992,
+ 9232824,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/traceconv',
'sha256':
- 'c03c4a901ed23f1e20a12c98ce4556353a62bddcd260fb4d797cd29ff6c49a05'
+ 'a76f954e8b6bba1e302ee136745ae5a478ba4737bf97bde1f8eeeec1b5238de2'
}, {
'arch':
'android-x86',
'file_name':
'traceconv',
'file_size':
- 9503704,
+ 10121840,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/traceconv',
'sha256':
- '704e58a7249de56aadec64d4c0d83bab0821d2c4fd77114a9b71705ff4224539'
+ '7dcbe7ce3962155a156cb3e85e7fe17389973f93bf22b14ce13e45173f263ea4'
}, {
'arch':
'android-x64',
'file_name':
'traceconv',
'file_size':
- 8964488,
+ 9554408,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/traceconv',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/traceconv',
'sha256':
- 'e4f07836fc2a5fb7cd997a9acc4183af7a06997d1e73aac71021af5114b921bc'
+ 'e44bc63def32674c99c67e5525ea36b66b6c1714e8fffe7606957813aaf212fa'
}, {
'arch':
'windows-amd64',
'file_name':
'traceconv.exe',
'file_size':
- 8763904,
+ 9316864,
'url':
- 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/traceconv.exe',
+ 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/windows-amd64/traceconv.exe',
'sha256':
- '084670ac28ed59a9642782a30e051735c1b7474b8cd569b9bc94c305af68290e',
+ '937df755c7a54484c1a1aa1bbbaf392d978c300d6ca631bd9d9a20fe2b974deb',
'platform':
'win32',
'machine': ['amd64']
diff --git a/traced_perf.rc b/traced_perf.rc
index 7b0ec6a..16b44ae 100644
--- a/traced_perf.rc
+++ b/traced_perf.rc
@@ -28,6 +28,7 @@
group nobody readproc readtracefs
capabilities KILL DAC_READ_SEARCH
task_profiles ProcessCapacityHigh
+ shared_kallsyms
# Daemon run state:
# * initially off
diff --git a/ui/build.js b/ui/build.js
index 0bf73f5..c44277a 100644
--- a/ui/build.js
+++ b/ui/build.js
@@ -120,7 +120,7 @@
{r: /ui\/src\/assets\/((.*)[.]png)/, f: copyAssets},
{r: /buildtools\/typefaces\/(.+[.]woff2)/, f: copyAssets},
{r: /buildtools\/catapult_trace_viewer\/(.+(js|html))/, f: copyAssets},
- {r: /ui\/src\/assets\/.+[.]scss/, f: compileScss},
+ {r: /ui\/src\/assets\/.+[.]scss|ui\/src\/(?:plugins|core_plugins)\/.+\/styles[.]scss/, f: compileScss},
{r: /ui\/src\/chrome_extension\/.*/, f: copyExtensionAssets},
{r: /.*\/dist\/.+\/(?!manifest\.json).*/, f: genServiceWorkerManifestJson},
{r: /.*\/dist\/.*[.](js|html|css|wasm)$/, f: notifyLiveServer},
@@ -251,6 +251,8 @@
generateImports('ui/src/core_plugins', 'all_core_plugins');
generateImports('ui/src/plugins', 'all_plugins');
scanDir('ui/src/assets');
+ scanDir('ui/src/plugins', /styles[.]scss$/);
+ scanDir('ui/src/core_plugins', /styles[.]scss$/);
scanDir('ui/src/chrome_extension');
scanDir('buildtools/typefaces');
scanDir('buildtools/catapult_trace_viewer');
diff --git a/ui/release/channels.json b/ui/release/channels.json
index f8af51d..0fb01f1 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -2,11 +2,11 @@
"channels": [
{
"name": "stable",
- "rev": "54a19ff810b506017d082f6ebfd9feac2885c558"
+ "rev": "6f67beebb7f4ae453b78b327e8ec4113851192c5"
},
{
"name": "canary",
- "rev": "94595b08a72b0338c55d947271a37829a0271729"
+ "rev": "e31359d73a91c34bebb86494dff0d67a18e8bbc4"
},
{
"name": "autopush",
diff --git a/ui/src/assets/panel_container.scss b/ui/src/assets/panel_container.scss
deleted file mode 100644
index 0500294..0000000
--- a/ui/src/assets/panel_container.scss
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-.pf-panel-container {
- // We need to drag over this element for various reasons, so just disable
- // selection over the entire thing.
- // TODO(stevegolton): If we enable this, we can get scrolling while dragging,
- // so we might want to enable this here and disable selection in titles
- // instead.
- user-select: none;
-
- .pf-panel-stack {
- position: relative; // Position overlay relative to this element
-
- .pf-overlay {
- position: absolute;
- inset: 0; // Shorthand for [top, left, right, bottom]: 0
- pointer-events: none; // Make this overlay invisible to pointer events
- }
- }
-}
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index e8b636d..0e90b46 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -14,7 +14,6 @@
@import "typefaces";
@import "common";
-@import "panel_container";
@import "viewer_page";
@import "home_page";
@import "query_page";
@@ -58,7 +57,7 @@
@import "widgets/text_input";
@import "widgets/text_paragraph";
@import "widgets/timestamp";
-@import "widgets/track_widget";
+@import "widgets/track_shell";
@import "widgets/tree";
@import "widgets/treetable";
@import "widgets/vega_view";
diff --git a/ui/src/assets/query_page.scss b/ui/src/assets/query_page.scss
index 2903240..18af8f7 100644
--- a/ui/src/assets/query_page.scss
+++ b/ui/src/assets/query_page.scss
@@ -16,6 +16,7 @@
overflow-y: auto;
overflow-x: hidden;
.pf-editor {
+ contain: strict; // See b/388579546
width: 100%;
height: 0;
min-height: 3rem;
diff --git a/ui/src/assets/record.scss b/ui/src/assets/record.scss
index 62ad2c8..c99e9c3 100644
--- a/ui/src/assets/record.scss
+++ b/ui/src/assets/record.scss
@@ -339,6 +339,31 @@
color: #ee3326;
}
+ .record-ctl {
+ margin: 0.5em;
+ padding: 0.5em;
+ background-color: #eee;
+ display: grid;
+ grid-template-columns: 24px auto 24px;
+ box-sizing: content-box;
+ height: 24px;
+ line-height: 24px;
+ .hidden {
+ visibility: hidden;
+ }
+ .record-target {
+ font-size: 13px;
+ color: #666;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ .pf-button {
+ padding: 0;
+ margin: 0;
+ font-size: 20px;
+ }
+ }
+
background-color: #fcfcfc;
border-right: 1px solid #eee;
padding-bottom: 1em;
@@ -348,6 +373,12 @@
font-size: 14px;
font-weight: 700;
margin: 1em;
+
+ .pf-button {
+ float: right;
+ padding: 0;
+ font-size: 18px;
+ }
}
ul {
@@ -394,6 +425,12 @@
display: block;
}
+ .probe-count {
+ float: right;
+ font-size: 10px;
+ font-weight: 300;
+ }
+
.sub {
@include transition(0.5s);
grid-area: subtext;
@@ -417,6 +454,11 @@
}
}
+ &.disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
&.active {
background-color: hsl(214, 80%, 70%);
.title,
@@ -485,6 +527,10 @@
box-shadow: 0 0 3px 0 #aaa;
}
+ &:active:enabled {
+ background-color: hsl(0, 0%, 83%);
+ }
+
&:not(:enabled) {
background-color: hsl(0, 0%, 83%);
color: hsl(0, 0%, 50%);
@@ -701,7 +747,7 @@
.probe-config {
opacity: 1;
visibility: visible;
- max-height: 100vh;
+ max-height: 10000px;
}
> label span {
color: #4e80b7;
@@ -805,7 +851,9 @@
> * {
@include transition(0.2s);
cursor: pointer;
- border-radius: 15px;
+ border-radius: 4px;
+ box-shadow: 0 0 4px 0 #ccc;
+
margin: 5px;
text-align: center;
background-color: #eee;
@@ -817,13 +865,11 @@
padding-bottom: 10px;
&:hover {
- background-color: hsl(88, 50%, 84%);
- box-shadow: 0 0 4px 0 #999;
+ background-color: #c0def7;
}
&.selected {
- background-color: hsl(88, 50%, 67%);
- box-shadow: 0 0 4px 0 #999;
+ background-color: #64b5f6;
}
img {
@@ -1222,7 +1268,7 @@
font-size: 14px;
}
- line-height: 25px;
+ display: inline-block;
font-size: smaller;
padding: 2px 4px;
border: 1px solid #eee;
@@ -1241,7 +1287,7 @@
display: grid;
position: relative;
padding: 0;
- margin: var(--record-section-padding);
+ margin: 0 var(--record-section-padding);
background-color: #111;
border-radius: 4px;
box-shadow: 0 0 12px #999;
@@ -1325,3 +1371,152 @@
transform: scale(0.9);
}
} // code-snippet
+
+// For RecordingV2
+
+.record-section[id="target"] {
+ header {
+ font-size: 1rem;
+ font-weight: 600;
+ text-align: left;
+ padding-top: 20px;
+ padding-bottom: 5px;
+ }
+ .platform-selector button {
+ min-height: 60px;
+ box-shadow: 2px 2px 4px #eee;
+ border: 1px solid #eee;
+ font-size: 1.2rem;
+ margin: 0 5px;
+
+ &:hover {
+ background-color: #c0def7;
+ }
+ &.pf-active {
+ background-color: #64b5f6;
+ }
+ }
+
+ table {
+ width: 100%;
+ }
+
+ .record-transports {
+ border: 0;
+ input[type="radio"] + label {
+ cursor: pointer;
+ border: 1px solid #eee;
+ display: block;
+ &:hover .pf-icon {
+ background-color: #c0def7;
+ }
+
+ display: grid;
+ grid-template-columns: 50px auto;
+ grid-template-rows: auto auto;
+
+ grid-template-areas:
+ "icon title"
+ "icon description";
+ .pf-icon {
+ grid-area: icon;
+ font-size: 32px;
+ text-align: center;
+ padding: 10px 0;
+ margin-right: 10px;
+ }
+ .title {
+ grid-area: title;
+ font-weight: 600;
+ margin-top: 10px;
+ }
+ .description {
+ grid-area: description;
+ font-size: 0.75rem;
+ margin-bottom: 10px;
+ }
+ }
+ input[type="radio"]:checked + label {
+ .pf-icon {
+ background-color: #64b5f6;
+ }
+ }
+
+ input[type="radio"] {
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ margin: 0;
+ padding: 0;
+ display: none;
+ }
+ } // .record-transports
+
+ .record-targets {
+ display: flex;
+ flex-direction: row;
+ margin: 10px 0;
+ > * {
+ margin-right: 10px;
+ }
+ }
+
+ .preflight-checks-icon {
+ .error {
+ color: hsl(-10, 50%, 50%);
+ }
+ .ok {
+ color: hsl(131, 50%, 50%);
+ }
+ }
+
+ .preflight-checks-table {
+ font-family: var(--monospace-font);
+ font-size: 0.8rem;
+ display: block;
+ border-collapse: collapse;
+
+ td {
+ padding: 5px 10px;
+ border-bottom: 1px solid #eee;
+ white-space: pre-wrap;
+ }
+ .error {
+ color: hsl(-10, 50%, 50%);
+ &::before {
+ content: "❌";
+ margin-right: 5px;
+ }
+ }
+ .ok {
+ color: hsl(131, 50%, 50%);
+ &::before {
+ content: "✅";
+ margin-right: 5px;
+ }
+ }
+ }
+
+ .session-status {
+ border: 1px solid #ddd;
+ margin: 1em;
+ padding: 1em;
+ width: calc(100% - 2em);
+ background-color: #fafafa;
+ font-size: 14px;
+ td:first-of-type {
+ font-weight: 500;
+ width: 20%;
+ vertical-align: top;
+ }
+ tr {
+ margin: 10px 0;
+ }
+ .logs {
+ background-color: rgba(0, 0, 0, 0.02);
+ border: 1px solid #eee;
+ white-space: pre-wrap;
+ margin: 0;
+ font-size: 12px;
+ }
+ }
+}
diff --git a/ui/src/assets/viewer_page.scss b/ui/src/assets/viewer_page.scss
index 7a69912..b1847fe 100644
--- a/ui/src/assets/viewer_page.scss
+++ b/ui/src/assets/viewer_page.scss
@@ -12,102 +12,59 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-.viewer-page {
- position: relative;
+.pf-viewer-page {
+ isolation: isolate;
overflow: hidden;
- .pinned-panel-container {
- max-height: 50%;
- box-shadow: 1px 3px 15px rgba(23, 32, 44, 0.3);
- z-index: 1;
- flex-grow: 0;
- flex-shrink: 0;
- overflow-x: hidden;
- overflow-y: auto;
- scrollbar-gutter: stable;
+ &__overview {
+ z-index: 3;
+ background: white;
+
+ .pf-overview-timeline {
+ height: 70px;
+ }
}
- .scrolling-panel-container {
- overflow-x: hidden;
- overflow-y: auto;
- scrollbar-gutter: stable;
- flex: 1 1 auto;
- will-change: transform; // Force layer creation.
- }
-
- .pf-timeline-header {
- display: flex;
- flex-direction: row;
+ &__header {
+ display: grid;
+ grid-template-columns: 1fr auto;
box-shadow: 1px 3px 15px rgba(23, 32, 44, 0.3);
z-index: 2;
+ background: white;
- .header-panel-container {
- flex-grow: 1;
- }
-
- .scrollbar-spacer-vertical {
+ &:after {
+ content: "";
scrollbar-gutter: stable;
overflow-y: scroll;
visibility: hidden;
+ flex: 0 0 auto;
}
}
- .pan-and-zoom-content {
- height: 100%;
- position: relative;
- display: flex;
- flex-flow: column nowrap;
- overflow: hidden;
- }
-
- .overview-timeline {
- height: 70px;
- }
-
- .time-axis-panel {
- height: 22px;
- }
-
- .tickbar {
- height: 5px;
- }
-
- .notes-panel {
- height: 20px;
- .pf-toolbar {
- width: var(--track-shell-width);
- padding-inline: 3px;
-
- > .pf-text-input {
- flex: auto;
- }
- }
- }
-
- .time-selection-panel {
- height: 10px;
- }
-
- .helpful-hint {
- position: absolute;
- z-index: 10;
- right: 5px;
- top: 5px;
- width: 300px;
- background-color: white;
- font-size: 12px;
- color: #3f4040;
- display: grid;
- border-radius: 5px;
- padding: 8px;
+ &__pinned-track-tree {
+ flex: 0 0 auto;
+ max-height: 40%;
+ z-index: 1;
box-shadow: 1px 3px 15px rgba(23, 32, 44, 0.3);
+ scrollbar-gutter: stable;
+ }
+
+ &__scrolling-track-tree {
+ flex: 1 1 auto;
+ scrollbar-gutter: stable;
}
}
-.pf-track-crash-popup {
- font-family: $pf-font;
- max-width: 300px;
- display: flex;
- flex-direction: column;
- row-gap: 6px;
+.pf-timeline-toolbar {
+ width: var(--track-shell-width);
+ border-right: solid 1px transparent;
+ padding-inline: 2px;
+
+ &__workspace-selector {
+ flex: 1;
+ }
+}
+
+.pf-track__track-details-popup {
+ min-width: 350px;
}
diff --git a/ui/src/assets/widgets/track_shell.scss b/ui/src/assets/widgets/track_shell.scss
new file mode 100644
index 0000000..e4c1929
--- /dev/null
+++ b/ui/src/assets/widgets/track_shell.scss
@@ -0,0 +1,196 @@
+// 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.
+
+@use "sass:math";
+@import "theme";
+
+.pf-track {
+ --text-color-dark: hsl(213, 22%, 30%);
+ --text-color-light: white;
+ --drag-highlight-color: rgb(29, 85, 189);
+ --default-background: white;
+ --collapsed-background: hsla(190, 49%, 97%, 1);
+ --expanded-background: hsl(215, 22%, 19%);
+ --indent-size: 8px;
+
+ // Default values - should be overridden from JS
+ --sticky-top: 0;
+ --depth: 0;
+
+ font-family: "Roboto Condensed", sans-serif;
+ font-weight: 300;
+ font-size: 14px;
+ color: var(--text-color-dark);
+ background-color: var(--default-background);
+ user-select: none;
+
+ &__header {
+ height: calc(var(--height) * 1px);
+ display: grid;
+ grid-template-columns:
+ calc(var(--depth) * var(--indent-size)) calc(
+ var(--track-shell-width) - (var(--depth) * var(--indent-size))
+ )
+ 1fr;
+ grid-template-areas: "indent shell canvas";
+
+ &::before {
+ content: "";
+ grid-area: indent;
+ background: lightgray;
+ }
+
+ &--summary {
+ background-color: var(--collapsed-background);
+ }
+
+ &--expanded--summary {
+ position: sticky;
+ top: calc(var(--sticky-top) * 1px);
+ z-index: calc(16 - var(--depth));
+ background-color: var(--expanded-background);
+ color: var(--text-color-light);
+ }
+ }
+
+ &__shell {
+ grid-area: shell;
+ border-width: 0 1px 1px 0; // Borders on bottom and right
+ border-style: solid;
+ border-color: var(--track-border-color);
+
+ &--highlight {
+ background-color: #ffe263;
+ color: var(--text-color-dark);
+ }
+
+ &--drag-after {
+ box-shadow: inset 0 -6px 3px -3px var(--drag-highlight-color);
+ }
+
+ &--drag-before {
+ box-shadow: inset 0 6px 3px -3px var(--drag-highlight-color);
+ }
+
+ &--clickable {
+ cursor: pointer;
+ }
+
+ .pf-visible-on-hover {
+ visibility: hidden;
+
+ &.pf-active {
+ visibility: unset;
+ }
+ }
+
+ &:hover {
+ .pf-visible-on-hover {
+ visibility: unset;
+ }
+ }
+ }
+
+ &__buttons {
+ // Make the track buttons a little larger so they're easier to see & click
+ font-size: 16px;
+ margin-left: 2px;
+ }
+
+ &__canvas {
+ grid-area: canvas;
+ overflow: hidden; // Keeps subtitle width contained
+ border-bottom: solid 1px var(--track-border-color);
+
+ &--error {
+ // Necessary trig because we have 45deg stripes
+ $pattern-density: 1px * math.sqrt(2);
+ $pattern-col: #ddd;
+
+ background: repeating-linear-gradient(
+ -45deg,
+ $pattern-col,
+ $pattern-col $pattern-density,
+ white $pattern-density,
+ white $pattern-density * 4
+ );
+ }
+ }
+
+ &__menubar {
+ display: grid;
+ grid-template-columns: auto 1fr auto auto;
+ grid-template-rows: auto auto;
+ grid-template-areas:
+ "icon title chips buttons"
+ "none subtitle subtitle subtitle";
+ padding: 1px;
+ position: sticky;
+ top: calc(var(--sticky-top) * 1px);
+ }
+
+ &__title-spacer {
+ grid-area: icon;
+ width: 2px;
+ }
+
+ &__collapse-button {
+ grid-area: icon;
+ }
+
+ &__title {
+ grid-area: title;
+
+ &-popup {
+ position: absolute;
+ border-radius: 2px;
+ color: rgba(0, 0, 0, 0);
+ background: rgba(255, 255, 255, 0);
+ text-overflow: unset;
+ pointer-events: none;
+ white-space: nowrap;
+ user-select: none;
+ width: max-content;
+ z-index: 10; /* Ensures it appears above other elements */
+ }
+
+ &:hover &-popup--visible {
+ box-shadow: 1px 1px 2px 2px var(--track-border-color);
+ background: white;
+ color: hsl(213, 22%, 30%);
+ }
+ }
+
+ &__chips {
+ grid-area: chips;
+ margin-left: 2px;
+ }
+
+ &__subtitle {
+ grid-area: subtitle;
+ font-size: 11px;
+ }
+
+ &__buttons {
+ grid-area: buttons;
+ }
+
+ &__crash-popup {
+ font-family: $pf-font;
+ max-width: 300px;
+ display: flex;
+ flex-direction: column;
+ row-gap: 6px;
+ }
+}
diff --git a/ui/src/assets/widgets/track_widget.scss b/ui/src/assets/widgets/track_widget.scss
deleted file mode 100644
index ed9359d..0000000
--- a/ui/src/assets/widgets/track_widget.scss
+++ /dev/null
@@ -1,180 +0,0 @@
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-@use "sass:math";
-@import "theme";
-
-.pf-track {
- --text-color-dark: hsl(213, 22%, 30%);
- --text-color-light: white;
- --indent-size: 8px;
- --drag-highlight-color: rgb(29, 85, 189);
- --default-background: white;
- --collapsed-background: hsla(190, 49%, 97%, 1);
- --expanded-background: hsl(215, 22%, 19%);
-
- // Layout
- display: grid;
- grid-template-columns:
- calc(var(--indent) * var(--indent-size)) calc(
- 250px - (var(--indent) * var(--indent-size))
- )
- 1fr;
- grid-template-areas: "indent shell content";
-
- // Appearance
- font-family: "Roboto Condensed", sans-serif;
- font-weight: 300;
- font-size: 14px;
- color: var(--text-color-dark);
- background-color: var(--default-background);
-
- &::before {
- content: "";
- grid-area: indent;
- background: lightgray;
- display: block;
- height: 100%;
- }
-
- .pf-track-shell {
- grid-area: shell;
- background-color: inherit;
- box-shadow: inset 0px -1px 0px 0px var(--track-border-color);
-
- &.pf-drag-after {
- box-shadow: inset 0 -6px 3px -3px var(--drag-highlight-color);
- }
-
- &.pf-drag-before {
- box-shadow: inset 0 6px 3px -3px var(--drag-highlight-color);
- }
-
- &.pf-clickable {
- cursor: pointer;
- }
-
- .pf-visible-on-hover {
- visibility: hidden;
-
- &.pf-active {
- visibility: unset;
- }
- }
-
- &:hover {
- .pf-visible-on-hover {
- visibility: unset;
- }
- }
-
- .pf-track-menubar {
- display: grid;
- grid-template-columns: auto 1fr auto; // title, buttons
- grid-template-rows: auto auto;
- grid-template-areas:
- "icon title buttons"
- "none subtitle subtitle";
- padding: 1px 2px;
- column-gap: 2px;
-
- h1.pf-track-title {
- grid-area: title;
-
- // Override h1 formatting
- font-size: inherit;
- margin: inherit;
-
- display: flex;
- flex-direction: row;
- gap: 2px;
- overflow: hidden;
-
- .pf-track__title-popup {
- border-radius: 2px;
- color: rgba(0, 0, 0, 0);
- background: rgba(255, 255, 255, 0);
- position: absolute;
- text-overflow: unset;
- pointer-events: none;
- white-space: nowrap;
- user-select: none;
- }
-
- &:hover .pf-track__title-popup.pf-visible {
- box-shadow: 1px 1px 2px 2px var(--track-border-color);
- background: white;
- color: hsl(213, 22%, 30%);
- }
- }
-
- h2 {
- // Override h2 formatting
- margin: inherit;
-
- grid-area: subtitle;
- font-size: 11px;
- }
-
- .pf-track-buttons {
- grid-area: buttons;
- // Make the track buttons a little larger so they're easier to see &
- // click
- font-size: 16px;
- }
- }
- }
-
- &.pf-is-summary {
- background-color: var(--collapsed-background);
-
- &.pf-expanded {
- background-color: var(--expanded-background);
- color: var(--text-color-light);
- }
- }
-
- &.pf-highlight {
- .pf-track-shell {
- background-color: #ffe263;
- color: var(--text-color-dark);
- }
- }
-
- .pf-track-content {
- grid-area: content;
- box-shadow: inset 1px -1px 0px 0px var(--track-border-color);
- padding-inline: 2px;
- overflow: hidden;
-
- h2 {
- margin: inherit;
- font-size: inherit;
- }
-
- &.pf-track-content-error {
- // Necessary trig because we have 45deg stripes
- $pattern-density: 1px * math.sqrt(2);
- $pattern-col: #ddd;
-
- background: repeating-linear-gradient(
- -45deg,
- $pattern-col,
- $pattern-col $pattern-density,
- white $pattern-density,
- white $pattern-density * 4
- );
- }
- }
-}
diff --git a/ui/src/base/async_guard.ts b/ui/src/base/async_guard.ts
new file mode 100644
index 0000000..94cba84
--- /dev/null
+++ b/ui/src/base/async_guard.ts
@@ -0,0 +1,71 @@
+// 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.
+
+/**
+ * AsyncGuard<T> ensures that a given asynchronous operation does not overlap
+ * with itself.
+ *
+ * This class is useful in scenarios where you want to prevent concurrent
+ * executions of the same async function. If the function is already in
+ * progress, any subsequent calls to `run` will return the same promise,
+ * ensuring no new execution starts until the ongoing one completes.
+ *
+ * - Guarantees single execution: Only one instance of the provided async
+ * function will execute at a time.
+ * - Automatically resets: Once the function completes (either successfully
+ * or with an error), the guard resets and allows new executions.
+ *
+ * This class differs from AsyncLimiter in the fact that it has no queueing at
+ * all (AsyncLimiter instead keeps a queue of max_depth=1 and keeps over-writing
+ * the last task).
+ *
+ * Example:
+ * ```typescript
+ * const asyncTask = async () => {
+ * console.log("Task started...");
+ * await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate work.
+ * console.log("Task finished!");
+ * return "Result";
+ * };
+ *
+ * const guard = new AsyncGuard<string>();
+ *
+ * // Simultaneous calls
+ * guard.run(asyncTask).then(console.log); // Logs "Task started..." and
+ * // "Task finished!" -> "Result"
+ * guard.run(asyncTask).then(console.log); // Will not log "Task started..."
+ * // again, reuses the promise
+ * ```
+ */
+export class AsyncGuard<T> {
+ private pendingPromise?: Promise<T>;
+
+ /**
+ * Runs the provided async function, ensuring no overlap.
+ * If a previous call is still pending, it returns the same promise.
+ *
+ * @param func - The async function to execute.
+ * @returns A promise resolving to the function's result.
+ */
+ run(func: () => Promise<T>): Promise<T> {
+ if (this.pendingPromise !== undefined) {
+ return this.pendingPromise;
+ }
+ this.pendingPromise = func();
+ this.pendingPromise.finally(() => {
+ this.pendingPromise = undefined;
+ });
+ return this.pendingPromise;
+ }
+}
diff --git a/ui/src/base/async_guard_unittest.ts b/ui/src/base/async_guard_unittest.ts
new file mode 100644
index 0000000..2e5c474
--- /dev/null
+++ b/ui/src/base/async_guard_unittest.ts
@@ -0,0 +1,37 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {AsyncGuard} from './async_guard';
+import {defer} from './deferred';
+
+test('AsyncGuard', async () => {
+ const guard = new AsyncGuard<number>();
+ let counter = 0;
+
+ for (let i = 1; i <= 3; i++) {
+ const barrier = defer<void>();
+ const asyncTask = async () => {
+ await barrier;
+ return ++counter;
+ };
+ const promises = [
+ guard.run(asyncTask),
+ guard.run(asyncTask),
+ guard.run(asyncTask),
+ ];
+ setTimeout(() => barrier.resolve(), 0);
+ await barrier;
+ expect(await Promise.all(promises)).toEqual([i, i, i]);
+ }
+});
diff --git a/ui/src/base/async_lazy.ts b/ui/src/base/async_lazy.ts
new file mode 100644
index 0000000..1636e8d
--- /dev/null
+++ b/ui/src/base/async_lazy.ts
@@ -0,0 +1,55 @@
+// 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 {AsyncGuard} from './async_guard';
+import {okResult, Result} from './result';
+
+/**
+ * A utility class for lazily initializing and caching asynchronous values.
+ *
+ * This class ensures that a value is created only once using a provided
+ * asynchronous factory function and is cached for future access. It also
+ * provides methods to reset the cached value and retry the initialization.
+ *
+ * Internally, the class uses {@link AsyncGuard} to ensure non-overlapping of
+ * the initialization process, preventing race conditions when multiple
+ * callers attempt to initialize the value concurrently.
+ */
+export class AsyncLazy<T> {
+ private _value?: T;
+ private guard = new AsyncGuard<Result<T>>();
+
+ getOrCreate(factory: () => Promise<Result<T>>): Promise<Result<T>> {
+ if (this._value !== undefined) {
+ return Promise.resolve(okResult(this._value));
+ }
+
+ const promise = this.guard.run(factory);
+ promise.then((valueOrError) => {
+ if (valueOrError.ok) {
+ this._value = valueOrError.value;
+ }
+ });
+ return promise;
+ }
+
+ get value(): T | undefined {
+ return this._value;
+ }
+
+ reset() {
+ this._value = undefined;
+ this.guard = new AsyncGuard<Result<T>>();
+ }
+}
diff --git a/ui/src/base/async_lazy_unittest.ts b/ui/src/base/async_lazy_unittest.ts
new file mode 100644
index 0000000..66c7e2f
--- /dev/null
+++ b/ui/src/base/async_lazy_unittest.ts
@@ -0,0 +1,55 @@
+// 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 {AsyncLazy} from './async_lazy';
+import {defer} from './deferred';
+import {errResult, okResult, Result} from './result';
+
+async function slowFactory(res: number): Promise<Result<number>> {
+ const barrier = defer<void>();
+ setTimeout(() => barrier.resolve(), 0);
+ await barrier;
+ return isFinite(res) ? okResult(res) : errResult(`${res} is not a number`);
+}
+
+test('AsyncLazy', async () => {
+ const alazy = new AsyncLazy<number>();
+ expect(alazy.value).toBeUndefined();
+
+ // Failures during creation should not be cached.
+ expect(await alazy.getOrCreate(() => slowFactory(NaN))).toEqual(
+ errResult('NaN is not a number'),
+ );
+ expect(await alazy.getOrCreate(() => slowFactory(1 / 0))).toEqual(
+ errResult('Infinity is not a number'),
+ );
+
+ const promises = [
+ alazy.getOrCreate(() => slowFactory(42)),
+ alazy.getOrCreate(() => slowFactory(1)),
+ alazy.getOrCreate(() => slowFactory(2)),
+ ];
+
+ // Only the first promise will determine the result, which will be
+ // subsequently cached.
+ expect(await Promise.all(promises)).toEqual([
+ okResult(42),
+ okResult(42),
+ okResult(42),
+ ]);
+ expect(alazy.value).toEqual(42);
+
+ alazy.reset();
+ expect(await alazy.getOrCreate(() => slowFactory(99))).toEqual(okResult(99));
+});
diff --git a/ui/src/base/canvas/bezier_arrow.ts b/ui/src/base/bezier_arrow.ts
similarity index 98%
rename from ui/src/base/canvas/bezier_arrow.ts
rename to ui/src/base/bezier_arrow.ts
index c5f0a60..3ff04fd 100644
--- a/ui/src/base/canvas/bezier_arrow.ts
+++ b/ui/src/base/bezier_arrow.ts
@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {Point2D, Vector2D} from '../geom';
-import {assertUnreachable} from '../logging';
+import {Point2D, Vector2D} from './geom';
+import {assertUnreachable} from './logging';
export type CardinalDirection = 'north' | 'south' | 'east' | 'west';
diff --git a/ui/src/public/color.ts b/ui/src/base/color.ts
similarity index 99%
rename from ui/src/public/color.ts
rename to ui/src/base/color.ts
index f9e1430..4518e60 100644
--- a/ui/src/public/color.ts
+++ b/ui/src/base/color.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import {hsluvToRgb} from 'hsluv';
-import {clamp} from '../base/math_utils';
+import {clamp} from './math_utils';
// This file contains a library for working with colors in various color spaces
// and formats.
diff --git a/ui/src/public/color_scheme.ts b/ui/src/base/color_scheme.ts
similarity index 100%
rename from ui/src/public/color_scheme.ts
rename to ui/src/base/color_scheme.ts
diff --git a/ui/src/base/gcs_uploader.ts b/ui/src/base/gcs_uploader.ts
index b2f2bd5..3ecbece 100644
--- a/ui/src/base/gcs_uploader.ts
+++ b/ui/src/base/gcs_uploader.ts
@@ -62,7 +62,7 @@
this.start(data);
}
- async start(data: Blob | ArrayBuffer | string) {
+ private async start(data: Blob | ArrayBuffer | string) {
let fname = this.args.fileName;
if (fname === undefined) {
// If the file name is unspecified, hash the contents.
diff --git a/ui/src/base/geom.ts b/ui/src/base/geom.ts
index 385bb22..c766fdb 100644
--- a/ui/src/base/geom.ts
+++ b/ui/src/base/geom.ts
@@ -298,4 +298,13 @@
right: this.right + point.x,
});
}
+
+ equals(bounds: Bounds2D): boolean {
+ return (
+ bounds.top === this.top &&
+ bounds.left === this.left &&
+ bounds.right === this.right &&
+ bounds.bottom === this.bottom
+ );
+ }
}
diff --git a/ui/src/base/high_precision_time.ts b/ui/src/base/high_precision_time.ts
index f205c83..2fce5e5 100644
--- a/ui/src/base/high_precision_time.ts
+++ b/ui/src/base/high_precision_time.ts
@@ -47,6 +47,17 @@
this.fractional = fractional - fractionalFloor;
}
+ static max(a: HighPrecisionTime, b: HighPrecisionTime) {
+ if (a.integral > b.integral) return a;
+ if (a.integral < b.integral) return b;
+ if (a.fractional > b.fractional) return a;
+ return b;
+ }
+
+ static min(a: HighPrecisionTime, b: HighPrecisionTime) {
+ return HighPrecisionTime.max(a, b) === a ? b : a;
+ }
+
/**
* Converts to an integer time value.
*
diff --git a/ui/src/base/high_precision_time_span.ts b/ui/src/base/high_precision_time_span.ts
index 0bb6e75..f31a706 100644
--- a/ui/src/base/high_precision_time_span.ts
+++ b/ui/src/base/high_precision_time_span.ts
@@ -31,6 +31,19 @@
}
/**
+ * Create a new span from two high precision times. The earlier time is used
+ * as the start and and the later time as the end of the span.
+ */
+ static fromHpTimes(
+ a: HighPrecisionTime,
+ b: HighPrecisionTime,
+ ): HighPrecisionTimeSpan {
+ const start = HighPrecisionTime.min(a, b);
+ const end = HighPrecisionTime.max(a, b);
+ return new HighPrecisionTimeSpan(start, Number(end.sub(start)));
+ }
+
+ /**
* Create a new span from integral start and end points.
*
* @param start The start of the span.
diff --git a/ui/src/base/high_precision_time_span_unittest.ts b/ui/src/base/high_precision_time_span_unittest.ts
index 537d173..8e717d4 100644
--- a/ui/src/base/high_precision_time_span_unittest.ts
+++ b/ui/src/base/high_precision_time_span_unittest.ts
@@ -49,6 +49,20 @@
expect(span.duration).toBeCloseTo(10);
});
+ it('can be constructed from hp times', () => {
+ const span = HPTimeSpan.fromHpTimes(hptime('123'), hptime('456'));
+ expect(span.start.integral).toEqual(123n);
+ expect(span.start.fractional).toBeCloseTo(0);
+ expect(span.duration).toBeCloseTo(333);
+ });
+
+ it('can be constructed from hp times reversed', () => {
+ const span = HPTimeSpan.fromHpTimes(hptime('456'), hptime('123'));
+ expect(span.start.integral).toEqual(123n);
+ expect(span.start.fractional).toBeCloseTo(0);
+ expect(span.duration).toBeCloseTo(333);
+ });
+
test('end', () => {
const span = HPTimeSpan.fromTime(t(10n), t(20n));
expect(span.end.integral).toEqual(20n);
diff --git a/ui/src/base/mithril_utils.ts b/ui/src/base/mithril_utils.ts
index 9b0615d..0bfcc7c 100644
--- a/ui/src/base/mithril_utils.ts
+++ b/ui/src/base/mithril_utils.ts
@@ -85,3 +85,5 @@
},
};
}
+
+export type MithrilEvent<T extends Event = Event> = T & {redraw: boolean};
diff --git a/ui/src/base/resizable_array_buffer.ts b/ui/src/base/resizable_array_buffer.ts
new file mode 100644
index 0000000..a882c35
--- /dev/null
+++ b/ui/src/base/resizable_array_buffer.ts
@@ -0,0 +1,76 @@
+// 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 {assertTrue} from './logging';
+
+/**
+ * A dynamically resizable array buffer implementation for efficient
+ * storage and manipulation of binary data. It starts with a specified
+ * initial size and grows as needed to accommodate appended data.
+ * Efficiently grows the buffer using an exponential strategy up to 32MB,
+ * and then linearly in 32MB increments to minimize reallocation overhead.
+ * Provides methods to append data, shrink the size, clear the buffer,
+ * and retrieve the stored data as a `Uint8Array`.
+ */
+export class ResizableArrayBuffer {
+ private buf: Uint8Array;
+ private _size = 0;
+
+ constructor(private readonly initialSize = 128) {
+ this.buf = new Uint8Array(initialSize);
+ }
+
+ append(data: ArrayLike<number>) {
+ const capacityNeeded = this._size + data.length;
+ if (this.capacity < capacityNeeded) {
+ this.grow(capacityNeeded);
+ }
+ this.buf.set(data, this._size);
+ this._size = capacityNeeded;
+ }
+
+ shrink(newSize: number) {
+ assertTrue(newSize <= this._size);
+ this._size = newSize;
+ }
+
+ clear() {
+ this.buf = new Uint8Array(this.initialSize);
+ this._size = 0;
+ }
+
+ get(): Uint8Array {
+ return this.buf.subarray(0, this._size);
+ }
+
+ get size(): number {
+ return this._size;
+ }
+
+ get capacity(): number {
+ return this.buf.length;
+ }
+
+ private grow(capacityNeeded: number) {
+ let newSize = this.buf.length;
+ const MB32 = 32 * 1024 * 1024;
+ do {
+ newSize = newSize < MB32 ? newSize * 2 : newSize + MB32;
+ } while (newSize < capacityNeeded);
+ const newBuf = new Uint8Array(newSize);
+ assertTrue(newBuf.length >= capacityNeeded);
+ newBuf.set(this.buf);
+ this.buf = newBuf;
+ }
+}
diff --git a/ui/src/base/resizable_array_buffer_unittest.ts b/ui/src/base/resizable_array_buffer_unittest.ts
new file mode 100644
index 0000000..42adab3
--- /dev/null
+++ b/ui/src/base/resizable_array_buffer_unittest.ts
@@ -0,0 +1,55 @@
+// 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 {pseudoRand} from './rand';
+import {ResizableArrayBuffer} from './resizable_array_buffer';
+
+test('ResizableArrayBuffer.simple', () => {
+ const buf = new ResizableArrayBuffer();
+ buf.append([1, 2]);
+ expect(buf.get()).toEqual(new Uint8Array([1, 2]));
+
+ buf.append([3]);
+ expect(buf.get()).toEqual(new Uint8Array([1, 2, 3]));
+
+ buf.clear();
+ expect(buf.get().length).toEqual(0);
+ expect(buf.get()).toEqual(new Uint8Array([]));
+});
+
+test('ResizableArrayBuffer.merge', () => {
+ const buf = new ResizableArrayBuffer();
+
+ // Append to the buffer in batches of variables sizes, filling with a
+ // pseudo-random sequence.
+ const randState = {seed: 1};
+ const randU8 = () => (pseudoRand(randState) * 256) & 0xff;
+ const BATCH_SIZES = [37, 129, 1023, 1024 * 37, 1024 * 511];
+ for (const batchSize of BATCH_SIZES) {
+ const batch = new Uint8Array(batchSize);
+ for (let i = 0; i < batch.length; i++) {
+ batch[i] = randU8();
+ }
+ buf.append(batch);
+ }
+
+ // Now reset the seed and check that the random sequence of the merged array
+ // matches.
+ const merged = buf.get();
+ expect(merged.length).toEqual(BATCH_SIZES.reduce((a, s) => a + s, 0));
+ randState.seed = 1;
+ for (let i = 0; i < merged.length; i++) {
+ expect(merged[i]).toEqual(randU8());
+ }
+});
diff --git a/ui/src/base/result.ts b/ui/src/base/result.ts
index 9dd8cee..5b04a83 100644
--- a/ui/src/base/result.ts
+++ b/ui/src/base/result.ts
@@ -17,6 +17,7 @@
export interface ErrorResult {
ok: false;
error: string;
+ value: undefined;
}
export interface OkResult<T> {
@@ -27,7 +28,7 @@
export type Result<T> = ErrorResult | OkResult<T>;
export function errResult(message: string): ErrorResult {
- return {ok: false, error: message};
+ return {ok: false, error: message, value: undefined};
}
export function okResult<T>(value: T): OkResult<T> {
diff --git a/ui/src/base/string_utils.ts b/ui/src/base/string_utils.ts
index df0e3da..ad71674 100644
--- a/ui/src/base/string_utils.ts
+++ b/ui/src/base/string_utils.ts
@@ -127,3 +127,10 @@
}
return displayText;
}
+
+export function splitLinesNonEmpty(text: string): string[] {
+ return text
+ .split('\n')
+ .map((l) => l.trim())
+ .filter((l) => l !== '');
+}
diff --git a/ui/src/base/string_utils_unittest.ts b/ui/src/base/string_utils_unittest.ts
index 96f63dd..61e6bf6 100644
--- a/ui/src/base/string_utils_unittest.ts
+++ b/ui/src/base/string_utils_unittest.ts
@@ -20,6 +20,7 @@
binaryEncode,
cropText,
sqliteString,
+ splitLinesNonEmpty,
utf8Decode,
utf8Encode,
} from './string_utils';
@@ -94,3 +95,14 @@
expect(cropText(emoji + 'abc', 5, 2 * 5)).toBe(emoji);
expect(cropText(emoji + 'abc', 5, 5 * 5)).toBe(emoji + 'a' + tripleDot);
});
+
+test('string_utils.splitNonEmptyLines', () => {
+ expect(splitLinesNonEmpty('')).toEqual([]);
+ expect(splitLinesNonEmpty('foo')).toEqual(['foo']);
+ expect(splitLinesNonEmpty('foo ')).toEqual(['foo']);
+ expect(splitLinesNonEmpty('foo\n')).toEqual(['foo']);
+ expect(splitLinesNonEmpty('foo\nbar')).toEqual(['foo', 'bar']);
+ expect(splitLinesNonEmpty('foo\nbar\n')).toEqual(['foo', 'bar']);
+ expect(splitLinesNonEmpty('foo\nbar\n')).toEqual(['foo', 'bar']);
+ expect(splitLinesNonEmpty('foo\n \nbar\n')).toEqual(['foo', 'bar']);
+});
diff --git a/ui/src/base/zoned_interaction_handler.ts b/ui/src/base/zoned_interaction_handler.ts
index 1995103..257c228 100644
--- a/ui/src/base/zoned_interaction_handler.ts
+++ b/ui/src/base/zoned_interaction_handler.ts
@@ -91,6 +91,8 @@
// Default: 0 - drags start instantly.
readonly minDistance?: number;
+ onDragStart?(e: DragEvent, element: HTMLElement): void;
+
// Optional: Called whenever the mouse is moved during a drag event.
onDrag?(e: DragEvent, element: HTMLElement): void;
diff --git a/ui/src/bigtrace/index.ts b/ui/src/bigtrace/index.ts
index db3e71b..f872585 100644
--- a/ui/src/bigtrace/index.ts
+++ b/ui/src/bigtrace/index.ts
@@ -19,7 +19,6 @@
import {reportError, addErrorHandler, ErrorDetails} from '../base/logging';
import {initLiveReloadIfLocalhost} from '../core/live_reload';
import {raf} from '../core/raf_scheduler';
-import {setScheduleFullRedraw} from '../widgets/raf';
function getRoot() {
// Works out the root directory where the content should be served from
@@ -39,24 +38,12 @@
function setupContentSecurityPolicy() {
// Note: self and sha-xxx must be quoted, urls data: and blob: must not.
const policy = {
- 'default-src': [
- `'self'`,
- ],
- 'script-src': [
- `'self'`,
- ],
+ 'default-src': [`'self'`],
+ 'script-src': [`'self'`],
'object-src': ['none'],
- 'connect-src': [
- `'self'`,
- ],
- 'img-src': [
- `'self'`,
- 'data:',
- 'blob:',
- ],
- 'style-src': [
- `'self'`,
- ],
+ 'connect-src': [`'self'`],
+ 'img-src': [`'self'`, 'data:', 'blob:'],
+ 'style-src': [`'self'`],
'navigate-to': ['https://*.perfetto.dev', 'self'],
};
const meta = document.createElement('meta');
@@ -70,9 +57,6 @@
}
function main() {
- // Wire up raf for widgets.
- setScheduleFullRedraw(() => raf.scheduleFullRedraw());
-
setupContentSecurityPolicy();
// Load the css. The load is asynchronous and the CSS is not ready by the time
@@ -95,9 +79,13 @@
window.addEventListener('unhandledrejection', (e) => reportError(e));
// Prevent pinch zoom.
- document.body.addEventListener('wheel', (e: MouseEvent) => {
- if (e.ctrlKey) e.preventDefault();
- }, {passive: false});
+ document.body.addEventListener(
+ 'wheel',
+ (e: MouseEvent) => {
+ if (e.ctrlKey) e.preventDefault();
+ },
+ {passive: false},
+ );
cssLoadPromise.then(() => onCssLoaded());
}
diff --git a/ui/src/common/protos_unittest.ts b/ui/src/common/protos_unittest.ts
deleted file mode 100644
index daa9c57..0000000
--- a/ui/src/common/protos_unittest.ts
+++ /dev/null
@@ -1,25 +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 protos from '../protos';
-
-test('round trip config proto', () => {
- const input = protos.TraceConfig.create({
- durationMs: 42,
- });
- const output = protos.TraceConfig.decode(
- protos.TraceConfig.encode(input).finish(),
- );
- expect(output.durationMs).toBe(42);
-});
diff --git a/ui/src/components/colorizer.ts b/ui/src/components/colorizer.ts
index ed0ead6..6603cec 100644
--- a/ui/src/components/colorizer.ts
+++ b/ui/src/components/colorizer.ts
@@ -15,8 +15,8 @@
import {hsl} from 'color-convert';
import {hash} from '../base/hash';
import {featureFlags} from '../core/feature_flags';
-import {Color, HSLColor, HSLuvColor} from '../public/color';
-import {ColorScheme} from '../public/color_scheme';
+import {Color, HSLColor, HSLuvColor} from '../base/color';
+import {ColorScheme} from '../base/color_scheme';
import {RandState, pseudoRand} from '../base/rand';
// 128 would provide equal weighting between dark and light text.
diff --git a/ui/src/components/query_flamegraph.ts b/ui/src/components/query_flamegraph.ts
index c9b2a0e..47f44d0 100644
--- a/ui/src/components/query_flamegraph.ts
+++ b/ui/src/components/query_flamegraph.ts
@@ -157,7 +157,6 @@
state: this.state.state,
onStateChange: (state) => {
this.state.state = state;
- this.trace.scheduleFullRedraw();
},
});
}
diff --git a/ui/src/components/query_table/query_result_tab.ts b/ui/src/components/query_table/query_result_tab.ts
index dba01ee..937e9b1 100644
--- a/ui/src/components/query_table/query_result_tab.ts
+++ b/ui/src/components/query_table/query_result_tab.ts
@@ -76,9 +76,6 @@
// TODO(stevegolton): Do we really need to create this view upfront?
this.sqlViewName = await this.createViewForDebugTrack(uuidv4());
- if (this.sqlViewName) {
- this.trace.scheduleFullRedraw();
- }
}
getTitle(): string {
diff --git a/ui/src/components/sql_utils/slice.ts b/ui/src/components/sql_utils/slice.ts
index edad531..ef59ea9 100644
--- a/ui/src/components/sql_utils/slice.ts
+++ b/ui/src/components/sql_utils/slice.ts
@@ -105,7 +105,7 @@
threadDur: LONG_NULL,
threadTs: LONG_NULL,
category: STR_NULL,
- argSetId: NUM,
+ argSetId: NUM_NULL,
absTime: STR_NULL,
});
@@ -135,7 +135,9 @@
threadDur: it.threadDur ?? undefined,
threadTs: exists(it.threadTs) ? Time.fromRaw(it.threadTs) : undefined,
category: it.category ?? undefined,
- args: await getArgs(engine, asArgSetId(it.argSetId)),
+ args: exists(it.argSetId)
+ ? await getArgs(engine, asArgSetId(it.argSetId))
+ : undefined,
absTime: it.absTime ?? undefined,
});
}
diff --git a/ui/src/components/tracks/add_debug_track_menu.ts b/ui/src/components/tracks/add_debug_track_menu.ts
index 0870eae..c297172 100644
--- a/ui/src/components/tracks/add_debug_track_menu.ts
+++ b/ui/src/components/tracks/add_debug_track_menu.ts
@@ -93,7 +93,7 @@
}
}
- private renderTrackTypeSelect(trace: Trace) {
+ private renderTrackTypeSelect() {
const options = [];
for (const type of ['slice', 'counter']) {
options.push(
@@ -116,7 +116,6 @@
this.trackType = (e.target as HTMLSelectElement).value as
| 'slice'
| 'counter';
- trace.scheduleFullRedraw();
},
},
options,
@@ -253,7 +252,7 @@
},
}),
m(FormLabel, {for: 'track_type'}, 'Track type'),
- this.renderTrackTypeSelect(vnode.attrs.trace),
+ this.renderTrackTypeSelect(),
renderSelect('ts'),
this.trackType === 'slice' && renderSelect('dur'),
this.trackType === 'slice' && renderSelect('name'),
diff --git a/ui/src/components/tracks/base_slice_track.ts b/ui/src/components/tracks/base_slice_track.ts
index 323169f..6b5ffc1 100644
--- a/ui/src/components/tracks/base_slice_track.ts
+++ b/ui/src/components/tracks/base_slice_track.ts
@@ -21,7 +21,7 @@
drawTrackHoverTooltip,
} from '../../base/canvas_utils';
import {cropText} from '../../base/string_utils';
-import {colorCompare} from '../../public/color';
+import {colorCompare} from '../../base/color';
import {UNEXPECTED_PINK} from '../colorizer';
import {TrackEventDetails} from '../../public/selection';
import {featureFlags} from '../../core/feature_flags';
@@ -696,6 +696,9 @@
// rowToSlice() method.
slices.push(this.rowToSliceInternal(it));
}
+ for (const incomplete of this.incomplete) {
+ maxDataDepth = Math.max(maxDataDepth, incomplete.depth);
+ }
this.maxDataDepth = maxDataDepth;
this.onUpdatedSlices(slices);
this.slices = slices;
diff --git a/ui/src/components/tracks/sql_table_slice_track_details_tab.ts b/ui/src/components/tracks/sql_table_slice_track_details_tab.ts
index 0cba1fb..1aebc7d 100644
--- a/ui/src/components/tracks/sql_table_slice_track_details_tab.ts
+++ b/ui/src/components/tracks/sql_table_slice_track_details_tab.ts
@@ -220,8 +220,6 @@
sqlValueToReadableString(this.data.args['table_name']),
sqlValueToNumber(this.data.args['track_id']),
);
-
- this.trace.scheduleFullRedraw();
}
render() {
diff --git a/ui/src/components/widgets/duration_precision_menu_items.ts b/ui/src/components/widgets/duration_precision_menu_items.ts
index 7a332f8..4eb4e30 100644
--- a/ui/src/components/widgets/duration_precision_menu_items.ts
+++ b/ui/src/components/widgets/duration_precision_menu_items.ts
@@ -31,7 +31,6 @@
active: value === attrs.trace.timeline.durationPrecision,
onclick: () => {
attrs.trace.timeline.durationPrecision = value;
- attrs.trace.scheduleFullRedraw();
},
});
}
diff --git a/ui/src/components/widgets/sql/legacy_table/argument_selector.ts b/ui/src/components/widgets/sql/legacy_table/argument_selector.ts
index e8184bd..c38a4a7 100644
--- a/ui/src/components/widgets/sql/legacy_table/argument_selector.ts
+++ b/ui/src/components/widgets/sql/legacy_table/argument_selector.ts
@@ -22,7 +22,6 @@
LegacyTableManager,
} from './column';
import {TextInput} from '../../../../widgets/text_input';
-import {scheduleFullRedraw} from '../../../../widgets/raf';
import {hasModKey, modKey} from '../../../../base/hotkeys';
import {MenuItem} from '../../../../widgets/menu';
import {uuidv4} from '../../../../base/uuid';
@@ -82,7 +81,6 @@
oninput: (event: Event) => {
const eventTarget = event.target as HTMLTextAreaElement;
this.searchText = eventTarget.value;
- scheduleFullRedraw();
},
onkeydown: (event: KeyboardEvent) => {
if (filtered.length === 0) return;
diff --git a/ui/src/components/widgets/sql/legacy_table/state.ts b/ui/src/components/widgets/sql/legacy_table/state.ts
index 3f174eb..fa07f8d 100644
--- a/ui/src/components/widgets/sql/legacy_table/state.ts
+++ b/ui/src/components/widgets/sql/legacy_table/state.ts
@@ -454,7 +454,6 @@
--toIndex;
}
this.columns.splice(toIndex, 0, column);
- raf.scheduleFullRedraw();
}
getSelectedColumns(): LegacyTableColumn[] {
diff --git a/ui/src/components/widgets/timestamp_format_menu.ts b/ui/src/components/widgets/timestamp_format_menu.ts
index 121d76c..58a5299 100644
--- a/ui/src/components/widgets/timestamp_format_menu.ts
+++ b/ui/src/components/widgets/timestamp_format_menu.ts
@@ -31,7 +31,6 @@
active: value === attrs.trace.timeline.timestampFormat,
onclick: () => {
attrs.trace.timeline.timestampFormat = value;
- attrs.trace.scheduleFullRedraw();
},
});
}
diff --git a/ui/src/components/widgets/treetable.ts b/ui/src/components/widgets/treetable.ts
index 46f78a8..6789690 100644
--- a/ui/src/components/widgets/treetable.ts
+++ b/ui/src/components/widgets/treetable.ts
@@ -14,7 +14,6 @@
import m from 'mithril';
import {classNames} from '../../base/classnames';
-import {raf} from '../../core/raf_scheduler';
interface ColumnDescriptor<T> {
name: string;
@@ -73,7 +72,6 @@
} else {
this.collapsePath(thisPath);
}
- raf.scheduleFullRedraw();
},
}),
getData(row),
diff --git a/ui/src/components/widgets/vega_view.ts b/ui/src/components/widgets/vega_view.ts
index 067a037..5436bcd 100644
--- a/ui/src/components/widgets/vega_view.ts
+++ b/ui/src/components/widgets/vega_view.ts
@@ -20,7 +20,6 @@
import {SimpleResizeObserver} from '../../base/resize_observer';
import {Engine} from '../../trace_processor/engine';
import {QueryError} from '../../trace_processor/query_result';
-import {scheduleFullRedraw} from '../../widgets/raf';
import {Spinner} from '../../widgets/spinner';
function isVegaLite(spec: unknown): boolean {
@@ -228,7 +227,7 @@
}
this._status = Status.Done;
this.pending = undefined;
- scheduleFullRedraw('force');
+ m.redraw();
}
private handleError(pending: Promise<vega.View>, err: unknown) {
@@ -242,7 +241,7 @@
private setError(err: unknown) {
this._status = Status.Error;
this._error = getErrorMessage(err);
- scheduleFullRedraw('force');
+ m.redraw();
}
[Symbol.dispose]() {
diff --git a/ui/src/widgets/virtual_overlay_canvas.ts b/ui/src/components/widgets/virtual_overlay_canvas.ts
similarity index 87%
rename from ui/src/widgets/virtual_overlay_canvas.ts
rename to ui/src/components/widgets/virtual_overlay_canvas.ts
index 22e5384..c2d3443 100644
--- a/ui/src/widgets/virtual_overlay_canvas.ts
+++ b/ui/src/components/widgets/virtual_overlay_canvas.ts
@@ -30,11 +30,12 @@
*/
import m from 'mithril';
-import {DisposableStack} from '../base/disposable_stack';
-import {findRef, toHTMLElement} from '../base/dom_utils';
-import {Rect2D, Size2D} from '../base/geom';
-import {assertExists} from '../base/logging';
-import {VirtualCanvas} from '../base/virtual_canvas';
+import {DisposableStack} from '../../base/disposable_stack';
+import {findRef, toHTMLElement} from '../../base/dom_utils';
+import {Rect2D, Size2D} from '../../base/geom';
+import {assertExists} from '../../base/logging';
+import {VirtualCanvas} from '../../base/virtual_canvas';
+import {Raf} from '../../public/raf';
const CANVAS_CONTAINER_REF = 'canvas-container';
const CANVAS_OVERDRAW_PX = 300;
@@ -58,13 +59,15 @@
// Which axes should be scrollable.
readonly scrollAxes?: 'none' | 'x' | 'y' | 'both';
+ // Access to the raf. If not supplied, the canvas won't be redrawn when
+ // redraws are scheduled using the raf, only when the floating canvas moves
+ // around or is resized. Thus, this might be OK for static canvas content, but
+ // for dynamic content, you really should pass a raf.
+ readonly raf?: Raf;
+
// Called when the canvas needs to be repainted due to a layout shift or
// or resize.
onCanvasRedraw?(ctx: VirtualOverlayCanvasDrawContext): void;
-
- // Called when the canvas is resized. This will immediately be followed by a
- // call to onCanvasRedraw().
- onCanvasResized?(size: Size2D): void;
}
// This mithril component acts as scrolling container for tall and/or wide
@@ -73,7 +76,7 @@
export class VirtualOverlayCanvas
implements m.ClassComponent<VirtualOverlayCanvasAttrs>
{
- private readonly trash = new DisposableStack();
+ readonly trash = new DisposableStack();
private ctx?: CanvasRenderingContext2D;
private virtualCanvas?: VirtualCanvas;
private attrs?: VirtualOverlayCanvasAttrs;
@@ -134,7 +137,6 @@
const dpr = window.devicePixelRatio;
canvas.width = width * dpr;
canvas.height = height * dpr;
- assertExists(this.attrs).onCanvasResized?.(virtualCanvas.size);
});
// Whenever the canvas changes size or moves around (e.g. when scrolling),
@@ -143,13 +145,19 @@
virtualCanvas.setLayoutShiftListener(() => {
this.redrawCanvas();
});
+
+ if (this.attrs?.raf) {
+ this.trash.use(
+ this.attrs.raf.addCanvasRedrawCallback(() => this.redrawCanvas()),
+ );
+ }
}
onremove() {
this.trash.dispose();
}
- private redrawCanvas() {
+ redrawCanvas() {
const ctx = assertExists(this.ctx);
const virtualCanvas = assertExists(this.virtualCanvas);
diff --git a/ui/src/core/app_impl.ts b/ui/src/core/app_impl.ts
index c1045e2..e21c745 100644
--- a/ui/src/core/app_impl.ts
+++ b/ui/src/core/app_impl.ts
@@ -36,6 +36,8 @@
import {ServiceWorkerController} from '../frontend/service_worker_controller';
import {FeatureFlagManager, FlagSettings} from '../public/feature_flag';
import {featureFlags} from './feature_flags';
+import {Raf} from '../public/raf';
+import {AsyncLimiter} from '../base/async_limiter';
// The args that frontend/index.ts passes when calling AppImpl.initialize().
// This is to deal with injections that would otherwise cause circular deps.
@@ -71,6 +73,7 @@
readonly initArgs: AppInitArgs;
readonly embeddedMode: boolean;
readonly testingMode: boolean;
+ readonly openTraceAsyncLimiter = new AsyncLimiter();
// This is normally empty and is injected with extra google-internal packages
// via is_internal_user.js
@@ -214,8 +217,8 @@
return this.appCtx.currentTrace?.forPlugin(this.pluginId);
}
- scheduleFullRedraw(force?: 'force'): void {
- raf.scheduleFullRedraw(force);
+ get raf(): Raf {
+ return raf;
}
get httpRpc() {
@@ -249,30 +252,35 @@
}
private async openTrace(src: TraceSource) {
- this.appCtx.closeCurrentTrace();
- this.appCtx.isLoadingTrace = true;
- try {
- // loadTrace() in trace_loader.ts will do the following:
- // - Create a new engine.
- // - Pump the data from the TraceSource into the engine.
- // - Do the initial queries to build the TraceImpl object
- // - Call AppImpl.setActiveTrace(TraceImpl)
- // - Continue with the trace loading logic (track decider, plugins, etc)
- // - Resolve the promise when everything is done.
- await loadTrace(this, src);
- this.omnibox.reset(/* focus= */ false);
- // loadTrace() internally will call setActiveTrace() and change our
- // _currentTrace in the middle of its ececution. We cannot wait for
- // loadTrace to be finished before setting it because some internal
- // implementation details of loadTrace() rely on that trace to be current
- // to work properly (mainly the router hash uuid).
- } catch (err) {
- this.omnibox.showStatusMessage(`${err}`);
- throw err;
- } finally {
- this.appCtx.isLoadingTrace = false;
- raf.scheduleFullRedraw();
- }
+ // Rationale for asyncLimiter: openTrace takes several seconds and involves
+ // a long sequence of async tasks (e.g. invoking plugins' onLoad()). These
+ // tasks cannot overlap if the user opens traces in rapid succession, as
+ // they will mess up the state of registries. So once we start, we must
+ // complete trace loading (we don't bother supporting cancellations. If the
+ // user is too bothered, they can reload the tab).
+ this.appCtx.openTraceAsyncLimiter.schedule(async () => {
+ this.appCtx.closeCurrentTrace();
+ this.appCtx.isLoadingTrace = true;
+ try {
+ // loadTrace() in trace_loader.ts will do the following:
+ // - Create a new engine.
+ // - Pump the data from the TraceSource into the engine.
+ // - Do the initial queries to build the TraceImpl object
+ // - Call AppImpl.setActiveTrace(TraceImpl)
+ // - Continue with the trace loading logic (track decider, plugins, etc)
+ // - Resolve the promise when everything is done.
+ await loadTrace(this, src);
+ this.omnibox.reset(/* focus= */ false);
+ // loadTrace() internally will call setActiveTrace() and change our
+ // _currentTrace in the middle of its ececution. We cannot wait for
+ // loadTrace to be finished before setting it because some internal
+ // implementation details of loadTrace() rely on that trace to be current
+ // to work properly (mainly the router hash uuid).
+ } finally {
+ this.appCtx.isLoadingTrace = false;
+ raf.scheduleFullRedraw();
+ }
+ });
}
// Called by trace_loader.ts soon after it has created a new TraceImpl.
diff --git a/ui/src/core/channels.ts b/ui/src/core/channels.ts
index 22cf8e0..c8fcc99 100644
--- a/ui/src/core/channels.ts
+++ b/ui/src/core/channels.ts
@@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {raf} from './raf_scheduler';
-
export const DEFAULT_CHANNEL = 'stable';
const CHANNEL_KEY = 'perfettoUiChannel';
@@ -45,5 +43,4 @@
getCurrentChannel(); // Cache the current channel before mangling next one.
nextChannel = channel;
localStorage.setItem(CHANNEL_KEY, channel);
- raf.scheduleFullRedraw();
}
diff --git a/ui/src/core/color_unittest.ts b/ui/src/core/color_unittest.ts
index acc5cb5..9bf75ea 100644
--- a/ui/src/core/color_unittest.ts
+++ b/ui/src/core/color_unittest.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {HSLColor, hslToRgb, HSLuvColor} from '../public/color';
+import {HSLColor, hslToRgb, HSLuvColor} from '../base/color';
describe('HSLColor', () => {
const col = new HSLColor({h: 123, s: 66, l: 45});
diff --git a/ui/src/core/command_manager.ts b/ui/src/core/command_manager.ts
index ad0f482..2f6f0c3 100644
--- a/ui/src/core/command_manager.ts
+++ b/ui/src/core/command_manager.ts
@@ -43,7 +43,7 @@
runCommand(id: string, ...args: unknown[]): unknown {
const cmd = this.registry.get(id);
const res = cmd.callback(...args);
- Promise.resolve(res).finally(() => raf.scheduleFullRedraw('force'));
+ Promise.resolve(res).finally(() => raf.scheduleFullRedraw());
return res;
}
diff --git a/ui/src/core/cookie_consent.ts b/ui/src/core/cookie_consent.ts
index 471f928..c1aff04 100644
--- a/ui/src/core/cookie_consent.ts
+++ b/ui/src/core/cookie_consent.ts
@@ -13,7 +13,6 @@
// limitations under the License.
import m from 'mithril';
-import {raf} from './raf_scheduler';
import {AppImpl} from './app_impl';
const COOKIE_ACK_KEY = 'cookieAck';
@@ -59,7 +58,6 @@
onclick: () => {
this.showCookieConsent = false;
localStorage.setItem(COOKIE_ACK_KEY, 'true');
- raf.scheduleFullRedraw();
},
},
'OK',
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index be3a414..8a17ad3 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -34,9 +34,7 @@
'dev.perfetto.AndroidPerf',
'dev.perfetto.AndroidPerfTraceCounters',
'dev.perfetto.AndroidStartup',
- 'dev.perfetto.AsyncSlices',
'dev.perfetto.BookmarkletApi',
- 'dev.perfetto.Counter',
'dev.perfetto.CpuFreq',
'dev.perfetto.CpuProfile',
'dev.perfetto.CpuSlices',
@@ -46,7 +44,9 @@
'dev.perfetto.FlagsPage',
'dev.perfetto.Frames',
'dev.perfetto.Ftrace',
+ 'dev.perfetto.GpuFreq',
'dev.perfetto.HeapProfile',
+ 'dev.perfetto.InstrumentsSamplesProfile',
'dev.perfetto.LargeScreensPerf',
'dev.perfetto.MetricsPage',
'dev.perfetto.PerfSamplesProfile',
@@ -57,15 +57,20 @@
'dev.perfetto.ProcessThreadGroups',
'dev.perfetto.QueryPage',
'dev.perfetto.RecordTrace',
+ 'dev.perfetto.RecordTraceV2',
'dev.perfetto.RestorePinnedTrack',
'dev.perfetto.Sched',
'dev.perfetto.Screenshots',
+ 'dev.perfetto.StandardGroups',
'dev.perfetto.SqlModules',
+ 'dev.perfetto.SysUIWorkspace',
'dev.perfetto.Thread',
'dev.perfetto.ThreadState',
'dev.perfetto.TimelineSync',
'dev.perfetto.TraceInfoPage',
'dev.perfetto.TraceMetadata',
+ 'dev.perfetto.TraceProcessorTrack',
+ 'dev.perfetto.TrackEvent',
'dev.perfetto.VizPage',
'org.Chromium.OpenTableCommands',
'org.kernel.LinuxKernelSubsystems',
diff --git a/ui/src/core/flow_manager.ts b/ui/src/core/flow_manager.ts
index 7529ef7..ac7b5d7 100644
--- a/ui/src/core/flow_manager.ts
+++ b/ui/src/core/flow_manager.ts
@@ -23,7 +23,6 @@
} from '../public/track_kinds';
import {TrackDescriptor, TrackManager} from '../public/track';
import {AreaSelection, Selection, SelectionManager} from '../public/selection';
-import {raf} from './raf_scheduler';
import {Engine} from '../trace_processor/engine';
const SHOW_INDIRECT_PRECEDING_FLOWS_FLAG = featureFlags.register({
@@ -448,12 +447,10 @@
}
}
}
- raf.scheduleFullRedraw();
}
private setSelectedFlows(selectedFlows: Flow[]) {
this._selectedFlows = selectedFlows;
- raf.scheduleFullRedraw();
}
updateFlows(selection: Selection) {
@@ -511,7 +508,6 @@
);
this._focusedFlowIdRight = nextFlowId;
}
- raf.scheduleFullRedraw();
}
// Select the slice connected to the flow in focus
@@ -563,7 +559,6 @@
setCategoryVisible(name: string, value: boolean) {
this._visibleCategories.set(name, value);
- raf.scheduleFullRedraw();
}
}
diff --git a/ui/src/core/load_trace.ts b/ui/src/core/load_trace.ts
index ae24b1c..a888659 100644
--- a/ui/src/core/load_trace.ts
+++ b/ui/src/core/load_trace.ts
@@ -147,7 +147,7 @@
ftraceDropUntilAllCpusValid: FTRACE_DROP_UNTIL_FLAG.get(),
});
}
- engine.onResponseReceived = () => raf.scheduleFullRedraw('force');
+ engine.onResponseReceived = () => raf.scheduleFullRedraw();
if (isMetatracingEnabled()) {
engine.enableMetatrace(assertExists(getEnabledMetatracingCategories()));
@@ -287,9 +287,6 @@
updateStatus(trace, 'Creating processes summaries');
await engine.query(`include perfetto module viz.summary.processes;`);
-
- updateStatus(trace, 'Creating track summaries');
- await engine.query(`include perfetto module viz.summary.tracks;`);
}
function updateStatus(traceOrApp: TraceImpl | AppImpl, msg: string): void {
diff --git a/ui/src/core/note_manager.ts b/ui/src/core/note_manager.ts
index 20a17fe..639cc09 100644
--- a/ui/src/core/note_manager.ts
+++ b/ui/src/core/note_manager.ts
@@ -20,7 +20,6 @@
SpanNote,
} from '../public/note';
import {randomColor} from '../components/colorizer';
-import {raf} from './raf_scheduler';
export class NoteManagerImpl implements NoteManager {
private _lastNodeId = 0;
@@ -55,7 +54,6 @@
color: args.color ?? randomColor(),
text: args.text ?? '',
});
- raf.scheduleFullRedraw();
return id;
}
@@ -68,7 +66,6 @@
color: args.color ?? randomColor(),
text: args.text ?? '',
});
- raf.scheduleFullRedraw();
return id;
}
@@ -81,11 +78,9 @@
color: args.color ?? note.color,
text: args.text ?? note.text,
});
- raf.scheduleFullRedraw();
}
removeNote(id: string) {
- raf.scheduleFullRedraw();
this._notes.delete(id);
this.onNoteDeleted?.(id);
}
diff --git a/ui/src/core/omnibox_manager.ts b/ui/src/core/omnibox_manager.ts
index 08f67f3..fdeed19 100644
--- a/ui/src/core/omnibox_manager.ts
+++ b/ui/src/core/omnibox_manager.ts
@@ -79,7 +79,6 @@
focus(cursorPlacement?: number): void {
this._focusOmniboxNextRender = true;
this._pendingCursorPlacement = cursorPlacement;
- raf.scheduleFullRedraw();
}
clearFocusFlag(): void {
@@ -92,7 +91,6 @@
this._focusOmniboxNextRender = focus;
this._omniboxSelectionIndex = 0;
this.rejectPendingPrompt();
- raf.scheduleFullRedraw();
}
showStatusMessage(msg: string, durationMs = 2000) {
@@ -104,7 +102,6 @@
}, durationMs);
}
this._statusMessageContainer = statusMessageContainer;
- raf.scheduleFullRedraw();
}
get statusMessage(): string | undefined {
@@ -127,7 +124,6 @@
this._omniboxSelectionIndex = 0;
this.rejectPendingPrompt();
this._focusOmniboxNextRender = true;
- raf.scheduleFullRedraw();
if (choices && 'getName' in choices) {
return new Promise<T | undefined>((resolve) => {
@@ -175,7 +171,6 @@
this.setMode(defaultMode, focus);
this._omniboxSelectionIndex = 0;
this._statusMessageContainer = {};
- raf.scheduleFullRedraw();
}
private rejectPendingPrompt() {
diff --git a/ui/src/core/perf_manager.ts b/ui/src/core/perf_manager.ts
index e63e7e8..e144179 100644
--- a/ui/src/core/perf_manager.ts
+++ b/ui/src/core/perf_manager.ts
@@ -15,6 +15,7 @@
import m from 'mithril';
import {raf} from './raf_scheduler';
import {PerfStats, PerfStatsContainer, runningStatStr} from './perf_stats';
+import {MithrilEvent} from '../base/mithril_utils';
export class PerfManager {
private _enabled = false;
@@ -79,12 +80,15 @@
m(
'button.close-button',
{
- onclick: () => (attrs.perfMgr.enabled = false),
+ onclick: () => {
+ attrs.perfMgr.enabled = false;
+ raf.scheduleFullRedraw();
+ },
},
m('i.material-icons', 'close'),
),
attrs.perfMgr.containers.map((c, i) =>
- m('section', m('div', `Panel Container ${i + 1}`), c.renderPerfStats()),
+ m('section', m('div', `Container #${i + 1}`), c.renderPerfStats()),
),
);
}
@@ -95,7 +99,12 @@
m('div', [
m(
'button',
- {onclick: () => raf.scheduleCanvasRedraw()},
+ {
+ onclick: (e: MithrilEvent) => {
+ e.redraw = false;
+ raf.scheduleCanvasRedraw();
+ },
+ },
'Do Canvas Redraw',
),
' | ',
diff --git a/ui/src/core/raf_scheduler.ts b/ui/src/core/raf_scheduler.ts
index e4124bf..2d9bb0f 100644
--- a/ui/src/core/raf_scheduler.ts
+++ b/ui/src/core/raf_scheduler.ts
@@ -14,17 +14,9 @@
import {PerfStats} from './perf_stats';
import m from 'mithril';
-import {featureFlags} from './feature_flags';
+import {Raf, RedrawCallback} from '../public/raf';
export type AnimationCallback = (lastFrameMs: number) => void;
-export type RedrawCallback = () => void;
-
-export const AUTOREDRAW_FLAG = featureFlags.register({
- id: 'mithrilAutoredraw',
- name: 'Enable Mithril autoredraw',
- description: 'Turns calls to schedulefullRedraw() a no-op',
- defaultValue: true,
-});
// 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
@@ -34,7 +26,7 @@
// - redraw callbacks that will repaint canvases.
// This class guarantees that, on each frame, redraw callbacks are called after
// all action callbacks.
-export class RafScheduler {
+export class RafScheduler implements Raf {
// These happen at the beginning of any animation frame. Used by Animation.
private animationCallbacks = new Set<AnimationCallback>();
@@ -61,7 +53,7 @@
constructor() {
// Patch m.redraw() to our RAF full redraw.
const origSync = m.redraw.sync;
- const redrawFn = () => this.scheduleFullRedraw('force');
+ const redrawFn = () => this.scheduleFullRedraw();
redrawFn.sync = origSync;
m.redraw = redrawFn;
@@ -71,10 +63,7 @@
// Schedule re-rendering of virtual DOM and canvas.
// If a callback is passed it will be executed after the DOM redraw has
// completed.
- scheduleFullRedraw(force?: 'force', cb?: RedrawCallback) {
- // If we are using autoredraw mode, make this function a no-op unless
- // 'force' is passed.
- if (AUTOREDRAW_FLAG.get() && force !== 'force') return;
+ scheduleFullRedraw(cb?: RedrawCallback) {
this.requestedFullRedraw = true;
cb && this.postRedrawCallbacks.push(cb);
this.maybeScheduleAnimationFrame(true);
@@ -120,7 +109,6 @@
setPerfStatsEnabled(enabled: boolean) {
this.recordPerfStats = enabled;
- this.scheduleFullRedraw();
}
get hasPendingRedraws(): boolean {
@@ -153,10 +141,8 @@
redraw?: () => void,
) => void;
- mithrilRender(
- element,
- component !== null ? m(component) : null,
- AUTOREDRAW_FLAG.get() ? () => raf.scheduleFullRedraw('force') : undefined,
+ mithrilRender(element, component !== null ? m(component) : null, () =>
+ this.scheduleFullRedraw(),
);
}
diff --git a/ui/src/core/scroll_helper.ts b/ui/src/core/scroll_helper.ts
index c732b91..00524d8 100644
--- a/ui/src/core/scroll_helper.ts
+++ b/ui/src/core/scroll_helper.ts
@@ -114,7 +114,7 @@
private verticalScrollToTrack(trackUri: string, openGroup: boolean) {
// Find the actual track node that uses that URI, we need various properties
// from it.
- const trackNode = this.workspace.findTrackByUri(trackUri);
+ const trackNode = this.workspace.getTrackByUri(trackUri);
if (!trackNode) return;
// Try finding the track directly.
diff --git a/ui/src/core/search_manager.ts b/ui/src/core/search_manager.ts
index 3f2c6a9..f084b00 100644
--- a/ui/src/core/search_manager.ts
+++ b/ui/src/core/search_manager.ts
@@ -90,7 +90,6 @@
raf.scheduleFullRedraw();
});
}
- raf.scheduleFullRedraw();
}
reset() {
@@ -158,7 +157,6 @@
source: this._results.sources[this._resultIndex],
});
}
- raf.scheduleFullRedraw();
}
private setResultIndexWithSaturation(nextIndex: number) {
diff --git a/ui/src/core/selection_aggregation_manager.ts b/ui/src/core/selection_aggregation_manager.ts
index cba366f..f293e12 100644
--- a/ui/src/core/selection_aggregation_manager.ts
+++ b/ui/src/core/selection_aggregation_manager.ts
@@ -16,6 +16,8 @@
import {isString} from '../base/object_utils';
import {AggregateData, Column, ColumnDef, Sorting} from '../public/aggregation';
import {AreaSelection, AreaSelectionAggregator} from '../public/selection';
+import {TrackDescriptor} from '../public/track';
+import {Dataset, UnionDataset} from '../trace_processor/dataset';
import {Engine} from '../trace_processor/engine';
import {NUM} from '../trace_processor/query_result';
import {raf} from './raf_scheduler';
@@ -104,7 +106,13 @@
aggr: AreaSelectionAggregator,
area: AreaSelection,
): Promise<AggregateData | undefined> {
- const viewExists = await aggr.createAggregateView(this.engine, area);
+ const dataset = this.createDatasetForAggregator(aggr, area.tracks);
+ const viewExists = await aggr.createAggregateView(
+ this.engine,
+ area,
+ dataset,
+ );
+
if (!viewExists) {
return undefined;
}
@@ -173,6 +181,26 @@
return data;
}
+ private createDatasetForAggregator(
+ aggr: AreaSelectionAggregator,
+ tracks: ReadonlyArray<TrackDescriptor>,
+ ): Dataset | undefined {
+ const filteredDatasets = tracks
+ .filter(
+ (td) =>
+ aggr.trackKind === undefined || aggr.trackKind === td.tags?.kind,
+ )
+ .map((td) => td.track.getDataset?.())
+ .filter((dataset) => dataset !== undefined)
+ .filter(
+ (dataset) =>
+ aggr.schema === undefined || dataset.implements(aggr.schema),
+ );
+
+ if (filteredDatasets.length === 0) return undefined;
+ return new UnionDataset(filteredDatasets).optimize();
+ }
+
private async getSum(tableName: string, def: ColumnDef): Promise<string> {
if (!def.sum) return '';
const result = await this.engine.query(
diff --git a/ui/src/core/selection_manager.ts b/ui/src/core/selection_manager.ts
index 44303d2..a688e08 100644
--- a/ui/src/core/selection_manager.ts
+++ b/ui/src/core/selection_manager.ts
@@ -237,7 +237,6 @@
private setSelection(selection: Selection, opts?: SelectionOpts) {
this._selection = selection;
this.onSelectionChange(selection, opts ?? {});
- raf.scheduleFullRedraw();
if (opts?.scrollToSelection) {
this.scrollToCurrentSelection();
@@ -403,7 +402,11 @@
}
}
} else if (sel.kind === 'track_event') {
- return TimeSpan.fromTimeAndDuration(sel.ts, sel.dur);
+ // Pretend incomplete slices are instants. The -1 duration here is just a
+ // flag, and doesn't actually represent the duration of the event.
+ // Besides, TimeSpan's will throw if created with a negative duration.
+ const dur = sel.dur === -1n ? 0n : sel.dur;
+ return TimeSpan.fromTimeAndDuration(sel.ts, dur);
}
return undefined;
diff --git a/ui/src/core/sidebar_manager.ts b/ui/src/core/sidebar_manager.ts
index 9de9b90..e8000ae 100644
--- a/ui/src/core/sidebar_manager.ts
+++ b/ui/src/core/sidebar_manager.ts
@@ -14,7 +14,6 @@
import {Registry} from '../base/registry';
import {SidebarManager, SidebarMenuItem} from '../public/sidebar';
-import {raf} from './raf_scheduler';
export type SidebarMenuItemInternal = SidebarMenuItem & {
id: string; // A unique id generated by this class at registration time.
@@ -47,6 +46,5 @@
public toggleVisibility() {
if (!this.enabled) return;
this._visible = !this._visible;
- raf.scheduleFullRedraw();
}
}
diff --git a/ui/src/core/state_serialization.ts b/ui/src/core/state_serialization.ts
index 0ad4872..ddbaecc 100644
--- a/ui/src/core/state_serialization.ts
+++ b/ui/src/core/state_serialization.ts
@@ -179,7 +179,7 @@
// Restore the pinned tracks, if they exist.
for (const uri of appState.pinnedTracks) {
- const track = trace.workspace.findTrackByUri(uri);
+ const track = trace.workspace.getTrackByUri(uri);
if (track) {
track.pin();
}
diff --git a/ui/src/core/tab_manager.ts b/ui/src/core/tab_manager.ts
index d889f96..3273122 100644
--- a/ui/src/core/tab_manager.ts
+++ b/ui/src/core/tab_manager.ts
@@ -18,7 +18,6 @@
SplitPanelDrawerVisibility,
toggleVisibility,
} from '../widgets/split_panel';
-import {raf} from './raf_scheduler';
export interface ResolvedTab {
uri: string;
@@ -98,8 +97,6 @@
) {
this.setTabPanelVisibility(SplitPanelDrawerVisibility.VISIBLE);
}
-
- raf.scheduleFullRedraw();
}
// Hide a tab in the tab bar pick a new tab to show.
@@ -131,7 +128,6 @@
// Otherwise just remove the tab
this._openTabs = this._openTabs.filter((x) => x !== uri);
}
- raf.scheduleFullRedraw();
}
toggleTab(uri: string): void {
diff --git a/ui/src/core/timeline.ts b/ui/src/core/timeline.ts
index 0efc660..80d32a9 100644
--- a/ui/src/core/timeline.ts
+++ b/ui/src/core/timeline.ts
@@ -12,10 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {assertTrue, assertUnreachable} from '../base/logging';
+import {assertUnreachable} from '../base/logging';
import {Time, time, TimeSpan} from '../base/time';
import {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
-import {Area} from '../public/selection';
import {raf} from './raf_scheduler';
import {HighPrecisionTime} from '../base/high_precision_time';
import {DurationPrecision, Timeline, TimestampFormat} from '../public/timeline';
@@ -45,6 +44,14 @@
private _hoveredUtid?: number;
private _hoveredPid?: number;
+ // This is used to mark the timeline of the area that is currently being
+ // selected.
+ //
+ // TODO(stevegolton): This shouldn't really be in the global timeline state,
+ // it's really only a concept of the viewer page and should be moved there
+ // instead.
+ selectedSpan?: {start: time; end: time};
+
get highlightedSliceId() {
return this._highlightedSliceId;
}
@@ -81,9 +88,6 @@
raf.scheduleCanvasRedraw();
}
- // This is used to calculate the tracks within a Y range for area selection.
- private _selectedArea?: Area;
-
constructor(private readonly traceInfo: TraceInfo) {
this._visibleWindow = HighPrecisionTimeSpan.fromTime(
traceInfo.start,
@@ -125,29 +129,6 @@
this.updateVisibleTimeHP(newWindow);
}
- // Set the highlight box to draw
- selectArea(
- start: time,
- end: time,
- tracks = this._selectedArea ? this._selectedArea.trackUris : [],
- ) {
- assertTrue(
- end >= start,
- `Impossible select area: start [${start}] >= end [${end}]`,
- );
- this._selectedArea = {start, end, trackUris: tracks};
- raf.scheduleFullRedraw();
- }
-
- deselectArea() {
- this._selectedArea = undefined;
- raf.scheduleCanvasRedraw();
- }
-
- get selectedArea(): Area | undefined {
- return this._selectedArea;
- }
-
// Set visible window using an integer time span
updateVisibleTime(ts: TimeSpan) {
this.updateVisibleTimeHP(HighPrecisionTimeSpan.fromTime(ts.start, ts.end));
@@ -159,6 +140,24 @@
this.updateVisibleTime(new TimeSpan(start, end));
}
+ moveStart(delta: number) {
+ this.updateVisibleTimeHP(
+ new HighPrecisionTimeSpan(
+ this._visibleWindow.start.addNumber(delta),
+ this.visibleWindow.duration - delta,
+ ),
+ );
+ }
+
+ moveEnd(delta: number) {
+ this.updateVisibleTimeHP(
+ new HighPrecisionTimeSpan(
+ this._visibleWindow.start,
+ this.visibleWindow.duration + delta,
+ ),
+ );
+ }
+
// Set visible window using a high precision time span
updateVisibleTimeHP(ts: HighPrecisionTimeSpan) {
this._visibleWindow = ts
diff --git a/ui/src/core/trace_impl.ts b/ui/src/core/trace_impl.ts
index d86a310..88d05f6 100644
--- a/ui/src/core/trace_impl.ts
+++ b/ui/src/core/trace_impl.ts
@@ -52,6 +52,7 @@
import {PostedTrace} from './trace_source';
import {PerfManager} from './perf_manager';
import {EvtSource} from '../base/events';
+import {Raf} from '../public/raf';
/**
* Handles the per-trace state of the UI
@@ -419,8 +420,8 @@
};
}
- scheduleFullRedraw(): void {
- this.appImpl.scheduleFullRedraw();
+ get raf(): Raf {
+ return this.appImpl.raf;
}
navigate(newHash: string): void {
diff --git a/ui/src/core/workspace_manager.ts b/ui/src/core/workspace_manager.ts
index 77803b7..6990421 100644
--- a/ui/src/core/workspace_manager.ts
+++ b/ui/src/core/workspace_manager.ts
@@ -14,36 +14,44 @@
import {assertTrue} from '../base/logging';
import {Workspace, WorkspaceManager} from '../public/workspace';
-import {raf} from './raf_scheduler';
const DEFAULT_WORKSPACE_NAME = 'Default Workspace';
export class WorkspaceManagerImpl implements WorkspaceManager {
+ readonly defaultWorkspace = new Workspace();
private _workspaces: Workspace[] = [];
private _currentWorkspace: Workspace;
constructor() {
- // TS compiler cannot see that we are indirectly initializing
- // _currentWorkspace via resetWorkspaces(), hence the re-assignment.
- this._currentWorkspace = this.createEmptyWorkspace(DEFAULT_WORKSPACE_NAME);
+ this.defaultWorkspace.title = DEFAULT_WORKSPACE_NAME;
+ this._currentWorkspace = this.defaultWorkspace;
}
createEmptyWorkspace(title: string): Workspace {
const workspace = new Workspace();
workspace.title = title;
- workspace.onchange = () => raf.scheduleFullRedraw();
this._workspaces.push(workspace);
return workspace;
}
+ removeWorkspace(ws: Workspace) {
+ if (ws === this.currentWorkspace) {
+ this._currentWorkspace = this.defaultWorkspace;
+ }
+ this._workspaces = this._workspaces.filter((w) => w !== ws);
+ }
+
switchWorkspace(workspace: Workspace): void {
// If this fails the workspace doesn't come from createEmptyWorkspace().
- assertTrue(this._workspaces.includes(workspace));
+ assertTrue(
+ this._workspaces.includes(workspace) ||
+ workspace === this.defaultWorkspace,
+ );
this._currentWorkspace = workspace;
}
get all(): ReadonlyArray<Workspace> {
- return this._workspaces;
+ return [this.defaultWorkspace].concat(this._workspaces);
}
get currentWorkspace() {
diff --git a/ui/src/core_plugins/commands/index.ts b/ui/src/core_plugins/commands/index.ts
index adc3bce..e6087fe 100644
--- a/ui/src/core_plugins/commands/index.ts
+++ b/ui/src/core_plugins/commands/index.ts
@@ -99,6 +99,17 @@
defaultValue: false,
});
+function getOrPromptForTimestamp(tsRaw: unknown): time | undefined {
+ if (exists(tsRaw)) {
+ if (typeof tsRaw !== 'bigint') {
+ throw Error(`${tsRaw} is not a bigint`);
+ }
+ return Time.fromRaw(tsRaw);
+ }
+ // No args passed, probably run from the command palette.
+ return promptForTimestamp('Enter a timestamp');
+}
+
export default class implements PerfettoPlugin {
static readonly id = 'perfetto.CoreCommands';
static onActivate(ctx: App) {
@@ -250,17 +261,22 @@
id: 'perfetto.CoreCommands#PanToTimestamp',
name: 'Pan to timestamp',
callback: (tsRaw: unknown) => {
- if (exists(tsRaw)) {
- if (typeof tsRaw !== 'bigint') {
- throw Error(`${tsRaw} is not a bigint`);
- }
- ctx.timeline.panToTimestamp(Time.fromRaw(tsRaw));
- } else {
- // No args passed, probably run from the command palette.
- const ts = promptForTimestamp('Enter a timestamp');
- if (exists(ts)) {
- ctx.timeline.panToTimestamp(Time.fromRaw(ts));
- }
+ const ts = getOrPromptForTimestamp(tsRaw);
+ if (ts !== undefined) {
+ ctx.timeline.panToTimestamp(ts);
+ }
+ },
+ });
+
+ ctx.commands.registerCommand({
+ id: 'perfetto.CoreCommands#MarkTimestamp',
+ name: 'Mark timestamp',
+ callback: (tsRaw: unknown) => {
+ const ts = getOrPromptForTimestamp(tsRaw);
+ if (ts !== undefined) {
+ ctx.notes.addNote({
+ timestamp: ts,
+ });
}
},
});
@@ -277,7 +293,7 @@
id: 'createNewEmptyWorkspace',
name: 'Create new empty workspace',
callback: async () => {
- const workspaces = AppImpl.instance.trace?.workspaces;
+ const workspaces = ctx.workspaces;
if (workspaces === undefined) return; // No trace loaded.
const name = await ctx.omnibox.prompt('Give it a name...');
if (name === undefined || name === '') return;
@@ -289,7 +305,7 @@
id: 'switchWorkspace',
name: 'Switch workspace',
callback: async () => {
- const workspaces = AppImpl.instance.trace?.workspaces;
+ const workspaces = ctx.workspaces;
if (workspaces === undefined) return; // No trace loaded.
const workspace = await ctx.omnibox.prompt('Choose a workspace...', {
values: workspaces.all,
diff --git a/ui/src/core_plugins/flags_page/flags_page.ts b/ui/src/core_plugins/flags_page/flags_page.ts
index d8cfdf6..fc21d3f 100644
--- a/ui/src/core_plugins/flags_page/flags_page.ts
+++ b/ui/src/core_plugins/flags_page/flags_page.ts
@@ -16,7 +16,6 @@
import {channelChanged, getNextChannel, setChannel} from '../../core/channels';
import {featureFlags} from '../../core/feature_flags';
import {Flag, OverrideState} from '../../public/feature_flag';
-import {raf} from '../../core/raf_scheduler';
import {PageAttrs} from '../../public/page';
import {Router} from '../../core/router';
@@ -52,7 +51,6 @@
onchange: (e: InputEvent) => {
const value = (e.target as HTMLSelectElement).value;
attrs.onSelect(value);
- raf.scheduleFullRedraw();
},
},
attrs.options.map((o) => {
@@ -139,7 +137,6 @@
{
onclick: () => {
featureFlags.resetAll();
- raf.scheduleFullRedraw();
},
},
'Reset all below',
diff --git a/ui/src/core_plugins/flags_page/plugins_page.ts b/ui/src/core_plugins/flags_page/plugins_page.ts
index ed5bd0b..4d8c7bf 100644
--- a/ui/src/core_plugins/flags_page/plugins_page.ts
+++ b/ui/src/core_plugins/flags_page/plugins_page.ts
@@ -20,7 +20,6 @@
import {PageAttrs} from '../../public/page';
import {AppImpl} from '../../core/app_impl';
import {PluginWrapper} from '../../core/plugin_manager';
-import {raf} from '../../core/raf_scheduler';
// This flag indicated whether we need to restart the UI to apply plugin
// changes. It is purposely a global as we want it to outlive the Mithril
@@ -50,7 +49,6 @@
plugin.enableFlag.set(false);
}
needsRestart = true;
- raf.scheduleFullRedraw();
},
}),
m(Button, {
@@ -61,7 +59,6 @@
plugin.enableFlag.set(true);
}
needsRestart = true;
- raf.scheduleFullRedraw();
},
}),
m(Button, {
@@ -72,7 +69,6 @@
plugin.enableFlag.reset();
}
needsRestart = true;
- raf.scheduleFullRedraw();
},
}),
),
@@ -114,7 +110,6 @@
plugin.enableFlag.set(true);
}
needsRestart = true;
- raf.scheduleFullRedraw();
},
}),
exists(loadTime)
diff --git a/ui/src/core_plugins/global_groups/index.ts b/ui/src/core_plugins/global_groups/index.ts
index 4ae9e27..fbc4388 100644
--- a/ui/src/core_plugins/global_groups/index.ts
+++ b/ui/src/core_plugins/global_groups/index.ts
@@ -14,66 +14,12 @@
import {PerfettoPlugin} from '../../public/plugin';
import {Trace} from '../../public/trace';
-import {TrackNode} from '../../public/workspace';
-
-const MEM_DMA_COUNTER_NAME = 'mem.dma_heap';
-const MEM_DMA = 'mem.dma_buffer';
-const MEM_ION = 'mem.ion';
-const F2FS_IOSTAT_TAG = 'f2fs_iostat.';
-const F2FS_IOSTAT_GROUP_NAME = 'f2fs_iostat';
-const F2FS_IOSTAT_LAT_TAG = 'f2fs_iostat_latency.';
-const F2FS_IOSTAT_LAT_GROUP_NAME = 'f2fs_iostat_latency';
-const DISK_IOSTAT_TAG = 'diskstat.';
-const DISK_IOSTAT_GROUP_NAME = 'diskstat';
-const BUDDY_INFO_TAG = 'mem.buddyinfo';
-const UFS_CMD_TAG_REGEX = new RegExp('^io.ufs.command.tag.*$');
-const UFS_CMD_TAG_GROUP = 'io.ufs.command.tags';
-// NB: Userspace wakelocks start with "WakeLock" not "Wakelock".
-const KERNEL_WAKELOCK_REGEX = new RegExp('^Wakelock.*$');
-const KERNEL_WAKELOCK_GROUP = 'Kernel wakelocks';
-const NETWORK_TRACK_REGEX = new RegExp('^.* (Received|Transmitted)( KB)?$');
-const NETWORK_TRACK_GROUP = 'Networking';
-const ENTITY_RESIDENCY_REGEX = new RegExp('^Entity residency:');
-const ENTITY_RESIDENCY_GROUP = 'Entity residency';
-const UCLAMP_REGEX = new RegExp('^UCLAMP_');
-const UCLAMP_GROUP = 'Scheduler Utilization Clamping';
-const POWER_RAILS_GROUP = 'Power Rails';
-const POWER_RAILS_REGEX = new RegExp('^power.');
-const FREQUENCY_GROUP = 'Frequency Scaling';
-const TEMPERATURE_REGEX = new RegExp('^.* Temperature$');
-const TEMPERATURE_GROUP = 'Temperature';
-const IRQ_GROUP = 'IRQs';
-const IRQ_REGEX = new RegExp('^(Irq|SoftIrq) Cpu.*');
-const CHROME_TRACK_REGEX = new RegExp('^Chrome.*|^InputLatency::.*');
-const CHROME_TRACK_GROUP = 'Chrome Global Tracks';
-const MISC_GROUP = 'Misc Global Tracks';
// This plugin is responsible for organizing all the global tracks.
export default class implements PerfettoPlugin {
static readonly id = 'perfetto.GlobalGroups';
async onTraceLoad(trace: Trace): Promise<void> {
trace.onTraceReady.addListener(() => {
- groupGlobalIonTracks(trace);
- groupGlobalIostatTracks(trace, F2FS_IOSTAT_TAG, F2FS_IOSTAT_GROUP_NAME);
- groupGlobalIostatTracks(
- trace,
- F2FS_IOSTAT_LAT_TAG,
- F2FS_IOSTAT_LAT_GROUP_NAME,
- );
- groupGlobalIostatTracks(trace, DISK_IOSTAT_TAG, DISK_IOSTAT_GROUP_NAME);
- groupTracksByRegex(trace, UFS_CMD_TAG_REGEX, UFS_CMD_TAG_GROUP);
- groupGlobalBuddyInfoTracks(trace);
- groupTracksByRegex(trace, KERNEL_WAKELOCK_REGEX, KERNEL_WAKELOCK_GROUP);
- groupTracksByRegex(trace, NETWORK_TRACK_REGEX, NETWORK_TRACK_GROUP);
- groupTracksByRegex(trace, ENTITY_RESIDENCY_REGEX, ENTITY_RESIDENCY_GROUP);
- groupTracksByRegex(trace, UCLAMP_REGEX, UCLAMP_GROUP);
- groupFrequencyTracks(trace, FREQUENCY_GROUP);
- groupTracksByRegex(trace, POWER_RAILS_REGEX, POWER_RAILS_GROUP);
- groupTracksByRegex(trace, TEMPERATURE_REGEX, TEMPERATURE_GROUP);
- groupTracksByRegex(trace, IRQ_REGEX, IRQ_GROUP);
- groupTracksByRegex(trace, CHROME_TRACK_REGEX, CHROME_TRACK_GROUP);
- groupMiscNonAllowlistedTracks(trace, MISC_GROUP);
-
// Move groups underneath tracks
Array.from(trace.workspace.children)
.sort((a, b) => {
@@ -92,171 +38,3 @@
});
}
}
-
-function groupGlobalIonTracks(trace: Trace): void {
- const ionTracks: TrackNode[] = [];
- let hasSummary = false;
-
- for (const track of trace.workspace.children) {
- if (track.hasChildren) continue;
-
- const isIon = track.title.startsWith(MEM_ION);
- const isIonCounter = track.title === MEM_ION;
- const isDmaHeapCounter = track.title === MEM_DMA_COUNTER_NAME;
- const isDmaBuffferSlices = track.title === MEM_DMA;
- if (isIon || isIonCounter || isDmaHeapCounter || isDmaBuffferSlices) {
- ionTracks.push(track);
- }
- hasSummary = hasSummary || isIonCounter;
- hasSummary = hasSummary || isDmaHeapCounter;
- }
-
- if (ionTracks.length === 0 || !hasSummary) {
- return;
- }
-
- const tracksToAddToGroup: TrackNode[] = [];
- let memGroupNode: TrackNode | undefined;
- for (const track of ionTracks) {
- if (
- !memGroupNode &&
- [MEM_DMA_COUNTER_NAME, MEM_ION].includes(track.title)
- ) {
- // Create a new group that copies the details from this track
- memGroupNode = new TrackNode({
- uri: track.uri,
- title: track.title,
- isSummary: true,
- });
- // Remove it from the workspace as we're going to add the group later
- track.remove();
- } else {
- tracksToAddToGroup.push(track);
- }
- }
-
- if (memGroupNode) {
- tracksToAddToGroup.forEach((t) => memGroupNode.addChildInOrder(t));
- trace.workspace.addChildInOrder(memGroupNode);
- }
-}
-
-function groupGlobalIostatTracks(
- trace: Trace,
- tag: string,
- groupName: string,
-): void {
- const devMap = new Map<string, TrackNode>();
-
- for (const track of trace.workspace.children) {
- if (track.hasChildren) continue;
- if (track.title.startsWith(tag)) {
- const name = track.title.split('.', 3);
- const key = name[1];
-
- let parentGroup = devMap.get(key);
- if (!parentGroup) {
- const group = new TrackNode({title: groupName, isSummary: true});
- trace.workspace.addChildInOrder(group);
- devMap.set(key, group);
- parentGroup = group;
- }
-
- track.title = name[2];
- parentGroup.addChildInOrder(track);
- }
- }
-}
-
-function groupGlobalBuddyInfoTracks(trace: Trace): void {
- const devMap = new Map<string, TrackNode>();
-
- for (const track of trace.workspace.children) {
- if (track.hasChildren) continue;
- if (track.title.startsWith(BUDDY_INFO_TAG)) {
- const tokens = track.title.split('[');
- const node = tokens[1].slice(0, -1);
- const zone = tokens[2].slice(0, -1);
- const size = tokens[3].slice(0, -1);
-
- const groupName = 'Buddyinfo: Node: ' + node + ' Zone: ' + zone;
- if (!devMap.has(groupName)) {
- const group = new TrackNode({title: groupName, isSummary: true});
- devMap.set(groupName, group);
- trace.workspace.addChildInOrder(group);
- }
- track.title = 'Chunk size: ' + size;
- const group = devMap.get(groupName)!;
- group.addChildInOrder(track);
- }
- }
-}
-
-function groupFrequencyTracks(trace: Trace, groupName: string): void {
- const group = new TrackNode({title: groupName, isSummary: true});
-
- for (const track of trace.workspace.children) {
- if (track.hasChildren) continue;
- // Group all the frequency tracks together (except the CPU and GPU
- // frequency ones).
- if (
- track.title.endsWith('Frequency') &&
- !track.title.startsWith('Cpu') &&
- !track.title.startsWith('Gpu')
- ) {
- group.addChildInOrder(track);
- }
- }
-
- if (group.children.length > 0) {
- trace.workspace.addChildInOrder(group);
- }
-}
-
-function groupMiscNonAllowlistedTracks(trace: Trace, groupName: string): void {
- // List of allowlisted track names.
- const ALLOWLIST_REGEXES = [
- new RegExp('^Cpu .*$', 'i'),
- new RegExp('^Gpu .*$', 'i'),
- new RegExp('^Trace Triggers$'),
- new RegExp('^Android App Startups$'),
- new RegExp('^Device State.*$'),
- new RegExp('^Android logs$'),
- ];
-
- const group = new TrackNode({title: groupName, isSummary: true});
- for (const track of trace.workspace.children) {
- if (track.hasChildren) continue;
- let allowlisted = false;
- for (const regex of ALLOWLIST_REGEXES) {
- allowlisted = allowlisted || regex.test(track.title);
- }
- if (allowlisted) {
- continue;
- }
- group.addChildInOrder(track);
- }
-
- if (group.children.length > 0) {
- trace.workspace.addChildInOrder(group);
- }
-}
-
-function groupTracksByRegex(
- trace: Trace,
- regex: RegExp,
- groupName: string,
-): void {
- const group = new TrackNode({title: groupName, isSummary: true});
-
- for (const track of trace.workspace.children) {
- if (track.hasChildren) continue;
- if (regex.test(track.title)) {
- group.addChildInOrder(track);
- }
- }
-
- if (group.children.length > 0) {
- trace.workspace.addChildInOrder(group);
- }
-}
diff --git a/ui/src/frontend/aggregation_tab.ts b/ui/src/frontend/aggregation_tab.ts
index bad2bfb..708096a 100644
--- a/ui/src/frontend/aggregation_tab.ts
+++ b/ui/src/frontend/aggregation_tab.ts
@@ -17,7 +17,6 @@
import {isEmptyData} from '../public/aggregation';
import {DetailsShell} from '../widgets/details_shell';
import {Button, ButtonBar} from '../widgets/button';
-import {raf} from '../core/raf_scheduler';
import {EmptyState} from '../widgets/empty_state';
import {FlowEventsAreaSelectedPanel} from './flow_events_panel';
import {PivotTable} from './pivot_table';
@@ -26,6 +25,7 @@
import {
CPU_PROFILE_TRACK_KIND,
PERF_SAMPLES_PROFILE_TRACK_KIND,
+ INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND,
SLICE_TRACK_KIND,
} from '../public/track_kinds';
import {
@@ -39,9 +39,13 @@
import {Flamegraph} from '../widgets/flamegraph';
interface View {
- key: string;
- name: string;
- content: m.Children;
+ readonly key: string;
+ readonly name: string;
+ readonly specificity?: {
+ readonly kind: number;
+ readonly schema: number;
+ };
+ readonly content: m.Children;
}
export type AreaDetailsPanelAttrs = {trace: TraceImpl};
@@ -52,6 +56,7 @@
private currentTab: string | undefined = undefined;
private cpuProfileFlamegraph?: QueryFlamegraph;
private perfSampleFlamegraph?: QueryFlamegraph;
+ private instrumentsSampleFlamegraph?: QueryFlamegraph;
private sliceFlamegraph?: QueryFlamegraph;
constructor({attrs}: m.CVnode<AreaDetailsPanelAttrs>) {
@@ -88,6 +93,12 @@
views.push({
key: value.tabName,
name: value.tabName,
+ specificity: {
+ kind: aggregator.trackKind ? 1 : 0,
+ schema: aggregator.schema
+ ? Object.keys(aggregator.schema).length
+ : 0,
+ },
content: m(AggregationPanel, {
aggregatorId,
data: value,
@@ -97,6 +108,23 @@
}
}
+ views.sort((a, b) => {
+ if (a.specificity === undefined || b.specificity === undefined) {
+ return 0;
+ }
+
+ if (a.specificity.kind !== b.specificity.kind) {
+ return b.specificity.kind - a.specificity.kind;
+ }
+
+ if (a.specificity.schema !== b.specificity.schema) {
+ return b.specificity.schema - a.specificity.schema;
+ }
+
+ // If all else is equal, fall back to the registration order.
+ return 0;
+ });
+
const pivotTableState = this.trace.pivotTable.state;
const tree = pivotTableState.queryResult?.tree;
if (
@@ -135,7 +163,6 @@
return m(Button, {
onclick: () => {
this.currentTab = key;
- raf.scheduleFullRedraw();
},
key,
label: name,
@@ -196,6 +223,17 @@
content: this.perfSampleFlamegraph.render(),
});
}
+ this.instrumentsSampleFlamegraph = this.computeInstrumentsSampleFlamegraph(
+ trace,
+ isChanged,
+ );
+ if (this.instrumentsSampleFlamegraph !== undefined) {
+ views.push({
+ key: 'instruments_sample_flamegraph_selection',
+ name: 'Instruments Sample Flamegraph',
+ content: this.instrumentsSampleFlamegraph.render(),
+ });
+ }
this.sliceFlamegraph = this.computeSliceFlamegraph(trace, isChanged);
if (this.sliceFlamegraph !== undefined) {
views.push({
@@ -318,6 +356,52 @@
});
}
+ private computeInstrumentsSampleFlamegraph(trace: Trace, isChanged: boolean) {
+ const currentSelection = trace.selection.selection;
+ if (currentSelection.kind !== 'area') {
+ return undefined;
+ }
+ if (!isChanged) {
+ // If the selection has not changed, just return a copy of the last seen
+ // attrs.
+ return this.instrumentsSampleFlamegraph;
+ }
+ const upids = getUpidsFromInstrumentsSampleAreaSelection(currentSelection);
+ const utids = getUtidsFromInstrumentsSampleAreaSelection(currentSelection);
+ if (utids.length === 0 && upids.length === 0) {
+ return undefined;
+ }
+ const metrics = metricsFromTableOrSubquery(
+ `
+ (
+ select id, parent_id as parentId, name, self_count
+ from _callstacks_for_callsites!((
+ select p.callsite_id
+ from instruments_sample p
+ join thread t using (utid)
+ where p.ts >= ${currentSelection.start}
+ and p.ts <= ${currentSelection.end}
+ and (
+ p.utid in (${utids.join(',')})
+ or t.upid in (${upids.join(',')})
+ )
+ ))
+ )
+ `,
+ [
+ {
+ name: 'Instruments Samples',
+ unit: '',
+ columnName: 'self_count',
+ },
+ ],
+ 'include perfetto module appleos.instruments.samples',
+ );
+ return new QueryFlamegraph(trace, metrics, {
+ state: Flamegraph.createDefaultState(metrics),
+ });
+ }
+
private computeSliceFlamegraph(trace: Trace, isChanged: boolean) {
const currentSelection = trace.selection.selection;
if (currentSelection.kind !== 'area') {
@@ -423,3 +507,33 @@
}
return utids;
}
+
+function getUpidsFromInstrumentsSampleAreaSelection(
+ currentSelection: AreaSelection,
+) {
+ const upids = [];
+ for (const trackInfo of currentSelection.tracks) {
+ if (
+ trackInfo?.tags?.kind === INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND &&
+ trackInfo.tags?.utid === undefined
+ ) {
+ upids.push(assertExists(trackInfo.tags?.upid));
+ }
+ }
+ return upids;
+}
+
+function getUtidsFromInstrumentsSampleAreaSelection(
+ currentSelection: AreaSelection,
+) {
+ const utids = [];
+ for (const trackInfo of currentSelection.tracks) {
+ if (
+ trackInfo?.tags?.kind === INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND &&
+ trackInfo.tags?.utid !== undefined
+ ) {
+ utids.push(trackInfo.tags?.utid);
+ }
+ }
+ return utids;
+}
diff --git a/ui/src/frontend/drag/border_drag_strategy.ts b/ui/src/frontend/drag/border_drag_strategy.ts
deleted file mode 100644
index c98b02d..0000000
--- a/ui/src/frontend/drag/border_drag_strategy.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-import {TimeScale} from '../../base/time_scale';
-import {DragStrategy, DragStrategyUpdateTimeFn} from './drag_strategy';
-
-export class BorderDragStrategy extends DragStrategy {
- private moveStart = false;
-
- constructor(
- map: TimeScale,
- private pixelBounds: [number, number],
- updateVizTime: DragStrategyUpdateTimeFn,
- ) {
- super(map, updateVizTime);
- }
-
- onDrag(x: number) {
- const moveStartPx = this.moveStart ? x : this.pixelBounds[0];
- const moveEndPx = !this.moveStart ? x : this.pixelBounds[1];
- const tStart = this.map.pxToHpTime(Math.min(moveStartPx, moveEndPx));
- const tEnd = this.map.pxToHpTime(Math.max(moveStartPx, moveEndPx));
- if (moveStartPx > moveEndPx) {
- this.moveStart = !this.moveStart;
- }
- super.updateGlobals(tStart, tEnd);
- this.pixelBounds = [this.map.hpTimeToPx(tStart), this.map.hpTimeToPx(tEnd)];
- }
-
- onDragStart(x: number) {
- this.moveStart =
- Math.abs(x - this.pixelBounds[0]) < Math.abs(x - this.pixelBounds[1]);
- }
-}
diff --git a/ui/src/frontend/drag/drag_strategy.ts b/ui/src/frontend/drag/drag_strategy.ts
deleted file mode 100644
index 0a0f3d4..0000000
--- a/ui/src/frontend/drag/drag_strategy.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-import {HighPrecisionTime} from '../../base/high_precision_time';
-import {HighPrecisionTimeSpan} from '../../base/high_precision_time_span';
-import {TimeScale} from '../../base/time_scale';
-
-export type DragStrategyUpdateTimeFn = (ts: HighPrecisionTimeSpan) => void;
-
-export abstract class DragStrategy {
- constructor(
- protected map: TimeScale,
- private updateVizTime: DragStrategyUpdateTimeFn,
- ) {}
-
- abstract onDrag(x: number): void;
-
- abstract onDragStart(x: number): void;
-
- protected updateGlobals(tStart: HighPrecisionTime, tEnd: HighPrecisionTime) {
- const vizTime = new HighPrecisionTimeSpan(
- tStart,
- tEnd.sub(tStart).toNumber(),
- );
- this.updateVizTime(vizTime);
- }
-}
diff --git a/ui/src/frontend/drag/inner_drag_strategy.ts b/ui/src/frontend/drag/inner_drag_strategy.ts
deleted file mode 100644
index 61ad694..0000000
--- a/ui/src/frontend/drag/inner_drag_strategy.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-import {TimeScale} from '../../base/time_scale';
-import {DragStrategy, DragStrategyUpdateTimeFn} from './drag_strategy';
-
-export class InnerDragStrategy extends DragStrategy {
- private dragStartPx = 0;
-
- constructor(
- timeScale: TimeScale,
- private pixelBounds: [number, number],
- updateVizTime: DragStrategyUpdateTimeFn,
- ) {
- super(timeScale, updateVizTime);
- }
-
- onDrag(x: number) {
- const move = x - this.dragStartPx;
- const tStart = this.map.pxToHpTime(this.pixelBounds[0] + move);
- const tEnd = this.map.pxToHpTime(this.pixelBounds[1] + move);
- super.updateGlobals(tStart, tEnd);
- }
-
- onDragStart(x: number) {
- this.dragStartPx = x;
- }
-}
diff --git a/ui/src/frontend/drag/outer_drag_strategy.ts b/ui/src/frontend/drag/outer_drag_strategy.ts
deleted file mode 100644
index 1374193..0000000
--- a/ui/src/frontend/drag/outer_drag_strategy.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-import {DragStrategy} from './drag_strategy';
-
-export class OuterDragStrategy extends DragStrategy {
- private dragStartPx = 0;
-
- onDrag(x: number) {
- const tStart = this.map.pxToHpTime(Math.min(x, this.dragStartPx));
- const tEnd = this.map.pxToHpTime(Math.max(x, this.dragStartPx));
- super.updateGlobals(tStart, tEnd);
- }
-
- onDragStart(x: number) {
- this.dragStartPx = x;
- }
-}
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index 3e27b07..862333d 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -34,6 +34,7 @@
// Here we rely on the exception message from onCannotGrowMemory function
if (
err.message.includes('Cannot enlarge memory') ||
+ err.stack.some((entry) => entry.name.includes('base::AlignedAlloc')) ||
err.stack.some((entry) => entry.name.includes('OutOfMemoryHandler')) ||
err.stack.some((entry) => entry.name.includes('_emscripten_resize_heap')) ||
err.stack.some((entry) => entry.name.includes('sbrk')) ||
@@ -230,7 +231,6 @@
}
private onUploadCheckboxChange(checked: boolean) {
- raf.scheduleFullRedraw();
this.attachTrace = checked;
if (
@@ -242,7 +242,7 @@
this.uploadStatus = '';
const uploader = new GcsUploader(this.traceData, {
onProgress: () => {
- raf.scheduleFullRedraw('force');
+ raf.scheduleFullRedraw();
this.uploadStatus = uploader.getEtaString();
if (uploader.state === 'UPLOADED') {
this.traceState = 'UPLOADED';
diff --git a/ui/src/frontend/flow_events_panel.ts b/ui/src/frontend/flow_events_panel.ts
index 434d549..acf5f6a 100644
--- a/ui/src/frontend/flow_events_panel.ts
+++ b/ui/src/frontend/flow_events_panel.ts
@@ -14,7 +14,6 @@
import m from 'mithril';
import {Icons} from '../base/semantic_icons';
-import {raf} from '../core/raf_scheduler';
import {Flow} from '../core/flow_types';
import {TraceImpl} from '../core/trace_impl';
@@ -124,7 +123,6 @@
flows.setCategoryVisible(ALL_CATEGORIES, false);
}
flows.setCategoryVisible(cat, !wasChecked);
- raf.scheduleFullRedraw();
},
},
wasChecked ? Icons.Checkbox : Icons.BlankCheckbox,
diff --git a/ui/src/frontend/help_modal.ts b/ui/src/frontend/help_modal.ts
index acf092a..7051d36 100644
--- a/ui/src/frontend/help_modal.ts
+++ b/ui/src/frontend/help_modal.ts
@@ -23,7 +23,8 @@
nativeKeyboardLayoutMap,
NotSupportedError,
} from '../base/keyboard_layout_map';
-import {KeyMapping} from './viewer_page/pan_and_zoom_handler';
+import {KeyMapping} from './viewer_page/wasd_navigation_handler';
+import {raf} from '../core/raf_scheduler';
export function toggleHelp() {
AppImpl.instance.analytics.logEvent('User Actions', 'Show help');
@@ -54,7 +55,7 @@
nativeKeyboardLayoutMap()
.then((keyMap: KeyboardLayoutMap) => {
this.keyMap = keyMap;
- AppImpl.instance.scheduleFullRedraw('force');
+ raf.scheduleFullRedraw();
})
.catch((e) => {
if (
@@ -69,7 +70,7 @@
// The alternative would be to show key mappings for all keyboard
// layouts which is not feasible.
this.keyMap = new EnglishQwertyKeyboardLayoutMap();
- AppImpl.instance.scheduleFullRedraw('force');
+ raf.scheduleFullRedraw();
} else {
// Something unexpected happened. Either the browser doesn't conform
// to the keyboard API spec, or the keyboard API spec has changed!
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index d03a904..2d0fa7a 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -24,7 +24,6 @@
import {initLiveReload} from '../core/live_reload';
import {raf} from '../core/raf_scheduler';
import {initWasm} from '../trace_processor/wasm_engine_proxy';
-import {setScheduleFullRedraw} from '../widgets/raf';
import {UiMain} from './ui_main';
import {initCssConstants} from './css_constants';
import {registerDebugGlobals} from './debug';
@@ -63,7 +62,7 @@
});
function routeChange(route: Route) {
- raf.scheduleFullRedraw('force', () => {
+ 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
@@ -147,9 +146,6 @@
initialRouteArgs: Router.parseUrl(window.location.href).args,
});
- // Wire up raf for widgets.
- setScheduleFullRedraw((force?: 'force') => raf.scheduleFullRedraw(force));
-
// Load the css. The load is asynchronous and the CSS is not ready by the time
// appendChild returns.
const cssLoadPromise = defer<void>();
diff --git a/ui/src/frontend/omnibox.ts b/ui/src/frontend/omnibox.ts
index 36efe8e..e0562bc 100644
--- a/ui/src/frontend/omnibox.ts
+++ b/ui/src/frontend/omnibox.ts
@@ -333,7 +333,7 @@
private onMouseDown = (e: Event) => {
// We need to schedule a redraw manually as this event handler was added
// manually to the DOM and doesn't use Mithril's auto-redraw system.
- raf.scheduleFullRedraw('force');
+ raf.scheduleFullRedraw();
// Don't close if the click was within ourselves or our popup.
if (e.target instanceof Node) {
@@ -349,7 +349,6 @@
private close(attrs: OmniboxAttrs): void {
const {onClose = () => {}} = attrs;
- raf.scheduleFullRedraw();
onClose();
}
diff --git a/ui/src/frontend/permalink.ts b/ui/src/frontend/permalink.ts
index b69916f..306ee1a 100644
--- a/ui/src/frontend/permalink.ts
+++ b/ui/src/frontend/permalink.ts
@@ -13,7 +13,6 @@
// limitations under the License.
import m from 'mithril';
-import {assertExists} from '../base/logging';
import {
JsonSerialize,
parseAppState,
@@ -33,6 +32,7 @@
import {showModal} from '../widgets/modal';
import {AppImpl} from '../core/app_impl';
import {CopyableLink} from '../widgets/copyable_link';
+import {TraceImpl} from '../core/trace_impl';
// Permalink serialization has two layers:
// 1. Serialization of the app state (state_serialization.ts):
@@ -59,19 +59,18 @@
type PermalinkState = z.infer<typeof PERMALINK_SCHEMA>;
-export async function createPermalink(): Promise<void> {
- const hash = await createPermalinkInternal();
+export async function createPermalink(trace: TraceImpl): Promise<void> {
+ const hash = await createPermalinkInternal(trace);
showPermalinkDialog(hash);
}
// Returns the file name, not the full url (i.e. the name of the GCS object).
-async function createPermalinkInternal(): Promise<string> {
+async function createPermalinkInternal(trace: TraceImpl): Promise<string> {
const permalinkData: PermalinkState = {};
// Check if we need to upload the trace file, before serializing the app
// state.
let alreadyUploadedUrl = '';
- const trace = assertExists(AppImpl.instance.trace);
const traceSource = trace.traceInfo.source;
let dataToUpload: File | ArrayBuffer | undefined = undefined;
let traceName = trace.traceInfo.traceTitle || 'trace';
diff --git a/ui/src/frontend/pivot_table.ts b/ui/src/frontend/pivot_table.ts
index da3ddb1..4c23ffb 100644
--- a/ui/src/frontend/pivot_table.ts
+++ b/ui/src/frontend/pivot_table.ts
@@ -26,7 +26,6 @@
COUNT_AGGREGATION,
} from '../core/pivot_table_types';
import {AreaSelection} from '../public/selection';
-import {raf} from '../core/raf_scheduler';
import {ColumnType} from '../trace_processor/query_result';
import {
aggregationIndex,
@@ -172,7 +171,6 @@
{
onclick: () => {
tree.isCollapsed = !tree.isCollapsed;
- raf.scheduleFullRedraw();
},
},
m('i.material-icons', tree.isCollapsed ? 'expand_more' : 'expand_less'),
diff --git a/ui/src/frontend/pivot_table_argument_popup.ts b/ui/src/frontend/pivot_table_argument_popup.ts
index a4d67c2..6e99273 100644
--- a/ui/src/frontend/pivot_table_argument_popup.ts
+++ b/ui/src/frontend/pivot_table_argument_popup.ts
@@ -13,7 +13,6 @@
// limitations under the License.
import m from 'mithril';
-import {raf} from '../core/raf_scheduler';
interface ArgumentPopupArgs {
onArgumentChange: (arg: string) => void;
@@ -26,7 +25,6 @@
setArgument(attrs: ArgumentPopupArgs, arg: string) {
this.argument = arg;
attrs.onArgumentChange(arg);
- raf.scheduleFullRedraw();
}
view({attrs}: m.Vnode<ArgumentPopupArgs>): m.Child {
diff --git a/ui/src/frontend/reorderable_cells.ts b/ui/src/frontend/reorderable_cells.ts
index e8e87f9..c327591 100644
--- a/ui/src/frontend/reorderable_cells.ts
+++ b/ui/src/frontend/reorderable_cells.ts
@@ -14,7 +14,6 @@
import m from 'mithril';
import {DropDirection} from '../core/pivot_table_manager';
-import {raf} from '../core/raf_scheduler';
export interface ReorderableCell {
content: m.Children;
@@ -74,8 +73,6 @@
if (e.dataTransfer !== null) {
e.dataTransfer.setDragImage(placeholderElement, 0, 0);
}
-
- raf.scheduleFullRedraw();
},
ondragover: (e: DragEvent) => {
let target = e.target as HTMLElement;
@@ -100,15 +97,8 @@
const offset = e.clientX - target.getBoundingClientRect().x;
const newDropDirection =
offset > target.clientWidth / 2 ? 'right' : 'left';
- const redraw =
- newDropDirection !== this.dropDirection ||
- index !== this.draggingTo;
this.dropDirection = newDropDirection;
this.draggingTo = index;
-
- if (redraw) {
- raf.scheduleFullRedraw();
- }
},
ondragenter: (e: DragEvent) => {
this.enterCounters[index]++;
@@ -128,7 +118,6 @@
}
this.draggingTo = -1;
- raf.scheduleFullRedraw();
},
ondragend: () => {
if (
@@ -144,7 +133,6 @@
this.draggingFrom = -1;
this.draggingTo = -1;
- raf.scheduleFullRedraw();
},
},
cell.content,
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index d8a8f46..a76bc73 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -121,19 +121,12 @@
downloadUrl(fileName, url);
}
-function highPrecisionTimersAvailable(): boolean {
- // High precision timers are available either when the page is cross-origin
- // isolated or when the trace processor is a standalone binary.
- return (
- window.crossOriginIsolated ||
- AppImpl.instance.trace?.engine.mode === 'HTTP_RPC'
- );
-}
-
function recordMetatrace(engine: Engine) {
AppImpl.instance.analytics.logEvent('Trace Actions', 'Record metatrace');
- if (!highPrecisionTimersAvailable()) {
+ const highPrecisionTimersAvailable =
+ window.crossOriginIsolated || engine.mode === 'HTTP_RPC';
+ if (!highPrecisionTimersAvailable) {
const PROMPT = `High-precision timers are not available to WASM trace processor yet.
Modern browsers restrict high-precision timers to cross-origin-isolated pages.
@@ -352,14 +345,12 @@
}
export class Sidebar implements m.ClassComponent<OptionalTraceImplAttrs> {
- private _redrawWhileAnimating = new Animation(() =>
- raf.scheduleFullRedraw('force'),
- );
+ private _redrawWhileAnimating = new Animation(() => raf.scheduleFullRedraw());
private _asyncJobPending = new Set<string>();
private _sectionExpanded = new Map<string, boolean>();
- constructor() {
- registerMenuItems();
+ constructor({attrs}: m.CVnode<OptionalTraceImplAttrs>) {
+ registerMenuItems(attrs.trace);
}
view({attrs}: m.CVnode<OptionalTraceImplAttrs>) {
@@ -430,7 +421,6 @@
{
onclick: () => {
this._sectionExpanded.set(sectionId, !expanded);
- raf.scheduleFullRedraw();
},
},
m('h1', {title: section.title}, section.title),
@@ -522,10 +512,9 @@
return; // Don't queue up another action if not yet finished.
}
this._asyncJobPending.add(itemId);
- raf.scheduleFullRedraw();
res.finally(() => {
this._asyncJobPending.delete(itemId);
- raf.scheduleFullRedraw('force');
+ raf.scheduleFullRedraw();
});
};
}
@@ -539,12 +528,11 @@
let globalItemsRegistered = false;
const traceItemsRegistered = new WeakSet<TraceImpl>();
-function registerMenuItems() {
+function registerMenuItems(trace: TraceImpl | undefined) {
if (!globalItemsRegistered) {
globalItemsRegistered = true;
registerGlobalSidebarEntries();
}
- const trace = AppImpl.instance.trace;
if (trace !== undefined && !traceItemsRegistered.has(trace)) {
traceItemsRegistered.add(trace);
registerTraceMenuItems(trace);
diff --git a/ui/src/frontend/trace_share_utils.ts b/ui/src/frontend/trace_share_utils.ts
index cf8f185..4e07b4f 100644
--- a/ui/src/frontend/trace_share_utils.ts
+++ b/ui/src/frontend/trace_share_utils.ts
@@ -61,6 +61,6 @@
);
if (result) {
AppImpl.instance.analytics.logEvent('Trace Actions', 'Create permalink');
- return await createPermalink();
+ return await createPermalink(trace);
}
}
diff --git a/ui/src/frontend/ui_main.ts b/ui/src/frontend/ui_main.ts
index 33303ab..7f20299 100644
--- a/ui/src/frontend/ui_main.ts
+++ b/ui/src/frontend/ui_main.ts
@@ -22,7 +22,6 @@
setDurationPrecision,
setTimestampFormat,
} from '../core/timestamp_format';
-import {raf} from '../core/raf_scheduler';
import {Command} from '../public/command';
import {HotkeyConfig, HotkeyContext} from '../widgets/hotkey_context';
import {HotkeyGlyphs} from '../widgets/hotkey_glyphs';
@@ -140,7 +139,6 @@
getName: (x) => x.name,
});
result && setTimestampFormat(result.format);
- raf.scheduleFullRedraw();
},
},
{
@@ -159,7 +157,6 @@
},
);
result && setDurationPrecision(result.format);
- raf.scheduleFullRedraw();
},
},
{
@@ -171,7 +168,7 @@
{
id: 'perfetto.ShareTrace',
name: 'Share trace',
- callback: shareTrace,
+ callback: () => shareTrace(trace),
},
{
id: 'perfetto.SearchNext',
@@ -429,12 +426,10 @@
selectedOptionIndex: omnibox.selectionIndex,
onSelectedOptionChanged: (index) => {
omnibox.setSelectionIndex(index);
- raf.scheduleFullRedraw();
},
onInput: (value) => {
omnibox.setText(value);
omnibox.setSelectionIndex(0);
- raf.scheduleFullRedraw();
},
onSubmit: (value, _alt) => {
omnibox.resolvePrompt(value);
@@ -490,12 +485,10 @@
selectedOptionIndex: omnibox.selectionIndex,
onSelectedOptionChanged: (index) => {
omnibox.setSelectionIndex(index);
- raf.scheduleFullRedraw();
},
onInput: (value) => {
omnibox.setText(value);
omnibox.setSelectionIndex(0);
- raf.scheduleFullRedraw();
},
onClose: () => {
if (this.omniboxInputEl) {
@@ -531,7 +524,6 @@
onInput: (value) => {
AppImpl.instance.omnibox.setText(value);
- raf.scheduleFullRedraw();
},
onSubmit: (query, alt) => {
const config = {
@@ -539,9 +531,8 @@
title: alt ? 'Pinned query' : 'Omnibox query',
};
const tag = alt ? undefined : 'omnibox_query';
- const trace = AppImpl.instance.trace;
- if (trace === undefined) return; // No trace loaded
- addQueryResultsTab(trace, config, tag);
+ if (this.trace === undefined) return; // No trace loaded
+ addQueryResultsTab(this.trace, config, tag);
},
onClose: () => {
AppImpl.instance.omnibox.setText('');
@@ -549,7 +540,6 @@
this.omniboxInputEl.blur();
}
AppImpl.instance.omnibox.reset();
- raf.scheduleFullRedraw();
},
onGoBack: () => {
AppImpl.instance.omnibox.reset();
diff --git a/ui/src/frontend/viewer_page/flow_events_renderer.ts b/ui/src/frontend/viewer_page/flow_events_renderer.ts
index f6561ef..682d09c 100644
--- a/ui/src/frontend/viewer_page/flow_events_renderer.ts
+++ b/ui/src/frontend/viewer_page/flow_events_renderer.ts
@@ -12,14 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {ArrowHeadStyle, drawBezierArrow} from '../../base/canvas/bezier_arrow';
-import {HorizontalBounds, Point2D, Size2D} from '../../base/geom';
+import {ArrowHeadStyle, drawBezierArrow} from '../../base/bezier_arrow';
+import {
+ HorizontalBounds,
+ Point2D,
+ Size2D,
+ VerticalBounds,
+} from '../../base/geom';
import {TimeScale} from '../../base/time_scale';
import {Flow} from '../../core/flow_types';
import {TraceImpl} from '../../core/trace_impl';
import {TrackNode} from '../../public/workspace';
import {ALL_CATEGORIES, getFlowCategories} from '../flow_events_panel';
-import {RenderedPanelInfo} from './panel_container';
const TRACK_GROUP_CONNECTION_OFFSET = 5;
const TRIANGLE_SIZE = 5;
@@ -40,6 +44,11 @@
| ({kind: 'vertical_edge'} & Point2D)
| ({kind: 'point'} & Point2D);
+export interface TrackInfo {
+ readonly node: TrackNode;
+ readonly verticalBounds: VerticalBounds;
+}
+
/**
* Renders the flows overlay on top of the timeline, given the set of panels and
* a canvas to draw on.
@@ -50,24 +59,25 @@
* @param trace - The Trace instance, which holds onto the FlowManager.
* @param ctx - The canvas to draw on.
* @param size - The size of the canvas.
- * @param panels - A list of panels and their locations on the canvas.
+ * @param tracks - A list of tracks and their vertical positions on the canvas.
+ * @param trackRoot - The root node of the tracks - used to find tracks quickly
+ * by URI.
+ * @param timescale - The current timescale used to convert flow timings into
+ * canvas positions.
+ *
*/
export function renderFlows(
trace: TraceImpl,
ctx: CanvasRenderingContext2D,
size: Size2D,
- panels: ReadonlyArray<RenderedPanelInfo>,
+ tracks: ReadonlyArray<TrackInfo>,
trackRoot: TrackNode,
+ timescale: TimeScale,
): void {
- const timescale = new TimeScale(trace.timeline.visibleWindow, {
- left: 0,
- right: size.width,
- });
-
// Create an index of track node instances to panels. This doesn't need to be
// a WeakMap because it's thrown away every render cycle.
- const panelsByTrackNode = new Map(
- panels.map((panel) => [panel.panel.trackNode, panel]),
+ const trackInfoByNode = new Map(
+ tracks.map((trackInfo) => [trackInfo.node, trackInfo]),
);
const drawFlow = (flow: Flow, hue: number) => {
@@ -128,15 +138,17 @@
return undefined;
}
- const track = trackRoot.findTrackByUri(trackUri);
+ const track = trackRoot.getTrackByUri(trackUri);
if (!track) {
return undefined;
}
- const trackPanel = panelsByTrackNode.get(track);
+ const trackPanel = trackInfoByNode.get(track);
if (trackPanel) {
- const trackRect = trackPanel.rect;
- const sliceRectRaw = trackPanel.panel.getSliceVerticalBounds?.(depth);
+ const trackRect = trackPanel.verticalBounds;
+ const sliceRectRaw = trace.tracks
+ .getTrack(trackUri)
+ ?.track.getSliceVerticalBounds?.(depth);
if (sliceRectRaw) {
const sliceRect = {
top: sliceRectRaw.top + trackRect.top,
@@ -159,12 +171,12 @@
} else {
// If we didn't find a track, it might inside a group, so check for the group
const containerNode = track.findClosestVisibleAncestor();
- const groupPanel = panelsByTrackNode.get(containerNode);
+ const groupPanel = trackInfoByNode.get(containerNode);
if (groupPanel) {
return {
kind: 'point',
x,
- y: groupPanel.rect.bottom - TRACK_GROUP_CONNECTION_OFFSET,
+ y: groupPanel.verticalBounds.bottom - TRACK_GROUP_CONNECTION_OFFSET,
};
}
}
diff --git a/ui/src/frontend/viewer_page/notes_panel.ts b/ui/src/frontend/viewer_page/notes_panel.ts
index 49c9576..be0b63c 100644
--- a/ui/src/frontend/viewer_page/notes_panel.ts
+++ b/ui/src/frontend/viewer_page/notes_panel.ts
@@ -17,15 +17,17 @@
import {currentTargetOffset} from '../../base/dom_utils';
import {Size2D} from '../../base/geom';
import {assertUnreachable} from '../../base/logging';
+import {Icons} from '../../base/semantic_icons';
import {TimeScale} from '../../base/time_scale';
import {randomColor} from '../../components/colorizer';
import {raf} from '../../core/raf_scheduler';
import {TraceImpl} from '../../core/trace_impl';
import {Note, SpanNote} from '../../public/note';
import {Button, ButtonBar} from '../../widgets/button';
+import {MenuItem, PopupMenu2} from '../../widgets/menu';
+import {Select} from '../../widgets/select';
import {TRACK_SHELL_WIDTH} from '../css_constants';
import {generateTicks, getMaxMajorTicks, TickType} from './gridline_helper';
-import {Panel} from './panel_container';
const FLAG_WIDTH = 16;
const AREA_TRIANGLE_WIDTH = 10;
@@ -48,13 +50,12 @@
}
}
-export class NotesPanel implements Panel {
- readonly kind = 'panel';
- readonly selectable = false;
+export class NotesPanel {
private readonly trace: TraceImpl;
private timescale?: TimeScale; // The timescale from the last render()
private hoveredX: null | number = null;
private mouseDragging = false;
+ readonly height = 20;
constructor(trace: TraceImpl) {
this.trace = trace;
@@ -65,9 +66,12 @@
(n) => n.collapsed,
);
+ const workspaces = this.trace.workspaces;
+
return m(
- '.notes-panel',
+ '',
{
+ style: {height: `${this.height}px`},
onmousedown: () => {
// If the user clicks & drags, very likely they just want to measure
// the time horizontally, not set a flag. This debouncing is done to
@@ -98,7 +102,7 @@
},
m(
ButtonBar,
- {className: 'pf-toolbar'},
+ {className: 'pf-timeline-toolbar'},
m(Button, {
onclick: (e: Event) => {
e.preventDefault();
@@ -122,12 +126,77 @@
this.trace.workspace.pinnedTracks.forEach((t) =>
this.trace.workspace.unpinTrack(t),
);
- raf.scheduleFullRedraw();
},
title: 'Clear all pinned tracks',
icon: 'clear_all',
compact: true,
}),
+ m(
+ Select,
+ {
+ className: 'pf-timeline-toolbar__workspace-selector',
+ onchange: async (e) => {
+ const value = (e.target as HTMLSelectElement).value;
+ if (value === 'new-workspace') {
+ const ws =
+ workspaces.createEmptyWorkspace('Untitled Workspace');
+ workspaces.switchWorkspace(ws);
+ } else {
+ const ws = workspaces.all.find(({id}) => id === value);
+ ws && this.trace?.workspaces.switchWorkspace(ws);
+ }
+ },
+ },
+ workspaces.all
+ .map((ws) => {
+ return m('option', {
+ value: `${ws.id}`,
+ label: ws.title,
+ selected: ws === this.trace?.workspace,
+ });
+ })
+ .concat([
+ m('option', {
+ value: 'new-workspace',
+ label: 'New workspace...',
+ }),
+ ]),
+ ),
+ m(
+ PopupMenu2,
+ {
+ trigger: m(Button, {
+ icon: 'more_vert',
+ title: 'Workspace options',
+ compact: true,
+ }),
+ },
+ m(MenuItem, {
+ icon: Icons.Delete,
+ label: 'Delete current workspace',
+ disabled:
+ workspaces.currentWorkspace === workspaces.defaultWorkspace,
+ onclick: () => {
+ workspaces.removeWorkspace(workspaces.currentWorkspace);
+ raf.scheduleFullRedraw();
+ },
+ }),
+ m(MenuItem, {
+ icon: 'edit',
+ label: 'Rename current workspace',
+ disabled:
+ workspaces.currentWorkspace === workspaces.defaultWorkspace,
+ onclick: async () => {
+ const newName = await this.trace.omnibox.prompt(
+ 'Enter a new name...',
+ );
+ if (newName) {
+ workspaces.currentWorkspace.title = newName;
+ }
+ raf.scheduleFullRedraw();
+ },
+ }),
+ ),
// TODO(stevegolton): Re-introduce this when we fix track filtering
// m(TextInput, {
// placeholder: 'Filter tracks...',
@@ -155,7 +224,7 @@
renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
ctx.fillStyle = '#999';
- ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
+ ctx.fillRect(TRACK_SHELL_WIDTH - 1, 0, 1, size.height);
const trackSize = {...size, width: size.width - TRACK_SHELL_WIDTH};
diff --git a/ui/src/frontend/viewer_page/overview_timeline_panel.ts b/ui/src/frontend/viewer_page/overview_timeline_panel.ts
index 4adedf1..0095019 100644
--- a/ui/src/frontend/viewer_page/overview_timeline_panel.ts
+++ b/ui/src/frontend/viewer_page/overview_timeline_panel.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2018 The Android Open Source Project
+// 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.
@@ -13,134 +13,117 @@
// limitations under the License.
import m from 'mithril';
-import {DragGestureHandler} from '../../base/drag_gesture_handler';
-import {Size2D} from '../../base/geom';
+import {DisposableStack} from '../../base/disposable_stack';
+import {toHTMLElement} from '../../base/dom_utils';
+import {Rect2D, Size2D} from '../../base/geom';
import {HighPrecisionTimeSpan} from '../../base/high_precision_time_span';
-import {assertUnreachable} from '../../base/logging';
+import {assertExists, assertUnreachable} from '../../base/logging';
import {Duration, duration, Time, time, TimeSpan} from '../../base/time';
import {TimeScale} from '../../base/time_scale';
import {getOrCreate} from '../../base/utils';
+import {ZonedInteractionHandler} from '../../base/zoned_interaction_handler';
import {colorForCpu} from '../../components/colorizer';
import {raf} from '../../core/raf_scheduler';
import {timestampFormat} from '../../core/timestamp_format';
import {TraceImpl} from '../../core/trace_impl';
import {TimestampFormat} from '../../public/timeline';
import {LONG, NUM} from '../../trace_processor/query_result';
+import {VirtualOverlayCanvas} from '../../components/widgets/virtual_overlay_canvas';
import {
OVERVIEW_TIMELINE_NON_VISIBLE_COLOR,
TRACK_SHELL_WIDTH,
} from '../css_constants';
-import {BorderDragStrategy} from '../drag/border_drag_strategy';
-import {DragStrategy} from '../drag/drag_strategy';
-import {InnerDragStrategy} from '../drag/inner_drag_strategy';
-import {OuterDragStrategy} from '../drag/outer_drag_strategy';
import {
generateTicks,
getMaxMajorTicks,
MIN_PX_PER_STEP,
TickType,
} from './gridline_helper';
-import {Panel} from './panel_container';
+
+const HANDLE_SIZE_PX = 5;
+
+export interface OverviewTimelineAttrs {
+ readonly trace: TraceImpl;
+ readonly className?: string;
+}
const tracesData = new WeakMap<TraceImpl, OverviewDataLoader>();
-export class OverviewTimelinePanel implements Panel {
- private static HANDLE_SIZE_PX = 5;
- readonly kind = 'panel';
- readonly selectable = false;
- private width = 0;
- private gesture?: DragGestureHandler;
- private timeScale?: TimeScale;
- private dragStrategy?: DragStrategy;
- private readonly boundOnMouseMove = this.onMouseMove.bind(this);
+export class OverviewTimeline
+ implements m.ClassComponent<OverviewTimelineAttrs>
+{
private readonly overviewData: OverviewDataLoader;
+ private readonly trash = new DisposableStack();
+ private interactions?: ZonedInteractionHandler;
- constructor(private trace: TraceImpl) {
+ constructor({attrs}: m.CVnode<OverviewTimelineAttrs>) {
this.overviewData = getOrCreate(
tracesData,
- trace,
- () => new OverviewDataLoader(trace),
+ attrs.trace,
+ () => new OverviewDataLoader(attrs.trace),
);
}
- // Must explicitly type now; arguments types are no longer auto-inferred.
- // https://github.com/Microsoft/TypeScript/issues/1373
- onupdate({dom}: m.CVnodeDOM) {
- this.width = dom.getBoundingClientRect().width;
- const traceTime = this.trace.traceInfo;
- if (this.width > TRACK_SHELL_WIDTH) {
- const pxBounds = {left: TRACK_SHELL_WIDTH, right: this.width};
- const hpTraceTime = HighPrecisionTimeSpan.fromTime(
- traceTime.start,
- traceTime.end,
- );
- this.timeScale = new TimeScale(hpTraceTime, pxBounds);
- if (this.gesture === undefined) {
- this.gesture = new DragGestureHandler(
- dom as HTMLElement,
- this.onDrag.bind(this),
- this.onDragStart.bind(this),
- this.onDragEnd.bind(this),
- );
- }
- } else {
- this.timeScale = undefined;
- }
- }
-
- oncreate(vnode: m.CVnodeDOM) {
- this.onupdate(vnode);
- (vnode.dom as HTMLElement).addEventListener(
- 'mousemove',
- this.boundOnMouseMove,
+ view({attrs}: m.CVnode<OverviewTimelineAttrs>) {
+ return m(
+ VirtualOverlayCanvas,
+ {
+ raf: attrs.trace.raf,
+ className: attrs.className,
+ onCanvasRedraw: ({ctx, virtualCanvasSize}) => {
+ this.renderCanvas(attrs.trace, ctx, virtualCanvasSize);
+ },
+ },
+ m('.pf-overview-timeline'),
);
}
- onremove({dom}: m.CVnodeDOM) {
- if (this.gesture) {
- this.gesture[Symbol.dispose]();
- this.gesture = undefined;
- }
- (dom as HTMLElement).removeEventListener(
- 'mousemove',
- this.boundOnMouseMove,
+ oncreate({dom}: m.VnodeDOM<OverviewTimelineAttrs, this>) {
+ this.interactions = new ZonedInteractionHandler(toHTMLElement(dom));
+ this.trash.use(this.interactions);
+ }
+
+ onremove(_: m.VnodeDOM<OverviewTimelineAttrs, this>) {
+ this.trash.dispose();
+ }
+
+ private renderCanvas(
+ trace: TraceImpl,
+ ctx: CanvasRenderingContext2D,
+ size: Size2D,
+ ) {
+ if (size.width <= TRACK_SHELL_WIDTH) return;
+
+ const traceTime = trace.traceInfo;
+ const pxBounds = {left: TRACK_SHELL_WIDTH, right: size.width};
+ const hpTraceTime = HighPrecisionTimeSpan.fromTime(
+ traceTime.start,
+ traceTime.end,
);
- }
-
- render(): m.Children {
- return m('.overview-timeline', {
- oncreate: (vnode) => this.oncreate(vnode),
- onupdate: (vnode) => this.onupdate(vnode),
- onremove: (vnode) => this.onremove(vnode),
- });
- }
-
- renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
- if (this.width === undefined) return;
- if (this.timeScale === undefined) return;
+ const timescale = new TimeScale(hpTraceTime, pxBounds);
const headerHeight = 20;
const tracksHeight = size.height - headerHeight;
const traceContext = new TimeSpan(
- this.trace.traceInfo.start,
- this.trace.traceInfo.end,
+ trace.traceInfo.start,
+ trace.traceInfo.end,
);
if (size.width > TRACK_SHELL_WIDTH && traceContext.duration > 0n) {
- const maxMajorTicks = getMaxMajorTicks(this.width - TRACK_SHELL_WIDTH);
- const offset = this.trace.timeline.timestampOffset();
+ const maxMajorTicks = getMaxMajorTicks(size.width - TRACK_SHELL_WIDTH);
+ const offset = trace.timeline.timestampOffset();
const tickGen = generateTicks(traceContext, maxMajorTicks, offset);
// Draw time labels
ctx.font = '10px Roboto Condensed';
ctx.fillStyle = '#999';
for (const {type, time} of tickGen) {
- const xPos = Math.floor(this.timeScale.timeToPx(time));
+ const xPos = Math.floor(timescale.timeToPx(time));
if (xPos <= 0) continue;
- if (xPos > this.width) break;
+ if (xPos > size.width) break;
if (type === TickType.MAJOR) {
ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
- const domainTime = this.trace.timeline.toDomainTime(time);
+ const domainTime = trace.timeline.toDomainTime(time);
renderTimestamp(ctx, domainTime, xPos + 5, 18, MIN_PX_PER_STEP);
} else if (type == TickType.MEDIUM) {
ctx.fillRect(xPos - 1, 0, 1, 8);
@@ -159,8 +142,8 @@
for (const key of overviewData.keys()) {
const loads = overviewData.get(key)!;
for (let i = 0; i < loads.length; i++) {
- const xStart = Math.floor(this.timeScale.timeToPx(loads[i].start));
- const xEnd = Math.ceil(this.timeScale.timeToPx(loads[i].end));
+ const xStart = Math.floor(timescale.timeToPx(loads[i].start));
+ const xEnd = Math.ceil(timescale.timeToPx(loads[i].end));
const yOff = Math.floor(headerHeight + y * trackHeight);
const lightness = Math.ceil((1 - loads[i].load * 0.7) * 100);
const color = colorForCpu(y).setHSL({s: 50, l: lightness});
@@ -173,10 +156,15 @@
// Draw bottom border.
ctx.fillStyle = '#dadada';
- ctx.fillRect(0, size.height - 1, this.width, 1);
+ ctx.fillRect(0, size.height - 1, size.width, 1);
// Draw semi-opaque rects that occlude the non-visible time range.
- const [vizStartPx, vizEndPx] = this.extractBounds(this.timeScale);
+ const {left, right} = timescale.hpTimeSpanToPxSpan(
+ trace.timeline.visibleWindow,
+ );
+
+ const vizStartPx = Math.floor(left);
+ const vizEndPx = Math.ceil(right);
ctx.fillStyle = OVERVIEW_TIMELINE_NON_VISIBLE_COLOR;
ctx.fillRect(
@@ -185,14 +173,14 @@
vizStartPx - TRACK_SHELL_WIDTH,
tracksHeight,
);
- ctx.fillRect(vizEndPx, headerHeight, this.width - vizEndPx, tracksHeight);
+ ctx.fillRect(vizEndPx, headerHeight, size.width - vizEndPx, tracksHeight);
// Draw brushes.
ctx.fillStyle = '#999';
ctx.fillRect(vizStartPx - 1, headerHeight, 1, tracksHeight);
ctx.fillRect(vizEndPx, headerHeight, 1, tracksHeight);
- const hbarWidth = OverviewTimelinePanel.HANDLE_SIZE_PX;
+ const hbarWidth = HANDLE_SIZE_PX;
const hbarHeight = tracksHeight * 0.4;
// Draw handlebar
ctx.fillRect(
@@ -207,73 +195,79 @@
hbarWidth,
hbarHeight,
);
- }
- private onMouseMove(e: MouseEvent) {
- if (this.gesture === undefined || this.gesture.isDragging) {
- return;
- }
- (e.target as HTMLElement).style.cursor = this.chooseCursor(e.offsetX);
- }
-
- private chooseCursor(x: number) {
- if (this.timeScale === undefined) return 'default';
- const [startBound, endBound] = this.extractBounds(this.timeScale);
- if (
- OverviewTimelinePanel.inBorderRange(x, startBound) ||
- OverviewTimelinePanel.inBorderRange(x, endBound)
- ) {
- return 'ew-resize';
- } else if (x < TRACK_SHELL_WIDTH) {
- return 'default';
- } else if (x < startBound || endBound < x) {
- return 'crosshair';
- } else {
- return 'all-scroll';
- }
- }
-
- onDrag(x: number) {
- if (this.dragStrategy === undefined) return;
- this.dragStrategy.onDrag(x);
- }
-
- onDragStart(x: number) {
- if (this.timeScale === undefined) return;
-
- const cb = (vizTime: HighPrecisionTimeSpan) => {
- this.trace.timeline.updateVisibleTimeHP(vizTime);
- raf.scheduleCanvasRedraw();
- };
- const pixelBounds = this.extractBounds(this.timeScale);
- const timeScale = this.timeScale;
- if (
- OverviewTimelinePanel.inBorderRange(x, pixelBounds[0]) ||
- OverviewTimelinePanel.inBorderRange(x, pixelBounds[1])
- ) {
- this.dragStrategy = new BorderDragStrategy(timeScale, pixelBounds, cb);
- } else if (x < pixelBounds[0] || pixelBounds[1] < x) {
- this.dragStrategy = new OuterDragStrategy(timeScale, cb);
- } else {
- this.dragStrategy = new InnerDragStrategy(timeScale, pixelBounds, cb);
- }
- this.dragStrategy.onDragStart(x);
- }
-
- onDragEnd() {
- this.dragStrategy = undefined;
- }
-
- private extractBounds(timeScale: TimeScale): [number, number] {
- const vizTime = this.trace.timeline.visibleWindow;
- return [
- Math.floor(timeScale.hpTimeToPx(vizTime.start)),
- Math.ceil(timeScale.hpTimeToPx(vizTime.end)),
- ];
- }
-
- private static inBorderRange(a: number, b: number): boolean {
- return Math.abs(a - b) < this.HANDLE_SIZE_PX / 2;
+ assertExists(this.interactions).update([
+ {
+ id: 'left-handle',
+ area: Rect2D.fromPointAndSize({
+ x: vizStartPx - Math.floor(hbarWidth / 2) - 1,
+ y: 0,
+ width: hbarWidth,
+ height: size.height,
+ }),
+ cursor: 'col-resize',
+ drag: {
+ cursorWhileDragging: 'col-resize',
+ onDrag: (event) => {
+ const delta = timescale.pxToDuration(event.deltaSinceLastEvent.x);
+ trace.timeline.moveStart(delta);
+ },
+ },
+ },
+ {
+ id: 'right-handle',
+ area: Rect2D.fromPointAndSize({
+ x: vizEndPx - Math.floor(hbarWidth / 2) - 1,
+ y: 0,
+ width: hbarWidth,
+ height: size.height,
+ }),
+ cursor: 'col-resize',
+ drag: {
+ cursorWhileDragging: 'col-resize',
+ onDrag: (event) => {
+ const delta = timescale.pxToDuration(event.deltaSinceLastEvent.x);
+ trace.timeline.moveEnd(delta);
+ },
+ },
+ },
+ {
+ id: 'drag',
+ area: new Rect2D({
+ left: vizStartPx,
+ right: vizEndPx,
+ top: 0,
+ bottom: size.height,
+ }),
+ cursor: 'grab',
+ drag: {
+ cursorWhileDragging: 'grabbing',
+ onDrag: (event) => {
+ const delta = timescale.pxToDuration(event.deltaSinceLastEvent.x);
+ trace.timeline.panVisibleWindow(delta);
+ },
+ },
+ },
+ {
+ id: 'select',
+ area: new Rect2D({
+ left: TRACK_SHELL_WIDTH,
+ right: size.width,
+ top: 0,
+ bottom: size.height,
+ }),
+ cursor: 'text',
+ drag: {
+ cursorWhileDragging: 'text',
+ onDrag: (event) => {
+ const span = timescale.pxSpanToHpTimeSpan(
+ Rect2D.fromPoints(event.dragStart, event.dragCurrent),
+ );
+ trace.timeline.updateVisibleTimeHP(span);
+ },
+ },
+ },
+ ]);
}
}
diff --git a/ui/src/frontend/viewer_page/panel_container.ts b/ui/src/frontend/viewer_page/panel_container.ts
deleted file mode 100644
index 2e2a253..0000000
--- a/ui/src/frontend/viewer_page/panel_container.ts
+++ /dev/null
@@ -1,553 +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';
-import {canvasClip} from '../../base/canvas_utils';
-import {DisposableStack} from '../../base/disposable_stack';
-import {findRef, toHTMLElement} from '../../base/dom_utils';
-import {Bounds2D, Size2D, VerticalBounds} from '../../base/geom';
-import {assertExists, assertFalse} from '../../base/logging';
-import {SimpleResizeObserver} from '../../base/resize_observer';
-import {TimeScale} from '../../base/time_scale';
-import {VirtualCanvas} from '../../base/virtual_canvas';
-import {
- PerfStats,
- PerfStatsContainer,
- runningStatStr,
-} from '../../core/perf_stats';
-import {raf} from '../../core/raf_scheduler';
-import {TraceImpl, TraceImplAttrs} from '../../core/trace_impl';
-import {TrackNode} from '../../public/workspace';
-import {HTMLAttrs} from '../../widgets/common';
-import {SELECTION_STROKE_COLOR, TRACK_SHELL_WIDTH} from '../css_constants';
-
-const CANVAS_OVERDRAW_PX = 300;
-const CANVAS_TOLERANCE_PX = 100;
-
-export interface Panel {
- readonly kind: 'panel';
- render(): m.Children;
- readonly selectable: boolean;
- // TODO(stevegolton): Remove this - panel container should know nothing of
- // tracks!
- readonly trackNode?: TrackNode;
- renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D): void;
- getSliceVerticalBounds?(depth: number): VerticalBounds | undefined;
-}
-
-export interface PanelGroup {
- readonly kind: 'group';
- readonly collapsed: boolean;
- readonly header?: Panel;
- readonly topOffsetPx: number;
- readonly sticky: boolean;
- readonly childPanels: PanelOrGroup[];
-}
-
-export type PanelOrGroup = Panel | PanelGroup;
-
-export interface PanelContainerAttrs extends TraceImplAttrs {
- panels: PanelOrGroup[];
- className?: string;
- selectedYRange: VerticalBounds | undefined;
-
- onPanelStackResize?: (width: number, height: number) => void;
-
- // Called after all panels have been rendered to the canvas, to give the
- // caller the opportunity to render an overlay on top of the panels.
- renderOverlay?(
- ctx: CanvasRenderingContext2D,
- size: Size2D,
- panels: ReadonlyArray<RenderedPanelInfo>,
- ): void;
-
- // Called before the panels are rendered
- renderUnderlay?(ctx: CanvasRenderingContext2D, size: Size2D): void;
-}
-
-interface PanelInfo {
- trackNode?: TrackNode; // Can be undefined for singleton panels.
- panel: Panel;
- height: number;
- width: number;
- clientX: number;
- clientY: number;
- absY: number;
-}
-
-export interface RenderedPanelInfo {
- panel: Panel;
- rect: Bounds2D;
-}
-
-export class PanelContainer
- implements m.ClassComponent<PanelContainerAttrs>, PerfStatsContainer
-{
- private readonly trace: TraceImpl;
- private attrs: PanelContainerAttrs;
-
- // Updated every render cycle in the view() hook
- private panelById = new Map<string, Panel>();
-
- // Updated every render cycle in the oncreate/onupdate hook
- private panelInfos: PanelInfo[] = [];
-
- private perfStatsEnabled = false;
- private panelPerfStats = new WeakMap<Panel, PerfStats>();
- private perfStats = {
- totalPanels: 0,
- panelsOnCanvas: 0,
- renderStats: new PerfStats(10),
- };
-
- private ctx?: CanvasRenderingContext2D;
-
- private readonly trash = new DisposableStack();
-
- private readonly OVERLAY_REF = 'overlay';
- private readonly PANEL_STACK_REF = 'panel-stack';
-
- constructor({attrs}: m.CVnode<PanelContainerAttrs>) {
- this.attrs = attrs;
- this.trace = attrs.trace;
- this.trash.use(raf.addCanvasRedrawCallback(() => this.renderCanvas()));
- this.trash.use(attrs.trace.perfDebugging.addContainer(this));
- }
-
- getPanelsInRegion(
- startX: number,
- endX: number,
- startY: number,
- endY: number,
- ): Panel[] {
- const minX = Math.min(startX, endX);
- const maxX = Math.max(startX, endX);
- const minY = Math.min(startY, endY);
- const maxY = Math.max(startY, endY);
- const panels: Panel[] = [];
- for (let i = 0; i < this.panelInfos.length; i++) {
- const pos = this.panelInfos[i];
- const realPosX = pos.clientX - TRACK_SHELL_WIDTH;
- if (
- realPosX + pos.width >= minX &&
- realPosX <= maxX &&
- pos.absY + pos.height >= minY &&
- pos.absY <= maxY &&
- pos.panel.selectable
- ) {
- panels.push(pos.panel);
- }
- }
- return panels;
- }
-
- // This finds the tracks covered by the in-progress area selection. When
- // editing areaY is not set, so this will not be used.
- handleAreaSelection() {
- const {selectedYRange} = this.attrs;
- const area = this.trace.timeline.selectedArea;
- if (
- area === undefined ||
- selectedYRange === undefined ||
- this.panelInfos.length === 0
- ) {
- return;
- }
-
- // TODO(stevegolton): We shouldn't know anything about visible time scale
- // right now, that's a job for our parent, but we can put one together so we
- // don't have to refactor this entire bit right now...
-
- const visibleTimeScale = new TimeScale(this.trace.timeline.visibleWindow, {
- left: 0,
- right: this.virtualCanvas!.size.width - TRACK_SHELL_WIDTH,
- });
-
- // The Y value is given from the top of the pan and zoom region, we want it
- // from the top of the panel container. The parent offset corrects that.
- const panels = this.getPanelsInRegion(
- visibleTimeScale.timeToPx(area.start),
- visibleTimeScale.timeToPx(area.end),
- selectedYRange.top,
- selectedYRange.bottom,
- );
-
- // Get the track ids from the panels.
- const trackUris: string[] = [];
- for (const panel of panels) {
- if (panel.trackNode) {
- if (panel.trackNode.isSummary) {
- const groupNode = panel.trackNode;
- // Select a track group and all child tracks if it is collapsed
- if (groupNode.collapsed) {
- for (const track of groupNode.flatTracks) {
- track.uri && trackUris.push(track.uri);
- }
- }
- } else {
- panel.trackNode.uri && trackUris.push(panel.trackNode.uri);
- }
- }
- }
- this.trace.timeline.selectArea(area.start, area.end, trackUris);
- }
-
- private virtualCanvas?: VirtualCanvas;
-
- oncreate(vnode: m.CVnodeDOM<PanelContainerAttrs>) {
- const {dom, attrs} = vnode;
-
- const overlayElement = toHTMLElement(
- assertExists(findRef(dom, this.OVERLAY_REF)),
- );
-
- const virtualCanvas = new VirtualCanvas(overlayElement, dom, {
- overdrawAxes: 'y',
- overdrawPx: CANVAS_OVERDRAW_PX,
- tolerancePx: CANVAS_TOLERANCE_PX,
- });
- this.trash.use(virtualCanvas);
- this.virtualCanvas = virtualCanvas;
-
- const ctx = virtualCanvas.canvasElement.getContext('2d');
- if (!ctx) {
- throw Error('Cannot create canvas context');
- }
- this.ctx = ctx;
-
- virtualCanvas.setCanvasResizeListener((canvas, width, height) => {
- const dpr = window.devicePixelRatio;
- canvas.width = width * dpr;
- canvas.height = height * dpr;
- });
-
- virtualCanvas.setLayoutShiftListener(() => {
- this.renderCanvas();
- });
-
- this.onupdate(vnode);
-
- const panelStackElement = toHTMLElement(
- assertExists(findRef(dom, this.PANEL_STACK_REF)),
- );
-
- // Listen for when the panel stack changes size
- this.trash.use(
- new SimpleResizeObserver(panelStackElement, () => {
- attrs.onPanelStackResize?.(
- panelStackElement.clientWidth,
- panelStackElement.clientHeight,
- );
- }),
- );
- }
-
- onremove() {
- this.trash.dispose();
- }
-
- renderPanel(node: Panel, panelId: string, htmlAttrs?: HTMLAttrs): m.Vnode {
- assertFalse(this.panelById.has(panelId));
- this.panelById.set(panelId, node);
- return m(
- `.pf-panel`,
- {...htmlAttrs, 'data-panel-id': panelId},
- node.render(),
- );
- }
-
- // Render a tree of panels into one vnode. Argument `path` is used to build
- // `key` attribute for intermediate tree vnodes: otherwise Mithril internals
- // will complain about keyed and non-keyed vnodes mixed together.
- renderTree(node: PanelOrGroup, panelId: string): m.Vnode {
- if (node.kind === 'group') {
- const style = {
- position: 'sticky',
- top: `${node.topOffsetPx}px`,
- zIndex: `${2000 - node.topOffsetPx}`,
- };
- return m(
- 'div.pf-panel-group',
- node.header &&
- this.renderPanel(node.header, `${panelId}-header`, {
- style: !node.collapsed && node.sticky ? style : {},
- }),
- ...node.childPanels.map((child, index) =>
- this.renderTree(child, `${panelId}-${index}`),
- ),
- );
- }
- return this.renderPanel(node, panelId);
- }
-
- view({attrs}: m.CVnode<PanelContainerAttrs>) {
- this.attrs = attrs;
- this.panelById.clear();
- const children = attrs.panels.map((panel, index) =>
- this.renderTree(panel, `${index}`),
- );
-
- return m(
- '.pf-panel-container',
- {className: attrs.className},
- m(
- '.pf-panel-stack',
- {ref: this.PANEL_STACK_REF},
- m('.pf-overlay', {ref: this.OVERLAY_REF}),
- children,
- ),
- );
- }
-
- onupdate({dom}: m.CVnodeDOM<PanelContainerAttrs>) {
- this.readPanelRectsFromDom(dom);
- }
-
- private readPanelRectsFromDom(dom: Element): void {
- this.panelInfos = [];
-
- const panel = dom.querySelectorAll('.pf-panel');
- const panels = assertExists(findRef(dom, this.PANEL_STACK_REF));
- const {top} = panels.getBoundingClientRect();
- panel.forEach((panelElement) => {
- const panelHTMLElement = toHTMLElement(panelElement);
- const panelId = assertExists(panelHTMLElement.dataset.panelId);
- const panel = assertExists(this.panelById.get(panelId));
-
- // NOTE: the id can be undefined for singletons like overview timeline.
- const rect = panelElement.getBoundingClientRect();
- this.panelInfos.push({
- trackNode: panel.trackNode,
- height: rect.height,
- width: rect.width,
- clientX: rect.x,
- clientY: rect.y,
- absY: rect.y - top,
- panel,
- });
- });
- }
-
- private renderCanvas() {
- if (!this.ctx) return;
- if (!this.virtualCanvas) return;
-
- const ctx = this.ctx;
- const vc = this.virtualCanvas;
- const redrawStart = performance.now();
-
- ctx.resetTransform();
- ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
-
- const dpr = window.devicePixelRatio;
- ctx.scale(dpr, dpr);
- ctx.translate(-vc.canvasRect.left, -vc.canvasRect.top);
-
- this.handleAreaSelection();
-
- const totalRenderedPanels = this.renderPanels(ctx, vc);
- this.drawTopLayerOnCanvas(ctx, vc);
-
- // Collect performance as the last thing we do.
- const redrawDur = performance.now() - redrawStart;
- this.updatePerfStats(
- redrawDur,
- this.panelInfos.length,
- totalRenderedPanels,
- );
- }
-
- private renderPanels(
- ctx: CanvasRenderingContext2D,
- vc: VirtualCanvas,
- ): number {
- this.attrs.renderUnderlay?.(ctx, vc.size);
-
- let panelTop = 0;
- let totalOnCanvas = 0;
-
- const renderedPanels = Array<RenderedPanelInfo>();
-
- for (let i = 0; i < this.panelInfos.length; i++) {
- const {
- panel,
- width: panelWidth,
- height: panelHeight,
- } = this.panelInfos[i];
-
- const panelRect = {
- left: 0,
- top: panelTop,
- bottom: panelTop + panelHeight,
- right: panelWidth,
- };
- const panelSize = {width: panelWidth, height: panelHeight};
-
- if (vc.overlapsCanvas(panelRect)) {
- totalOnCanvas++;
-
- ctx.save();
- ctx.translate(0, panelTop);
- canvasClip(ctx, 0, 0, panelWidth, panelHeight);
- const beforeRender = performance.now();
- panel.renderCanvas(ctx, panelSize);
- this.updatePanelStats(
- i,
- panel,
- performance.now() - beforeRender,
- ctx,
- panelSize,
- );
- ctx.restore();
- }
-
- renderedPanels.push({
- panel,
- rect: {
- top: panelTop,
- bottom: panelTop + panelHeight,
- left: 0,
- right: panelWidth,
- },
- });
-
- panelTop += panelHeight;
- }
-
- this.attrs.renderOverlay?.(ctx, vc.size, renderedPanels);
-
- return totalOnCanvas;
- }
-
- // The panels each draw on the canvas but some details need to be drawn across
- // the whole canvas rather than per panel.
- private drawTopLayerOnCanvas(
- ctx: CanvasRenderingContext2D,
- vc: VirtualCanvas,
- ): void {
- const {selectedYRange} = this.attrs;
- const area = this.trace.timeline.selectedArea;
- if (area === undefined || selectedYRange === undefined) {
- return;
- }
- if (this.panelInfos.length === 0 || area.trackUris.length === 0) {
- return;
- }
-
- // Find the minY and maxY of the selected tracks in this panel container.
- let selectedTracksMinY = selectedYRange.top;
- let selectedTracksMaxY = selectedYRange.bottom;
- for (let i = 0; i < this.panelInfos.length; i++) {
- const trackUri = this.panelInfos[i].trackNode?.uri;
- if (trackUri && area.trackUris.includes(trackUri)) {
- selectedTracksMinY = Math.min(
- selectedTracksMinY,
- this.panelInfos[i].absY,
- );
- selectedTracksMaxY = Math.max(
- selectedTracksMaxY,
- this.panelInfos[i].absY + this.panelInfos[i].height,
- );
- }
- }
-
- // TODO(stevegolton): We shouldn't know anything about visible time scale
- // right now, that's a job for our parent, but we can put one together so we
- // don't have to refactor this entire bit right now...
-
- const visibleTimeScale = new TimeScale(this.trace.timeline.visibleWindow, {
- left: 0,
- right: vc.size.width - TRACK_SHELL_WIDTH,
- });
-
- const startX = visibleTimeScale.timeToPx(area.start);
- const endX = visibleTimeScale.timeToPx(area.end);
- ctx.save();
- ctx.strokeStyle = SELECTION_STROKE_COLOR;
- ctx.lineWidth = 1;
-
- ctx.translate(TRACK_SHELL_WIDTH, 0);
-
- // Clip off any drawing happening outside the bounds of the timeline area
- canvasClip(ctx, 0, 0, vc.size.width - TRACK_SHELL_WIDTH, vc.size.height);
-
- ctx.strokeRect(
- startX,
- selectedTracksMaxY,
- endX - startX,
- selectedTracksMinY - selectedTracksMaxY,
- );
- ctx.restore();
- }
-
- private updatePanelStats(
- panelIndex: number,
- panel: Panel,
- renderTime: number,
- ctx: CanvasRenderingContext2D,
- size: Size2D,
- ) {
- if (!this.perfStatsEnabled) return;
- let renderStats = this.panelPerfStats.get(panel);
- if (renderStats === undefined) {
- renderStats = new PerfStats();
- this.panelPerfStats.set(panel, renderStats);
- }
- renderStats.addValue(renderTime);
-
- // Draw a green box around the whole panel
- ctx.strokeStyle = 'rgba(69, 187, 73, 0.5)';
- const lineWidth = 1;
- ctx.lineWidth = lineWidth;
- ctx.strokeRect(
- lineWidth / 2,
- lineWidth / 2,
- size.width - lineWidth,
- size.height - lineWidth,
- );
-
- const statW = 300;
- ctx.fillStyle = 'hsl(97, 100%, 96%)';
- ctx.fillRect(size.width - statW, size.height - 20, statW, 20);
- ctx.fillStyle = 'hsla(122, 77%, 22%)';
- const statStr = `Panel ${panelIndex + 1} | ` + runningStatStr(renderStats);
- ctx.fillText(statStr, size.width - statW, size.height - 10);
- }
-
- private updatePerfStats(
- renderTime: number,
- totalPanels: number,
- panelsOnCanvas: number,
- ) {
- 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(
- 'div',
- `${this.perfStats.totalPanels} panels, ` +
- `${this.perfStats.panelsOnCanvas} on canvas.`,
- ),
- m('div', runningStatStr(this.perfStats.renderStats)),
- ];
- }
-}
diff --git a/ui/src/frontend/viewer_page/tickmark_panel.ts b/ui/src/frontend/viewer_page/tickmark_panel.ts
index 8b56938..2dc8bd3 100644
--- a/ui/src/frontend/viewer_page/tickmark_panel.ts
+++ b/ui/src/frontend/viewer_page/tickmark_panel.ts
@@ -20,7 +20,6 @@
import {TraceImpl} from '../../core/trace_impl';
import {TRACK_SHELL_WIDTH} from '../css_constants';
import {generateTicks, getMaxMajorTicks, TickType} from './gridline_helper';
-import {Panel} from './panel_container';
import {SearchOverviewTrack} from './search_overview_track';
// We want to create the overview track only once per trace, but this
@@ -30,10 +29,9 @@
const trackTraceMap = new WeakMap<TraceImpl, SearchOverviewTrack>();
// This is used to display the summary of search results.
-export class TickmarkPanel implements Panel {
- readonly kind = 'panel';
- readonly selectable = false;
+export class TickmarkPanel {
private searchOverviewTrack: SearchOverviewTrack;
+ readonly height = 5;
constructor(private readonly trace: TraceImpl) {
this.searchOverviewTrack = getOrCreate(
@@ -44,12 +42,12 @@
}
render(): m.Children {
- return m('.tickbar');
+ return m('', {style: {height: `${this.height}px`}});
}
renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D): void {
ctx.fillStyle = '#999';
- ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
+ ctx.fillRect(TRACK_SHELL_WIDTH - 1, 0, 1, size.height);
const trackSize = {...size, width: size.width - TRACK_SHELL_WIDTH};
ctx.save();
diff --git a/ui/src/frontend/viewer_page/time_axis_panel.ts b/ui/src/frontend/viewer_page/time_axis_panel.ts
index d9d558c..f75738f 100644
--- a/ui/src/frontend/viewer_page/time_axis_panel.ts
+++ b/ui/src/frontend/viewer_page/time_axis_panel.ts
@@ -28,17 +28,15 @@
MIN_PX_PER_STEP,
TickType,
} from './gridline_helper';
-import {Panel} from './panel_container';
-export class TimeAxisPanel implements Panel {
- readonly kind = 'panel';
- readonly selectable = false;
+export class TimeAxisPanel {
readonly id = 'time-axis-panel';
+ readonly height = 22;
constructor(private readonly trace: Trace) {}
render(): m.Children {
- return m('.time-axis-panel');
+ return m('', {style: {height: `${this.height}px`}});
}
renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
@@ -55,7 +53,7 @@
this.renderPanel(ctx, trackSize);
ctx.restore();
- ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
+ ctx.fillRect(TRACK_SHELL_WIDTH - 1, 0, 1, size.height);
}
private renderOffsetTimestamp(ctx: CanvasRenderingContext2D): void {
diff --git a/ui/src/frontend/viewer_page/time_selection_panel.ts b/ui/src/frontend/viewer_page/time_selection_panel.ts
index 22f9cd1..67e591d 100644
--- a/ui/src/frontend/viewer_page/time_selection_panel.ts
+++ b/ui/src/frontend/viewer_page/time_selection_panel.ts
@@ -28,7 +28,6 @@
TRACK_SHELL_WIDTH,
} from '../css_constants';
import {generateTicks, getMaxMajorTicks, TickType} from './gridline_helper';
-import {Panel} from './panel_container';
export interface BBox {
x: number;
@@ -133,19 +132,18 @@
ctx.fillText(label, xPosLabel, yMid);
}
-export class TimeSelectionPanel implements Panel {
- readonly kind = 'panel';
- readonly selectable = false;
+export class TimeSelectionPanel {
+ readonly height = 10;
constructor(private readonly trace: TraceImpl) {}
render(): m.Children {
- return m('.time-selection-panel');
+ return m('', {style: {height: `${this.height}px`}});
}
renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
ctx.fillStyle = '#999';
- ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
+ ctx.fillRect(TRACK_SHELL_WIDTH - 1, 0, 1, size.height);
const trackSize = {...size, width: size.width - TRACK_SHELL_WIDTH};
@@ -176,11 +174,11 @@
}
}
- const localArea = this.trace.timeline.selectedArea;
+ const localSpan = this.trace.timeline.selectedSpan;
const selection = this.trace.selection.selection;
- if (localArea !== undefined) {
- const start = Time.min(localArea.start, localArea.end);
- const end = Time.max(localArea.start, localArea.end);
+ if (localSpan !== undefined) {
+ const start = Time.min(localSpan.start, localSpan.end);
+ const end = Time.max(localSpan.start, localSpan.end);
this.renderSpan(ctx, timescale, size, start, end);
} else {
if (selection.kind === 'area') {
diff --git a/ui/src/frontend/viewer_page/timeline_header.ts b/ui/src/frontend/viewer_page/timeline_header.ts
new file mode 100644
index 0000000..4496fd2
--- /dev/null
+++ b/ui/src/frontend/viewer_page/timeline_header.ts
@@ -0,0 +1,176 @@
+// 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 {canvasSave} from '../../base/canvas_utils';
+import {DisposableStack} from '../../base/disposable_stack';
+import {toHTMLElement} from '../../base/dom_utils';
+import {Rect2D, Size2D} from '../../base/geom';
+import {assertExists} from '../../base/logging';
+import {TimeScale} from '../../base/time_scale';
+import {ZonedInteractionHandler} from '../../base/zoned_interaction_handler';
+import {TraceImpl} from '../../core/trace_impl';
+import {
+ VirtualOverlayCanvas,
+ VirtualOverlayCanvasDrawContext,
+} from '../../components/widgets/virtual_overlay_canvas';
+import {TRACK_SHELL_WIDTH} from '../css_constants';
+import {NotesPanel} from './notes_panel';
+import {TickmarkPanel} from './tickmark_panel';
+import {TimeAxisPanel} from './time_axis_panel';
+import {TimeSelectionPanel} from './time_selection_panel';
+import {
+ shiftDragPanInteraction,
+ wheelNavigationInteraction,
+} from './timeline_interactions';
+
+export interface TimelineHeaderAttrs {
+ // The trace to use for timeline access et al.
+ readonly trace: TraceImpl;
+
+ // Called when the visible area of the timeline changes size. This is the area
+ // to the right of the header is actually rendered on.
+ onTimelineBoundsChange?(rect: Rect2D): void;
+
+ readonly className?: string;
+}
+
+// TODO(stevegolton): The panel concept has been largely removed. It's just
+// defined here so that we don't have to change the implementation of the
+// various header panels listed here. We should consolidate this in the future.
+interface Panel {
+ readonly height: number;
+ render(): m.Children;
+ renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D): void;
+}
+
+/**
+ * This component defines the header of the timeline and handles it's mouse
+ * interactions.
+ *
+ * The timeline header contains:
+ * - The axis (ticks) and time labels
+ * - The selection bar
+ * - The notes bar
+ * - The tickmark bar (highlights that appear when searching)
+ */
+export class TimelineHeader implements m.ClassComponent<TimelineHeaderAttrs> {
+ private readonly trash = new DisposableStack();
+ private readonly trace: TraceImpl;
+ private readonly panels: ReadonlyArray<Panel>;
+ private interactions?: ZonedInteractionHandler;
+
+ constructor({attrs}: m.Vnode<TimelineHeaderAttrs>) {
+ this.trace = attrs.trace;
+ this.panels = [
+ new TimeAxisPanel(attrs.trace),
+ new TimeSelectionPanel(attrs.trace),
+ new NotesPanel(attrs.trace),
+ new TickmarkPanel(attrs.trace),
+ ];
+ }
+
+ view({attrs}: m.Vnode<TimelineHeaderAttrs>) {
+ return m(
+ '.pf-timeline-header',
+ {className: attrs.className},
+ m(
+ VirtualOverlayCanvas,
+ {
+ raf: attrs.trace.raf,
+ onCanvasRedraw: (ctx) => {
+ const rect = new Rect2D({
+ left: TRACK_SHELL_WIDTH,
+ right: ctx.virtualCanvasSize.width,
+ top: 0,
+ bottom: 0,
+ });
+ attrs.onTimelineBoundsChange?.(rect);
+ this.drawCanvas(ctx);
+ },
+ },
+ this.panels.map((p) => p.render()),
+ ),
+ );
+ }
+
+ oncreate({dom}: m.VnodeDOM<TimelineHeaderAttrs>) {
+ const timelineHeaderElement = toHTMLElement(dom);
+ this.interactions = new ZonedInteractionHandler(timelineHeaderElement);
+ this.trash.use(this.interactions);
+ }
+
+ onremove() {
+ this.trash.dispose();
+ }
+
+ private drawCanvas({
+ ctx,
+ virtualCanvasSize,
+ }: VirtualOverlayCanvasDrawContext) {
+ let top = 0;
+ for (const p of this.panels) {
+ using _ = canvasSave(ctx);
+ ctx.translate(0, top);
+ p.renderCanvas(ctx, {width: virtualCanvasSize.width, height: p.height});
+ top += p.height;
+ }
+
+ const timelineRect = new Rect2D({
+ left: TRACK_SHELL_WIDTH,
+ top: 0,
+ right: virtualCanvasSize.width,
+ bottom: virtualCanvasSize.height,
+ });
+
+ // Always grab the latest visible window and create a timescale
+ // out of it.
+ const visibleWindow = this.trace.timeline.visibleWindow;
+ const timescale = new TimeScale(visibleWindow, timelineRect);
+
+ assertExists(this.interactions).update([
+ shiftDragPanInteraction(this.trace, timelineRect, timescale),
+ wheelNavigationInteraction(this.trace, timelineRect, timescale),
+ {
+ // Allow making area selections (no tracks) by dragging on the header
+ // timeline.
+ id: 'area-selection',
+ area: timelineRect,
+ drag: {
+ minDistance: 1,
+ cursorWhileDragging: 'text',
+ onDrag: (e) => {
+ const dragRect = Rect2D.fromPoints(e.dragStart, e.dragCurrent);
+ const timeSpan = timescale
+ .pxSpanToHpTimeSpan(dragRect)
+ .toTimeSpan();
+ this.trace.timeline.selectedSpan = timeSpan;
+ },
+ onDragEnd: (e) => {
+ const dragRect = Rect2D.fromPoints(e.dragStart, e.dragCurrent);
+ const timeSpan = timescale
+ .pxSpanToHpTimeSpan(dragRect)
+ .toTimeSpan();
+ this.trace.selection.selectArea({
+ start: timeSpan.start,
+ end: timeSpan.end,
+ trackUris: [],
+ });
+ this.trace.timeline.selectedSpan = undefined;
+ },
+ },
+ },
+ ]);
+ }
+}
diff --git a/ui/src/frontend/viewer_page/timeline_interactions.ts b/ui/src/frontend/viewer_page/timeline_interactions.ts
new file mode 100644
index 0000000..4872d2e
--- /dev/null
+++ b/ui/src/frontend/viewer_page/timeline_interactions.ts
@@ -0,0 +1,75 @@
+// 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.
+
+/**
+ * These interactions may be added to a ZonedInteractionHandler. They define
+ * some common operations that are used by several parts of the timeline such as
+ * shift+drag to pan and mouse wheel navigation.
+ */
+
+import {Rect2D} from '../../base/geom';
+import {TimeScale} from '../../base/time_scale';
+import {Zone} from '../../base/zoned_interaction_handler';
+import {TraceImpl} from '../../core/trace_impl';
+
+const WHEEL_ZOOM_SPEED = -0.02;
+
+export function shiftDragPanInteraction(
+ trace: TraceImpl,
+ rect: Rect2D,
+ timescale: TimeScale,
+): Zone {
+ return {
+ id: 'drag-pan',
+ area: rect,
+ cursor: 'grab',
+ keyModifier: 'shift',
+ drag: {
+ cursorWhileDragging: 'grabbing',
+ onDrag: (e) => {
+ trace.timeline.panVisibleWindow(
+ timescale.pxToDuration(-e.deltaSinceLastEvent.x),
+ );
+ },
+ },
+ };
+}
+
+export function wheelNavigationInteraction(
+ trace: TraceImpl,
+ rect: Rect2D,
+ timescale: TimeScale,
+): Zone {
+ return {
+ id: 'mouse-wheel-navigation',
+ area: rect,
+ onWheel: (e) => {
+ if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
+ const tDelta = timescale.pxToDuration(e.deltaX);
+ trace.timeline.panVisibleWindow(tDelta);
+ } else {
+ if (e.ctrlKey) {
+ const sign = e.deltaY < 0 ? -1 : 1;
+ const deltaY = sign * Math.log2(1 + Math.abs(e.deltaY));
+ const zoomPx = e.position.x - rect.left;
+ const centerPoint = zoomPx / rect.width;
+ trace.timeline.zoomVisibleWindow(
+ 1 - deltaY * WHEEL_ZOOM_SPEED,
+ centerPoint,
+ );
+ }
+ }
+ },
+ };
+}
diff --git a/ui/src/frontend/viewer_page/track_panel.ts b/ui/src/frontend/viewer_page/track_panel.ts
deleted file mode 100644
index bc39777..0000000
--- a/ui/src/frontend/viewer_page/track_panel.ts
+++ /dev/null
@@ -1,471 +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';
-import {canvasClip, canvasSave} from '../../base/canvas_utils';
-import {classNames} from '../../base/classnames';
-import {Bounds2D, Size2D, VerticalBounds} from '../../base/geom';
-import {Icons} from '../../base/semantic_icons';
-import {TimeScale} from '../../base/time_scale';
-import {RequiredField} from '../../base/utils';
-import {featureFlags} from '../../core/feature_flags';
-import {raf} from '../../core/raf_scheduler';
-import {TraceImpl} from '../../core/trace_impl';
-import {TrackRenderer} from '../../core/track_manager';
-import {TrackDescriptor, TrackRenderContext} from '../../public/track';
-import {TrackNode} from '../../public/workspace';
-import {Button} from '../../widgets/button';
-import {Intent} from '../../widgets/common';
-import {Popup, PopupPosition} from '../../widgets/popup';
-import {TrackWidget} from '../../widgets/track_widget';
-import {Tree, TreeNode} from '../../widgets/tree';
-import {SELECTION_FILL_COLOR, TRACK_SHELL_WIDTH} from '../css_constants';
-import {Panel} from './panel_container';
-import {calculateResolution} from './resolution';
-
-const SHOW_TRACK_DETAILS_BUTTON = featureFlags.register({
- id: 'showTrackDetailsButton',
- name: 'Show track details button',
- description: 'Show track details button in track shells.',
- defaultValue: false,
-});
-
-// Default height of a track element that has no track, or is collapsed.
-// Note: This is designed to roughly match the height of a cpu slice track.
-export const DEFAULT_TRACK_HEIGHT_PX = 30;
-
-interface TrackPanelAttrs {
- readonly trace: TraceImpl;
- readonly node: TrackNode;
- readonly indentationLevel: number;
- readonly trackRenderer?: TrackRenderer;
- readonly revealOnCreate?: boolean;
- readonly topOffsetPx: number;
- readonly reorderable?: boolean;
-}
-
-export class TrackPanel implements Panel {
- readonly kind = 'panel';
- readonly selectable = true;
- readonly trackNode?: TrackNode;
-
- private readonly attrs: TrackPanelAttrs;
-
- constructor(attrs: TrackPanelAttrs) {
- this.attrs = attrs;
- this.trackNode = attrs.node;
- }
-
- get heightPx(): number {
- const {trackRenderer, node} = this.attrs;
-
- // If the node is a summary track and is expanded, shrink it to save
- // vertical real estate).
- if (node.isSummary && node.expanded) return DEFAULT_TRACK_HEIGHT_PX;
-
- // Otherwise return the height of the track, if we have one.
- return trackRenderer?.track.getHeight() ?? DEFAULT_TRACK_HEIGHT_PX;
- }
-
- render(): m.Children {
- const {
- node,
- indentationLevel,
- trackRenderer,
- revealOnCreate,
- topOffsetPx,
- reorderable = false,
- } = this.attrs;
-
- const error = trackRenderer?.getError();
-
- const buttons = [
- SHOW_TRACK_DETAILS_BUTTON.get() &&
- renderTrackDetailsButton(node, trackRenderer?.desc),
- trackRenderer?.track.getTrackShellButtons?.(),
- node.removable && renderCloseButton(node),
- // We don't want summary tracks to be pinned as they rarely have
- // useful information.
- !node.isSummary && renderPinButton(node),
- this.renderAreaSelectionCheckbox(node),
- error && renderCrashButton(error, trackRenderer?.desc.pluginId),
- ];
-
- let scrollIntoView = false;
- const tracks = this.attrs.trace.tracks;
- if (tracks.scrollToTrackNodeId === node.id) {
- tracks.scrollToTrackNodeId = undefined;
- scrollIntoView = true;
- }
-
- return m(TrackWidget, {
- id: node.id,
- title: node.title,
- subtitle: trackRenderer?.desc.subtitle,
- path: node.fullPath.join('/'),
- heightPx: this.heightPx,
- error: Boolean(trackRenderer?.getError()),
- chips: trackRenderer?.desc.chips,
- indentationLevel,
- topOffsetPx,
- buttons,
- revealOnCreate: revealOnCreate || scrollIntoView,
- collapsible: node.hasChildren,
- collapsed: node.collapsed,
- highlight: this.isHighlighted(node),
- isSummary: node.isSummary,
- reorderable,
- onToggleCollapsed: () => {
- node.hasChildren && node.toggleCollapsed();
- },
- onTrackContentMouseMove: (pos, bounds) => {
- const timescale = this.getTimescaleForBounds(bounds);
- trackRenderer?.track.onMouseMove?.({
- ...pos,
- timescale,
- });
- raf.scheduleCanvasRedraw();
- },
- onTrackContentMouseOut: () => {
- trackRenderer?.track.onMouseOut?.();
- raf.scheduleCanvasRedraw();
- },
- onTrackContentClick: (pos, bounds) => {
- const timescale = this.getTimescaleForBounds(bounds);
- raf.scheduleCanvasRedraw();
- return (
- trackRenderer?.track.onMouseClick?.({
- ...pos,
- timescale,
- }) ?? false
- );
- },
- onupdate: () => {
- trackRenderer?.track.onFullRedraw?.();
- },
- onMoveBefore: (nodeId: string) => {
- const targetNode = node.workspace?.getTrackById(nodeId);
- if (targetNode !== undefined) {
- // Insert the target node before this one
- targetNode.parent?.addChildBefore(targetNode, node);
- }
- },
- onMoveAfter: (nodeId: string) => {
- const targetNode = node.workspace?.getTrackById(nodeId);
- if (targetNode !== undefined) {
- // Insert the target node after this one
- targetNode.parent?.addChildAfter(targetNode, node);
- }
- },
- });
- }
-
- renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
- const {trackRenderer: tr, node} = this.attrs;
-
- // Don't render if expanded and isSummary
- if (node.isSummary && node.expanded) {
- return;
- }
-
- const trackSize = {
- width: size.width - TRACK_SHELL_WIDTH,
- height: size.height,
- };
-
- using _ = canvasSave(ctx);
- ctx.translate(TRACK_SHELL_WIDTH, 0);
- canvasClip(ctx, 0, 0, trackSize.width, trackSize.height);
-
- const visibleWindow = this.attrs.trace.timeline.visibleWindow;
- const timescale = new TimeScale(visibleWindow, {
- left: 0,
- right: trackSize.width,
- });
-
- if (tr) {
- if (!tr.getError()) {
- const trackRenderCtx: TrackRenderContext = {
- trackUri: tr.desc.uri,
- visibleWindow,
- size: trackSize,
- resolution: calculateResolution(visibleWindow, trackSize.width),
- ctx,
- timescale,
- };
- tr.render(trackRenderCtx);
- }
- }
-
- this.highlightIfTrackInAreaSelection(ctx, timescale, node, trackSize);
- }
-
- getSliceVerticalBounds(depth: number): VerticalBounds | undefined {
- if (this.attrs.trackRenderer === undefined) {
- return undefined;
- }
- return this.attrs.trackRenderer.track.getSliceVerticalBounds?.(depth);
- }
-
- private getTimescaleForBounds(bounds: Bounds2D) {
- const timeWindow = this.attrs.trace.timeline.visibleWindow;
- return new TimeScale(timeWindow, {
- left: 0,
- right: bounds.right - bounds.left,
- });
- }
-
- private isHighlighted(node: TrackNode) {
- // The track should be highlighted if the current search result matches this
- // track or one of its children.
- const searchIndex = this.attrs.trace.search.resultIndex;
- const searchResults = this.attrs.trace.search.searchResults;
-
- if (searchIndex !== -1 && searchResults !== undefined) {
- const uri = searchResults.trackUris[searchIndex];
- // Highlight if this or any children match the search results
- if (
- uri === node.uri ||
- node.flatTracksOrdered.find((t) => t.uri === uri)
- ) {
- return true;
- }
- }
-
- const curSelection = this.attrs.trace.selection;
- if (
- curSelection.selection.kind === 'track' &&
- curSelection.selection.trackUri === node.uri
- ) {
- return true;
- }
-
- return false;
- }
-
- private highlightIfTrackInAreaSelection(
- ctx: CanvasRenderingContext2D,
- timescale: TimeScale,
- node: TrackNode,
- size: Size2D,
- ) {
- const selection = this.attrs.trace.selection.selection;
- if (selection.kind !== 'area') {
- return;
- }
-
- const tracksWithUris = node.flatTracks.filter(
- (t) => t.uri !== undefined,
- ) as ReadonlyArray<RequiredField<TrackNode, 'uri'>>;
-
- let selected = false;
- if (node.isSummary) {
- selected = tracksWithUris.some((track) =>
- selection.trackUris.includes(track.uri),
- );
- } else {
- if (node.uri) {
- selected = selection.trackUris.includes(node.uri);
- }
- }
-
- if (selected) {
- const selectedAreaDuration = selection.end - selection.start;
- ctx.fillStyle = SELECTION_FILL_COLOR;
- ctx.fillRect(
- timescale.timeToPx(selection.start),
- 0,
- timescale.durationToPx(selectedAreaDuration),
- size.height,
- );
- }
- }
-
- private renderAreaSelectionCheckbox(node: TrackNode): m.Children {
- const selectionManager = this.attrs.trace.selection;
- const selection = selectionManager.selection;
- if (selection.kind === 'area') {
- if (node.isSummary) {
- const tracksWithUris = node.flatTracks.filter(
- (t) => t.uri !== undefined,
- ) as ReadonlyArray<RequiredField<TrackNode, 'uri'>>;
- // Check if any nodes within are selected
- const childTracksInSelection = tracksWithUris.map((t) =>
- selection.trackUris.includes(t.uri),
- );
- if (childTracksInSelection.every((b) => b)) {
- return m(Button, {
- onclick: (e: MouseEvent) => {
- const uris = tracksWithUris.map((t) => t.uri);
- selectionManager.toggleGroupAreaSelection(uris);
- e.stopPropagation();
- },
- compact: true,
- icon: Icons.Checkbox,
- title: 'Remove child tracks from selection',
- });
- } else if (childTracksInSelection.some((b) => b)) {
- return m(Button, {
- onclick: (e: MouseEvent) => {
- const uris = tracksWithUris.map((t) => t.uri);
- selectionManager.toggleGroupAreaSelection(uris);
- e.stopPropagation();
- },
- compact: true,
- icon: Icons.IndeterminateCheckbox,
- title: 'Add remaining child tracks to selection',
- });
- } else {
- return m(Button, {
- onclick: (e: MouseEvent) => {
- const uris = tracksWithUris.map((t) => t.uri);
- selectionManager.toggleGroupAreaSelection(uris);
- e.stopPropagation();
- },
- compact: true,
- icon: Icons.BlankCheckbox,
- title: 'Add child tracks to selection',
- });
- }
- } else {
- const nodeUri = node.uri;
- if (nodeUri) {
- return (
- selection.kind === 'area' &&
- m(Button, {
- onclick: (e: MouseEvent) => {
- selectionManager.toggleTrackAreaSelection(nodeUri);
- e.stopPropagation();
- },
- compact: true,
- ...(selection.trackUris.includes(nodeUri)
- ? {icon: Icons.Checkbox, title: 'Remove track'}
- : {icon: Icons.BlankCheckbox, title: 'Add track to selection'}),
- })
- );
- }
- }
- }
- return undefined;
- }
-}
-
-function renderCrashButton(error: Error, pluginId?: string) {
- return m(
- Popup,
- {
- trigger: m(Button, {
- icon: Icons.Crashed,
- compact: true,
- }),
- },
- m(
- '.pf-track-crash-popup',
- m('span', 'This track has crashed.'),
- pluginId && m('span', `Owning plugin: ${pluginId}`),
- m(Button, {
- label: 'View & Report Crash',
- intent: Intent.Primary,
- className: Popup.DISMISS_POPUP_GROUP_CLASS,
- onclick: () => {
- throw error;
- },
- }),
- // TODO(stevegolton): In the future we should provide a quick way to
- // disable the plugin, or provide a link to the plugin page, but this
- // relies on the plugin page being fully functional.
- ),
- );
-}
-
-function renderCloseButton(node: TrackNode) {
- return m(Button, {
- onclick: (e) => {
- node.remove();
- e.stopPropagation();
- },
- icon: Icons.Close,
- title: 'Close track',
- compact: true,
- });
-}
-
-function renderPinButton(node: TrackNode): m.Children {
- const isPinned = node.isPinned;
- return m(Button, {
- className: classNames(!isPinned && 'pf-visible-on-hover'),
- onclick: (e) => {
- isPinned ? node.unpin() : node.pin();
- e.stopPropagation();
- },
- icon: Icons.Pin,
- iconFilled: isPinned,
- title: isPinned ? 'Unpin' : 'Pin to top',
- compact: true,
- });
-}
-
-function renderTrackDetailsButton(
- node: TrackNode,
- td?: TrackDescriptor,
-): m.Children {
- let parent = node.parent;
- let fullPath: m.ChildArray = [node.title];
- while (parent && parent instanceof TrackNode) {
- fullPath = [parent.title, ' \u2023 ', ...fullPath];
- parent = parent.parent;
- }
- return m(
- Popup,
- {
- trigger: m(Button, {
- className: 'pf-visible-on-hover',
- icon: 'info',
- title: 'Show track details',
- compact: true,
- }),
- position: PopupPosition.Bottom,
- },
- m(
- '.pf-track-details-dropdown',
- m(
- Tree,
- m(TreeNode, {left: 'Track Node ID', right: node.id}),
- m(TreeNode, {left: 'Collapsed', right: `${node.collapsed}`}),
- m(TreeNode, {left: 'URI', right: node.uri}),
- m(TreeNode, {left: 'Is Summary Track', right: `${node.isSummary}`}),
- m(TreeNode, {
- left: 'SortOrder',
- right: node.sortOrder ?? '0 (undefined)',
- }),
- m(TreeNode, {left: 'Path', right: fullPath}),
- m(TreeNode, {left: 'Title', right: node.title}),
- m(TreeNode, {
- left: 'Workspace',
- right: node.workspace?.title ?? '[no workspace]',
- }),
- td && m(TreeNode, {left: 'Plugin ID', right: td.pluginId}),
- td &&
- m(
- TreeNode,
- {left: 'Tags'},
- td.tags &&
- Object.entries(td.tags).map(([key, value]) => {
- return m(TreeNode, {left: key, right: value?.toString()});
- }),
- ),
- ),
- ),
- );
-}
diff --git a/ui/src/frontend/viewer_page/track_tree_view.ts b/ui/src/frontend/viewer_page/track_tree_view.ts
new file mode 100644
index 0000000..2583b09
--- /dev/null
+++ b/ui/src/frontend/viewer_page/track_tree_view.ts
@@ -0,0 +1,725 @@
+// 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.
+
+/**
+ * This module provides the TrackNodeTree mithril component, which is
+ * responsible for rendering out a tree of tracks and drawing their content
+ * onto the canvas.
+ * - Rendering track panels and handling nested and sticky headers.
+ * - Managing the virtual canvas & drawing the grid-lines, tracks and overlays
+ * onto the canvas.
+ * - Handling track interaction events such as dragging, panning and scrolling.
+ */
+
+import {hex} from 'color-convert';
+import m from 'mithril';
+import {canvasClip, canvasSave} from '../../base/canvas_utils';
+import {classNames} from '../../base/classnames';
+import {DisposableStack} from '../../base/disposable_stack';
+import {findRef, toHTMLElement} from '../../base/dom_utils';
+import {
+ HorizontalBounds,
+ Rect2D,
+ Size2D,
+ VerticalBounds,
+} from '../../base/geom';
+import {HighPrecisionTime} from '../../base/high_precision_time';
+import {HighPrecisionTimeSpan} from '../../base/high_precision_time_span';
+import {assertExists} from '../../base/logging';
+import {Time} from '../../base/time';
+import {TimeScale} from '../../base/time_scale';
+import {
+ DragEvent,
+ ZonedInteractionHandler,
+} from '../../base/zoned_interaction_handler';
+import {PerfStats, runningStatStr} from '../../core/perf_stats';
+import {TraceImpl} from '../../core/trace_impl';
+import {TrackNode} from '../../public/workspace';
+import {VirtualOverlayCanvas} from '../../components/widgets/virtual_overlay_canvas';
+import {
+ SELECTION_STROKE_COLOR,
+ TRACK_BORDER_COLOR,
+ TRACK_SHELL_WIDTH,
+} from '../css_constants';
+import {renderFlows} from './flow_events_renderer';
+import {generateTicks, getMaxMajorTicks, TickType} from './gridline_helper';
+import {
+ shiftDragPanInteraction,
+ wheelNavigationInteraction,
+} from './timeline_interactions';
+import {TrackView} from './track_view';
+import {drawVerticalLineAtTime} from './vertical_line_helper';
+import {featureFlags} from '../../core/feature_flags';
+
+const VIRTUAL_TRACK_SCROLLING = featureFlags.register({
+ id: 'virtualTrackScrolling',
+ name: 'Virtual track scrolling',
+ description: `[Experimental] Use virtual scrolling in the timeline view to
+ improve performance on large traces.`,
+ defaultValue: false,
+});
+
+export interface TrackTreeViewAttrs {
+ // Access to the trace, for accessing the track registry / selection manager.
+ readonly trace: TraceImpl;
+
+ // The root track node for tracks to display in this stack. This node is not
+ // actually displayed, only its children are, but it's used for reordering
+ // purposes if `reorderable` is set to true.
+ readonly rootNode: TrackNode;
+
+ // Additional class names to add to the root level element.
+ readonly className?: string;
+
+ // Whether tracks in this stack can be reordered amongst themselves.
+ // Default: false
+ readonly reorderable?: boolean;
+
+ // Scroll to scroll to new tracks as they are added.
+ // Default: false
+ readonly scrollToNewTracks?: boolean;
+}
+
+const TRACK_CONTAINER_REF = 'track-container';
+
+export class TrackTreeView implements m.ClassComponent<TrackTreeViewAttrs> {
+ private readonly trace: TraceImpl;
+ private readonly trash = new DisposableStack();
+ private interactions?: ZonedInteractionHandler;
+ private perfStatsEnabled = false;
+ private trackPerfStats = new WeakMap<TrackNode, PerfStats>();
+ private perfStats = {
+ totalTracks: 0,
+ tracksOnCanvas: 0,
+ renderStats: new PerfStats(10),
+ };
+ private areaDrag?: InProgressAreaSelection;
+ private handleDrag?: InProgressHandleDrag;
+ private canvasRect?: Rect2D;
+
+ constructor({attrs}: m.Vnode<TrackTreeViewAttrs>) {
+ this.trace = attrs.trace;
+ }
+
+ view({attrs}: m.Vnode<TrackTreeViewAttrs>): m.Children {
+ const {trace, scrollToNewTracks, reorderable, className, rootNode} = attrs;
+ const renderedTracks = new Array<TrackView>();
+ let top = 0;
+
+ const renderTrack = (
+ node: TrackNode,
+ depth = 0,
+ stickyTop = 0,
+ ): m.Children => {
+ const trackView = new TrackView(trace, node, top);
+ renderedTracks.push(trackView);
+
+ let childDepth = depth;
+ let childStickyTop = stickyTop;
+ if (!node.headless) {
+ top += trackView.height;
+ ++childDepth;
+ childStickyTop += trackView.height;
+ }
+
+ const children =
+ (node.headless || node.expanded) &&
+ node.hasChildren &&
+ node.children.map((track) =>
+ renderTrack(track, childDepth, childStickyTop),
+ );
+
+ if (node.headless) {
+ return children;
+ } else {
+ const isTrackOnScreen = () => {
+ if (VIRTUAL_TRACK_SCROLLING.get()) {
+ return this.canvasRect?.overlaps({
+ left: 0,
+ right: 1,
+ ...trackView.verticalBounds,
+ });
+ } else {
+ return true;
+ }
+ };
+
+ return trackView.renderDOM(
+ {
+ lite: !Boolean(isTrackOnScreen()),
+ scrollToOnCreate: scrollToNewTracks,
+ reorderable,
+ stickyTop,
+ depth,
+ },
+ children,
+ );
+ }
+ };
+
+ return m(
+ VirtualOverlayCanvas,
+ {
+ raf: attrs.trace.raf,
+ className: classNames(className, 'pf-track-tree'),
+ scrollAxes: 'y',
+ onCanvasRedraw: ({ctx, virtualCanvasSize, canvasRect}) => {
+ this.drawCanvas(
+ ctx,
+ virtualCanvasSize,
+ renderedTracks,
+ canvasRect,
+ rootNode,
+ );
+
+ if (VIRTUAL_TRACK_SCROLLING.get()) {
+ // The VOC can ask us to redraw the canvas for any number of
+ // reasons, we're interested in the case where the canvas rect has
+ // moved (which indicates that the user has scrolled enough to
+ // warrant drawing more content). If so, we should redraw the DOM in
+ // order to keep the track nodes inside the viewport rendering in
+ // full-fat mode.
+ if (
+ this.canvasRect === undefined ||
+ !this.canvasRect.equals(canvasRect)
+ ) {
+ this.canvasRect = canvasRect;
+ m.redraw();
+ }
+ }
+ },
+ },
+ m(
+ '',
+ {ref: TRACK_CONTAINER_REF},
+ rootNode.children.map((track) => renderTrack(track)),
+ ),
+ );
+ }
+
+ oncreate({attrs, dom}: m.VnodeDOM<TrackTreeViewAttrs>) {
+ const interactionTarget = toHTMLElement(
+ assertExists(findRef(dom, TRACK_CONTAINER_REF)),
+ );
+ this.interactions = new ZonedInteractionHandler(interactionTarget);
+ this.trash.use(this.interactions);
+ this.trash.use(
+ attrs.trace.perfDebugging.addContainer({
+ setPerfStatsEnabled: (enable: boolean) => {
+ this.perfStatsEnabled = enable;
+ },
+ renderPerfStats: () => {
+ return [
+ m(
+ '',
+ `${this.perfStats.totalTracks} tracks, ` +
+ `${this.perfStats.tracksOnCanvas} on canvas.`,
+ ),
+ m('', runningStatStr(this.perfStats.renderStats)),
+ ];
+ },
+ }),
+ );
+ }
+
+ onremove() {
+ this.trash.dispose();
+ }
+
+ private drawCanvas(
+ ctx: CanvasRenderingContext2D,
+ size: Size2D,
+ renderedTracks: ReadonlyArray<TrackView>,
+ floatingCanvasRect: Rect2D,
+ rootNode: TrackNode,
+ ) {
+ const timelineRect = new Rect2D({
+ left: TRACK_SHELL_WIDTH,
+ top: 0,
+ right: size.width,
+ bottom: size.height,
+ });
+
+ // Always grab the latest visible window and create a timescale out of
+ // it.
+ const visibleWindow = this.trace.timeline.visibleWindow;
+ const timescale = new TimeScale(visibleWindow, timelineRect);
+
+ const start = performance.now();
+
+ // Save, translate & clip the canvas to the area of the timeline.
+ using _ = canvasSave(ctx);
+ canvasClip(ctx, timelineRect);
+
+ this.drawGridLines(ctx, timescale, timelineRect);
+
+ const tracksOnCanvas = this.drawTracks(
+ renderedTracks,
+ floatingCanvasRect,
+ size,
+ ctx,
+ timelineRect,
+ visibleWindow,
+ );
+
+ renderFlows(this.trace, ctx, size, renderedTracks, rootNode, timescale);
+ this.drawHoveredNoteVertical(ctx, timescale, size);
+ this.drawHoveredCursorVertical(ctx, timescale, size);
+ this.drawWakeupVertical(ctx, timescale, size);
+ this.drawNoteVerticals(ctx, timescale, size);
+ this.drawAreaSelection(ctx, timescale, size);
+ this.updateInteractions(timelineRect, timescale, size, renderedTracks);
+
+ const renderTime = performance.now() - start;
+ this.updatePerfStats(renderTime, renderedTracks.length, tracksOnCanvas);
+ }
+
+ private drawGridLines(
+ ctx: CanvasRenderingContext2D,
+ timescale: TimeScale,
+ size: Size2D,
+ ): void {
+ ctx.strokeStyle = TRACK_BORDER_COLOR;
+ ctx.lineWidth = 1;
+
+ if (size.width > 0 && timescale.timeSpan.duration > 0n) {
+ const maxMajorTicks = getMaxMajorTicks(size.width);
+ const offset = this.trace.timeline.timestampOffset();
+ for (const {type, time} of generateTicks(
+ timescale.timeSpan.toTimeSpan(),
+ maxMajorTicks,
+ offset,
+ )) {
+ const px = Math.floor(timescale.timeToPx(time));
+ if (type === TickType.MAJOR) {
+ ctx.beginPath();
+ ctx.moveTo(px + 0.5, 0);
+ ctx.lineTo(px + 0.5, size.height);
+ ctx.stroke();
+ }
+ }
+ }
+ }
+
+ private drawTracks(
+ renderedTracks: ReadonlyArray<TrackView>,
+ floatingCanvasRect: Rect2D,
+ size: Size2D,
+ ctx: CanvasRenderingContext2D,
+ timelineRect: Rect2D,
+ visibleWindow: HighPrecisionTimeSpan,
+ ) {
+ let tracksOnCanvas = 0;
+ for (const trackView of renderedTracks) {
+ const {verticalBounds} = trackView;
+ if (
+ floatingCanvasRect.overlaps({
+ ...verticalBounds,
+ left: 0,
+ right: size.width,
+ })
+ ) {
+ trackView.drawCanvas(
+ ctx,
+ timelineRect,
+ visibleWindow,
+ this.perfStatsEnabled,
+ this.trackPerfStats,
+ );
+ ++tracksOnCanvas;
+ }
+ }
+ return tracksOnCanvas;
+ }
+
+ private updateInteractions(
+ timelineRect: Rect2D,
+ timescale: TimeScale,
+ size: Size2D,
+ renderedTracks: ReadonlyArray<TrackView>,
+ ) {
+ const trace = this.trace;
+ const areaSelection =
+ trace.selection.selection.kind === 'area' && trace.selection.selection;
+
+ assertExists(this.interactions).update([
+ shiftDragPanInteraction(trace, timelineRect, timescale),
+ areaSelection !== false && {
+ id: 'start-edit',
+ area: new Rect2D({
+ left: timescale.timeToPx(areaSelection.start) - 5,
+ right: timescale.timeToPx(areaSelection.start) + 5,
+ top: 0,
+ bottom: size.height,
+ }),
+ cursor: 'col-resize',
+ drag: {
+ cursorWhileDragging: 'col-resize',
+ onDrag: (e) => {
+ if (!this.handleDrag) {
+ this.handleDrag = new InProgressHandleDrag(
+ new HighPrecisionTime(areaSelection.end),
+ );
+ }
+ this.handleDrag.currentTime = timescale.pxToHpTime(e.dragCurrent.x);
+ trace.timeline.selectedSpan = this.handleDrag
+ .timeSpan()
+ .toTimeSpan();
+ },
+ onDragEnd: (e) => {
+ const newStartTime = timescale
+ .pxToHpTime(e.dragCurrent.x)
+ .toTime('ceil');
+ trace.selection.selectArea({
+ ...areaSelection,
+ end: Time.max(newStartTime, areaSelection.end),
+ start: Time.min(newStartTime, areaSelection.end),
+ });
+ trace.timeline.selectedSpan = undefined;
+ this.handleDrag = undefined;
+ },
+ },
+ },
+ areaSelection !== false && {
+ id: 'end-edit',
+ area: new Rect2D({
+ left: timescale.timeToPx(areaSelection.end) - 5,
+ right: timescale.timeToPx(areaSelection.end) + 5,
+ top: 0,
+ bottom: size.height,
+ }),
+ cursor: 'col-resize',
+ drag: {
+ cursorWhileDragging: 'col-resize',
+ onDrag: (e) => {
+ if (!this.handleDrag) {
+ this.handleDrag = new InProgressHandleDrag(
+ new HighPrecisionTime(areaSelection.start),
+ );
+ }
+ this.handleDrag.currentTime = timescale.pxToHpTime(e.dragCurrent.x);
+ trace.timeline.selectedSpan = this.handleDrag
+ .timeSpan()
+ .toTimeSpan();
+ },
+ onDragEnd: (e) => {
+ const newEndTime = timescale
+ .pxToHpTime(e.dragCurrent.x)
+ .toTime('ceil');
+ trace.selection.selectArea({
+ ...areaSelection,
+ end: Time.max(newEndTime, areaSelection.start),
+ start: Time.min(newEndTime, areaSelection.start),
+ });
+ trace.timeline.selectedSpan = undefined;
+ this.handleDrag = undefined;
+ },
+ },
+ },
+ {
+ id: 'area-selection',
+ area: timelineRect,
+ onClick: () => {
+ // If a track hasn't intercepted the click, treat this as a
+ // deselection event.
+ trace.selection.clear();
+ },
+ drag: {
+ minDistance: 1,
+ cursorWhileDragging: 'crosshair',
+ onDrag: (e) => {
+ if (!this.areaDrag) {
+ this.areaDrag = new InProgressAreaSelection(
+ timescale.pxToHpTime(e.dragStart.x),
+ e.dragStart.y,
+ );
+ }
+ this.areaDrag.update(e, timescale);
+ trace.timeline.selectedSpan = this.areaDrag.timeSpan().toTimeSpan();
+ },
+ onDragEnd: (e) => {
+ if (!this.areaDrag) {
+ this.areaDrag = new InProgressAreaSelection(
+ timescale.pxToHpTime(e.dragStart.x),
+ e.dragStart.y,
+ );
+ }
+ this.areaDrag?.update(e, timescale);
+
+ // Find the list of tracks that intersect this selection
+ const trackUris = findTracksInRect(
+ renderedTracks,
+ this.areaDrag.rect(timescale),
+ true,
+ )
+ .map((t) => t.uri)
+ .filter((uri) => uri !== undefined);
+
+ const timeSpan = this.areaDrag.timeSpan().toTimeSpan();
+ trace.selection.selectArea({
+ start: timeSpan.start,
+ end: timeSpan.end,
+ trackUris,
+ });
+
+ trace.timeline.selectedSpan = undefined;
+ this.areaDrag = undefined;
+ },
+ },
+ },
+ wheelNavigationInteraction(trace, timelineRect, timescale),
+ ]);
+ }
+
+ private updatePerfStats(
+ renderTime: number,
+ totalTracks: number,
+ tracksOnCanvas: number,
+ ) {
+ if (!this.perfStatsEnabled) return;
+ this.perfStats.renderStats.addValue(renderTime);
+ this.perfStats.totalTracks = totalTracks;
+ this.perfStats.tracksOnCanvas = tracksOnCanvas;
+ }
+
+ private drawAreaSelection(
+ ctx: CanvasRenderingContext2D,
+ timescale: TimeScale,
+ size: Size2D,
+ ) {
+ if (this.areaDrag) {
+ ctx.strokeStyle = SELECTION_STROKE_COLOR;
+ ctx.lineWidth = 1;
+ const rect = this.areaDrag.rect(timescale);
+ ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
+ }
+
+ if (this.handleDrag) {
+ const rect = this.handleDrag.hBounds(timescale);
+
+ ctx.strokeStyle = SELECTION_STROKE_COLOR;
+ ctx.lineWidth = 1;
+
+ ctx.beginPath();
+ ctx.moveTo(rect.left, 0);
+ ctx.lineTo(rect.left, size.height);
+ ctx.stroke();
+ ctx.closePath();
+
+ ctx.beginPath();
+ ctx.moveTo(rect.right, 0);
+ ctx.lineTo(rect.right, size.height);
+ ctx.stroke();
+ ctx.closePath();
+ }
+
+ const selection = this.trace.selection.selection;
+ if (selection.kind === 'area') {
+ const startPx = timescale.timeToPx(selection.start);
+ const endPx = timescale.timeToPx(selection.end);
+
+ ctx.strokeStyle = '#8398e6';
+ ctx.lineWidth = 2;
+
+ ctx.beginPath();
+ ctx.moveTo(startPx, 0);
+ ctx.lineTo(startPx, size.height);
+ ctx.stroke();
+ ctx.closePath();
+
+ ctx.beginPath();
+ ctx.moveTo(endPx, 0);
+ ctx.lineTo(endPx, size.height);
+ ctx.stroke();
+ ctx.closePath();
+ }
+ }
+
+ private drawHoveredCursorVertical(
+ ctx: CanvasRenderingContext2D,
+ timescale: TimeScale,
+ size: Size2D,
+ ) {
+ if (this.trace.timeline.hoverCursorTimestamp !== undefined) {
+ drawVerticalLineAtTime(
+ ctx,
+ timescale,
+ this.trace.timeline.hoverCursorTimestamp,
+ size.height,
+ `#344596`,
+ );
+ }
+ }
+
+ private drawHoveredNoteVertical(
+ ctx: CanvasRenderingContext2D,
+ timescale: TimeScale,
+ size: Size2D,
+ ) {
+ if (this.trace.timeline.hoveredNoteTimestamp !== undefined) {
+ drawVerticalLineAtTime(
+ ctx,
+ timescale,
+ this.trace.timeline.hoveredNoteTimestamp,
+ size.height,
+ `#aaa`,
+ );
+ }
+ }
+
+ private drawWakeupVertical(
+ ctx: CanvasRenderingContext2D,
+ timescale: TimeScale,
+ size: Size2D,
+ ) {
+ const selection = this.trace.selection.selection;
+ if (selection.kind === 'track_event' && selection.wakeupTs) {
+ drawVerticalLineAtTime(
+ ctx,
+ timescale,
+ selection.wakeupTs,
+ size.height,
+ `black`,
+ );
+ }
+ }
+
+ private drawNoteVerticals(
+ ctx: CanvasRenderingContext2D,
+ timescale: TimeScale,
+ size: Size2D,
+ ) {
+ // All marked areas should have semi-transparent vertical lines
+ // marking the start and end.
+ for (const note of this.trace.notes.notes.values()) {
+ if (note.noteType === 'SPAN') {
+ const transparentNoteColor =
+ 'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)';
+ drawVerticalLineAtTime(
+ ctx,
+ timescale,
+ note.start,
+ size.height,
+ transparentNoteColor,
+ 1,
+ );
+ drawVerticalLineAtTime(
+ ctx,
+ timescale,
+ note.end,
+ size.height,
+ transparentNoteColor,
+ 1,
+ );
+ } else if (note.noteType === 'DEFAULT') {
+ drawVerticalLineAtTime(
+ ctx,
+ timescale,
+ note.timestamp,
+ size.height,
+ note.color,
+ );
+ }
+ }
+ }
+}
+
+/**
+ * Returns a list of track nodes that are contained within a given set of
+ * vertical bounds.
+ *
+ * @param renderedTracks - The list of tracks and their positions.
+ * @param bounds - The bounds in which to check.
+ * @returns - A list of tracks.
+ */
+function findTracksInRect(
+ renderedTracks: ReadonlyArray<TrackView>,
+ bounds: VerticalBounds,
+ recurseCollapsedSummaryTracks = false,
+): TrackNode[] {
+ const tracks: TrackNode[] = [];
+ for (const {node, verticalBounds} of renderedTracks) {
+ const trackRect = new Rect2D({...verticalBounds, left: 0, right: 1});
+ if (trackRect.overlaps({...bounds, left: 0, right: 1})) {
+ // Recurse all child tracks if group node is collapsed and is a summary
+ if (recurseCollapsedSummaryTracks && node.isSummary && node.collapsed) {
+ for (const childTrack of node.flatTracks) {
+ tracks.push(childTrack);
+ }
+ } else {
+ tracks.push(node);
+ }
+ }
+ }
+ return tracks;
+}
+
+// Stores an in-progress area selection.
+class InProgressAreaSelection {
+ currentTime: HighPrecisionTime;
+ currentY: number;
+
+ constructor(
+ readonly startTime: HighPrecisionTime,
+ readonly startY: number,
+ ) {
+ this.currentTime = startTime;
+ this.currentY = startY;
+ }
+
+ update(e: DragEvent, timescale: TimeScale) {
+ this.currentTime = timescale.pxToHpTime(e.dragCurrent.x);
+ this.currentY = e.dragCurrent.y;
+ }
+
+ timeSpan() {
+ return HighPrecisionTimeSpan.fromHpTimes(this.startTime, this.currentTime);
+ }
+
+ rect(timescale: TimeScale) {
+ const horizontal = timescale.hpTimeSpanToPxSpan(this.timeSpan());
+ return Rect2D.fromPoints(
+ {
+ x: horizontal.left,
+ y: this.startY,
+ },
+ {
+ x: horizontal.right,
+ y: this.currentY,
+ },
+ );
+ }
+}
+
+// Stores an in-progress handle drag.
+class InProgressHandleDrag {
+ currentTime: HighPrecisionTime;
+
+ constructor(readonly startTime: HighPrecisionTime) {
+ this.currentTime = startTime;
+ }
+
+ timeSpan() {
+ return HighPrecisionTimeSpan.fromHpTimes(this.startTime, this.currentTime);
+ }
+
+ hBounds(timescale: TimeScale): HorizontalBounds {
+ const horizontal = timescale.hpTimeSpanToPxSpan(this.timeSpan());
+ return new Rect2D({
+ ...horizontal,
+ top: 0,
+ bottom: 0,
+ });
+ }
+}
diff --git a/ui/src/frontend/viewer_page/track_view.ts b/ui/src/frontend/viewer_page/track_view.ts
new file mode 100644
index 0000000..39d4918
--- /dev/null
+++ b/ui/src/frontend/viewer_page/track_view.ts
@@ -0,0 +1,577 @@
+// 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.
+
+/**
+ * This module provides the TrackNodeTree mithril component, which is
+ * responsible for rendering out a tree of tracks and drawing their content
+ * onto the canvas.
+ * - Rendering track panels and handling nested and sticky headers.
+ * - Managing the virtual canvas & drawing the grid-lines, tracks and overlays
+ * onto the canvas.
+ * - Handling track interaction events such as dragging, panning and scrolling.
+ */
+
+import m from 'mithril';
+import {canvasClip, canvasSave} from '../../base/canvas_utils';
+import {classNames} from '../../base/classnames';
+import {Bounds2D, Rect2D, Size2D, VerticalBounds} from '../../base/geom';
+import {HighPrecisionTimeSpan} from '../../base/high_precision_time_span';
+import {Icons} from '../../base/semantic_icons';
+import {TimeScale} from '../../base/time_scale';
+import {RequiredField} from '../../base/utils';
+import {PerfStats, runningStatStr} from '../../core/perf_stats';
+import {raf} from '../../core/raf_scheduler';
+import {TraceImpl} from '../../core/trace_impl';
+import {TrackRenderer} from '../../core/track_manager';
+import {Track, TrackDescriptor} from '../../public/track';
+import {TrackNode, Workspace} from '../../public/workspace';
+import {Button} from '../../widgets/button';
+import {MenuDivider, MenuItem, PopupMenu2} from '../../widgets/menu';
+import {TrackShell} from '../../widgets/track_shell';
+import {Tree, TreeNode} from '../../widgets/tree';
+import {SELECTION_FILL_COLOR} from '../css_constants';
+import {calculateResolution} from './resolution';
+
+const TRACK_HEIGHT_MIN_PX = 18;
+const TRACK_HEIGHT_DEFAULT_PX = 30;
+
+function getTrackHeight(node: TrackNode, track?: Track) {
+ // Headless tracks have an effective height of 0.
+ if (node.headless) return 0;
+
+ // Expanded summary tracks don't show any data, so make them a little more
+ // compact to save space.
+ if (node.isSummary && node.expanded) return TRACK_HEIGHT_DEFAULT_PX;
+
+ const trackHeight = track?.getHeight();
+ if (trackHeight === undefined) return TRACK_HEIGHT_DEFAULT_PX;
+
+ // Limit the minimum height of a track, and also round up to the nearest
+ // integer, as sub-integer DOM alignment can cause issues e.g. with sticky
+ // positioning.
+ return Math.ceil(Math.max(trackHeight, TRACK_HEIGHT_MIN_PX));
+}
+
+export interface TrackViewAttrs {
+ // Render a lighter version of this track view (for when tracks are offscreen).
+ readonly lite: boolean;
+ readonly scrollToOnCreate?: boolean;
+ readonly reorderable?: boolean;
+ readonly depth: number;
+ readonly stickyTop: number;
+}
+
+/**
+ * The `TrackView` class is responsible for managing and rendering individual
+ * tracks in the `TrackTreeView` Mithril component. It handles operations such
+ * as:
+ *
+ * - Rendering track content in the DOM and virtual canvas.
+ * - Managing user interactions like dragging, panning, scrolling, and area
+ * selection.
+ * - Tracking and displaying rendering performance metrics.
+ */
+export class TrackView {
+ readonly node: TrackNode;
+ readonly renderer?: TrackRenderer;
+ readonly height: number;
+ readonly verticalBounds: VerticalBounds;
+
+ private readonly trace: TraceImpl;
+ private readonly descriptor?: TrackDescriptor;
+
+ constructor(trace: TraceImpl, node: TrackNode, top: number) {
+ this.trace = trace;
+ this.node = node;
+
+ if (node.uri) {
+ this.descriptor = trace.tracks.getTrack(node.uri);
+ this.renderer = this.trace.tracks.getTrackRenderer(node.uri);
+ }
+
+ const heightPx = getTrackHeight(node, this.renderer?.track);
+ this.height = heightPx;
+ this.verticalBounds = {top, bottom: top + heightPx};
+ }
+
+ renderDOM(attrs: TrackViewAttrs, children: m.Children) {
+ const {scrollToOnCreate, reorderable = false} = attrs;
+ const {node, renderer, height} = this;
+
+ const buttons = attrs.lite
+ ? []
+ : [
+ renderer?.track.getTrackShellButtons?.(),
+ node.removable && this.renderCloseButton(),
+ // We don't want summary tracks to be pinned as they rarely have
+ // useful information.
+ !node.isSummary && this.renderPinButton(),
+ this.renderTrackMenuButton(),
+ this.renderAreaSelectionCheckbox(),
+ ];
+
+ let scrollIntoView = false;
+ const tracks = this.trace.tracks;
+ if (tracks.scrollToTrackNodeId === node.id) {
+ tracks.scrollToTrackNodeId = undefined;
+ scrollIntoView = true;
+ }
+
+ return m(
+ TrackShell,
+ {
+ id: node.id,
+ title: node.title,
+ subtitle: renderer?.desc.subtitle,
+ ref: node.fullPath.join('/'),
+ heightPx: height,
+ error: renderer?.getError(),
+ chips: renderer?.desc.chips,
+ buttons,
+ scrollToOnCreate: scrollToOnCreate || scrollIntoView,
+ collapsible: node.hasChildren,
+ collapsed: node.collapsed,
+ highlight: this.isHighlighted(),
+ summary: node.isSummary,
+ reorderable,
+ depth: attrs.depth,
+ stickyTop: attrs.stickyTop,
+ pluginId: renderer?.desc.pluginId,
+ lite: attrs.lite,
+ onToggleCollapsed: () => {
+ node.hasChildren && node.toggleCollapsed();
+ },
+ onTrackContentMouseMove: (pos, bounds) => {
+ const timescale = this.getTimescaleForBounds(bounds);
+ renderer?.track.onMouseMove?.({
+ ...pos,
+ timescale,
+ });
+ raf.scheduleCanvasRedraw();
+ },
+ onTrackContentMouseOut: () => {
+ renderer?.track.onMouseOut?.();
+ raf.scheduleCanvasRedraw();
+ },
+ onTrackContentClick: (pos, bounds) => {
+ const timescale = this.getTimescaleForBounds(bounds);
+ raf.scheduleCanvasRedraw();
+ return (
+ renderer?.track.onMouseClick?.({
+ ...pos,
+ timescale,
+ }) ?? false
+ );
+ },
+ onupdate: () => {
+ renderer?.track.onFullRedraw?.();
+ },
+ onMoveBefore: (nodeId: string) => {
+ const targetNode = node.workspace?.getTrackById(nodeId);
+ if (targetNode !== undefined) {
+ // Insert the target node before this one
+ targetNode.parent?.addChildBefore(targetNode, node);
+ }
+ },
+ onMoveAfter: (nodeId: string) => {
+ const targetNode = node.workspace?.getTrackById(nodeId);
+ if (targetNode !== undefined) {
+ // Insert the target node after this one
+ targetNode.parent?.addChildAfter(targetNode, node);
+ }
+ },
+ },
+ children,
+ );
+ }
+
+ drawCanvas(
+ ctx: CanvasRenderingContext2D,
+ rect: Rect2D,
+ visibleWindow: HighPrecisionTimeSpan,
+ perfStatsEnabled: boolean,
+ trackPerfStats: WeakMap<TrackNode, PerfStats>,
+ ) {
+ // For each track we rendered in view(), render it to the canvas. We know the
+ // vertical bounds, so we just need to combine it with the horizontal bounds
+ // and we're golden.
+ const {node, renderer, verticalBounds} = this;
+
+ if (node.isSummary && node.expanded) return;
+ if (renderer?.getError()) return;
+
+ const trackRect = new Rect2D({
+ ...rect,
+ ...verticalBounds,
+ });
+
+ // Track renderers expect to start rendering at (0, 0), so we need to
+ // translate the canvas and create a new timescale.
+ using _ = canvasSave(ctx);
+ canvasClip(ctx, trackRect);
+ ctx.translate(trackRect.left, trackRect.top);
+
+ const timescale = new TimeScale(visibleWindow, {
+ left: 0,
+ right: trackRect.width,
+ });
+
+ const start = performance.now();
+
+ node.uri &&
+ renderer?.render({
+ trackUri: node.uri,
+ visibleWindow,
+ size: trackRect,
+ resolution: calculateResolution(visibleWindow, trackRect.width),
+ ctx,
+ timescale,
+ });
+
+ this.highlightIfTrackInAreaSelection(ctx, timescale, trackRect);
+
+ const renderTime = performance.now() - start;
+
+ if (!perfStatsEnabled) return;
+ this.updateAndRenderTrackPerfStats(
+ ctx,
+ trackRect,
+ renderTime,
+ trackPerfStats,
+ );
+ }
+
+ private renderCloseButton() {
+ return m(Button, {
+ onclick: () => {
+ this.node.remove();
+ },
+ icon: Icons.Close,
+ title: 'Close track',
+ compact: true,
+ });
+ }
+
+ private renderPinButton(): m.Children {
+ const isPinned = this.node.isPinned;
+ return m(Button, {
+ className: classNames(!isPinned && 'pf-visible-on-hover'),
+ onclick: () => {
+ isPinned ? this.node.unpin() : this.node.pin();
+ },
+ icon: Icons.Pin,
+ iconFilled: isPinned,
+ title: isPinned ? 'Unpin' : 'Pin to top',
+ compact: true,
+ });
+ }
+
+ private renderTrackMenuButton(): m.Children {
+ return m(
+ PopupMenu2,
+ {
+ trigger: m(Button, {
+ className: 'pf-visible-on-hover',
+ icon: 'more_vert',
+ compact: true,
+ title: 'Track options',
+ }),
+ },
+ m(MenuItem, {
+ label: 'Select track',
+ disabled: !this.node.uri,
+ onclick: () => {
+ this.trace.selection.selectTrack(this.node.uri!);
+ },
+ title: this.node.uri
+ ? 'Select track'
+ : 'Track has no URI and cannot be selected',
+ }),
+ m(MenuItem, {label: 'Track details'}, this.renderTrackDetails()),
+ m(MenuDivider),
+ m(
+ MenuItem,
+ {label: 'Copy to workspace'},
+ this.trace.workspaces.all.map((ws) =>
+ m(MenuItem, {
+ label: ws.title,
+ onclick: () => this.copyToWorkspace(ws),
+ }),
+ ),
+ m(MenuDivider),
+ m(MenuItem, {
+ label: 'New workspace',
+ onclick: () => this.copyToWorkspace(),
+ }),
+ ),
+ m(
+ MenuItem,
+ {label: 'Take to workspace'},
+ this.trace.workspaces.all.map((ws) =>
+ m(MenuItem, {
+ label: ws.title,
+ onclick: async () => {
+ await this.copyToWorkspace(ws);
+ this.trace.workspaces.switchWorkspace(ws);
+ },
+ }),
+ ),
+ m(MenuDivider),
+ m(MenuItem, {
+ label: 'New workspace',
+ onclick: async () => {
+ const ws = await this.copyToWorkspace();
+ ws && this.trace.workspaces.switchWorkspace(ws);
+ },
+ }),
+ ),
+ );
+ }
+
+ private async copyToWorkspace(ws?: Workspace) {
+ if (!ws) {
+ const name = await this.trace.omnibox.prompt(
+ 'Enter a name for the new workspace...',
+ );
+ if (!name) return;
+ ws = this.trace.workspaces.createEmptyWorkspace(name);
+ }
+ const newNode = this.node.clone();
+ newNode.removable = true;
+ ws.addChildLast(newNode);
+ return ws;
+ }
+
+ private renderTrackDetails(): m.Children {
+ let parent = this.node.parent;
+ let fullPath: m.ChildArray = [this.node.title];
+ while (parent && parent instanceof TrackNode) {
+ fullPath = [parent.title, ' \u2023 ', ...fullPath];
+ parent = parent.parent;
+ }
+
+ return m(
+ '.pf-track__track-details-popup',
+ m(
+ Tree,
+ m(TreeNode, {left: 'Track Node ID', right: this.node.id}),
+ m(TreeNode, {left: 'Collapsed', right: `${this.node.collapsed}`}),
+ m(TreeNode, {left: 'URI', right: this.node.uri}),
+ m(TreeNode, {
+ left: 'Is Summary Track',
+ right: `${this.node.isSummary}`,
+ }),
+ m(TreeNode, {
+ left: 'SortOrder',
+ right: this.node.sortOrder ?? '0 (undefined)',
+ }),
+ m(TreeNode, {left: 'Path', right: fullPath}),
+ m(TreeNode, {left: 'Title', right: this.node.title}),
+ m(TreeNode, {
+ left: 'Workspace',
+ right: this.node.workspace?.title ?? '[no workspace]',
+ }),
+ this.descriptor &&
+ m(TreeNode, {
+ left: 'Plugin ID',
+ right: this.descriptor.pluginId,
+ }),
+ this.descriptor &&
+ m(
+ TreeNode,
+ {left: 'Tags'},
+ this.descriptor.tags &&
+ Object.entries(this.descriptor.tags).map(([key, value]) => {
+ return m(TreeNode, {left: key, right: value?.toString()});
+ }),
+ ),
+ ),
+ );
+ }
+
+ private getTimescaleForBounds(bounds: Bounds2D) {
+ const timeWindow = this.trace.timeline.visibleWindow;
+ return new TimeScale(timeWindow, {
+ left: 0,
+ right: bounds.right - bounds.left,
+ });
+ }
+
+ private isHighlighted() {
+ const {trace, node} = this;
+ // The track should be highlighted if the current search result matches this
+ // track or one of its children.
+ const searchIndex = trace.search.resultIndex;
+ const searchResults = trace.search.searchResults;
+
+ if (searchIndex !== -1 && searchResults !== undefined) {
+ // using _ = autoTimer();
+ const uri = searchResults.trackUris[searchIndex];
+ // Highlight if this or any children match the search results
+ if (uri === node.uri || node.getTrackByUri(uri)) {
+ return true;
+ }
+ }
+
+ const curSelection = trace.selection;
+ if (
+ curSelection.selection.kind === 'track' &&
+ curSelection.selection.trackUri === node.uri
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private renderAreaSelectionCheckbox(): m.Children {
+ const {trace, node} = this;
+ const selectionManager = trace.selection;
+ const selection = selectionManager.selection;
+ if (selection.kind === 'area') {
+ if (node.isSummary) {
+ const tracksWithUris = node.flatTracks.filter(
+ (t) => t.uri !== undefined,
+ ) as ReadonlyArray<RequiredField<TrackNode, 'uri'>>;
+
+ // Check if any nodes within are selected
+ const childTracksInSelection = tracksWithUris.map((t) =>
+ selection.trackUris.includes(t.uri),
+ );
+
+ function renderButton(icon: string, title: string) {
+ return m(Button, {
+ onclick: () => {
+ const uris = tracksWithUris.map((t) => t.uri);
+ selectionManager.toggleGroupAreaSelection(uris);
+ },
+ compact: true,
+ icon,
+ title,
+ });
+ }
+
+ if (childTracksInSelection.every((b) => b)) {
+ return renderButton(
+ Icons.Checkbox,
+ 'Remove child tracks from selection',
+ );
+ } else if (childTracksInSelection.some((b) => b)) {
+ return renderButton(
+ Icons.IndeterminateCheckbox,
+ 'Add remaining child tracks to selection',
+ );
+ } else {
+ return renderButton(
+ Icons.BlankCheckbox,
+ 'Add child tracks to selection',
+ );
+ }
+ } else {
+ const nodeUri = node.uri;
+ if (nodeUri) {
+ return (
+ selection.kind === 'area' &&
+ m(Button, {
+ onclick: () => {
+ selectionManager.toggleTrackAreaSelection(nodeUri);
+ },
+ compact: true,
+ ...(selection.trackUris.includes(nodeUri)
+ ? {icon: Icons.Checkbox, title: 'Remove track'}
+ : {icon: Icons.BlankCheckbox, title: 'Add track to selection'}),
+ })
+ );
+ }
+ }
+ }
+ return undefined;
+ }
+
+ private highlightIfTrackInAreaSelection(
+ ctx: CanvasRenderingContext2D,
+ timescale: TimeScale,
+ size: Size2D,
+ ) {
+ const selection = this.trace.selection.selection;
+
+ if (selection.kind !== 'area') {
+ return;
+ }
+
+ let selected = false;
+ if (this.node.isSummary) {
+ // Summary tracks cannot themselves be area-selected. So, as a visual aid,
+ // if this track is a summary track and some of its children are in the
+ // area selection, highlight this track as if it were in the area
+ // selection too.
+ selected = selection.trackUris.some((uri) =>
+ this.node.getTrackByUri(uri),
+ );
+ } else {
+ // For non-summary tracks, simply highlight this track if it's in the area
+ // selection.
+ if (this.node.uri !== undefined) {
+ selected = selection.trackUris.includes(this.node.uri);
+ }
+ }
+
+ if (selected) {
+ const selectedAreaDuration = selection.end - selection.start;
+ ctx.fillStyle = SELECTION_FILL_COLOR;
+ ctx.fillRect(
+ timescale.timeToPx(selection.start),
+ 0,
+ timescale.durationToPx(selectedAreaDuration),
+ size.height,
+ );
+ }
+ }
+
+ private updateAndRenderTrackPerfStats(
+ ctx: CanvasRenderingContext2D,
+ size: Size2D,
+ renderTime: number,
+ trackPerfStats: WeakMap<TrackNode, PerfStats>,
+ ) {
+ let renderStats = trackPerfStats.get(this.node);
+ if (renderStats === undefined) {
+ renderStats = new PerfStats();
+ trackPerfStats.set(this.node, renderStats);
+ }
+ renderStats.addValue(renderTime);
+
+ // Draw a green box around the whole track
+ ctx.strokeStyle = 'rgba(69, 187, 73, 0.5)';
+ const lineWidth = 1;
+ ctx.lineWidth = lineWidth;
+ ctx.strokeRect(
+ lineWidth / 2,
+ lineWidth / 2,
+ size.width - lineWidth,
+ size.height - lineWidth,
+ );
+
+ const statW = 300;
+ ctx.font = '10px sans-serif';
+ ctx.textAlign = 'start';
+ ctx.textBaseline = 'alphabetic';
+ ctx.direction = 'inherit';
+ ctx.fillStyle = 'hsl(97, 100%, 96%)';
+ ctx.fillRect(size.width - statW, size.height - 20, statW, 20);
+ ctx.fillStyle = 'hsla(122, 77%, 22%)';
+ const statStr = `Track ${this.node.id} | ` + runningStatStr(renderStats);
+ ctx.fillText(statStr, size.width - statW, size.height - 10);
+ }
+}
diff --git a/ui/src/frontend/viewer_page/viewer_page.ts b/ui/src/frontend/viewer_page/viewer_page.ts
index 45750a5..9a4fc70 100644
--- a/ui/src/frontend/viewer_page/viewer_page.ts
+++ b/ui/src/frontend/viewer_page/viewer_page.ts
@@ -12,39 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {hex} from 'color-convert';
import m from 'mithril';
-import {removeFalsyValues} from '../../base/array_utils';
-import {canvasClip, canvasSave} from '../../base/canvas_utils';
-import {findRef, toHTMLElement} from '../../base/dom_utils';
-import {Size2D, VerticalBounds} from '../../base/geom';
-import {assertExists} from '../../base/logging';
-import {clamp} from '../../base/math_utils';
-import {Time, TimeSpan} from '../../base/time';
+import {DisposableStack} from '../../base/disposable_stack';
+import {toHTMLElement} from '../../base/dom_utils';
+import {Rect2D} from '../../base/geom';
import {TimeScale} from '../../base/time_scale';
import {AppImpl} from '../../core/app_impl';
import {featureFlags} from '../../core/feature_flags';
import {PageWithTraceImplAttrs} from '../../core/page_manager';
import {raf} from '../../core/raf_scheduler';
-import {TraceImpl} from '../../core/trace_impl';
-import {TrackNode} from '../../public/workspace';
-import {TRACK_BORDER_COLOR, TRACK_SHELL_WIDTH} from '../css_constants';
-import {renderFlows} from './flow_events_renderer';
-import {generateTicks, getMaxMajorTicks, TickType} from './gridline_helper';
-import {NotesPanel} from './notes_panel';
-import {OverviewTimelinePanel} from './overview_timeline_panel';
-import {PanAndZoomHandler} from './pan_and_zoom_handler';
-import {
- PanelContainer,
- PanelOrGroup,
- RenderedPanelInfo,
-} from './panel_container';
+import {OverviewTimeline} from './overview_timeline_panel';
import {TabPanel} from './tab_panel';
-import {TickmarkPanel} from './tickmark_panel';
-import {TimeAxisPanel} from './time_axis_panel';
-import {TimeSelectionPanel} from './time_selection_panel';
-import {TrackPanel} from './track_panel';
-import {drawVerticalLineAtTime} from './vertical_line_helper';
+import {TimelineHeader} from './timeline_header';
+import {TrackTreeView} from './track_tree_view';
+import {KeyboardNavigationHandler} from './wasd_navigation_handler';
const OVERVIEW_PANEL_FLAG = featureFlags.register({
id: 'overviewVisible',
@@ -53,613 +34,94 @@
defaultValue: true,
});
-// Checks if the mousePos is within 3px of the start or end of the
-// current selected time range.
-function onTimeRangeBoundary(
- trace: TraceImpl,
- timescale: TimeScale,
- mousePos: number,
-): 'START' | 'END' | null {
- const selection = trace.selection.selection;
- if (selection.kind === 'area') {
- // If frontend selectedArea exists then we are in the process of editing the
- // time range and need to use that value instead.
- const area = trace.timeline.selectedArea
- ? trace.timeline.selectedArea
- : selection;
- const start = timescale.timeToPx(area.start);
- const end = timescale.timeToPx(area.end);
- const startDrag = mousePos - TRACK_SHELL_WIDTH;
- const startDistance = Math.abs(start - startDrag);
- const endDistance = Math.abs(end - startDrag);
- const range = 3 * window.devicePixelRatio;
- // We might be within 3px of both boundaries but we should choose
- // the closest one.
- if (startDistance < range && startDistance <= endDistance) return 'START';
- if (endDistance < range && endDistance <= startDistance) return 'END';
- }
- return null;
-}
-
-interface SelectedContainer {
- readonly containerClass: string;
- readonly dragStartAbsY: number;
- readonly dragEndAbsY: number;
-}
-
-/**
- * Top-most level component for the viewer page. Holds tracks, brush timeline,
- * panels, and everything else that's part of the main trace viewer page.
- */
export class ViewerPage implements m.ClassComponent<PageWithTraceImplAttrs> {
- private zoomContent?: PanAndZoomHandler;
- // Used to prevent global deselection if a pan/drag select occurred.
- private keepCurrentSelection = false;
+ private readonly trash = new DisposableStack();
+ private timelineBounds?: Rect2D;
- private overviewTimelinePanel: OverviewTimelinePanel;
- private timeAxisPanel: TimeAxisPanel;
- private timeSelectionPanel: TimeSelectionPanel;
- private notesPanel: NotesPanel;
- private tickmarkPanel: TickmarkPanel;
- private timelineWidthPx?: number;
- private selectedContainer?: SelectedContainer;
- private showPanningHint = false;
+ view({attrs}: m.CVnode<PageWithTraceImplAttrs>) {
+ const {trace} = attrs;
- private readonly PAN_ZOOM_CONTENT_REF = 'pan-and-zoom-content';
-
- constructor(vnode: m.CVnode<PageWithTraceImplAttrs>) {
- this.notesPanel = new NotesPanel(vnode.attrs.trace);
- this.timeAxisPanel = new TimeAxisPanel(vnode.attrs.trace);
- this.timeSelectionPanel = new TimeSelectionPanel(vnode.attrs.trace);
- this.tickmarkPanel = new TickmarkPanel(vnode.attrs.trace);
- this.overviewTimelinePanel = new OverviewTimelinePanel(vnode.attrs.trace);
- this.notesPanel = new NotesPanel(vnode.attrs.trace);
- this.timeSelectionPanel = new TimeSelectionPanel(vnode.attrs.trace);
+ return m(
+ '.pf-viewer-page.page',
+ m(
+ TabPanel,
+ {trace},
+ OVERVIEW_PANEL_FLAG.get() &&
+ m(OverviewTimeline, {
+ trace,
+ className: 'pf-viewer-page__overview',
+ }),
+ m(TimelineHeader, {
+ trace,
+ className: 'pf-viewer-page__header',
+ // There are three independent canvases on this page which we could
+ // use keep track of the timeline width, but we use the header one
+ // because it's always rendered.
+ onTimelineBoundsChange: (rect) => (this.timelineBounds = rect),
+ }),
+ // Hide tracks while the trace is loading to prevent thrashing.
+ !AppImpl.instance.isLoadingTrace && [
+ // Don't render pinned tracks if we have none.
+ trace.workspace.pinnedTracks.length > 0 &&
+ m(TrackTreeView, {
+ trace,
+ className: 'pf-viewer-page__pinned-track-tree',
+ rootNode: trace.workspace.pinnedTracksNode,
+ reorderable: true,
+ scrollToNewTracks: true,
+ }),
+ m(TrackTreeView, {
+ trace,
+ className: 'pf-viewer-page__scrolling-track-tree',
+ rootNode: trace.workspace.tracks,
+ }),
+ ],
+ ),
+ );
}
- oncreate({dom, attrs}: m.CVnodeDOM<PageWithTraceImplAttrs>) {
- const panZoomElRaw = findRef(dom, this.PAN_ZOOM_CONTENT_REF);
- const panZoomEl = toHTMLElement(assertExists(panZoomElRaw));
+ oncreate(vnode: m.VnodeDOM<PageWithTraceImplAttrs>) {
+ const {attrs, dom} = vnode;
- const {top: panTop} = panZoomEl.getBoundingClientRect();
- this.zoomContent = new PanAndZoomHandler({
- element: panZoomEl,
+ // Handles WASD keybindings to pan & zoom
+ const panZoomHandler = new KeyboardNavigationHandler({
+ element: toHTMLElement(dom),
onPanned: (pannedPx: number) => {
+ if (!this.timelineBounds) return;
const timeline = attrs.trace.timeline;
-
- if (this.timelineWidthPx === undefined) return;
-
- this.keepCurrentSelection = true;
- const timescale = new TimeScale(timeline.visibleWindow, {
- left: 0,
- right: this.timelineWidthPx,
- });
+ const timescale = new TimeScale(
+ timeline.visibleWindow,
+ this.timelineBounds,
+ );
const tDelta = timescale.pxToDuration(pannedPx);
timeline.panVisibleWindow(tDelta);
+ raf.scheduleCanvasRedraw();
},
onZoomed: (zoomedPositionPx: number, zoomRatio: number) => {
+ if (!this.timelineBounds) return;
const timeline = attrs.trace.timeline;
- // TODO(hjd): Avoid hardcoding TRACK_SHELL_WIDTH.
- // TODO(hjd): Improve support for zooming in overview timeline.
- const zoomPx = zoomedPositionPx - TRACK_SHELL_WIDTH;
- const rect = dom.getBoundingClientRect();
- const centerPoint = zoomPx / (rect.width - TRACK_SHELL_WIDTH);
+ const zoomPx = zoomedPositionPx - this.timelineBounds.left;
+ const centerPoint = zoomPx / this.timelineBounds.width;
timeline.zoomVisibleWindow(1 - zoomRatio, centerPoint);
raf.scheduleCanvasRedraw();
},
- editSelection: (currentPx: number) => {
- if (this.timelineWidthPx === undefined) return false;
- const timescale = new TimeScale(attrs.trace.timeline.visibleWindow, {
- left: 0,
- right: this.timelineWidthPx,
- });
- return onTimeRangeBoundary(attrs.trace, timescale, currentPx) !== null;
- },
- onSelection: (
- dragStartX: number,
- dragStartY: number,
- prevX: number,
- currentX: number,
- currentY: number,
- editing: boolean,
- ) => {
- const traceTime = attrs.trace.traceInfo;
- const timeline = attrs.trace.timeline;
-
- if (this.timelineWidthPx === undefined) return;
-
- // TODO(stevegolton): Don't get the windowSpan from globals, get it from
- // here!
- const {visibleWindow} = timeline;
- const timespan = visibleWindow.toTimeSpan();
- this.keepCurrentSelection = true;
-
- const timescale = new TimeScale(timeline.visibleWindow, {
- left: 0,
- right: this.timelineWidthPx,
- });
-
- if (editing) {
- const selection = attrs.trace.selection.selection;
- if (selection.kind === 'area') {
- const area = attrs.trace.timeline.selectedArea
- ? attrs.trace.timeline.selectedArea
- : selection;
- let newTime = timescale
- .pxToHpTime(currentX - TRACK_SHELL_WIDTH)
- .toTime();
- // Have to check again for when one boundary crosses over the other.
- const curBoundary = onTimeRangeBoundary(
- attrs.trace,
- timescale,
- prevX,
- );
- if (curBoundary == null) return;
- const keepTime = curBoundary === 'START' ? area.end : area.start;
- // Don't drag selection outside of current screen.
- if (newTime < keepTime) {
- newTime = Time.max(newTime, timespan.start);
- } else {
- newTime = Time.min(newTime, timespan.end);
- }
- // When editing the time range we always use the saved tracks,
- // since these will not change.
- timeline.selectArea(
- Time.max(Time.min(keepTime, newTime), traceTime.start),
- Time.min(Time.max(keepTime, newTime), traceTime.end),
- selection.trackUris,
- );
- }
- } else {
- let startPx = Math.min(dragStartX, currentX) - TRACK_SHELL_WIDTH;
- let endPx = Math.max(dragStartX, currentX) - TRACK_SHELL_WIDTH;
- if (startPx < 0 && endPx < 0) return;
- startPx = clamp(startPx, 0, this.timelineWidthPx);
- endPx = clamp(endPx, 0, this.timelineWidthPx);
- timeline.selectArea(
- timescale.pxToHpTime(startPx).toTime('floor'),
- timescale.pxToHpTime(endPx).toTime('ceil'),
- );
-
- const absStartY = dragStartY + panTop;
- const absCurrentY = currentY + panTop;
- if (this.selectedContainer === undefined) {
- for (const c of dom.querySelectorAll('.pf-panel-container')) {
- const {top, bottom} = c.getBoundingClientRect();
- if (top <= absStartY && absCurrentY <= bottom) {
- const stack = assertExists(c.querySelector('.pf-panel-stack'));
- const stackTop = stack.getBoundingClientRect().top;
- this.selectedContainer = {
- containerClass: Array.from(c.classList).filter(
- (x) => x !== 'pf-panel-container',
- )[0],
- dragStartAbsY: -stackTop + absStartY,
- dragEndAbsY: -stackTop + absCurrentY,
- };
- break;
- }
- }
- } else {
- const c = assertExists(
- dom.querySelector(`.${this.selectedContainer.containerClass}`),
- );
- const {top, bottom} = c.getBoundingClientRect();
- const boundedCurrentY = Math.min(
- Math.max(top, absCurrentY),
- bottom,
- );
- const stack = assertExists(c.querySelector('.pf-panel-stack'));
- const stackTop = stack.getBoundingClientRect().top;
- this.selectedContainer = {
- ...this.selectedContainer,
- dragEndAbsY: -stackTop + boundedCurrentY,
- };
- }
- this.showPanningHint = true;
- }
- raf.scheduleCanvasRedraw();
- },
- endSelection: (edit: boolean) => {
- this.selectedContainer = undefined;
- const area = attrs.trace.timeline.selectedArea;
- // If we are editing we need to pass the current id through to ensure
- // the marked area with that id is also updated.
- if (edit) {
- const selection = attrs.trace.selection.selection;
- if (selection.kind === 'area' && area) {
- attrs.trace.selection.selectArea({...area});
- }
- } else if (area) {
- attrs.trace.selection.selectArea({...area});
- }
- // Now the selection has ended we stored the final selected area in the
- // global state and can remove the in progress selection from the
- // timeline.
- attrs.trace.timeline.deselectArea();
- // Full redraw to color track shell.
- raf.scheduleFullRedraw();
- },
});
+ this.trash.use(panZoomHandler);
+ this.onupdate(vnode);
+ }
+
+ onupdate({attrs}: m.VnodeDOM<PageWithTraceImplAttrs>) {
+ // TODO(stevegolton): It's assumed that the TrackStacks will call into
+ // trace.tracks.getTrackRenderer() in their view() functions which will mark
+ // track renderers as used. We call flushOldTracks() here as it's guaranteed
+ // to be called after view() on all child elements, and is only called once
+ // per render cycle. However, this approach involves a bit too much magic.
+ // The TODO is to sort this out and make it so the track flushing is
+ // consolidated into one place.
+ attrs.trace.tracks.flushOldTracks();
}
onremove() {
- if (this.zoomContent) this.zoomContent[Symbol.dispose]();
- }
-
- view({attrs}: m.CVnode<PageWithTraceImplAttrs>) {
- const scrollingPanels = renderToplevelPanels(attrs.trace);
-
- const result = m(
- '.page.viewer-page',
- m(
- TabPanel,
- {
- trace: attrs.trace,
- },
- m(
- '.pan-and-zoom-content',
- {
- ref: this.PAN_ZOOM_CONTENT_REF,
- onclick: () => {
- // We don't want to deselect when panning/drag selecting.
- if (this.keepCurrentSelection) {
- this.keepCurrentSelection = false;
- return;
- }
- attrs.trace.selection.clear();
- },
- },
- m(
- '.pf-timeline-header',
- m(PanelContainer, {
- trace: attrs.trace,
- className: 'header-panel-container',
- panels: removeFalsyValues([
- OVERVIEW_PANEL_FLAG.get() && this.overviewTimelinePanel,
- this.timeAxisPanel,
- this.timeSelectionPanel,
- this.notesPanel,
- this.tickmarkPanel,
- ]),
- selectedYRange: this.getYRange('header-panel-container'),
- }),
- m('.scrollbar-spacer-vertical'),
- ),
- m(PanelContainer, {
- trace: attrs.trace,
- className: 'pinned-panel-container',
- panels: AppImpl.instance.isLoadingTrace
- ? []
- : attrs.trace.workspace.pinnedTracks.map((trackNode) => {
- if (trackNode.uri) {
- const tr = attrs.trace.tracks.getTrackRenderer(
- trackNode.uri,
- );
- return new TrackPanel({
- trace: attrs.trace,
- reorderable: true,
- node: trackNode,
- trackRenderer: tr,
- revealOnCreate: true,
- indentationLevel: 0,
- topOffsetPx: 0,
- });
- } else {
- return new TrackPanel({
- trace: attrs.trace,
- node: trackNode,
- revealOnCreate: true,
- indentationLevel: 0,
- topOffsetPx: 0,
- });
- }
- }),
- renderUnderlay: (ctx, size) =>
- renderUnderlay(attrs.trace, ctx, size),
- renderOverlay: (ctx, size, panels) =>
- renderOverlay(
- attrs.trace,
- ctx,
- size,
- panels,
- attrs.trace.workspace.pinnedTracksNode,
- ),
- selectedYRange: this.getYRange('pinned-panel-container'),
- }),
- m(PanelContainer, {
- trace: attrs.trace,
- className: 'scrolling-panel-container',
- panels: AppImpl.instance.isLoadingTrace ? [] : scrollingPanels,
- onPanelStackResize: (width) => {
- const timelineWidth = width - TRACK_SHELL_WIDTH;
- this.timelineWidthPx = timelineWidth;
- },
- renderUnderlay: (ctx, size) =>
- renderUnderlay(attrs.trace, ctx, size),
- renderOverlay: (ctx, size, panels) =>
- renderOverlay(
- attrs.trace,
- ctx,
- size,
- panels,
- attrs.trace.workspace.tracks,
- ),
- selectedYRange: this.getYRange('scrolling-panel-container'),
- }),
- ),
- ),
-
- this.showPanningHint && m(HelpPanningNotification),
- );
-
- attrs.trace.tracks.flushOldTracks();
- return result;
- }
-
- private getYRange(cls: string): VerticalBounds | undefined {
- if (this.selectedContainer?.containerClass !== cls) {
- return undefined;
- }
- const {dragStartAbsY, dragEndAbsY} = this.selectedContainer;
- return {
- top: Math.min(dragStartAbsY, dragEndAbsY),
- bottom: Math.max(dragStartAbsY, dragEndAbsY),
- };
- }
-}
-
-function renderUnderlay(
- trace: TraceImpl,
- ctx: CanvasRenderingContext2D,
- canvasSize: Size2D,
-): void {
- const size = {
- width: canvasSize.width - TRACK_SHELL_WIDTH,
- height: canvasSize.height,
- };
-
- using _ = canvasSave(ctx);
- ctx.translate(TRACK_SHELL_WIDTH, 0);
-
- const timewindow = trace.timeline.visibleWindow;
- const timescale = new TimeScale(timewindow, {left: 0, right: size.width});
-
- // Just render the gridlines - these should appear underneath all tracks
- drawGridLines(trace, ctx, timewindow.toTimeSpan(), timescale, size);
-}
-
-function renderOverlay(
- trace: TraceImpl,
- ctx: CanvasRenderingContext2D,
- canvasSize: Size2D,
- panels: ReadonlyArray<RenderedPanelInfo>,
- trackContainer: TrackNode,
-): void {
- const size = {
- width: canvasSize.width - TRACK_SHELL_WIDTH,
- height: canvasSize.height,
- };
-
- using _ = canvasSave(ctx);
- ctx.translate(TRACK_SHELL_WIDTH, 0);
- canvasClip(ctx, 0, 0, size.width, size.height);
-
- // TODO(primiano): plumb the TraceImpl obj throughout the viwer page.
- renderFlows(trace, ctx, size, panels, trackContainer);
-
- const timewindow = trace.timeline.visibleWindow;
- const timescale = new TimeScale(timewindow, {left: 0, right: size.width});
-
- renderHoveredNoteVertical(trace, ctx, timescale, size);
- renderHoveredCursorVertical(trace, ctx, timescale, size);
- renderWakeupVertical(trace, ctx, timescale, size);
- renderNoteVerticals(trace, ctx, timescale, size);
-}
-
-// Render the toplevel "scrolling" tracks and track groups
-function renderToplevelPanels(trace: TraceImpl): PanelOrGroup[] {
- return renderNodes(trace, trace.workspace.children, 0, 0);
-}
-
-// Given a list of tracks and a filter term, return a list pf panels filtered by
-// the filter term
-function renderNodes(
- trace: TraceImpl,
- nodes: ReadonlyArray<TrackNode>,
- indent: number,
- topOffsetPx: number,
-): PanelOrGroup[] {
- return nodes.flatMap((node) => {
- if (node.headless) {
- // Render children as if this node doesn't exist
- return renderNodes(trace, node.children, indent, topOffsetPx);
- } else if (node.children.length === 0) {
- return renderTrackPanel(trace, node, indent, topOffsetPx);
- } else {
- const headerPanel = renderTrackPanel(trace, node, indent, topOffsetPx);
- const isSticky = node.isSummary;
- const nextTopOffsetPx = isSticky
- ? topOffsetPx + headerPanel.heightPx
- : topOffsetPx;
- return {
- kind: 'group',
- collapsed: node.collapsed,
- header: headerPanel,
- sticky: isSticky, // && node.collapsed??
- topOffsetPx,
- childPanels: node.collapsed
- ? []
- : renderNodes(trace, node.children, indent + 1, nextTopOffsetPx),
- };
- }
- });
-}
-
-function renderTrackPanel(
- trace: TraceImpl,
- trackNode: TrackNode,
- indent: number,
- topOffsetPx: number,
-) {
- let tr = undefined;
- if (trackNode.uri) {
- tr = trace.tracks.getTrackRenderer(trackNode.uri);
- }
- return new TrackPanel({
- trace,
- node: trackNode,
- trackRenderer: tr,
- indentationLevel: indent,
- topOffsetPx,
- });
-}
-
-export function drawGridLines(
- trace: TraceImpl,
- ctx: CanvasRenderingContext2D,
- timespan: TimeSpan,
- timescale: TimeScale,
- size: Size2D,
-): void {
- ctx.strokeStyle = TRACK_BORDER_COLOR;
- ctx.lineWidth = 1;
-
- if (size.width > 0 && timespan.duration > 0n) {
- const maxMajorTicks = getMaxMajorTicks(size.width);
- const offset = trace.timeline.timestampOffset();
- for (const {type, time} of generateTicks(timespan, maxMajorTicks, offset)) {
- const px = Math.floor(timescale.timeToPx(time));
- if (type === TickType.MAJOR) {
- ctx.beginPath();
- ctx.moveTo(px + 0.5, 0);
- ctx.lineTo(px + 0.5, size.height);
- ctx.stroke();
- }
- }
- }
-}
-
-export function renderHoveredCursorVertical(
- trace: TraceImpl,
- ctx: CanvasRenderingContext2D,
- timescale: TimeScale,
- size: Size2D,
-) {
- if (trace.timeline.hoverCursorTimestamp !== undefined) {
- drawVerticalLineAtTime(
- ctx,
- timescale,
- trace.timeline.hoverCursorTimestamp,
- size.height,
- `#344596`,
- );
- }
-}
-
-export function renderHoveredNoteVertical(
- trace: TraceImpl,
- ctx: CanvasRenderingContext2D,
- timescale: TimeScale,
- size: Size2D,
-) {
- if (trace.timeline.hoveredNoteTimestamp !== undefined) {
- drawVerticalLineAtTime(
- ctx,
- timescale,
- trace.timeline.hoveredNoteTimestamp,
- size.height,
- `#aaa`,
- );
- }
-}
-
-export function renderWakeupVertical(
- trace: TraceImpl,
- ctx: CanvasRenderingContext2D,
- timescale: TimeScale,
- size: Size2D,
-) {
- const selection = trace.selection.selection;
- if (selection.kind === 'track_event' && selection.wakeupTs) {
- drawVerticalLineAtTime(
- ctx,
- timescale,
- selection.wakeupTs,
- size.height,
- `black`,
- );
- }
-}
-
-export function renderNoteVerticals(
- trace: TraceImpl,
- ctx: CanvasRenderingContext2D,
- timescale: TimeScale,
- size: Size2D,
-) {
- // All marked areas should have semi-transparent vertical lines
- // marking the start and end.
- for (const note of trace.notes.notes.values()) {
- if (note.noteType === 'SPAN') {
- const transparentNoteColor =
- 'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)';
- drawVerticalLineAtTime(
- ctx,
- timescale,
- note.start,
- size.height,
- transparentNoteColor,
- 1,
- );
- drawVerticalLineAtTime(
- ctx,
- timescale,
- note.end,
- size.height,
- transparentNoteColor,
- 1,
- );
- } else if (note.noteType === 'DEFAULT') {
- drawVerticalLineAtTime(
- ctx,
- timescale,
- note.timestamp,
- size.height,
- note.color,
- );
- }
- }
-}
-
-class HelpPanningNotification implements m.ClassComponent {
- private readonly PANNING_HINT_KEY = 'dismissedPanningHint';
- private dismissed = localStorage.getItem(this.PANNING_HINT_KEY) === 'true';
-
- view() {
- // Do not show the help notification in embedded mode because local storage
- // does not persist for iFrames. The host is responsible for communicating
- // to users that they can press '?' for help.
- if (AppImpl.instance.embeddedMode || this.dismissed) {
- return;
- }
- return m(
- '.helpful-hint',
- m(
- '.hint-text',
- 'Are you trying to pan? Use the WASD keys or hold shift to click ' +
- "and drag. Press '?' for more help.",
- ),
- m(
- 'button.hint-dismiss-button',
- {
- onclick: () => {
- this.dismissed = true;
- localStorage.setItem(this.PANNING_HINT_KEY, 'true');
- raf.scheduleFullRedraw();
- },
- },
- 'Dismiss',
- ),
- );
+ this.trash.dispose();
}
}
diff --git a/ui/src/frontend/viewer_page/pan_and_zoom_handler.ts b/ui/src/frontend/viewer_page/wasd_navigation_handler.ts
similarity index 65%
rename from ui/src/frontend/viewer_page/pan_and_zoom_handler.ts
rename to ui/src/frontend/viewer_page/wasd_navigation_handler.ts
index 758e124..b43074c 100644
--- a/ui/src/frontend/viewer_page/pan_and_zoom_handler.ts
+++ b/ui/src/frontend/viewer_page/wasd_navigation_handler.ts
@@ -14,8 +14,6 @@
import {DisposableStack} from '../../base/disposable_stack';
import {currentTargetOffset, elementIsEditable} from '../../base/dom_utils';
-import {DragGestureHandler} from '../../base/drag_gesture_handler';
-import {raf} from '../../core/raf_scheduler';
import {Animation} from '../animation';
// When first starting to pan or zoom, move at least this many units.
@@ -39,14 +37,6 @@
const ZOOM_RATIO_PER_FRAME = 0.008;
const KEYBOARD_PAN_PX_PER_FRAME = 8;
-// Scroll wheel animation steps.
-const HORIZONTAL_WHEEL_PAN_SPEED = 1;
-const WHEEL_ZOOM_SPEED = -0.02;
-
-const EDITING_RANGE_CURSOR = 'ew-resize';
-const DRAG_CURSOR = 'default';
-const PAN_CURSOR = 'move';
-
// Use key mapping based on the 'KeyboardEvent.code' property vs the
// 'KeyboardEvent.key', because the former corresponds to the physical key
// position rather than the glyph printed on top of it, and is unaffected by
@@ -88,15 +78,13 @@
}
/**
- * Enables horizontal pan and zoom with mouse-based drag and WASD navigation.
+ * Enables horizontal pan and zoom with WASD navigation.
*/
-export class PanAndZoomHandler implements Disposable {
+export class KeyboardNavigationHandler implements Disposable {
private mousePositionX: number | null = null;
private boundOnMouseMove = this.onMouseMove.bind(this);
- private boundOnWheel = this.onWheel.bind(this);
private boundOnKeyDown = this.onKeyDown.bind(this);
private boundOnKeyUp = this.onKeyUp.bind(this);
- private shiftDown = false;
private panning: Pan = Pan.None;
private panOffsetPx = 0;
private targetPanOffsetPx = 0;
@@ -109,96 +97,30 @@
private element: HTMLElement;
private onPanned: (movedPx: number) => void;
private onZoomed: (zoomPositionPx: number, zoomRatio: number) => void;
- private editSelection: (currentPx: number) => boolean;
- private onSelection: (
- dragStartX: number,
- dragStartY: number,
- prevX: number,
- currentX: number,
- currentY: number,
- editing: boolean,
- ) => void;
- private endSelection: (edit: boolean) => void;
private trash: DisposableStack;
constructor({
element,
onPanned,
onZoomed,
- editSelection,
- onSelection,
- endSelection,
}: {
element: HTMLElement;
onPanned: (movedPx: number) => void;
onZoomed: (zoomPositionPx: number, zoomRatio: number) => void;
- editSelection: (currentPx: number) => boolean;
- onSelection: (
- dragStartX: number,
- dragStartY: number,
- prevX: number,
- currentX: number,
- currentY: number,
- editing: boolean,
- ) => void;
- endSelection: (edit: boolean) => void;
}) {
this.element = element;
this.onPanned = onPanned;
this.onZoomed = onZoomed;
- this.editSelection = editSelection;
- this.onSelection = onSelection;
- this.endSelection = endSelection;
this.trash = new DisposableStack();
document.body.addEventListener('keydown', this.boundOnKeyDown);
document.body.addEventListener('keyup', this.boundOnKeyUp);
this.element.addEventListener('mousemove', this.boundOnMouseMove);
- this.element.addEventListener('wheel', this.boundOnWheel, {passive: true});
this.trash.defer(() => {
- this.element.removeEventListener('wheel', this.boundOnWheel);
this.element.removeEventListener('mousemove', this.boundOnMouseMove);
document.body.removeEventListener('keyup', this.boundOnKeyUp);
document.body.removeEventListener('keydown', this.boundOnKeyDown);
});
-
- let prevX = -1;
- let dragStartX = -1;
- let dragStartY = -1;
- let edit = false;
- this.trash.use(
- new DragGestureHandler(
- this.element,
- /* onDrag */ (x, y) => {
- if (this.shiftDown) {
- this.onPanned(prevX - x);
- } else {
- this.onSelection(dragStartX, dragStartY, prevX, x, y, edit);
- }
- prevX = x;
- },
- /* onDragStarted */ (x, y) => {
- prevX = x;
- dragStartX = x;
- dragStartY = y;
- edit = this.editSelection(x);
- // Set the cursor style based on where the cursor is when the drag
- // starts.
- if (edit) {
- this.element.style.cursor = EDITING_RANGE_CURSOR;
- } else if (!this.shiftDown) {
- this.element.style.cursor = DRAG_CURSOR;
- }
- },
- /* onDragFinished */ () => {
- // Reset the cursor now the drag has ended.
- this.element.style.cursor = this.shiftDown ? PAN_CURSOR : DRAG_CURSOR;
- dragStartX = -1;
- dragStartY = -1;
- this.endSelection(edit);
- },
- ),
- );
}
[Symbol.dispose]() {
@@ -242,30 +164,6 @@
private onMouseMove(e: MouseEvent) {
this.mousePositionX = currentTargetOffset(e).x;
-
- // Only change the cursor when hovering, the DragGestureHandler handles
- // changing the cursor during drag events. This avoids the problem of
- // the cursor flickering between styles if you drag fast and get too
- // far from the current time range.
- if (e.buttons === 0) {
- if (this.editSelection(this.mousePositionX)) {
- this.element.style.cursor = EDITING_RANGE_CURSOR;
- } else {
- this.element.style.cursor = this.shiftDown ? PAN_CURSOR : DRAG_CURSOR;
- }
- }
- }
-
- private onWheel(e: WheelEvent) {
- if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
- this.onPanned(e.deltaX * HORIZONTAL_WHEEL_PAN_SPEED);
- 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.scheduleCanvasRedraw();
- }
}
// Due to a bug in chrome, we get onKeyDown events fired where the payload is
@@ -276,8 +174,6 @@
if (e instanceof KeyboardEvent) {
if (elementIsEditable(e.target)) return;
- this.updateShift(e.shiftKey);
-
if (e.ctrlKey || e.metaKey) return;
if (keyToPan(e) !== Pan.None) {
@@ -304,8 +200,6 @@
private onKeyUp(e: Event) {
if (e instanceof KeyboardEvent) {
- this.updateShift(e.shiftKey);
-
if (e.ctrlKey || e.metaKey) return;
if (keyToPan(e) === this.panning) {
@@ -316,15 +210,4 @@
}
}
}
-
- // TODO(hjd): Move this shift handling into the viewer page.
- private updateShift(down: boolean) {
- if (down === this.shiftDown) return;
- this.shiftDown = down;
- if (this.shiftDown) {
- this.element.style.cursor = PAN_CURSOR;
- } else if (this.mousePositionX !== null) {
- this.element.style.cursor = DRAG_CURSOR;
- }
- }
}
diff --git a/ui/src/plugins/com.android.InputEvents/index.ts b/ui/src/plugins/com.android.InputEvents/index.ts
index 180a81c..737bed3 100644
--- a/ui/src/plugins/com.android.InputEvents/index.ts
+++ b/ui/src/plugins/com.android.InputEvents/index.ts
@@ -17,10 +17,11 @@
import {Trace} from '../../public/trace';
import {createQuerySliceTrack} from '../../components/tracks/query_slice_track';
import {TrackNode} from '../../public/workspace';
-import {getOrCreateUserInteractionGroup} from '../../public/standard_groups';
+import StandardGroupsPlugin from '../dev.perfetto.StandardGroups';
export default class implements PerfettoPlugin {
static readonly id = 'com.android.InputEvents';
+ static readonly dependencies = [StandardGroupsPlugin];
async onTraceLoad(ctx: Trace): Promise<void> {
const cnt = await ctx.engine.query(`
@@ -58,7 +59,9 @@
track,
});
const node = new TrackNode({uri, title});
- const group = getOrCreateUserInteractionGroup(ctx.workspace);
+ const group = ctx.plugins
+ .getPlugin(StandardGroupsPlugin)
+ .getOrCreateStandardGroup(ctx.workspace, 'USER_INTERACTION');
group.addChildInOrder(node);
}
}
diff --git a/ui/src/plugins/dev.perfetto.AndroidDmabuf/index.ts b/ui/src/plugins/dev.perfetto.AndroidDmabuf/index.ts
index d81ca13..b6b4b91 100644
--- a/ui/src/plugins/dev.perfetto.AndroidDmabuf/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidDmabuf/index.ts
@@ -18,9 +18,14 @@
} from '../../components/tracks/query_counter_track';
import {PerfettoPlugin} from '../../public/plugin';
import {Trace} from '../../public/trace';
+import {COUNTER_TRACK_KIND, SLICE_TRACK_KIND} from '../../public/track_kinds';
import {TrackNode} from '../../public/workspace';
-import {NUM_NULL} from '../../trace_processor/query_result';
+import {NUM, NUM_NULL, STR} from '../../trace_processor/query_result';
import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
+import StandardGroupsPlugin from '../dev.perfetto.StandardGroups';
+import TraceProcessorTrackPlugin from '../dev.perfetto.TraceProcessorTrack';
+import {TraceProcessorCounterTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_counter_track';
+import {TraceProcessorSliceTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_slice_track';
async function registerAllocsTrack(
ctx: Trace,
@@ -41,7 +46,11 @@
export default class implements PerfettoPlugin {
static readonly id = 'dev.perfetto.AndroidDmabuf';
- static readonly dependencies = [ProcessThreadGroupsPlugin];
+ static readonly dependencies = [
+ ProcessThreadGroupsPlugin,
+ StandardGroupsPlugin,
+ TraceProcessorTrackPlugin,
+ ];
async onTraceLoad(ctx: Trace): Promise<void> {
const e = ctx.engine;
@@ -82,5 +91,73 @@
?.addChildInOrder(new TrackNode({uri, title: 'dmabuf allocs'}));
}
}
+ const memoryGroupFn = () => {
+ return ctx.plugins
+ .getPlugin(StandardGroupsPlugin)
+ .getOrCreateStandardGroup(ctx.workspace, 'MEMORY');
+ };
+ const node = await addGlobalCounter(ctx, memoryGroupFn);
+ await addGlobalAllocs(ctx, () => {
+ return node ?? memoryGroupFn();
+ });
}
}
+
+async function addGlobalCounter(ctx: Trace, parent: () => TrackNode) {
+ const track = await ctx.engine.query(`
+ select id, name
+ from track
+ where type = 'android_dma_heap'
+ `);
+ const it = track.maybeFirstRow({id: NUM, name: STR});
+ if (!it) {
+ return undefined;
+ }
+ const {id, name: title} = it;
+ const uri = `/android_dmabuf_counter`;
+ ctx.tracks.registerTrack({
+ uri,
+ title,
+ tags: {
+ kind: COUNTER_TRACK_KIND,
+ trackIds: [id],
+ },
+ track: new TraceProcessorCounterTrack(ctx, uri, {}, id, title),
+ });
+ const node = new TrackNode({
+ uri,
+ title,
+ });
+ parent().addChildInOrder(node);
+ return node;
+}
+
+async function addGlobalAllocs(ctx: Trace, parent: () => TrackNode) {
+ const track = await ctx.engine.query(`
+ select name, group_concat(id) as trackIds
+ from track
+ where type = 'android_dma_allocations'
+ group by name
+ `);
+ const it = track.maybeFirstRow({trackIds: STR, name: STR});
+ if (!it) {
+ return undefined;
+ }
+ const {trackIds, name: title} = it;
+ const uri = `/android_dmabuf_allocs`;
+ const ids = trackIds.split(',').map((x) => Number(x));
+ ctx.tracks.registerTrack({
+ uri,
+ title,
+ tags: {
+ kind: SLICE_TRACK_KIND,
+ trackIds: ids,
+ },
+ track: new TraceProcessorSliceTrack(ctx, uri, undefined, ids),
+ });
+ const node = new TrackNode({
+ uri,
+ title,
+ });
+ parent().addChildInOrder(node);
+}
diff --git a/ui/src/plugins/dev.perfetto.AndroidLog/logs_panel.ts b/ui/src/plugins/dev.perfetto.AndroidLog/logs_panel.ts
index 63d888a..1cdba09 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLog/logs_panel.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLog/logs_panel.ts
@@ -144,8 +144,6 @@
visibleSpan,
this.pagination,
);
-
- attrs.trace.scheduleFullRedraw();
});
}
@@ -221,7 +219,6 @@
onchange: (e: Event) => {
const selectionValue = (e.target as HTMLSelectElement).value;
attrs.onSelect(Number(selectionValue));
- attrs.trace.scheduleFullRedraw();
},
},
optionComponents,
@@ -243,7 +240,6 @@
// updated with the latest key (onkeyup).
const htmlElement = e.target as HTMLInputElement;
attrs.onChange(htmlElement.value);
- attrs.trace.scheduleFullRedraw();
},
});
}
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts b/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts
deleted file mode 100644
index e1c94d1..0000000
--- a/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts
+++ /dev/null
@@ -1,451 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {removeFalsyValues} from '../../base/array_utils';
-import {TrackNode} from '../../public/workspace';
-import {SLICE_TRACK_KIND} from '../../public/track_kinds';
-import {Trace} from '../../public/trace';
-import {PerfettoPlugin} from '../../public/plugin';
-import {getThreadUriPrefix, getTrackName} from '../../public/utils';
-import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
-import {AsyncSliceTrack} from './async_slice_track';
-import {exists} from '../../base/utils';
-import {assertExists, assertTrue} from '../../base/logging';
-import {SliceSelectionAggregator} from './slice_selection_aggregator';
-import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
-
-export default class implements PerfettoPlugin {
- static readonly id = 'dev.perfetto.AsyncSlices';
- static readonly dependencies = [ProcessThreadGroupsPlugin];
-
- async onTraceLoad(ctx: Trace): Promise<void> {
- const trackIdsToUris = new Map<number, string>();
-
- await this.addGlobalAsyncTracks(ctx, trackIdsToUris);
- await this.addProcessAsyncSliceTracks(ctx, trackIdsToUris);
- await this.addThreadAsyncSliceTracks(ctx, trackIdsToUris);
-
- ctx.selection.registerSqlSelectionResolver({
- sqlTableName: 'slice',
- callback: async (id: number) => {
- // Locate the track for a given id in the slice table
- const result = await ctx.engine.query(`
- select
- track_id as trackId
- from
- slice
- where slice.id = ${id}
- `);
-
- if (result.numRows() === 0) {
- return undefined;
- }
-
- const {trackId} = result.firstRow({
- trackId: NUM,
- });
-
- const trackUri = trackIdsToUris.get(trackId);
- if (!trackUri) {
- return undefined;
- }
-
- return {
- trackUri,
- eventId: id,
- };
- },
- });
-
- ctx.selection.registerAreaSelectionAggregator(
- new SliceSelectionAggregator(),
- );
- }
-
- async addGlobalAsyncTracks(
- ctx: Trace,
- trackIdsToUris: Map<number, string>,
- ): Promise<void> {
- const {engine} = ctx;
- const rawGlobalAsyncTracks = await engine.query(`
- include perfetto module graphs.search;
- include perfetto module viz.summary.tracks;
-
- with global_tracks_grouped as (
- select
- t.parent_id,
- t.name,
- group_concat(t.id) as trackIds,
- count() as trackCount,
- ifnull(min(a.order_id), 0) as order_id
- from track t
- join _slice_track_summary s using (id)
- left join _track_event_tracks_ordered a USING (id)
- where
- s.is_legacy_global
- and (name != 'Suspend/Resume Latency' or name is null)
- group by parent_id, name
- order by parent_id, order_id
- ),
- intermediate_groups as (
- select
- t.name,
- t.id,
- t.parent_id,
- ifnull(a.order_id, 0) as order_id
- from graph_reachable_dfs!(
- (
- select id as source_node_id, parent_id as dest_node_id
- from track
- where parent_id is not null
- ),
- (
- select distinct parent_id as node_id
- from global_tracks_grouped
- where parent_id is not null
- )
- ) g
- join track t on g.node_id = t.id
- left join _track_event_tracks_ordered a USING (id)
- )
- select
- t.name as name,
- t.parent_id as parentId,
- t.trackIds as trackIds,
- t.order_id as orderId,
- __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
- from global_tracks_grouped t
- union all
- select
- t.name as name,
- t.parent_id as parentId,
- cast_string!(t.id) as trackIds,
- t.order_id as orderId,
- NULL as maxDepth
- from intermediate_groups t
- left join _slice_track_summary s using (id)
- where s.id is null
- order by parentId, orderId
- `);
- const it = rawGlobalAsyncTracks.iter({
- name: STR_NULL,
- parentId: NUM_NULL,
- trackIds: STR,
- orderId: NUM,
- maxDepth: NUM_NULL,
- });
-
- // Create a map of track nodes by id
- const trackMap = new Map<
- number,
- {parentId: number | null; trackNode: TrackNode}
- >();
-
- for (; it.valid(); it.next()) {
- const rawName = it.name === null ? undefined : it.name;
- const title = getTrackName({
- name: rawName,
- kind: SLICE_TRACK_KIND,
- });
- const rawTrackIds = it.trackIds;
- const trackIds = rawTrackIds.split(',').map((v) => Number(v));
- const maxDepth = it.maxDepth;
-
- if (maxDepth === null) {
- assertTrue(trackIds.length == 1);
- const trackNode = new TrackNode({title, sortOrder: -25});
- trackMap.set(trackIds[0], {parentId: it.parentId, trackNode});
- } else {
- const uri = `/async_slices_${rawName}_${it.parentId}`;
- ctx.tracks.registerTrack({
- uri,
- title,
- tags: {
- trackIds,
- kind: SLICE_TRACK_KIND,
- scope: 'global',
- },
- track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds),
- });
- const trackNode = new TrackNode({
- uri,
- title,
- sortOrder: it.orderId,
- });
- trackIds.forEach((id) => {
- trackMap.set(id, {parentId: it.parentId, trackNode});
- trackIdsToUris.set(id, uri);
- });
- }
- }
-
- // Attach track nodes to parents / or the workspace if they have no parent
- trackMap.forEach(({parentId, trackNode}) => {
- if (exists(parentId)) {
- const parent = assertExists(trackMap.get(parentId));
- parent.trackNode.addChildInOrder(trackNode);
- } else {
- ctx.workspace.addChildInOrder(trackNode);
- }
- });
- }
-
- async addCpuTracks(
- ctx: Trace,
- trackIdsToUris: Map<number, string>,
- ): Promise<void> {
- const {engine} = ctx;
- const res = await engine.query(`
- include perfetto module viz.summary.tracks;
-
- with global_tracks_grouped as (
- select
- t.name,
- group_concat(t.id) as trackIds,
- count() as trackCount
- from cpu_track t
- join _slice_track_summary using (id)
- group by name
- )
- select
- t.name as name,
- t.trackIds as trackIds,
- __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
- from global_tracks_grouped t
- `);
- const it = res.iter({
- name: STR_NULL,
- trackIds: STR,
- maxDepth: NUM,
- });
-
- for (; it.valid(); it.next()) {
- const rawName = it.name === null ? undefined : it.name;
- const title = getTrackName({
- name: rawName,
- kind: SLICE_TRACK_KIND,
- });
- const rawTrackIds = it.trackIds;
- const trackIds = rawTrackIds.split(',').map((v) => Number(v));
- const maxDepth = it.maxDepth;
-
- const uri = `/cpu_slices_${rawName}`;
- ctx.tracks.registerTrack({
- uri,
- title,
- tags: {
- trackIds,
- kind: SLICE_TRACK_KIND,
- scope: 'global',
- },
- track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds),
- });
- const trackNode = new TrackNode({
- uri,
- title,
- });
- ctx.workspace.addChildInOrder(trackNode);
- trackIds.forEach((id) => {
- trackIdsToUris.set(id, uri);
- });
- }
- }
-
- async addProcessAsyncSliceTracks(
- ctx: Trace,
- trackIdsToUris: Map<number, string>,
- ): Promise<void> {
- const result = await ctx.engine.query(`
- select
- upid,
- t.name as trackName,
- t.track_ids as trackIds,
- process.name as processName,
- process.pid as pid,
- t.parent_id as parentId,
- __max_layout_depth(t.track_count, t.track_ids) as maxDepth
- from _process_track_summary_by_upid_and_parent_id_and_name t
- join process using (upid)
- where t.name is null or t.name not glob "* Timeline"
- `);
-
- const it = result.iter({
- upid: NUM,
- parentId: NUM_NULL,
- trackName: STR_NULL,
- trackIds: STR,
- processName: STR_NULL,
- pid: NUM_NULL,
- maxDepth: NUM,
- });
-
- const trackMap = new Map<
- number,
- {parentId: number | null; upid: number; trackNode: TrackNode}
- >();
-
- for (; it.valid(); it.next()) {
- const upid = it.upid;
- const trackName = it.trackName;
- const rawTrackIds = it.trackIds;
- const trackIds = rawTrackIds.split(',').map((v) => Number(v));
- const processName = it.processName;
- const pid = it.pid;
- const maxDepth = it.maxDepth;
-
- const kind = SLICE_TRACK_KIND;
- const title = getTrackName({
- name: trackName,
- upid,
- pid,
- processName,
- kind,
- });
-
- const uri = `/process_${upid}/async_slices_${rawTrackIds}`;
- ctx.tracks.registerTrack({
- uri,
- title,
- tags: {
- trackIds,
- kind: SLICE_TRACK_KIND,
- scope: 'process',
- upid,
- },
- track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds),
- });
- const track = new TrackNode({uri, title, sortOrder: 30});
- trackIds.forEach((id) => {
- trackMap.set(id, {trackNode: track, parentId: it.parentId, upid});
- trackIdsToUris.set(id, uri);
- });
- }
-
- // Attach track nodes to parents / or the workspace if they have no parent
- trackMap.forEach((t) => {
- const parent = exists(t.parentId) && trackMap.get(t.parentId);
- if (parent !== false && parent !== undefined) {
- parent.trackNode.addChildInOrder(t.trackNode);
- } else {
- const processGroup = ctx.plugins
- .getPlugin(ProcessThreadGroupsPlugin)
- .getGroupForProcess(t.upid);
- processGroup?.addChildInOrder(t.trackNode);
- }
- });
- }
-
- async addThreadAsyncSliceTracks(
- ctx: Trace,
- trackIdsToUris: Map<number, string>,
- ): Promise<void> {
- const result = await ctx.engine.query(`
- include perfetto module viz.summary.slices;
- include perfetto module viz.summary.threads;
- include perfetto module viz.threads;
-
- select
- t.utid,
- t.parent_id as parentId,
- thread.upid,
- t.name as trackName,
- thread.name as threadName,
- thread.tid as tid,
- t.track_ids as trackIds,
- __max_layout_depth(t.track_count, t.track_ids) as maxDepth,
- k.is_main_thread as isMainThread,
- k.is_kernel_thread AS isKernelThread
- from _thread_track_summary_by_utid_and_name t
- join _threads_with_kernel_flag k using(utid)
- join thread using (utid)
- `);
-
- const it = result.iter({
- utid: NUM,
- parentId: NUM_NULL,
- upid: NUM_NULL,
- trackName: STR_NULL,
- trackIds: STR,
- maxDepth: NUM,
- isMainThread: NUM_NULL,
- isKernelThread: NUM,
- threadName: STR_NULL,
- tid: NUM_NULL,
- });
-
- const trackMap = new Map<
- number,
- {parentId: number | null; utid: number; trackNode: TrackNode}
- >();
-
- for (; it.valid(); it.next()) {
- const {
- utid,
- parentId,
- upid,
- trackName,
- isMainThread,
- isKernelThread,
- maxDepth,
- threadName,
- tid,
- } = it;
- const rawTrackIds = it.trackIds;
- const trackIds = rawTrackIds.split(',').map((v) => Number(v));
- const title = getTrackName({
- name: trackName,
- utid,
- tid,
- threadName,
- kind: 'Slices',
- });
-
- const uri = `/${getThreadUriPrefix(upid, utid)}_slice_${rawTrackIds}`;
- ctx.tracks.registerTrack({
- uri,
- title,
- tags: {
- trackIds,
- kind: SLICE_TRACK_KIND,
- scope: 'thread',
- utid,
- upid: upid ?? undefined,
- ...(isKernelThread === 1 && {kernelThread: true}),
- },
- chips: removeFalsyValues([
- isKernelThread === 0 && isMainThread === 1 && 'main thread',
- ]),
- track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds),
- });
- const track = new TrackNode({uri, title, sortOrder: 20});
- trackIds.forEach((id) => {
- trackMap.set(id, {trackNode: track, parentId, utid});
- trackIdsToUris.set(id, uri);
- });
- }
-
- // Attach track nodes to parents / or the workspace if they have no parent
- trackMap.forEach((t) => {
- const parent = exists(t.parentId) && trackMap.get(t.parentId);
- if (parent !== false && parent !== undefined) {
- parent.trackNode.addChildInOrder(t.trackNode);
- } else {
- const group = ctx.plugins
- .getPlugin(ProcessThreadGroupsPlugin)
- .getGroupForThread(t.utid);
- group?.addChildInOrder(t.trackNode);
- }
- });
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.Counter/index.ts b/ui/src/plugins/dev.perfetto.Counter/index.ts
deleted file mode 100644
index 879b139..0000000
--- a/ui/src/plugins/dev.perfetto.Counter/index.ts
+++ /dev/null
@@ -1,417 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {
- NUM_NULL,
- STR_NULL,
- LONG_NULL,
- NUM,
- STR,
-} from '../../trace_processor/query_result';
-import {Trace} from '../../public/trace';
-import {COUNTER_TRACK_KIND} from '../../public/track_kinds';
-import {PerfettoPlugin} from '../../public/plugin';
-import {getThreadUriPrefix, getTrackName} from '../../public/utils';
-import {CounterOptions} from '../../components/tracks/base_counter_track';
-import {TraceProcessorCounterTrack} from './trace_processor_counter_track';
-import {exists} from '../../base/utils';
-import {TrackNode} from '../../public/workspace';
-import {CounterSelectionAggregator} from './counter_selection_aggregator';
-import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
-
-const NETWORK_TRACK_REGEX = new RegExp('^.* (Received|Transmitted)( KB)?$');
-const ENTITY_RESIDENCY_REGEX = new RegExp('^Entity residency:');
-
-type Modes = CounterOptions['yMode'];
-
-// Sets the default 'mode' for counter tracks. If the regex matches
-// then the paired mode is used. Entries are in priority order so the
-// first match wins.
-const COUNTER_REGEX: [RegExp, Modes][] = [
- // Power counters make more sense in rate mode since you're typically
- // interested in the slope of the graph rather than the absolute
- // value.
- [new RegExp('^power..*$'), 'rate'],
- // Same for cumulative PSI stall time counters, e.g., psi.cpu.some.
- [new RegExp('^psi..*$'), 'rate'],
- // Same for network counters.
- [NETWORK_TRACK_REGEX, 'rate'],
- // Entity residency
- [ENTITY_RESIDENCY_REGEX, 'rate'],
-];
-
-function getCounterMode(name: string): Modes | undefined {
- for (const [re, mode] of COUNTER_REGEX) {
- if (name.match(re)) {
- return mode;
- }
- }
- return undefined;
-}
-
-function getDefaultCounterOptions(name: string): Partial<CounterOptions> {
- const options: Partial<CounterOptions> = {};
- options.yMode = getCounterMode(name);
-
- if (name.endsWith('_pct')) {
- options.yOverrideMinimum = 0;
- options.yOverrideMaximum = 100;
- options.unit = '%';
- }
-
- if (name.startsWith('power.')) {
- options.yRangeSharingKey = 'power';
- }
-
- // TODO(stevegolton): We need to rethink how this works for virtual memory.
- // The problem is we can easily have > 10GB virtual memory which dwarfs
- // physical memory making other memory tracks difficult to read.
-
- // if (name.startsWith('mem.')) {
- // options.yRangeSharingKey = 'mem';
- // }
-
- // All 'Entity residency: foo bar1234' tracks should share a y-axis
- // with 'Entity residency: foo baz5678' etc tracks:
- {
- const r = new RegExp('Entity residency: ([^ ]+) ');
- const m = r.exec(name);
- if (m) {
- options.yRangeSharingKey = `entity-residency-${m[1]}`;
- }
- }
-
- {
- const r = new RegExp('GPU .* Frequency');
- const m = r.exec(name);
- if (m) {
- options.yRangeSharingKey = 'gpu-frequency';
- }
- }
-
- return options;
-}
-
-export default class implements PerfettoPlugin {
- static readonly id = 'dev.perfetto.Counter';
- static readonly dependencies = [ProcessThreadGroupsPlugin];
-
- async onTraceLoad(ctx: Trace): Promise<void> {
- await this.addCounterTracks(ctx);
- await this.addGpuFrequencyTracks(ctx);
- await this.addCpuFreqLimitCounterTracks(ctx);
- await this.addCpuTimeCounterTracks(ctx);
- await this.addCpuPerfCounterTracks(ctx);
- await this.addThreadCounterTracks(ctx);
- await this.addProcessCounterTracks(ctx);
-
- ctx.selection.registerAreaSelectionAggregator(
- new CounterSelectionAggregator(),
- );
- }
-
- private async addCounterTracks(ctx: Trace) {
- const result = await ctx.engine.query(`
- select name, id, unit
- from (
- select name, id, unit
- from counter_track
- join _counter_track_summary using (id)
- where is_legacy_global
- union
- select name, id, unit
- from gpu_counter_track
- join _counter_track_summary using (id)
- where name != 'gpufreq'
- )
- order by name
- `);
-
- // Add global or GPU counter tracks that are not bound to any pid/tid.
- const it = result.iter({
- name: STR,
- unit: STR_NULL,
- id: NUM,
- });
-
- for (; it.valid(); it.next()) {
- const trackId = it.id;
- const title = it.name;
- const unit = it.unit ?? undefined;
-
- const uri = `/counter_${trackId}`;
- ctx.tracks.registerTrack({
- uri,
- title,
- tags: {
- kind: COUNTER_TRACK_KIND,
- trackIds: [trackId],
- },
- track: new TraceProcessorCounterTrack(
- ctx,
- uri,
- {
- ...getDefaultCounterOptions(title),
- unit,
- },
- trackId,
- title,
- ),
- });
- const track = new TrackNode({uri, title});
- ctx.workspace.addChildInOrder(track);
- }
- }
-
- async addCpuFreqLimitCounterTracks(ctx: Trace): Promise<void> {
- const cpuFreqLimitCounterTracksSql = `
- select name, id
- from cpu_counter_track
- join _counter_track_summary using (id)
- where name glob "Cpu * Freq Limit"
- order by name asc
- `;
-
- this.addCpuCounterTracks(ctx, cpuFreqLimitCounterTracksSql, 'cpuFreqLimit');
- }
-
- async addCpuTimeCounterTracks(ctx: Trace): Promise<void> {
- const cpuTimeCounterTracksSql = `
- select name, id
- from cpu_counter_track
- join _counter_track_summary using (id)
- where name glob "cpu.times.*"
- order by name asc
- `;
- this.addCpuCounterTracks(ctx, cpuTimeCounterTracksSql, 'cpuTime');
- }
-
- async addCpuPerfCounterTracks(ctx: Trace): Promise<void> {
- // Perf counter tracks are bound to CPUs, follow the scheduling and
- // frequency track naming convention ("Cpu N ...").
- // Note: we might not have a track for a given cpu if no data was seen from
- // it. This might look surprising in the UI, but placeholder tracks are
- // wasteful as there's no way of collapsing global counter tracks at the
- // moment.
- const addCpuPerfCounterTracksSql = `
- select printf("Cpu %u %s", cpu, name) as name, id
- from perf_counter_track as pct
- join _counter_track_summary using (id)
- order by perf_session_id asc, pct.name asc, cpu asc
- `;
- this.addCpuCounterTracks(ctx, addCpuPerfCounterTracksSql, 'cpuPerf');
- }
-
- async addCpuCounterTracks(
- ctx: Trace,
- sql: string,
- scope: string,
- ): Promise<void> {
- const result = await ctx.engine.query(sql);
-
- const it = result.iter({
- name: STR,
- id: NUM,
- });
-
- for (; it.valid(); it.next()) {
- const name = it.name;
- const trackId = it.id;
- const uri = `counter.cpu.${trackId}`;
- ctx.tracks.registerTrack({
- uri,
- title: name,
- tags: {
- kind: COUNTER_TRACK_KIND,
- trackIds: [trackId],
- scope,
- },
- track: new TraceProcessorCounterTrack(
- ctx,
- uri,
- getDefaultCounterOptions(name),
- trackId,
- name,
- ),
- });
- const trackNode = new TrackNode({uri, title: name, sortOrder: -20});
- ctx.workspace.addChildInOrder(trackNode);
- }
- }
-
- async addThreadCounterTracks(ctx: Trace): Promise<void> {
- const result = await ctx.engine.query(`
- select
- thread_counter_track.name as trackName,
- utid,
- upid,
- tid,
- thread.name as threadName,
- thread_counter_track.id as trackId,
- thread.start_ts as startTs,
- thread.end_ts as endTs
- from thread_counter_track
- join _counter_track_summary using (id)
- join thread using(utid)
- where thread_counter_track.name != 'thread_time'
- `);
-
- const it = result.iter({
- startTs: LONG_NULL,
- trackId: NUM,
- endTs: LONG_NULL,
- trackName: STR_NULL,
- utid: NUM,
- upid: NUM_NULL,
- tid: NUM_NULL,
- threadName: STR_NULL,
- });
- for (; it.valid(); it.next()) {
- const utid = it.utid;
- const upid = it.upid;
- const tid = it.tid;
- const trackId = it.trackId;
- const trackName = it.trackName;
- const threadName = it.threadName;
- const kind = COUNTER_TRACK_KIND;
- const name = getTrackName({
- name: trackName,
- utid,
- tid,
- kind,
- threadName,
- threadTrack: true,
- });
- const uri = `${getThreadUriPrefix(upid, utid)}_counter_${trackId}`;
- ctx.tracks.registerTrack({
- uri,
- title: name,
- tags: {
- kind,
- trackIds: [trackId],
- utid,
- upid: upid ?? undefined,
- scope: 'thread',
- },
- track: new TraceProcessorCounterTrack(
- ctx,
- uri,
- getDefaultCounterOptions(name),
- trackId,
- name,
- ),
- });
- const group = ctx.plugins
- .getPlugin(ProcessThreadGroupsPlugin)
- .getGroupForThread(utid);
- const track = new TrackNode({uri, title: name, sortOrder: 30});
- group?.addChildInOrder(track);
- }
- }
-
- async addProcessCounterTracks(ctx: Trace): Promise<void> {
- const result = await ctx.engine.query(`
- select
- process_counter_track.id as trackId,
- process_counter_track.name as trackName,
- upid,
- process.pid,
- process.name as processName
- from process_counter_track
- join _counter_track_summary using (id)
- join process using(upid)
- order by trackName;
- `);
- const it = result.iter({
- trackId: NUM,
- trackName: STR_NULL,
- upid: NUM,
- pid: NUM_NULL,
- processName: STR_NULL,
- });
- for (let i = 0; it.valid(); ++i, it.next()) {
- const trackId = it.trackId;
- const pid = it.pid;
- const trackName = it.trackName;
- const upid = it.upid;
- const processName = it.processName;
- const kind = COUNTER_TRACK_KIND;
- const name = getTrackName({
- name: trackName,
- upid,
- pid,
- kind,
- processName,
- ...(exists(trackName) && {trackName}),
- });
- const uri = `/process_${upid}/counter_${trackId}`;
- ctx.tracks.registerTrack({
- uri,
- title: name,
- tags: {
- kind,
- trackIds: [trackId],
- upid,
- scope: 'process',
- },
- track: new TraceProcessorCounterTrack(
- ctx,
- uri,
- getDefaultCounterOptions(name),
- trackId,
- name,
- ),
- });
- const group = ctx.plugins
- .getPlugin(ProcessThreadGroupsPlugin)
- .getGroupForProcess(upid);
- const track = new TrackNode({uri, title: name, sortOrder: 20});
- group?.addChildInOrder(track);
- }
- }
-
- private async addGpuFrequencyTracks(ctx: Trace) {
- const engine = ctx.engine;
-
- const result = await engine.query(`
- select id, gpu_id as gpuId
- from gpu_counter_track
- join _counter_track_summary using (id)
- where name = 'gpufreq'
- `);
- const it = result.iter({id: NUM, gpuId: NUM});
- for (; it.valid(); it.next()) {
- const uri = `/gpu_frequency_${it.gpuId}`;
- const name = `Gpu ${it.gpuId} Frequency`;
- ctx.tracks.registerTrack({
- uri,
- title: name,
- tags: {
- kind: COUNTER_TRACK_KIND,
- trackIds: [it.id],
- scope: 'gpuFreq',
- },
- track: new TraceProcessorCounterTrack(
- ctx,
- uri,
- getDefaultCounterOptions(name),
- it.id,
- name,
- ),
- });
- const track = new TrackNode({uri, title: name, sortOrder: -20});
- ctx.workspace.addChildInOrder(track);
- }
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.CpuFreq/cpu_freq_track.ts b/ui/src/plugins/dev.perfetto.CpuFreq/cpu_freq_track.ts
index 4880341..9811843 100644
--- a/ui/src/plugins/dev.perfetto.CpuFreq/cpu_freq_track.ts
+++ b/ui/src/plugins/dev.perfetto.CpuFreq/cpu_freq_track.ts
@@ -26,7 +26,11 @@
import {uuidv4Sql} from '../../base/uuid';
import {TrackMouseEvent, TrackRenderContext} from '../../public/track';
import {Point2D} from '../../base/geom';
-import {createView, createVirtualTable} from '../../trace_processor/sql_utils';
+import {
+ createPerfettoTable,
+ createView,
+ createVirtualTable,
+} from '../../trace_processor/sql_utils';
import {AsyncDisposableStack} from '../../base/disposable_stack';
import {Trace} from '../../public/trace';
@@ -68,6 +72,9 @@
async onCreate() {
this.trash = new AsyncDisposableStack();
+ await this.trace.engine.query(`
+ INCLUDE PERFETTO MODULE counters.intervals;
+ `);
if (this.config.idleTrackId === undefined) {
this.trash.use(
await createView(
@@ -75,26 +82,32 @@
`raw_freq_idle_${this.trackUuid}`,
`
select ts, dur, value as freqValue, -1 as idleValue
- from experimental_counter_dur c
- where track_id = ${this.config.freqTrackId}
+ from counter_leading_intervals!((
+ select id, ts, track_id, value
+ from counter
+ where track_id = ${this.config.freqTrackId}
+ ))
`,
),
);
} else {
this.trash.use(
- await createView(
+ await createPerfettoTable(
this.trace.engine,
`raw_freq_${this.trackUuid}`,
`
select ts, dur, value as freqValue
- from experimental_counter_dur c
- where track_id = ${this.config.freqTrackId}
+ from counter_leading_intervals!((
+ select id, ts, track_id, value
+ from counter
+ where track_id = ${this.config.freqTrackId}
+ ))
`,
),
);
this.trash.use(
- await createView(
+ await createPerfettoTable(
this.trace.engine,
`raw_idle_${this.trackUuid}`,
`
@@ -102,8 +115,11 @@
ts,
dur,
iif(value = 4294967295, -1, cast(value as int)) as idleValue
- from experimental_counter_dur c
- where track_id = ${this.config.idleTrackId}
+ from counter_leading_intervals!((
+ select id, ts, track_id, value
+ from counter
+ where track_id = ${this.config.idleTrackId}
+ ))
`,
),
);
diff --git a/ui/src/plugins/dev.perfetto.CpuFreq/index.ts b/ui/src/plugins/dev.perfetto.CpuFreq/index.ts
index bd7b96c..ba9b889 100644
--- a/ui/src/plugins/dev.perfetto.CpuFreq/index.ts
+++ b/ui/src/plugins/dev.perfetto.CpuFreq/index.ts
@@ -37,7 +37,7 @@
from counter c
join cpu_counter_track t on c.track_id = t.id
join _counter_track_summary s on t.id = s.id
- where name = 'cpufreq';
+ where t.type = 'cpu_frequency';
`);
const maxCpuFreq = maxCpuFreqResult.firstRow({freq: NUM}).freq;
@@ -48,14 +48,14 @@
id as cpuFreqId,
(
select id
- from cpu_counter_track
- where name = 'cpuidle'
- and cpu = ${cpu}
+ from cpu_counter_track t
+ where t.type = 'cpu_idle'
+ and t.cpu = ${cpu}
limit 1
) as cpuIdleId
- from cpu_counter_track
+ from cpu_counter_track t
join _counter_track_summary using (id)
- where name = 'cpufreq' and cpu = ${cpu}
+ where t.type = 'cpu_frequency' and t.cpu = ${cpu}
limit 1;
`);
diff --git a/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_by_process_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_by_process_selection_aggregator.ts
index 87d5555..cd70e6d 100644
--- a/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_by_process_selection_aggregator.ts
+++ b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_by_process_selection_aggregator.ts
@@ -12,26 +12,31 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {exists} from '../../base/utils';
import {ColumnDef, Sorting} from '../../public/aggregation';
import {AreaSelection} from '../../public/selection';
import {Engine} from '../../trace_processor/engine';
import {CPU_SLICE_TRACK_KIND} from '../../public/track_kinds';
import {AreaSelectionAggregator} from '../../public/selection';
+import {Dataset} from '../../trace_processor/dataset';
+import {LONG, NUM} from '../../trace_processor/query_result';
export class CpuSliceByProcessSelectionAggregator
implements AreaSelectionAggregator
{
readonly id = 'cpu_by_process_aggregation';
+ readonly trackKind = CPU_SLICE_TRACK_KIND;
+ readonly schema = {
+ dur: LONG,
+ ts: LONG,
+ utid: NUM,
+ } as const;
- async createAggregateView(engine: Engine, area: AreaSelection) {
- const selectedCpus: number[] = [];
- for (const trackInfo of area.tracks) {
- if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
- exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
- }
- }
- if (selectedCpus.length === 0) return false;
+ async createAggregateView(
+ engine: Engine,
+ area: AreaSelection,
+ dataset?: Dataset,
+ ) {
+ if (!dataset) return false;
await engine.query(`
create or replace perfetto table ${this.id} as
@@ -41,14 +46,12 @@
sum(dur) AS total_dur,
sum(dur) / count() as avg_dur,
count() as occurrences
- from sched
+ from (${dataset.query()})
join thread USING (utid)
join process USING (upid)
where
- cpu in (${selectedCpus})
- and ts + dur > ${area.start}
+ ts + dur > ${area.start}
and ts < ${area.end}
- and utid != 0
group by upid
`);
return true;
diff --git a/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_selection_aggregator.ts
index 064ba8a..39c6c16 100644
--- a/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_selection_aggregator.ts
+++ b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_selection_aggregator.ts
@@ -12,24 +12,29 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {exists} from '../../base/utils';
import {ColumnDef, Sorting} from '../../public/aggregation';
import {AreaSelection} from '../../public/selection';
import {CPU_SLICE_TRACK_KIND} from '../../public/track_kinds';
import {Engine} from '../../trace_processor/engine';
import {AreaSelectionAggregator} from '../../public/selection';
+import {LONG, NUM} from '../../trace_processor/query_result';
+import {Dataset} from '../../trace_processor/dataset';
export class CpuSliceSelectionAggregator implements AreaSelectionAggregator {
readonly id = 'cpu_aggregation';
+ readonly trackKind = CPU_SLICE_TRACK_KIND;
+ readonly schema = {
+ dur: LONG,
+ ts: LONG,
+ utid: NUM,
+ } as const;
- async createAggregateView(engine: Engine, area: AreaSelection) {
- const selectedCpus: number[] = [];
- for (const trackInfo of area.tracks) {
- if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
- exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
- }
- }
- if (selectedCpus.length === 0) return false;
+ async createAggregateView(
+ engine: Engine,
+ area: AreaSelection,
+ dataset?: Dataset,
+ ) {
+ if (!dataset) return false;
await engine.query(`
create or replace perfetto table ${this.id} as
@@ -43,11 +48,10 @@
count() as occurrences
from process
join thread using (upid)
- join sched using (utid)
- where cpu in (${selectedCpus})
- and sched.ts + sched.dur > ${area.start}
+ join (${dataset.query()}) as sched using (utid)
+ where
+ sched.ts + sched.dur > ${area.start}
and sched.ts < ${area.end}
- and utid != 0
group by utid
`);
return true;
diff --git a/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_track.ts b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_track.ts
index 0216121..ab49fa3 100644
--- a/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_track.ts
+++ b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_track.ts
@@ -22,7 +22,7 @@
drawTrackHoverTooltip,
} from '../../base/canvas_utils';
import {cropText} from '../../base/string_utils';
-import {Color} from '../../public/color';
+import {Color} from '../../base/color';
import {colorForThread} from '../../components/colorizer';
import {TrackData} from '../../components/tracks/track_data';
import {TimelineFetcher} from '../../components/tracks/track_helper';
@@ -96,11 +96,16 @@
getDataset(): Dataset | undefined {
return new SourceDataset({
- src: 'select id, ts, dur, cpu from sched where utid != 0',
+ // TODO(stevegolton): Once we allow datasets to have more than one filter,
+ // move this where clause to a dataset filter and change this src to
+ // 'sched'.
+ src: 'select id, ts, dur, cpu, utid from sched where utid != 0',
schema: {
id: NUM,
ts: LONG,
dur: LONG,
+ cpu: NUM,
+ utid: NUM,
},
filter: {
col: 'cpu',
diff --git a/ui/src/plugins/dev.perfetto.CpuSlices/sched_details_tab.ts b/ui/src/plugins/dev.perfetto.CpuSlices/sched_details_tab.ts
index e626ee3..3edecf3 100644
--- a/ui/src/plugins/dev.perfetto.CpuSlices/sched_details_tab.ts
+++ b/ui/src/plugins/dev.perfetto.CpuSlices/sched_details_tab.ts
@@ -69,7 +69,6 @@
}
const wakeup = await getSchedWakeupInfo(this.trace.engine, sched);
this.details = {sched, wakeup};
- this.trace.scheduleFullRedraw();
}
render() {
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/data_visualiser.ts b/ui/src/plugins/dev.perfetto.ExplorePage/data_visualiser.ts
new file mode 100644
index 0000000..111acd2
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/data_visualiser.ts
@@ -0,0 +1,176 @@
+// 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 {SqlTableState} from '../../components/widgets/sql/legacy_table/state';
+import {
+ Chart,
+ ChartOption,
+ createChartConfigFromSqlTableState,
+ renderChartComponent,
+} from '../../components/widgets/charts/chart';
+import {Trace} from '../../public/trace';
+import {Menu, MenuItem, MenuItemAttrs} from '../../widgets/menu';
+import SqlModulesPlugin from '../dev.perfetto.SqlModules';
+import {exists} from '../../base/utils';
+import {Button} from '../../widgets/button';
+import {Icons} from '../../base/semantic_icons';
+import {DetailsShell} from '../../widgets/details_shell';
+import {SqlTable} from '../../components/widgets/sql/legacy_table/table';
+import {AddChartMenuItem} from '../../components/widgets/charts/add_chart_menu';
+import {
+ SplitPanel,
+ SplitPanelDrawerVisibility,
+} from '../../widgets/split_panel';
+import {VerticalSplitContainer} from './vertical_split_container';
+
+export interface DataVisualiserState {
+ sqlTableViewState?: SqlTableState;
+ selectedTableName?: string;
+}
+
+export interface DataVisualiserAttrs {
+ trace: Trace;
+ readonly state: DataVisualiserState;
+ charts: Set<Chart>;
+}
+
+export class DataVisualiser implements m.ClassComponent<DataVisualiserAttrs> {
+ private visibility = SplitPanelDrawerVisibility.VISIBLE;
+
+ // Show menu with standard library tables
+ private renderSelectableTablesMenuItems(
+ trace: Trace,
+ state: DataVisualiserState,
+ ): m.Vnode<MenuItemAttrs, unknown>[] {
+ const sqlModules = trace.plugins
+ .getPlugin(SqlModulesPlugin)
+ .getSqlModules();
+ return sqlModules.listTables().map((tableName) => {
+ const sqlTable = sqlModules
+ .getModuleForTable(tableName)
+ ?.getTable(tableName);
+ const sqlTableViewDescription = sqlModules
+ .getModuleForTable(tableName)
+ ?.getSqlTableDescription(tableName);
+
+ return m(MenuItem, {
+ label: tableName,
+ onclick: () => {
+ if (
+ (state.selectedTableName &&
+ tableName === state.selectedTableName) ||
+ sqlTable === undefined ||
+ sqlTableViewDescription === undefined
+ ) {
+ return;
+ }
+
+ state.selectedTableName = sqlTable.name;
+ state.sqlTableViewState = new SqlTableState(
+ trace,
+ {
+ name: tableName,
+ columns: sqlTable.getTableColumns(),
+ },
+ {imports: sqlTableViewDescription.imports},
+ );
+ },
+ });
+ });
+ }
+
+ private renderSqlTable(state: DataVisualiserState, charts: Set<Chart>) {
+ const sqlTableViewState = state.sqlTableViewState;
+
+ if (sqlTableViewState === undefined) return;
+
+ const range = sqlTableViewState.getDisplayedRange();
+ const rowCount = sqlTableViewState.getTotalRowCount();
+
+ const navigation = [
+ exists(range) &&
+ exists(rowCount) &&
+ `Showing rows ${range.from}-${range.to} of ${rowCount}`,
+ m(Button, {
+ icon: Icons.GoBack,
+ disabled: !sqlTableViewState.canGoBack(),
+ onclick: () => sqlTableViewState!.goBack(),
+ }),
+ m(Button, {
+ icon: Icons.GoForward,
+ disabled: !sqlTableViewState.canGoForward(),
+ onclick: () => sqlTableViewState!.goForward(),
+ }),
+ ];
+
+ return m(
+ DetailsShell,
+ {
+ title: 'Explore Table',
+ buttons: navigation,
+ fillParent: false,
+ },
+ m(SqlTable, {
+ state: sqlTableViewState,
+ addColumnMenuItems: (column, columnAlias) =>
+ m(AddChartMenuItem, {
+ chartConfig: createChartConfigFromSqlTableState(
+ column,
+ columnAlias,
+ sqlTableViewState,
+ ),
+ chartOptions: [ChartOption.HISTOGRAM],
+ addChart: (chart) => charts.add(chart),
+ }),
+ }),
+ );
+ }
+
+ private renderRemovableChart(chart: Chart, charts: Set<Chart>) {
+ return m(
+ '.chart-card',
+ {
+ key: `${chart.option}-${chart.config.columnTitle}`,
+ },
+ m(Button, {
+ icon: Icons.Close,
+ onclick: () => {
+ charts.delete(chart);
+ },
+ }),
+ renderChartComponent(chart),
+ );
+ }
+
+ view({attrs}: m.CVnode<DataVisualiserAttrs>) {
+ const {trace, state, charts} = attrs;
+
+ return m(
+ SplitPanel,
+ {
+ visibility: this.visibility,
+ onVisibilityChange: (visibility) => {
+ this.visibility = visibility;
+ },
+ drawerContent: this.renderSqlTable(state, charts),
+ },
+ m(VerticalSplitContainer, {
+ leftPane: m(Menu, this.renderSelectableTablesMenuItems(trace, state)),
+ rightPane: Array.from(attrs.charts.values()).map((chart) =>
+ this.renderRemovableChart(chart, attrs.charts),
+ ),
+ }),
+ );
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts b/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
index 8407b4d..5a45b8a 100644
--- a/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
@@ -15,26 +15,9 @@
import m from 'mithril';
import {PageWithTraceAttrs} from '../../public/page';
import {SqlTableState as SqlTableViewState} from '../../components/widgets/sql/legacy_table/state';
-import {SqlTable as SqlTableView} from '../../components/widgets/sql/legacy_table/table';
-import {exists} from '../../base/utils';
-import {Menu, MenuItem, MenuItemAttrs} from '../../widgets/menu';
-import {Button} from '../../widgets/button';
-import {Icons} from '../../base/semantic_icons';
-import {DetailsShell} from '../../widgets/details_shell';
-import {
- Chart,
- ChartOption,
- createChartConfigFromSqlTableState,
- renderChartComponent,
-} from '../../components/widgets/charts/chart';
-import {AddChartMenuItem} from '../../components/widgets/charts/add_chart_menu';
-import {
- SplitPanel,
- SplitPanelDrawerVisibility,
-} from '../../widgets/split_panel';
-import {Trace} from '../../public/trace';
-import SqlModulesPlugin from '../dev.perfetto.SqlModules';
-import {scheduleFullRedraw} from '../../widgets/raf';
+import {Chart} from '../../components/widgets/charts/chart';
+import {SegmentedButtons} from '../../widgets/segmented_buttons';
+import {DataVisualiser} from './data_visualiser';
export interface ExploreTableState {
sqlTableViewState?: SqlTableViewState;
@@ -46,114 +29,18 @@
readonly charts: Set<Chart>;
}
+enum ExplorePageModes {
+ QUERY_BUILDER,
+ DATA_VISUALISER,
+}
+
+const ExplorePageModeToLabel: Record<ExplorePageModes, string> = {
+ [ExplorePageModes.QUERY_BUILDER]: 'Query Builder',
+ [ExplorePageModes.DATA_VISUALISER]: 'Data Visualiser',
+};
+
export class ExplorePage implements m.ClassComponent<ExplorePageAttrs> {
- private visibility = SplitPanelDrawerVisibility.VISIBLE;
-
- // Show menu with standard library tables
- private renderSelectableTablesMenuItems(
- trace: Trace,
- state: ExploreTableState,
- ): m.Vnode<MenuItemAttrs, unknown>[] {
- const sqlModules = trace.plugins
- .getPlugin(SqlModulesPlugin)
- .getSqlModules();
- return sqlModules.listTables().map((tableName) => {
- const sqlTable = sqlModules
- .getModuleForTable(tableName)
- ?.getTable(tableName);
- const sqlTableViewDescription = sqlModules
- .getModuleForTable(tableName)
- ?.getSqlTableDescription(tableName);
-
- return m(MenuItem, {
- label: tableName,
- onclick: () => {
- if (
- (state.selectedTableName &&
- tableName === state.selectedTableName) ||
- sqlTable === undefined ||
- sqlTableViewDescription === undefined
- ) {
- return;
- }
-
- state.selectedTableName = sqlTable.name;
- state.sqlTableViewState = new SqlTableViewState(
- trace,
- {
- name: tableName,
- columns: sqlTable.getTableColumns(),
- },
- {imports: sqlTableViewDescription.imports},
- );
- },
- });
- });
- }
-
- private renderSqlTable(state: ExploreTableState, charts: Set<Chart>) {
- const sqlTableViewState = state.sqlTableViewState;
-
- if (sqlTableViewState === undefined) return;
-
- const range = sqlTableViewState.getDisplayedRange();
- const rowCount = sqlTableViewState.getTotalRowCount();
-
- const navigation = [
- exists(range) &&
- exists(rowCount) &&
- `Showing rows ${range.from}-${range.to} of ${rowCount}`,
- m(Button, {
- icon: Icons.GoBack,
- disabled: !sqlTableViewState.canGoBack(),
- onclick: () => sqlTableViewState!.goBack(),
- }),
- m(Button, {
- icon: Icons.GoForward,
- disabled: !sqlTableViewState.canGoForward(),
- onclick: () => sqlTableViewState!.goForward(),
- }),
- ];
-
- return m(
- DetailsShell,
- {
- title: 'Explore Table',
- buttons: navigation,
- fillParent: false,
- },
- m(SqlTableView, {
- state: sqlTableViewState,
- addColumnMenuItems: (column, columnAlias) =>
- m(AddChartMenuItem, {
- chartConfig: createChartConfigFromSqlTableState(
- column,
- columnAlias,
- sqlTableViewState,
- ),
- chartOptions: [ChartOption.HISTOGRAM],
- addChart: (chart) => charts.add(chart),
- }),
- }),
- );
- }
-
- private renderRemovableChart(chart: Chart, charts: Set<Chart>) {
- return m(
- '.chart-card',
- {
- key: `${chart.option}-${chart.config.columnTitle}`,
- },
- m(Button, {
- icon: Icons.Close,
- onclick: () => {
- charts.delete(chart);
- scheduleFullRedraw();
- },
- }),
- renderChartComponent(chart),
- );
- }
+ private selectedMode = ExplorePageModes.QUERY_BUILDER;
view({attrs}: m.CVnode<ExplorePageAttrs>) {
const {trace, state, charts} = attrs;
@@ -161,25 +48,25 @@
return m(
'.page.explore-page',
m(
- SplitPanel,
- {
- visibility: this.visibility,
- onVisibilityChange: (visibility) => {
- this.visibility = visibility;
- },
- drawerContent: this.renderSqlTable(state, charts),
- },
- m(
- '.chart-container',
- m(Menu, this.renderSelectableTablesMenuItems(trace, state)),
- ),
- m(
- '.chart-container',
- Array.from(charts.values()).map((chart) =>
- this.renderRemovableChart(chart, charts),
- ),
- ),
+ '.explore-page__header',
+ m('h1', 'Exploration Mode: '),
+ m(SegmentedButtons, {
+ options: [
+ {label: ExplorePageModeToLabel[ExplorePageModes.QUERY_BUILDER]},
+ {label: ExplorePageModeToLabel[ExplorePageModes.DATA_VISUALISER]},
+ ],
+ selectedOption: this.selectedMode,
+ onOptionSelected: (i) => (this.selectedMode = i),
+ }),
),
+ this.selectedMode === ExplorePageModes.QUERY_BUILDER &&
+ m('div', 'Query builder goes here'),
+ this.selectedMode === ExplorePageModes.DATA_VISUALISER &&
+ m(DataVisualiser, {
+ trace,
+ state,
+ charts,
+ }),
);
}
}
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/styles.scss b/ui/src/plugins/dev.perfetto.ExplorePage/styles.scss
index f4ca569..04167c7 100644
--- a/ui/src/plugins/dev.perfetto.ExplorePage/styles.scss
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/styles.scss
@@ -13,9 +13,13 @@
// limitations under the License.
.explore-page {
position: relative;
- display: flex;
- flex-direction: column;
overflow: auto;
+ padding: 0.25rem;
+
+ &__header {
+ display: flex;
+ align-items: center;
+ }
.chart-card {
border-radius: $pf-border-radius;
@@ -34,11 +38,39 @@
}
}
- .chart-container {
- flex: 1;
- position: relative;
+ .pf-vertical-split-container {
display: flex;
- flex-flow: column nowrap;
- overflow: auto;
+ flex-direction: row;
+ height: 100%;
+
+ &__left-pane {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+
+ &__content {
+ flex: 1;
+ overflow: auto;
+ }
+
+ &__resize-handle {
+ display: block;
+ height: 100%;
+ width: 5px;
+ background-color: darkgrey;
+
+ // Ensures that the resize-handler is overlayed
+ // on top of content and stays in a fixed
+ // position at the right of the left-pane
+ z-index: 2;
+ position: absolute;
+ right: 0;
+ cursor: col-resize;
+ }
+ }
+
+ &__right-pane {
+ flex: 1;
+ }
}
}
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/vertical_split_container.ts b/ui/src/plugins/dev.perfetto.ExplorePage/vertical_split_container.ts
new file mode 100644
index 0000000..2d39fde
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/vertical_split_container.ts
@@ -0,0 +1,83 @@
+// 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 {DisposableStack} from '../../base/disposable_stack';
+import {DragGestureHandler} from '../../base/drag_gesture_handler';
+import {assertExists} from '../../base/logging';
+
+interface VerticalSplitContainerAttrs {
+ leftPane: m.Children;
+ rightPane: m.Children;
+}
+
+export class VerticalSplitContainer
+ implements m.ClassComponent<VerticalSplitContainerAttrs>
+{
+ // Note: For BEM class names (https://getbem.com/)
+ private readonly leftPaneClassName =
+ '.pf-vertical-split-container__left-pane';
+ private readonly leftPaneResizeHandle =
+ this.leftPaneClassName + '__resize-handle';
+ private readonly rightPaneClassName =
+ '.pf-vertical-split-container__right-pane';
+
+ private readonly trash = new DisposableStack();
+ private leftPaneWidth = 0;
+ private rightPaneWidth = 0;
+
+ oncreate({dom}: m.VnodeDOM<VerticalSplitContainerAttrs, this>) {
+ const leftPane = assertExists(
+ dom.querySelector(this.leftPaneClassName),
+ ) as HTMLElement;
+ const rightPane = assertExists(
+ dom.querySelector(this.rightPaneClassName),
+ ) as HTMLElement;
+
+ this.trash.use(
+ new DragGestureHandler(
+ assertExists(
+ dom.querySelector(this.leftPaneResizeHandle),
+ ) as HTMLElement,
+ /* onDrag */
+ (x, _y) => {
+ leftPane.style.width = `${this.leftPaneWidth + x}px`;
+ rightPane.style.width = `${this.rightPaneWidth - x}px`;
+ },
+ /* onDragStarted */
+ () => {
+ this.leftPaneWidth = leftPane.clientWidth;
+ },
+ /* onDragFinished */
+ () => {},
+ ),
+ );
+ }
+
+ onremove(): void {
+ this.trash.dispose();
+ }
+
+ view({attrs}: m.VnodeDOM<VerticalSplitContainerAttrs, this>) {
+ return m(
+ '.pf-vertical-split-container',
+ m(
+ this.leftPaneClassName,
+ m(this.leftPaneClassName + '__content', attrs.leftPane),
+ m(this.leftPaneResizeHandle),
+ ),
+ m(this.rightPaneClassName, attrs.rightPane),
+ );
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts b/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
index 7bad7dd..ac6dc8e 100644
--- a/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
+++ b/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
@@ -12,18 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {HSLColor} from '../../public/color';
+import {HSLColor} from '../../base/color';
import {makeColorScheme} from '../../components/colorizer';
-import {ColorScheme} from '../../public/color_scheme';
+import {ColorScheme} from '../../base/color_scheme';
import {
NAMED_ROW,
NamedSliceTrack,
} from '../../components/tracks/named_slice_track';
import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../components/tracks/slice_layout';
-import {STR_NULL} from '../../trace_processor/query_result';
+import {LONG, NUM, STR, STR_NULL} from '../../trace_processor/query_result';
import {Slice} from '../../public/track';
import {Trace} from '../../public/trace';
import {TrackEventDetails} from '../../public/selection';
+import {SourceDataset} from '../../trace_processor/dataset';
// color named and defined based on Material Design color palettes
// 500 colors indicate a timeline slice is not a partial jank (not a jank or
@@ -106,13 +107,23 @@
};
}
- // Override dataset from base class NamedSliceTrack as we don't want these
- // tracks to participate in generic area selection aggregation (frames tracks
- // have their own dedicated aggregation panel).
- // TODO(stevegolton): In future CLs this will be handled with aggregation keys
- // instead, as this track will have to expose a dataset anyway.
override getDataset() {
- return undefined;
+ return new SourceDataset({
+ src: 'actual_frame_timeline_slice',
+ schema: {
+ id: NUM,
+ // Don't expose name to avoid this track getting selected by the generic
+ // slice aggregator, which is useless for frames tracks.
+ // name: STR,
+ ts: LONG,
+ dur: LONG,
+ jank_type: STR,
+ },
+ filter: {
+ col: 'track_id',
+ in: this.trackIds,
+ },
+ });
}
}
diff --git a/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts b/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts
index e9d7586..921f620 100644
--- a/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts
+++ b/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {HSLColor} from '../../public/color';
+import {HSLColor} from '../../base/color';
import {makeColorScheme} from '../../components/colorizer';
import {
NAMED_ROW,
diff --git a/ui/src/plugins/dev.perfetto.Frames/frame_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.Frames/frame_selection_aggregator.ts
index 6bf4eba..98b1415 100644
--- a/ui/src/plugins/dev.perfetto.Frames/frame_selection_aggregator.ts
+++ b/ui/src/plugins/dev.perfetto.Frames/frame_selection_aggregator.ts
@@ -17,19 +17,25 @@
import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../../public/track_kinds';
import {Engine} from '../../trace_processor/engine';
import {AreaSelectionAggregator} from '../../public/selection';
+import {LONG, STR} from '../../trace_processor/query_result';
+import {Dataset} from '../../trace_processor/dataset';
export class FrameSelectionAggregator implements AreaSelectionAggregator {
readonly id = 'frame_aggregation';
+ readonly priority = 1;
+ readonly schema = {
+ ts: LONG,
+ dur: LONG,
+ jank_type: STR,
+ } as const;
+ readonly trackKind = ACTUAL_FRAMES_SLICE_TRACK_KIND;
- async createAggregateView(engine: Engine, area: AreaSelection) {
- const selectedSqlTrackIds: number[] = [];
- for (const trackInfo of area.tracks) {
- if (trackInfo?.tags?.kind === ACTUAL_FRAMES_SLICE_TRACK_KIND) {
- trackInfo.tags.trackIds &&
- selectedSqlTrackIds.push(...trackInfo.tags.trackIds);
- }
- }
- if (selectedSqlTrackIds.length === 0) return false;
+ async createAggregateView(
+ engine: Engine,
+ area: AreaSelection,
+ dataset?: Dataset,
+ ) {
+ if (!dataset) return false;
await engine.query(`
create or replace perfetto table ${this.id} as
@@ -39,9 +45,8 @@
min(dur) as minDur,
avg(dur) as meanDur,
max(dur) as maxDur
- from actual_frame_timeline_slice
- where track_id in (${selectedSqlTrackIds})
- AND ts + dur > ${area.start}
+ from (${dataset.query()})
+ where ts + dur > ${area.start}
AND ts < ${area.end}
group by jank_type
`);
diff --git a/ui/src/plugins/dev.perfetto.Frames/index.ts b/ui/src/plugins/dev.perfetto.Frames/index.ts
index a163da9..3e63963 100644
--- a/ui/src/plugins/dev.perfetto.Frames/index.ts
+++ b/ui/src/plugins/dev.perfetto.Frames/index.ts
@@ -18,14 +18,18 @@
} from '../../public/track_kinds';
import {Trace} from '../../public/trace';
import {PerfettoPlugin} from '../../public/plugin';
-import {getTrackName} from '../../public/utils';
import {TrackNode} from '../../public/workspace';
-import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
+import {NUM, STR} from '../../trace_processor/query_result';
import {ActualFramesTrack} from './actual_frames_track';
import {ExpectedFramesTrack} from './expected_frames_track';
import {FrameSelectionAggregator} from './frame_selection_aggregator';
import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
+// Build a standardized URI for a frames track
+function makeUri(upid: number, kind: 'expected_frames' | 'actual_frames') {
+ return `/process_${upid}/${kind}`;
+}
+
export default class implements PerfettoPlugin {
static readonly id = 'dev.perfetto.Frames';
static readonly dependencies = [ProcessThreadGroupsPlugin];
@@ -36,50 +40,80 @@
ctx.selection.registerAreaSelectionAggregator(
new FrameSelectionAggregator(),
);
+
+ ctx.selection.registerSqlSelectionResolver({
+ sqlTableName: 'slice',
+ callback: async (id: number) => {
+ const result = await ctx.engine.query(`
+ select
+ process_track.type as trackType,
+ process_track.upid as upid
+ from slice
+ join process_track on slice.track_id = process_track.id
+ where
+ slice.id = ${id}
+ and process_track.type in (
+ 'android_expected_frame_timeline',
+ 'android_actual_frame_timeline'
+ )
+ `);
+
+ if (result.numRows() === 0) {
+ return undefined;
+ }
+
+ const {trackType, upid} = result.firstRow({
+ trackType: STR,
+ upid: NUM,
+ });
+
+ const suffix =
+ trackType === 'expected_frame_timeline'
+ ? 'expected_frames'
+ : 'actual_frames';
+
+ return {
+ trackUri: makeUri(upid, suffix),
+ eventId: id,
+ };
+ },
+ });
}
async addExpectedFrames(ctx: Trace): Promise<void> {
const {engine} = ctx;
const result = await engine.query(`
+ with summary as (
+ select
+ pt.upid,
+ group_concat(id) AS track_ids,
+ count() AS track_count
+ from process_track pt
+ join _slice_track_summary USING (id)
+ where pt.type = 'android_expected_frame_timeline'
+ group by pt.upid
+ )
select
- upid,
- t.name as trackName,
+ t.upid,
t.track_ids as trackIds,
- process.name as processName,
- process.pid as pid,
__max_layout_depth(t.track_count, t.track_ids) as maxDepth
- from _process_track_summary_by_upid_and_parent_id_and_name t
- join process using(upid)
- where t.name = "Expected Timeline"
+ from summary t
`);
const it = result.iter({
upid: NUM,
- trackName: STR_NULL,
trackIds: STR,
- processName: STR_NULL,
- pid: NUM_NULL,
maxDepth: NUM,
});
for (; it.valid(); it.next()) {
const upid = it.upid;
- const trackName = it.trackName;
const rawTrackIds = it.trackIds;
const trackIds = rawTrackIds.split(',').map((v) => Number(v));
- const processName = it.processName;
- const pid = it.pid;
const maxDepth = it.maxDepth;
- const title = getTrackName({
- name: trackName,
- upid,
- pid,
- processName,
- kind: 'ExpectedFrames',
- });
-
- const uri = `/process_${upid}/expected_frames`;
+ const title = 'Expected Timeline';
+ const uri = makeUri(upid, 'expected_frames');
ctx.tracks.registerTrack({
uri,
title,
@@ -101,50 +135,36 @@
async addActualFrames(ctx: Trace): Promise<void> {
const {engine} = ctx;
const result = await engine.query(`
+ with summary as (
+ select
+ pt.upid,
+ group_concat(id) AS track_ids,
+ count() AS track_count
+ from process_track pt
+ join _slice_track_summary USING (id)
+ where pt.type = 'android_actual_frame_timeline'
+ group by pt.upid
+ )
select
- upid,
- t.name as trackName,
+ t.upid,
t.track_ids as trackIds,
- process.name as processName,
- process.pid as pid,
__max_layout_depth(t.track_count, t.track_ids) as maxDepth
- from _process_track_summary_by_upid_and_parent_id_and_name t
- join process using(upid)
- where t.name = "Actual Timeline"
+ from summary t
`);
const it = result.iter({
upid: NUM,
- trackName: STR_NULL,
trackIds: STR,
- processName: STR_NULL,
- pid: NUM_NULL,
- maxDepth: NUM_NULL,
+ maxDepth: NUM,
});
for (; it.valid(); it.next()) {
const upid = it.upid;
- const trackName = it.trackName;
const rawTrackIds = it.trackIds;
const trackIds = rawTrackIds.split(',').map((v) => Number(v));
- const processName = it.processName;
- const pid = it.pid;
const maxDepth = it.maxDepth;
- if (maxDepth === null) {
- // If there are no slices in this track, skip it.
- continue;
- }
-
- const kind = 'ActualFrames';
- const title = getTrackName({
- name: trackName,
- upid,
- pid,
- processName,
- kind,
- });
-
- const uri = `/process_${upid}/actual_frames`;
+ const title = 'Actual Timeline';
+ const uri = makeUri(upid, 'actual_frames');
ctx.tracks.registerTrack({
uri,
title,
diff --git a/ui/src/plugins/dev.perfetto.Ftrace/ftrace_explorer.ts b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_explorer.ts
index 8083fb3..31592d3 100644
--- a/ui/src/plugins/dev.perfetto.Ftrace/ftrace_explorer.ts
+++ b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_explorer.ts
@@ -174,7 +174,6 @@
this.pagination.count,
attrs.filterStore.state,
);
- attrs.trace.scheduleFullRedraw();
});
}
diff --git a/ui/src/plugins/dev.perfetto.GpuFreq/index.ts b/ui/src/plugins/dev.perfetto.GpuFreq/index.ts
new file mode 100644
index 0000000..19a24c0
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.GpuFreq/index.ts
@@ -0,0 +1,51 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {TrackNode} from '../../public/workspace';
+import {COUNTER_TRACK_KIND} from '../../public/track_kinds';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {NUM} from '../../trace_processor/query_result';
+import {TraceProcessorCounterTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_counter_track';
+import TraceProcessorTrackPlugin from '../dev.perfetto.TraceProcessorTrack/index';
+
+export default class implements PerfettoPlugin {
+ static readonly id = 'dev.perfetto.GpuFreq';
+ static readonly dependencies = [TraceProcessorTrackPlugin];
+
+ async onTraceLoad(ctx: Trace): Promise<void> {
+ const result = await ctx.engine.query(`
+ select id, gpu_id as gpuId
+ from gpu_counter_track
+ join _counter_track_summary using (id)
+ where name = 'gpufreq'
+ `);
+ const it = result.iter({id: NUM, gpuId: NUM});
+ for (; it.valid(); it.next()) {
+ const uri = `/gpu_frequency_${it.gpuId}`;
+ const name = `Gpu ${it.gpuId} Frequency`;
+ ctx.tracks.registerTrack({
+ uri,
+ title: name,
+ tags: {
+ kind: COUNTER_TRACK_KIND,
+ trackIds: [it.id],
+ },
+ track: new TraceProcessorCounterTrack(ctx, uri, {}, it.id, name),
+ });
+ const track = new TrackNode({uri, title: name, sortOrder: -20});
+ ctx.workspace.addChildInOrder(track);
+ }
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_details_panel.ts b/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_details_panel.ts
index 6805471..45f0a92 100644
--- a/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_details_panel.ts
+++ b/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_details_panel.ts
@@ -106,7 +106,6 @@
intent: Intent.Primary,
onclick: () => {
downloadPprof(this.trace, this.upid, ts);
- this.trace.scheduleFullRedraw();
},
}),
],
@@ -144,7 +143,6 @@
text: 'Skip',
action: () => {
this.flamegraphModalDismissed = true;
- trace.scheduleFullRedraw();
},
},
],
@@ -329,6 +327,8 @@
];
case ProfileType.PERF_SAMPLE:
throw new Error('Perf sample not supported');
+ case ProfileType.INSTRUMENTS_SAMPLE:
+ throw new Error('Instruments sample not supported');
}
}
@@ -396,6 +396,9 @@
case ProfileType.PERF_SAMPLE:
assertFalse(false, 'Perf sample not supported');
return 'Impossible';
+ case ProfileType.INSTRUMENTS_SAMPLE:
+ assertFalse(false, 'Instruments sample not supported');
+ return 'Impossible';
}
}
diff --git a/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/index.ts b/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/index.ts
new file mode 100644
index 0000000..7fddaf8
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/index.ts
@@ -0,0 +1,135 @@
+// Copyright (C) 2025 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 {TrackData} from '../../components/tracks/track_data';
+import {INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND} from '../../public/track_kinds';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
+import {assertExists} from '../../base/logging';
+import {
+ ProcessInstrumentsSamplesProfileTrack,
+ ThreadInstrumentsSamplesProfileTrack,
+} from './instruments_samples_profile_track';
+import {getThreadUriPrefix} from '../../public/utils';
+import {TrackNode} from '../../public/workspace';
+import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
+
+export interface Data extends TrackData {
+ tsStarts: BigInt64Array;
+}
+
+function makeUriForProc(upid: number) {
+ return `/process_${upid}/instruments_samples_profile`;
+}
+
+export default class implements PerfettoPlugin {
+ static readonly id = 'dev.perfetto.InstrumentsSamplesProfile';
+ static readonly dependencies = [ProcessThreadGroupsPlugin];
+
+ async onTraceLoad(ctx: Trace): Promise<void> {
+ const pResult = await ctx.engine.query(`
+ select distinct upid
+ from instruments_sample
+ join thread using (utid)
+ where callsite_id is not null and upid is not null
+ `);
+ for (const it = pResult.iter({upid: NUM}); it.valid(); it.next()) {
+ const upid = it.upid;
+ const uri = makeUriForProc(upid);
+ const title = `Process Callstacks`;
+ ctx.tracks.registerTrack({
+ uri,
+ title,
+ tags: {
+ kind: INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND,
+ upid,
+ },
+ track: new ProcessInstrumentsSamplesProfileTrack(ctx, uri, upid),
+ });
+ const group = ctx.plugins
+ .getPlugin(ProcessThreadGroupsPlugin)
+ .getGroupForProcess(upid);
+ const track = new TrackNode({uri, title, sortOrder: -40});
+ group?.addChildInOrder(track);
+ }
+ const tResult = await ctx.engine.query(`
+ select distinct
+ utid,
+ tid,
+ thread.name as threadName,
+ upid
+ from instruments_sample
+ join thread using (utid)
+ where callsite_id is not null
+ `);
+ for (
+ const it = tResult.iter({
+ utid: NUM,
+ tid: NUM,
+ threadName: STR_NULL,
+ upid: NUM_NULL,
+ });
+ it.valid();
+ it.next()
+ ) {
+ const {threadName, utid, tid, upid} = it;
+ const title =
+ threadName === null
+ ? `Thread Callstacks ${tid}`
+ : `${threadName} Callstacks ${tid}`;
+ const uri = `${getThreadUriPrefix(upid, utid)}_instruments_samples_profile`;
+ ctx.tracks.registerTrack({
+ uri,
+ title,
+ tags: {
+ kind: INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND,
+ utid,
+ upid: upid ?? undefined,
+ },
+ track: new ThreadInstrumentsSamplesProfileTrack(ctx, uri, utid),
+ });
+ const group = ctx.plugins
+ .getPlugin(ProcessThreadGroupsPlugin)
+ .getGroupForThread(utid);
+ const track = new TrackNode({uri, title, sortOrder: -50});
+ group?.addChildInOrder(track);
+ }
+
+ ctx.onTraceReady.addListener(async () => {
+ await selectInstrumentsSample(ctx);
+ });
+ }
+}
+
+async function selectInstrumentsSample(ctx: Trace) {
+ const profile = await assertExists(ctx.engine).query(`
+ select upid
+ from instruments_sample
+ join thread using (utid)
+ where callsite_id is not null
+ order by ts desc
+ limit 1
+ `);
+ if (profile.numRows() !== 1) return;
+ const row = profile.firstRow({upid: NUM});
+ const upid = row.upid;
+
+ // Create an area selection over the first process with a instruments samples track
+ ctx.selection.selectArea({
+ start: ctx.traceInfo.start,
+ end: ctx.traceInfo.end,
+ trackUris: [makeUriForProc(upid)],
+ });
+}
diff --git a/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/instruments_samples_profile_track.ts b/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/instruments_samples_profile_track.ts
new file mode 100644
index 0000000..1716c65
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/instruments_samples_profile_track.ts
@@ -0,0 +1,287 @@
+// Copyright (C) 2025 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 {NUM} from '../../trace_processor/query_result';
+import {Slice} from '../../public/track';
+import {BaseSliceTrack} from '../../components/tracks/base_slice_track';
+import {NAMED_ROW, NamedRow} from '../../components/tracks/named_slice_track';
+import {getColorForSample} from '../../components/colorizer';
+import {
+ ProfileType,
+ TrackEventDetails,
+ TrackEventSelection,
+} from '../../public/selection';
+import {assertExists} from '../../base/logging';
+import {
+ metricsFromTableOrSubquery,
+ QueryFlamegraph,
+} from '../../components/query_flamegraph';
+import {DetailsShell} from '../../widgets/details_shell';
+import {Timestamp} from '../../components/widgets/timestamp';
+import {time} from '../../base/time';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Flamegraph, FLAMEGRAPH_STATE_SCHEMA} from '../../widgets/flamegraph';
+import {Trace} from '../../public/trace';
+
+interface InstrumentsSampleRow extends NamedRow {
+ callsiteId: number;
+}
+
+abstract class BaseInstrumentsSamplesProfileTrack extends BaseSliceTrack<
+ Slice,
+ InstrumentsSampleRow
+> {
+ constructor(trace: Trace, uri: string) {
+ super(trace, uri);
+ }
+
+ protected getRowSpec(): InstrumentsSampleRow {
+ return {...NAMED_ROW, callsiteId: NUM};
+ }
+
+ protected rowToSlice(row: InstrumentsSampleRow): Slice {
+ const baseSlice = super.rowToSliceBase(row);
+ const name = assertExists(row.name);
+ const colorScheme = getColorForSample(row.callsiteId);
+ return {...baseSlice, title: name, colorScheme};
+ }
+
+ onUpdatedSlices(slices: Slice[]) {
+ for (const slice of slices) {
+ slice.isHighlighted = slice === this.hoveredSlice;
+ }
+ }
+}
+
+export class ProcessInstrumentsSamplesProfileTrack extends BaseInstrumentsSamplesProfileTrack {
+ constructor(
+ trace: Trace,
+ uri: string,
+ private readonly upid: number,
+ ) {
+ super(trace, uri);
+ }
+
+ getSqlSource(): string {
+ return `
+ select
+ p.id,
+ ts,
+ 0 as dur,
+ 0 as depth,
+ 'Instruments Sample' as name,
+ callsite_id as callsiteId
+ from instruments_sample p
+ join thread using (utid)
+ where upid = ${this.upid} and callsite_id is not null
+ order by ts
+ `;
+ }
+
+ async getSelectionDetails(
+ id: number,
+ ): Promise<TrackEventDetails | undefined> {
+ const details = await super.getSelectionDetails(id);
+ if (details === undefined) return undefined;
+ return {
+ ...details,
+ upid: this.upid,
+ profileType: ProfileType.INSTRUMENTS_SAMPLE,
+ };
+ }
+
+ detailsPanel(sel: TrackEventSelection) {
+ const upid = assertExists(sel.upid);
+ const ts = sel.ts;
+
+ const metrics = metricsFromTableOrSubquery(
+ `
+ (
+ select
+ id,
+ parent_id as parentId,
+ name,
+ mapping_name,
+ source_file,
+ cast(line_number AS text) as line_number,
+ self_count
+ from _callstacks_for_callsites!((
+ select p.callsite_id
+ from instruments_sample p
+ join thread t using (utid)
+ where p.ts >= ${ts}
+ and p.ts <= ${ts}
+ and t.upid = ${upid}
+ ))
+ )
+ `,
+ [
+ {
+ name: 'Instruments Samples',
+ unit: '',
+ columnName: 'self_count',
+ },
+ ],
+ 'include perfetto module appleos.instruments.samples',
+ [{name: 'mapping_name', displayName: 'Mapping'}],
+ [
+ {
+ name: 'source_file',
+ displayName: 'Source File',
+ mergeAggregation: 'ONE_OR_NULL',
+ },
+ {
+ name: 'line_number',
+ displayName: 'Line Number',
+ mergeAggregation: 'ONE_OR_NULL',
+ },
+ ],
+ );
+ const serialization = {
+ schema: FLAMEGRAPH_STATE_SCHEMA,
+ state: Flamegraph.createDefaultState(metrics),
+ };
+ const flamegraph = new QueryFlamegraph(this.trace, metrics, serialization);
+ return {
+ render: () => renderDetailsPanel(flamegraph, ts),
+ serialization,
+ };
+ }
+}
+
+export class ThreadInstrumentsSamplesProfileTrack extends BaseInstrumentsSamplesProfileTrack {
+ constructor(
+ trace: Trace,
+ uri: string,
+ private readonly utid: number,
+ ) {
+ super(trace, uri);
+ }
+
+ getSqlSource(): string {
+ return `
+ select
+ p.id,
+ ts,
+ 0 as dur,
+ 0 as depth,
+ 'Instruments Sample' as name,
+ callsite_id as callsiteId
+ from instruments_sample p
+ where utid = ${this.utid} and callsite_id is not null
+ order by ts
+ `;
+ }
+
+ async getSelectionDetails(
+ id: number,
+ ): Promise<TrackEventDetails | undefined> {
+ const details = await super.getSelectionDetails(id);
+ if (details === undefined) return undefined;
+ return {
+ ...details,
+ utid: this.utid,
+ profileType: ProfileType.INSTRUMENTS_SAMPLE,
+ };
+ }
+
+ detailsPanel(sel: TrackEventSelection): TrackEventDetailsPanel {
+ const utid = assertExists(sel.utid);
+ const ts = sel.ts;
+
+ const metrics = metricsFromTableOrSubquery(
+ `
+ (
+ select
+ id,
+ parent_id as parentId,
+ name,
+ mapping_name,
+ source_file,
+ cast(line_number AS text) as line_number,
+ self_count
+ from _callstacks_for_callsites!((
+ select p.callsite_id
+ from instruments_sample p
+ where p.ts >= ${ts}
+ and p.ts <= ${ts}
+ and p.utid = ${utid}
+ ))
+ )
+ `,
+ [
+ {
+ name: 'Instruments Samples',
+ unit: '',
+ columnName: 'self_count',
+ },
+ ],
+ 'include perfetto module appleos.instruments.samples',
+ [{name: 'mapping_name', displayName: 'Mapping'}],
+ [
+ {
+ name: 'source_file',
+ displayName: 'Source File',
+ mergeAggregation: 'ONE_OR_NULL',
+ },
+ {
+ name: 'line_number',
+ displayName: 'Line Number',
+ mergeAggregation: 'ONE_OR_NULL',
+ },
+ ],
+ );
+ const serialization = {
+ schema: FLAMEGRAPH_STATE_SCHEMA,
+ state: Flamegraph.createDefaultState(metrics),
+ };
+ const flamegraph = new QueryFlamegraph(this.trace, metrics, serialization);
+ return {
+ render: () => renderDetailsPanel(flamegraph, ts),
+ serialization,
+ };
+ }
+}
+
+function renderDetailsPanel(flamegraph: QueryFlamegraph, ts: time) {
+ return m(
+ '.flamegraph-profile',
+ m(
+ DetailsShell,
+ {
+ fillParent: true,
+ title: m('.title', 'Instruments Samples'),
+ description: [],
+ buttons: [
+ m(
+ 'div.time',
+ `First timestamp: `,
+ m(Timestamp, {
+ ts,
+ }),
+ ),
+ m(
+ 'div.time',
+ `Last timestamp: `,
+ m(Timestamp, {
+ ts,
+ }),
+ ),
+ ],
+ },
+ flamegraph.render(),
+ ),
+ );
+}
diff --git a/ui/src/plugins/dev.perfetto.MetricsPage/metrics_page.ts b/ui/src/plugins/dev.perfetto.MetricsPage/metrics_page.ts
index ffef258..6a1e53b 100644
--- a/ui/src/plugins/dev.perfetto.MetricsPage/metrics_page.ts
+++ b/ui/src/plugins/dev.perfetto.MetricsPage/metrics_page.ts
@@ -136,12 +136,8 @@
this._result = errResult(e);
this._json = {};
}
- })
- .finally(() => {
- this.trace.scheduleFullRedraw();
});
}
- this.trace.scheduleFullRedraw();
}
}
diff --git a/ui/src/plugins/dev.perfetto.ProcessSummary/index.ts b/ui/src/plugins/dev.perfetto.ProcessSummary/index.ts
index 20e1803..b995017 100644
--- a/ui/src/plugins/dev.perfetto.ProcessSummary/index.ts
+++ b/ui/src/plugins/dev.perfetto.ProcessSummary/index.ts
@@ -27,6 +27,8 @@
ProcessSummaryTrack,
} from './process_summary_track';
import ThreadPlugin from '../dev.perfetto.Thread';
+import {createPerfettoIndex} from '../../trace_processor/sql_utils';
+import {uuidv4Sql} from '../../base/uuid';
// This plugin is responsible for adding summary tracks for process and thread
// groups.
@@ -40,10 +42,25 @@
}
private async addProcessTrackGroups(ctx: Trace): Promise<void> {
+ // Makes the queries in `ProcessSchedulingTrack` significantly faster.
+ // TODO(lalitm): figure out a better way to do this without hardcoding this
+ // here.
+ await createPerfettoIndex(
+ ctx.engine,
+ `__process_scheduling_${uuidv4Sql()}`,
+ `__intrinsic_sched_slice(utid)`,
+ );
+ // Makes the queries in `ProcessSummaryTrack` significantly faster.
+ // TODO(lalitm): figure out a better way to do this without hardcoding this
+ // here.
+ await createPerfettoIndex(
+ ctx.engine,
+ `__process_summary_${uuidv4Sql()}`,
+ `__intrinsic_slice(track_id)`,
+ );
+
const threads = ctx.plugins.getPlugin(ThreadPlugin).getThreadMap();
-
const cpuCount = Math.max(...ctx.traceInfo.cpus, -1) + 1;
-
const result = await ctx.engine.query(`
INCLUDE PERFETTO MODULE android.process_metadata;
@@ -87,8 +104,7 @@
join thread using (utid)
where upid is null
)
- `);
-
+ `);
const it = result.iter({
upid: NUM_NULL,
utid: NUM_NULL,
diff --git a/ui/src/plugins/dev.perfetto.ProcessSummary/process_scheduling_track.ts b/ui/src/plugins/dev.perfetto.ProcessSummary/process_scheduling_track.ts
index 53d0519..bf4d29d 100644
--- a/ui/src/plugins/dev.perfetto.ProcessSummary/process_scheduling_track.ts
+++ b/ui/src/plugins/dev.perfetto.ProcessSummary/process_scheduling_track.ts
@@ -17,7 +17,7 @@
import {assertExists, assertTrue} from '../../base/logging';
import {duration, time, Time} from '../../base/time';
import {drawTrackHoverTooltip} from '../../base/canvas_utils';
-import {Color} from '../../public/color';
+import {Color} from '../../base/color';
import {colorForThread} from '../../components/colorizer';
import {TrackData} from '../../components/tracks/track_data';
import {TimelineFetcher} from '../../components/tracks/track_helper';
@@ -29,6 +29,11 @@
import {Point2D} from '../../base/geom';
import {Trace} from '../../public/trace';
import {ThreadMap} from '../dev.perfetto.Thread/threads';
+import {AsyncDisposableStack} from '../../base/disposable_stack';
+import {
+ createPerfettoTable,
+ createVirtualTable,
+} from '../../trace_processor/sql_utils';
export const PROCESS_SCHEDULING_TRACK_KIND = 'ProcessSchedulingTrack';
@@ -67,44 +72,70 @@
) {}
async onCreate(): Promise<void> {
- if (this.config.upid !== null) {
- await this.trace.engine.query(`
- create virtual table process_scheduling_${this.trackUuid}
- using __intrinsic_slice_mipmap((
+ const getQuery = () => {
+ if (this.config.upid !== null) {
+ // TODO(lalitm): remove the harcoding of the cross join here.
+ return `
select
- id,
- ts,
- iif(
- dur = -1,
- lead(ts, 1, trace_end()) over (partition by cpu order by ts) - ts,
- dur
- ) as dur,
- cpu as depth
- from experimental_sched_upid
+ s.id,
+ s.ts,
+ s.dur,
+ s.cpu
+ from thread t
+ cross join sched s using (utid)
where
- utid != 0 and
- upid = ${this.config.upid}
- ));
- `);
- } else {
+ s.utid != 0 and
+ t.upid = ${this.config.upid}
+ order by ts
+ `;
+ }
assertExists(this.config.utid);
- await this.trace.engine.query(`
- create virtual table process_scheduling_${this.trackUuid}
- using __intrinsic_slice_mipmap((
- select
- id,
- ts,
- iif(
- dur = -1,
- lead(ts, 1, trace_end()) over (partition by cpu order by ts) - ts,
- dur
- ) as dur,
- cpu as depth
- from sched
- where utid = ${this.config.utid}
- ));
- `);
- }
+ return `
+ select
+ s.id,
+ s.ts,
+ s.dur,
+ s.cpu
+ from sched s
+ where
+ s.utid = ${this.config.utid}
+ `;
+ };
+
+ const trash = new AsyncDisposableStack();
+ trash.use(
+ await createPerfettoTable(
+ this.trace.engine,
+ `tmp_${this.trackUuid}`,
+ getQuery(),
+ ),
+ );
+ await createVirtualTable(
+ this.trace.engine,
+ `process_scheduling_${this.trackUuid}`,
+ `__intrinsic_slice_mipmap((
+ select
+ s.id,
+ s.ts,
+ iif(
+ s.dur = -1,
+ ifnull(
+ (
+ select n.ts
+ from tmp_${this.trackUuid} n
+ where n.ts > s.ts and n.cpu = s.cpu
+ order by ts
+ limit 1
+ ),
+ trace_end()
+ ) - s.ts,
+ s.dur
+ ) as dur,
+ s.cpu as depth
+ from tmp_${this.trackUuid} s
+ ))`,
+ );
+ await trash.asyncDispose();
}
async onUpdate({
diff --git a/ui/src/plugins/dev.perfetto.ProcessSummary/process_summary_track.ts b/ui/src/plugins/dev.perfetto.ProcessSummary/process_summary_track.ts
index 50c73eb..2c671dd 100644
--- a/ui/src/plugins/dev.perfetto.ProcessSummary/process_summary_track.ts
+++ b/ui/src/plugins/dev.perfetto.ProcessSummary/process_summary_track.ts
@@ -24,6 +24,11 @@
import {LONG, NUM} from '../../trace_processor/query_result';
import {uuidv4Sql} from '../../base/uuid';
import {TrackRenderContext} from '../../public/track';
+import {AsyncDisposableStack} from '../../base/disposable_stack';
+import {
+ createPerfettoTable,
+ createVirtualTable,
+} from '../../trace_processor/sql_utils';
export const PROCESS_SUMMARY_TRACK = 'ProcessSummaryTrack';
@@ -55,50 +60,61 @@
}
async onCreate(): Promise<void> {
- let trackIdQuery: string;
- if (this.config.upid !== null) {
- trackIdQuery = `
- select tt.id as track_id
- from thread_track as tt
- join _thread_available_info_summary using (utid)
- join thread using (utid)
- where thread.upid = ${this.config.upid}
- order by slice_count desc
- `;
- } else {
- trackIdQuery = `
+ const getQuery = () => {
+ if (this.config.upid !== null) {
+ return `
+ select tt.id as track_id
+ from thread_track as tt
+ join _thread_available_info_summary using (utid)
+ join thread using (utid)
+ where thread.upid = ${this.config.upid}
+ order by slice_count desc
+ `;
+ }
+ return `
select tt.id as track_id
from thread_track as tt
join _thread_available_info_summary using (utid)
where tt.utid = ${assertExists(this.config.utid)}
order by slice_count desc
`;
- }
- await this.engine.query(`
- create virtual table process_summary_${this.uuid}
- using __intrinsic_counter_mipmap((
- with
- tt as materialized (
- ${trackIdQuery}
- ),
- ss as (
- select ts, 1.0 as value
- from slice
- join tt using (track_id)
- where slice.depth = 0
- union all
- select ts + dur as ts, -1.0 as value
- from slice
- join tt using (track_id)
- where slice.depth = 0
- )
+ };
+
+ const trash = new AsyncDisposableStack();
+ trash.use(
+ await createPerfettoTable(this.engine, `tmp_${this.uuid}`, getQuery()),
+ );
+ trash.use(
+ await createPerfettoTable(
+ this.engine,
+ `changes_${this.uuid}`,
+ `
+ select ts, 1.0 as value
+ from tmp_${this.uuid}
+ cross join slice using (track_id)
+ where slice.depth = 0
+ union all
+ select ts + dur as ts, -1.0 as value
+ from tmp_${this.uuid}
+ cross join slice using (track_id)
+ where slice.depth = 0
+ `,
+ ),
+ );
+ await createVirtualTable(
+ this.engine,
+ `process_summary_${this.uuid}`,
+ `__intrinsic_counter_mipmap((
select
ts,
- sum(value) over (order by ts) / (select count() from tt) as value
- from ss
+ sum(value) over (order by ts) / (
+ select count() from tmp_${this.uuid}
+ ) as value
+ from changes_${this.uuid}
order by ts
- ));
- `);
+ ))`,
+ );
+ await trash.asyncDispose();
}
async onUpdate({
diff --git a/ui/src/plugins/dev.perfetto.QueryPage/query_history.ts b/ui/src/plugins/dev.perfetto.QueryPage/query_history.ts
index a98c987..07f3121 100644
--- a/ui/src/plugins/dev.perfetto.QueryPage/query_history.ts
+++ b/ui/src/plugins/dev.perfetto.QueryPage/query_history.ts
@@ -77,7 +77,6 @@
vnode.attrs.index,
!vnode.attrs.entry.starred,
);
- vnode.attrs.trace.scheduleFullRedraw();
},
},
m(Icon, {icon: Icons.Star, filled: vnode.attrs.entry.starred}),
@@ -101,7 +100,6 @@
{
onclick: () => {
queryHistoryStorage.remove(vnode.attrs.index);
- vnode.attrs.trace.scheduleFullRedraw();
},
},
m(Icon, {icon: 'delete'}),
diff --git a/ui/src/plugins/dev.perfetto.QueryPage/query_page.ts b/ui/src/plugins/dev.perfetto.QueryPage/query_page.ts
index 62a169e..05ac152 100644
--- a/ui/src/plugins/dev.perfetto.QueryPage/query_page.ts
+++ b/ui/src/plugins/dev.perfetto.QueryPage/query_page.ts
@@ -58,7 +58,6 @@
return;
}
state.queryResult = resp;
- trace.scheduleFullRedraw();
},
);
}
@@ -97,7 +96,6 @@
onUpdate: (text: string) => {
state.enteredText = text;
- attrs.trace.scheduleFullRedraw();
},
});
}
@@ -131,7 +129,6 @@
setQuery: (q: string) => {
state.enteredText = q;
state.generation++;
- attrs.trace.scheduleFullRedraw();
},
}),
);
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/index.ts b/ui/src/plugins/dev.perfetto.RecordTrace/index.ts
index e0c5a1f..2eac7ef 100644
--- a/ui/src/plugins/dev.perfetto.RecordTrace/index.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/index.ts
@@ -14,43 +14,39 @@
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 useRecordingV2 = false;
static onActivate(app: App) {
+ const RECORDING_V2_FLAG = app.featureFlags.register({
+ id: 'recordingv2',
+ name: 'Recording V2',
+ description: 'Record using V2 interface',
+ defaultValue: true,
+ });
+ this.useRecordingV2 = RECORDING_V2_FLAG.get();
+ if (this.useRecordingV2) return;
+
app.sidebar.addMenuItem({
section: 'navigation',
- text: 'Record new trace',
+ text: 'Record new trace (legacy)',
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 recMgr = new RecordingManager(app);
+ const page: m.ClassComponent<PageAttrs> = bindMithrilAttrs(RecordPage, {
+ app,
+ recMgr,
});
- 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/plugins/dev.perfetto.RecordTrace/record_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
index 9ac37cb..e1370c7 100644
--- a/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
@@ -42,7 +42,6 @@
import {RecordConfig} from './record_config_types';
import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
import {RecordingManager} from './recording_manager';
-import {scheduleFullRedraw} from '../../widgets/raf';
import {App} from '../../public/app';
type RPCImplMethod = Method | rpc.ServiceMethod<Message<{}>, Message<{}>>;
@@ -221,7 +220,7 @@
refreshOnStateChange() {
// TODO(eseckler): Use ConsumerPort's QueryServiceState instead
// of posting a custom extension message to retrieve the category list.
- scheduleFullRedraw();
+ this.app.raf.scheduleFullRedraw();
if (this.state.fetchChromeCategories && !this.fetchedCategories) {
this.fetchedCategories = true;
if (this.state.extensionInstalled) {
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
index 021a5db..51cb720 100644
--- a/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
@@ -47,7 +47,6 @@
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';
@@ -152,7 +151,6 @@
recMgr.setRecordingTarget(recordingTarget);
recordTargetStore.save(target);
- scheduleFullRedraw();
}
function Instructions(recMgr: RecordingManager, cssClass: string) {
@@ -195,7 +193,6 @@
disabled: loadedConfigEqual(configType, recMgr.state.lastLoadedConfig),
onclick: () => {
recMgr.setRecordConfig(config, configType);
- scheduleFullRedraw();
},
},
m('i.material-icons', 'file_upload'),
@@ -241,7 +238,6 @@
type: 'NAMED',
name: item.title,
});
- scheduleFullRedraw();
}
},
},
@@ -254,7 +250,6 @@
title: 'Remove configuration',
onclick: () => {
recordConfigStore.delete(item.key);
- scheduleFullRedraw();
},
},
m('i.material-icons', 'delete'),
@@ -289,7 +284,6 @@
placeholder: 'Title for config',
oninput() {
ConfigTitleState.setTitle(this.value);
- scheduleFullRedraw();
},
}),
m(
@@ -305,7 +299,6 @@
recMgr.state.recordConfig,
ConfigTitleState.getTitle(),
);
- scheduleFullRedraw();
ConfigTitleState.clearTitle();
},
},
@@ -323,7 +316,6 @@
)
) {
recMgr.clearRecordConfig();
- scheduleFullRedraw();
}
},
},
@@ -570,7 +562,6 @@
function onStartRecordingPressed(recMgr: RecordingManager) {
location.href = '#!/record/instructions';
- scheduleFullRedraw();
autosaveConfigStore.save(recMgr.state.recordConfig);
const target = recMgr.state.recordingTarget;
@@ -739,7 +730,8 @@
'.record-menu',
{
class: recInProgress ? 'disabled' : '',
- onclick: () => scheduleFullRedraw(),
+ // Just setting this event handler will trigger redraws.
+ onclick: () => {},
},
m('header', 'Trace config'),
m(
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts
deleted file mode 100644
index 3559332..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts
+++ /dev/null
@@ -1,682 +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 {Attributes} from 'mithril';
-import {assertExists} from '../../base/logging';
-import {RecordingConfigUtils} from './recordingV2/recording_config_utils';
-import {
- ChromeTargetInfo,
- RecordingTargetV2,
- TargetInfo,
-} from './recordingV2/recording_interfaces_v2';
-import {
- RecordingPageController,
- RecordingState,
-} 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,
- loadRecordConfig,
- maybeGetActiveCss,
- RECORDING_SECTIONS,
- uploadRecordingConfig,
-} from './record_page';
-import {CodeSnippet} from './record_widgets';
-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;
-let recordConfigUtils: RecordingConfigUtils;
-
-// Options for displaying a target selection menu.
-export interface TargetSelectionOptions {
- // css attributes passed to the mithril components which displays the target
- // selection menu.
- attributes: Attributes;
- // Whether the selection should be preceded by a text label.
- shouldDisplayLabel: boolean;
-}
-
-function isChromeTargetInfo(
- targetInfo: TargetInfo,
-): targetInfo is ChromeTargetInfo {
- return ['CHROME', 'CHROME_OS', 'WINDOWS'].includes(targetInfo.targetType);
-}
-
-function RecordHeader(recMgr: RecordingManager) {
- const platformSelection = RecordingPlatformSelection();
- const statusLabel = RecordingStatusLabel(recMgr);
- const buttons = RecordingButton(recMgr.state.recordConfig);
- const notes = RecordingNotes(recMgr.state.recordConfig);
- if (!platformSelection && !statusLabel && !buttons && !notes) {
- // The header should not be displayed when it has no content.
- return undefined;
- }
- return m(
- '.record-header',
- m(
- '.top-part',
- m('.target-and-status', platformSelection, statusLabel),
- buttons,
- ),
- notes,
- );
-}
-
-function RecordingPlatformSelection() {
- // Don't show the platform selector while we are recording a trace.
- if (controller.getState() >= RecordingState.RECORDING) return undefined;
-
- return m(
- '.target',
- m(
- '.chip',
- {onclick: () => showAddNewTargetModal(controller)},
- m('button', 'Add new recording target'),
- m('i.material-icons', 'add'),
- ),
- targetSelection(),
- );
-}
-
-export function targetSelection(): m.Vnode | undefined {
- if (!controller.shouldShowTargetSelection()) {
- return undefined;
- }
-
- const targets: RecordingTargetV2[] = targetFactoryRegistry.listTargets();
- const targetNames = [];
- const targetInfo = controller.getTargetInfo();
- if (!targetInfo) {
- targetNames.push(m('option', 'PLEASE_SELECT_TARGET'));
- }
-
- let selectedIndex = 0;
- for (let i = 0; i < targets.length; i++) {
- const targetName = targets[i].getInfo().name;
- targetNames.push(m('option', targetName));
- if (targetInfo && targetName === targetInfo.name) {
- selectedIndex = i;
- }
- }
-
- return m(
- 'label',
- 'Target platform:',
- m(
- 'select',
- {
- selectedIndex,
- onchange: (e: Event) => {
- controller.onTargetSelection((e.target as HTMLSelectElement).value);
- },
- onupdate: (select) => {
- // Work around mithril bug
- // (https://github.com/MithrilJS/mithril.js/issues/2107): We may
- // update the select's options while also changing the
- // selectedIndex at the same time. The update of selectedIndex
- // may be applied before the new options are added to the select
- // element. Because the new selectedIndex may be outside of the
- // select's options at that time, we have to reselect the
- // correct index here after any new children were added.
- (select.dom as HTMLSelectElement).selectedIndex = selectedIndex;
- },
- },
- ...targetNames,
- ),
- );
-}
-
-// This will display status messages which are informative, but do not require
-// user action, such as: "Recording in progress for X seconds" in the recording
-// page header.
-function RecordingStatusLabel(recMgr: RecordingManager) {
- const recordingStatus = recMgr.state.recordingStatus;
- if (!recordingStatus) return undefined;
- return m('label', recordingStatus);
-}
-
-function Instructions(recCfg: RecordConfig, cssClass: string) {
- 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());
-
- return m(
- `.record-section.instructions${cssClass}`,
- m('header', 'Recording command'),
- m(
- 'button.permalinkconfig',
- {
- onclick: () => uploadRecordingConfig(recCfg),
- },
- 'Share recording settings',
- ),
- RecordingSnippet(recCfg, targetInfo),
- BufferUsageProgressBar(),
- m('.buttons', StopCancelButtons()),
- );
-}
-
-function BufferUsageProgressBar() {
- // Show the Buffer Usage bar only after we start recording a trace.
- if (controller.getState() !== RecordingState.RECORDING) {
- return undefined;
- }
-
- controller.fetchBufferUsage();
-
- const bufferUsage = controller.getBufferUsagePercentage();
- // Buffer usage is not available yet on Android.
- if (bufferUsage === 0) return undefined;
-
- return m(
- 'label',
- 'Buffer usage: ',
- m('progress', {max: 100, value: bufferUsage * 100}),
- );
-}
-
-function RecordingNotes(recCfg: RecordConfig) {
- 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 linuxUrl = 'https://perfetto.dev/docs/quickstart/linux-tracing';
- const cmdlineUrl =
- 'https://perfetto.dev/docs/quickstart/android-tracing#perfetto-cmdline';
-
- const notes: m.Children = [];
-
- const msgFeatNotSupported = m(
- 'span',
- `Some probes are only supported in Perfetto versions running
- on Android Q+. Therefore, Perfetto will sideload the latest version onto
- the device.`,
- );
-
- const msgPerfettoNotSupported = m(
- 'span',
- `Perfetto is not supported natively before Android P. Therefore, Perfetto
- will sideload the latest version onto the device.`,
- );
-
- const msgLinux = m(
- '.note',
- `Use this `,
- m('a', {href: linuxUrl, target: '_blank'}, `quickstart guide`),
- ` to get started with tracing on Linux.`,
- );
-
- const msgLongTraces = m(
- '.note',
- `Recording in long trace mode through the UI is not supported. Please copy
- the command and `,
- m(
- 'a',
- {href: cmdlineUrl, target: '_blank'},
- `collect the trace using ADB.`,
- ),
- );
-
- if (
- !recordConfigUtils.fetchLatestRecordCommand(recCfg, targetInfo)
- .hasDataSources
- ) {
- notes.push(
- m(
- '.note',
- "It looks like you didn't add any probes. " +
- 'Please add at least one to get a non-empty trace.',
- ),
- );
- }
-
- targetFactoryRegistry.listRecordingProblems().map((recordingProblem) => {
- if (recordingProblem.includes(EXTENSION_URL)) {
- // Special case for rendering the link to the Chrome extension.
- const parts = recordingProblem.split(EXTENSION_URL);
- notes.push(
- m(
- '.note',
- parts[0],
- m('a', {href: EXTENSION_URL, target: '_blank'}, EXTENSION_NAME),
- parts[1],
- ),
- );
- }
- });
-
- switch (targetInfo.targetType) {
- case 'LINUX':
- notes.push(msgLinux);
- break;
- case 'ANDROID': {
- const androidApiLevel = targetInfo.androidApiLevel;
- if (androidApiLevel === 28) {
- notes.push(m('.note', msgFeatNotSupported));
- /* eslint-disable @typescript-eslint/strict-boolean-expressions */
- } else if (androidApiLevel && androidApiLevel <= 27) {
- /* eslint-enable */
- notes.push(m('.note', msgPerfettoNotSupported));
- }
- break;
- }
- default:
- }
-
- if (recCfg.mode === 'LONG_TRACE') {
- notes.unshift(msgLongTraces);
- }
-
- return notes.length > 0 ? m('div', notes) : undefined;
-}
-
-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 the UI has started tracing, don't display a message guiding the user
- // to start recording.
- return undefined;
- }
- return m(
- 'div',
- m(
- 'label',
- `To trace Chrome from the Perfetto UI you just have to press
- '${START_RECORDING_MESSAGE}'.`,
- ),
- );
- }
- return m(CodeSnippet, {text: getRecordCommand(recCfg, targetInfo)});
-}
-
-function getRecordCommand(
- recCfg: RecordConfig,
- targetInfo: TargetInfo,
-): string {
- const recordCommand = recordConfigUtils.fetchLatestRecordCommand(
- recCfg,
- targetInfo,
- );
-
- const pbBase64 = recordCommand?.configProtoBase64 ?? '';
- const pbtx = recordCommand?.configProtoText ?? '';
- let cmd = '';
- if (
- targetInfo.targetType === 'ANDROID' &&
- targetInfo.androidApiLevel === 28
- ) {
- cmd += `echo '${pbBase64}' | \n`;
- cmd += 'base64 --decode | \n';
- cmd += 'adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace"\n';
- } else {
- cmd +=
- targetInfo.targetType === 'ANDROID'
- ? 'adb shell perfetto \\\n'
- : 'perfetto \\\n';
- cmd += ' -c - --txt \\\n';
- cmd += ' -o /data/misc/perfetto-traces/trace \\\n';
- cmd += '<<EOF\n\n';
- cmd += pbtx;
- cmd += '\nEOF\n';
- }
- return cmd;
-}
-
-function RecordingButton(recCfg: RecordConfig) {
- if (
- 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 hasDataSources = recordConfigUtils.fetchLatestRecordCommand(
- recCfg,
- targetInfo,
- ).hasDataSources;
- if (!hasDataSources) {
- return undefined;
- }
-
- return m(
- '.button',
- m(
- 'button',
- {
- class: 'selected',
- onclick: () => controller.onStartRecordingPressed(),
- },
- START_RECORDING_MESSAGE,
- ),
- );
-}
-
-function StopCancelButtons() {
- // Show the Stop/Cancel buttons only while we are recording a trace.
- if (!controller.shouldShowStopCancelButtons()) {
- return undefined;
- }
-
- const stop = m(
- `button.selected`,
- {onclick: () => controller.onStop()},
- 'Stop',
- );
-
- const cancel = m(`button`, {onclick: () => controller.onCancel()}, 'Cancel');
-
- return [stop, cancel];
-}
-
-function recordMenu(routePage: string) {
- const chromeProbe = m(
- 'a[href="#!/record/chrome"]',
- m(
- `li${routePage === 'chrome' ? '.active' : ''}`,
- m('i.material-icons', 'laptop_chromebook'),
- m('.title', 'Chrome'),
- m('.sub', 'Chrome traces'),
- ),
- );
- const cpuProbe = m(
- 'a[href="#!/record/cpu"]',
- m(
- `li${routePage === 'cpu' ? '.active' : ''}`,
- m('i.material-icons', 'subtitles'),
- m('.title', 'CPU'),
- m('.sub', 'CPU usage, scheduling, wakeups'),
- ),
- );
- const gpuProbe = m(
- 'a[href="#!/record/gpu"]',
- m(
- `li${routePage === 'gpu' ? '.active' : ''}`,
- m('i.material-icons', 'aspect_ratio'),
- m('.title', 'GPU'),
- m('.sub', 'GPU frequency, memory'),
- ),
- );
- const powerProbe = m(
- 'a[href="#!/record/power"]',
- m(
- `li${routePage === 'power' ? '.active' : ''}`,
- m('i.material-icons', 'battery_charging_full'),
- m('.title', 'Power'),
- m('.sub', 'Battery and other energy counters'),
- ),
- );
- const memoryProbe = m(
- 'a[href="#!/record/memory"]',
- m(
- `li${routePage === 'memory' ? '.active' : ''}`,
- m('i.material-icons', 'memory'),
- m('.title', 'Memory'),
- m('.sub', 'Physical mem, VM, LMK'),
- ),
- );
- const androidProbe = m(
- 'a[href="#!/record/android"]',
- m(
- `li${routePage === 'android' ? '.active' : ''}`,
- m('i.material-icons', 'android'),
- m('.title', 'Android apps & svcs'),
- m('.sub', 'atrace and logcat'),
- ),
- );
- const advancedProbe = m(
- 'a[href="#!/record/advanced"]',
- m(
- `li${routePage === 'advanced' ? '.active' : ''}`,
- m('i.material-icons', 'settings'),
- m('.title', 'Advanced settings'),
- m('.sub', 'Complicated stuff for wizards'),
- ),
- );
- const tracePerfProbe = m(
- 'a[href="#!/record/tracePerf"]',
- m(
- `li${routePage === 'tracePerf' ? '.active' : ''}`,
- m('i.material-icons', 'full_stacked_bar_chart'),
- m('.title', 'Stack Samples'),
- m('.sub', 'Lightweight stack polling'),
- ),
- );
- const etwProbe = m(
- 'a[href="#!/record/etw"]',
- m(
- `li${routePage === 'etw' ? '.active' : ''}`,
- m('i.material-icons', 'subtitles'),
- m('.title', 'ETW Tracing Config'),
- m('.sub', 'Context switch, Thread state'),
- ),
- );
-
- // 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 probes = [];
- if (targetType === 'LINUX') {
- probes.push(cpuProbe, powerProbe, memoryProbe, chromeProbe, advancedProbe);
- } else if (targetType === 'WINDOWS') {
- probes.push(chromeProbe, etwProbe);
- } else if (targetType === 'CHROME') {
- probes.push(chromeProbe);
- } else {
- probes.push(
- cpuProbe,
- gpuProbe,
- powerProbe,
- memoryProbe,
- androidProbe,
- chromeProbe,
- tracePerfProbe,
- advancedProbe,
- );
- }
-
- return m(
- '.record-menu',
- {
- class:
- controller.getState() > RecordingState.TARGET_INFO_DISPLAYED
- ? 'disabled'
- : '',
- onclick: () => scheduleFullRedraw(),
- },
- m('header', 'Trace config'),
- m(
- 'ul',
- m(
- 'a[href="#!/record/buffers"]',
- m(
- `li${routePage === 'buffers' ? '.active' : ''}`,
- m('i.material-icons', 'tune'),
- m('.title', 'Recording settings'),
- m('.sub', 'Buffer mode, size and duration'),
- ),
- ),
- m(
- 'a[href="#!/record/instructions"]',
- m(
- `li${routePage === 'instructions' ? '.active' : ''}`,
- m('i.material-icons-filled.rec', 'fiber_manual_record'),
- m('.title', 'Recording command'),
- m('.sub', 'Manually record trace'),
- ),
- ),
- m(
- 'a[href="#!/record/config"]',
- {
- onclick: () => {
- recordConfigStore.reloadFromLocalStorage();
- },
- },
- m(
- `li${routePage === 'config' ? '.active' : ''}`,
- m('i.material-icons', 'save'),
- m('.title', 'Saved configs'),
- m('.sub', 'Manage local configs'),
- ),
- ),
- ),
- m('header', 'Probes'),
- m('ul', probes),
- );
-}
-
-function getRecordContainer(recMgr: RecordingManager, subpage?: string) {
- const recCfg = recMgr.state.recordConfig;
- const components: m.Children[] = [RecordHeader(recMgr)];
- 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) {
- components.push(
- m(
- '.full-centered',
- 'Can not access the device without resetting the ' +
- `connection. Please refresh the page, then click ` +
- `'${FORCE_RESET_MESSAGE}.'`,
- ),
- );
- return m('.record-container', components);
- } 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
- ) {
- components.push(
- m('.full-centered', 'Waiting for the trace to be collected.'),
- );
- return m('.record-container', components);
- }
-
- const pages: m.Children = [];
- // we need to remove the `/` character from the route
- let routePage = subpage ? subpage.substr(1) : '';
- if (!RECORDING_SECTIONS.includes(routePage)) {
- routePage = 'buffers';
- }
- pages.push(recordMenu(routePage));
-
- pages.push(
- m(RecordingSettings, {
- dataSources: [],
- cssClass: maybeGetActiveCss(routePage, 'buffers'),
- recState: recMgr.state,
- }),
- );
- pages.push(
- Instructions(recCfg, maybeGetActiveCss(routePage, 'instructions')),
- );
- pages.push(Configurations(recMgr, maybeGetActiveCss(routePage, 'config')));
-
- const settingsSections = new Map([
- ['cpu', CpuSettings],
- ['gpu', GpuSettings],
- ['power', PowerSettings],
- ['memory', MemorySettings],
- ['android', AndroidSettings],
- ['chrome', ChromeSettings],
- ['tracePerf', LinuxPerfSettings],
- ['advanced', AdvancedSettings],
- ['etw', EtwSettings],
- ]);
- for (const [section, component] of settingsSections.entries()) {
- pages.push(
- m(component, {
- dataSources: controller.getTargetInfo()?.dataSources || [],
- cssClass: maybeGetActiveCss(routePage, section),
- recState: recMgr.state,
- }),
- );
- }
-
- components.push(m('.record-container-content', pages));
- return m('.record-container', components);
-}
-
-export interface RecordPageV2Attrs extends PageAttrs {
- app: App;
- recCtl: RecordingPageController;
- recMgr: RecordingManager;
-}
-
-export class RecordPageV2 implements m.ClassComponent<RecordPageV2Attrs> {
- private lastSubpage: string | undefined = undefined;
-
- 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(attrs.recMgr, hash);
- attrs.app.navigate('#!/record/instructions');
- }
- }
-
- 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.
- attrs.recMgr.setRecordConfig(attrs.recMgr.state.recordConfig);
- }
-
- return m(
- '.record-page',
- controller.getState() > RecordingState.TARGET_INFO_DISPLAYED
- ? m('.hider')
- : [],
- getRecordContainer(attrs.recMgr, attrs.subpage),
- );
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
index 325237b..8ffdd8e 100644
--- a/ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
@@ -17,7 +17,6 @@
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 +62,6 @@
view({attrs, children}: m.CVnode<ProbeAttrs>) {
const onToggle = (enabled: boolean) => {
attrs.setEnabled(attrs.recCfg, enabled);
- scheduleFullRedraw();
};
const enabled = attrs.isEnabled(attrs.recCfg);
@@ -130,7 +128,6 @@
view({attrs}: m.CVnode<ToggleAttrs>) {
const onToggle = (enabled: boolean) => {
attrs.setEnabled(attrs.recCfg, enabled);
- scheduleFullRedraw();
};
const enabled = attrs.isEnabled(attrs.recCfg);
@@ -175,7 +172,6 @@
export class Slider implements m.ClassComponent<SliderAttrs> {
onValueChange(attrs: SliderAttrs, newVal: number) {
attrs.set(attrs.recCfg, newVal);
- scheduleFullRedraw();
}
onTimeValueChange(attrs: SliderAttrs, hms: string) {
@@ -276,7 +272,6 @@
selKeys.push(item.value);
}
attrs.set(attrs.recCfg, selKeys);
- scheduleFullRedraw();
}
view({attrs}: m.CVnode<DropdownAttrs>) {
@@ -326,7 +321,6 @@
export class Textarea implements m.ClassComponent<TextareaAttrs> {
onChange(attrs: TextareaAttrs, dom: HTMLTextAreaElement) {
attrs.set(attrs.recCfg, dom.value);
- scheduleFullRedraw();
}
view({attrs}: m.CVnode<TextareaAttrs>) {
@@ -400,7 +394,6 @@
if (!enabled && index !== -1) {
values.splice(index, 1);
}
- scheduleFullRedraw();
}
view({attrs}: m.CVnode<CategoriesCheckboxListParams>) {
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts
deleted file mode 100644
index 33e0dc1..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts
+++ /dev/null
@@ -1,83 +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 {defer} from '../../../base/deferred';
-import {ArrayBufferBuilder} from '../../../base/array_buffer_builder';
-import {AdbFileHandler} from './adb_file_handler';
-import {
- AdbConnection,
- ByteStream,
- OnDisconnectCallback,
- OnMessageCallback,
-} from './recording_interfaces_v2';
-import {utf8Decode} from '../../../base/string_utils';
-
-export abstract class AdbConnectionImpl implements AdbConnection {
- // onStatus and onDisconnect are set to callbacks passed from the caller.
- // This happens for instance in the AndroidWebusbTarget, which instantiates
- // them with callbacks passed from the UI.
- onStatus: OnMessageCallback = () => {};
- onDisconnect: OnDisconnectCallback = (_) => {};
-
- // Starts a shell command, and returns a promise resolved when the command
- // completes.
- async shellAndWaitCompletion(cmd: string): Promise<void> {
- const adbStream = await this.shell(cmd);
- const onStreamingEnded = defer<void>();
-
- // We wait for the stream to be closed by the device, which happens
- // after the shell command is successfully received.
- adbStream.addOnStreamCloseCallback(() => {
- onStreamingEnded.resolve();
- });
- return onStreamingEnded;
- }
-
- // Starts a shell command, then gathers all its output and returns it as
- // a string.
- async shellAndGetOutput(cmd: string): Promise<string> {
- const adbStream = await this.shell(cmd);
- const commandOutput = new ArrayBufferBuilder();
- const onStreamingEnded = defer<string>();
-
- adbStream.addOnStreamDataCallback((data: Uint8Array) => {
- commandOutput.append(data);
- });
- adbStream.addOnStreamCloseCallback(() => {
- onStreamingEnded.resolve(utf8Decode(commandOutput.toArrayBuffer()));
- });
- return onStreamingEnded;
- }
-
- async push(binary: Uint8Array, path: string): Promise<void> {
- const byteStream = await this.openStream('sync:');
- await new AdbFileHandler(byteStream).pushBinary(binary, path);
- // We need to wait until the bytestream is closed. Otherwise, we can have a
- // race condition:
- // If this is the last stream, it will try to disconnect the device. In the
- // meantime, the caller might create another stream which will try to open
- // the device.
- await byteStream.closeAndWaitForTeardown();
- }
-
- abstract shell(cmd: string): Promise<ByteStream>;
-
- abstract canConnectWithoutContention(): Promise<boolean>;
-
- abstract connectSocket(path: string): Promise<ByteStream>;
-
- abstract disconnect(disconnectMessage?: string): Promise<void>;
-
- protected abstract openStream(destination: string): Promise<ByteStream>;
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts
deleted file mode 100644
index 9c9d139..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts
+++ /dev/null
@@ -1,240 +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 {defer, Deferred} from '../../../base/deferred';
-import {utf8Decode} from '../../../base/string_utils';
-import {AdbConnectionImpl} from './adb_connection_impl';
-import {RecordingError} from './recording_error_handling';
-import {
- ByteStream,
- OnDisconnectCallback,
- OnStreamCloseCallback,
- OnStreamDataCallback,
-} from './recording_interfaces_v2';
-import {
- ALLOW_USB_DEBUGGING,
- buildAbdWebsocketCommand,
- WEBSOCKET_UNABLE_TO_CONNECT,
-} from './recording_utils';
-
-export class AdbConnectionOverWebsocket extends AdbConnectionImpl {
- private streams = new Set<AdbOverWebsocketStream>();
-
- onDisconnect: OnDisconnectCallback = (_) => {};
-
- constructor(
- private deviceSerialNumber: string,
- private websocketUrl: string,
- ) {
- super();
- }
-
- shell(cmd: string): Promise<AdbOverWebsocketStream> {
- return this.openStream('shell:' + cmd);
- }
-
- connectSocket(path: string): Promise<AdbOverWebsocketStream> {
- return this.openStream(path);
- }
-
- protected async openStream(
- destination: string,
- ): Promise<AdbOverWebsocketStream> {
- return AdbOverWebsocketStream.create(
- this.websocketUrl,
- destination,
- this.deviceSerialNumber,
- this.closeStream.bind(this),
- );
- }
-
- // The disconnection for AdbConnectionOverWebsocket is synchronous, but this
- // method is async to have a common interface with other types of connections
- // which are async.
- async disconnect(disconnectMessage?: string): Promise<void> {
- for (const stream of this.streams) {
- stream.close();
- }
- this.onDisconnect(disconnectMessage);
- }
-
- closeStream(stream: AdbOverWebsocketStream): void {
- if (this.streams.has(stream)) {
- this.streams.delete(stream);
- }
- }
-
- // There will be no contention for the websocket connection, because it will
- // communicate with the 'adb server' running on the computer which opened
- // Perfetto.
- canConnectWithoutContention(): Promise<boolean> {
- return Promise.resolve(true);
- }
-}
-
-// An AdbOverWebsocketStream instantiates a websocket connection to the device.
-// It exposes an API to write commands to this websocket and read its output.
-export class AdbOverWebsocketStream implements ByteStream {
- private websocket: WebSocket;
-
- // commandSentSignal gets resolved if we successfully connect to the device
- // and send the command this socket wraps. commandSentSignal gets rejected if
- // we fail to connect to the device.
- private commandSentSignal = defer<AdbOverWebsocketStream>();
-
- // We store a promise for each messge while the message is processed.
- // This way, if the websocket server closes the connection, we first process
- // all previously received messages and only afterwards disconnect.
- // An application is when the stream wraps a shell command. The websocket
- // server will reply and then immediately disconnect.
- private messageProcessedSignals: Set<Deferred<void>> = new Set();
-
- private _isConnected = false;
- private onStreamDataCallbacks: OnStreamDataCallback[] = [];
- private onStreamCloseCallbacks: OnStreamCloseCallback[] = [];
-
- private constructor(
- websocketUrl: string,
- destination: string,
- deviceSerialNumber: string,
- private removeFromConnection: (stream: AdbOverWebsocketStream) => void,
- ) {
- this.websocket = new WebSocket(websocketUrl);
-
- this.websocket.onopen = this.onOpen.bind(this, deviceSerialNumber);
- this.websocket.onmessage = this.onMessage.bind(this, destination);
- // The websocket may be closed by the websocket server. This happens
- // for instance when we get the full result of a shell command.
- this.websocket.onclose = this.onClose.bind(this);
- }
-
- addOnStreamDataCallback(onStreamData: OnStreamDataCallback) {
- this.onStreamDataCallbacks.push(onStreamData);
- }
-
- addOnStreamCloseCallback(onStreamClose: OnStreamCloseCallback) {
- this.onStreamCloseCallbacks.push(onStreamClose);
- }
-
- // Used by the connection object to signal newly received data, not exposed
- // in the interface.
- signalStreamData(data: Uint8Array): void {
- for (const onStreamData of this.onStreamDataCallbacks) {
- onStreamData(data);
- }
- }
-
- // Used by the connection object to signal the stream is closed, not exposed
- // in the interface.
- signalStreamClosed(): void {
- for (const onStreamClose of this.onStreamCloseCallbacks) {
- onStreamClose();
- }
- this.onStreamDataCallbacks = [];
- this.onStreamCloseCallbacks = [];
- }
-
- // We close the websocket and notify the AdbConnection to remove this stream.
- close(): void {
- // If the websocket connection is still open (ie. the close did not
- // originate from the server), we close the websocket connection.
- if (this.websocket.readyState === this.websocket.OPEN) {
- this.websocket.close();
- // We remove the 'onclose' callback so the 'close' method doesn't get
- // executed twice.
- this.websocket.onclose = null;
- }
- this._isConnected = false;
- this.removeFromConnection(this);
- this.signalStreamClosed();
- }
-
- // For websocket, the teardown happens synchronously.
- async closeAndWaitForTeardown(): Promise<void> {
- this.close();
- }
-
- write(msg: string | Uint8Array): void {
- this.websocket.send(msg);
- }
-
- isConnected(): boolean {
- return this._isConnected;
- }
-
- private async onOpen(deviceSerialNumber: string): Promise<void> {
- this.websocket.send(
- buildAbdWebsocketCommand(`host:transport:${deviceSerialNumber}`),
- );
- }
-
- private async onMessage(
- destination: string,
- evt: MessageEvent,
- ): Promise<void> {
- const messageProcessed = defer<void>();
- this.messageProcessedSignals.add(messageProcessed);
- try {
- if (!this._isConnected) {
- const txt = (await evt.data.text()) as string;
- const prefix = txt.substring(0, 4);
- if (prefix === 'OKAY') {
- this._isConnected = true;
- this.websocket.send(buildAbdWebsocketCommand(destination));
- this.commandSentSignal.resolve(this);
- } else if (prefix === 'FAIL' && txt.includes('device unauthorized')) {
- this.commandSentSignal.reject(
- new RecordingError(ALLOW_USB_DEBUGGING),
- );
- this.close();
- } else {
- this.commandSentSignal.reject(
- new RecordingError(WEBSOCKET_UNABLE_TO_CONNECT),
- );
- this.close();
- }
- } else {
- // Upon a successful connection we first receive an 'OKAY' message.
- // After that, we receive messages with traced binary payloads.
- const arrayBufferResponse = await evt.data.arrayBuffer();
- if (utf8Decode(arrayBufferResponse) !== 'OKAY') {
- this.signalStreamData(new Uint8Array(arrayBufferResponse));
- }
- }
- messageProcessed.resolve();
- } finally {
- this.messageProcessedSignals.delete(messageProcessed);
- }
- }
-
- private async onClose(): Promise<void> {
- // Wait for all messages to be processed before closing the connection.
- await Promise.allSettled(this.messageProcessedSignals);
- this.close();
- }
-
- static create(
- websocketUrl: string,
- destination: string,
- deviceSerialNumber: string,
- removeFromConnection: (stream: AdbOverWebsocketStream) => void,
- ): Promise<AdbOverWebsocketStream> {
- return new AdbOverWebsocketStream(
- websocketUrl,
- destination,
- deviceSerialNumber,
- removeFromConnection,
- ).commandSentSignal;
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts
deleted file mode 100644
index 715d366..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts
+++ /dev/null
@@ -1,674 +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 {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';
-import {
- ByteStream,
- OnStreamCloseCallback,
- OnStreamDataCallback,
-} from './recording_interfaces_v2';
-import {ALLOW_USB_DEBUGGING, findInterfaceAndEndpoint} from './recording_utils';
-
-export const VERSION_WITH_CHECKSUM = 0x01000000;
-export const VERSION_NO_CHECKSUM = 0x01000001;
-export const DEFAULT_MAX_PAYLOAD_BYTES = 256 * 1024;
-
-export enum AdbState {
- DISCONNECTED = 0,
- // Authentication steps, see AdbConnectionOverWebUsb's handleAuthentication().
- AUTH_STARTED = 1,
- AUTH_WITH_PRIVATE = 2,
- AUTH_WITH_PUBLIC = 3,
-
- CONNECTED = 4,
-}
-
-enum AuthCmd {
- TOKEN = 1,
- SIGNATURE = 2,
- RSAPUBLICKEY = 3,
-}
-
-function generateChecksum(data: Uint8Array): number {
- let res = 0;
- for (let i = 0; i < data.byteLength; i++) res += data[i];
- return res & 0xffffffff;
-}
-
-// Message to be written to the adb connection. Contains the message itself
-// and the corresponding stream identifier.
-interface WriteQueueElement {
- message: Uint8Array;
- localStreamId: number;
-}
-
-export class AdbConnectionOverWebusb extends AdbConnectionImpl {
- private state: AdbState = AdbState.DISCONNECTED;
- private connectingStreams = new Map<number, Deferred<AdbOverWebusbStream>>();
- private streams = new Set<AdbOverWebusbStream>();
- private maxPayload = DEFAULT_MAX_PAYLOAD_BYTES;
- private writeInProgress = false;
- private writeQueue: WriteQueueElement[] = [];
-
- // Devices after Dec 2017 don't use checksum. This will be auto-detected
- // during the connection.
- private useChecksum = true;
-
- private lastStreamId = 0;
- private usbInterfaceNumber?: number;
- private usbReadEndpoint = -1;
- private usbWriteEpEndpoint = -1;
- private isUsbReceiveLoopRunning = false;
-
- private pendingConnPromises: Array<Deferred<void>> = [];
-
- // We use a key pair for authenticating with the device, which we do in
- // two ways:
- // - Firstly, signing with the private key.
- // - Secondly, sending over the public key (at which point the device asks the
- // user for permissions).
- // Once we've sent the public key, for future recordings we only need to
- // sign with the private key, so the user doesn't need to give permissions
- // again.
- constructor(
- private device: USBDevice,
- private keyManager: AdbKeyManager,
- ) {
- super();
- }
-
- shell(cmd: string): Promise<AdbOverWebusbStream> {
- return this.openStream('shell:' + cmd);
- }
-
- connectSocket(path: string): Promise<AdbOverWebusbStream> {
- return this.openStream(path);
- }
-
- async canConnectWithoutContention(): Promise<boolean> {
- await this.device.open();
- const usbInterfaceNumber = await this.setupUsbInterface();
- try {
- await this.device.claimInterface(usbInterfaceNumber);
- await this.device.releaseInterface(usbInterfaceNumber);
- return true;
- } catch (e) {
- return false;
- }
- }
-
- protected async openStream(
- destination: string,
- ): Promise<AdbOverWebusbStream> {
- const streamId = ++this.lastStreamId;
- const connectingStream = defer<AdbOverWebusbStream>();
- this.connectingStreams.set(streamId, connectingStream);
- // We create the stream before trying to establish the connection, so
- // that if we fail to connect, we will reject the connecting stream.
- await this.ensureConnectionEstablished();
- await this.sendMessage('OPEN', streamId, 0, destination);
- return connectingStream;
- }
-
- private async ensureConnectionEstablished(): Promise<void> {
- if (this.state === AdbState.CONNECTED) {
- return;
- }
-
- if (this.state === AdbState.DISCONNECTED) {
- await this.device.open();
- if (!(await this.canConnectWithoutContention())) {
- await this.device.reset();
- }
- const usbInterfaceNumber = await this.setupUsbInterface();
- await this.device.claimInterface(usbInterfaceNumber);
- }
-
- await this.startAdbAuth();
- if (!this.isUsbReceiveLoopRunning) {
- this.usbReceiveLoop();
- }
- const connPromise = defer<void>();
- this.pendingConnPromises.push(connPromise);
- await connPromise;
- }
-
- private async setupUsbInterface(): Promise<number> {
- const interfaceAndEndpoint = findInterfaceAndEndpoint(this.device);
- // `findInterfaceAndEndpoint` will always return a non-null value because
- // we check for this in 'android_webusb_target_factory'. If no interface and
- // endpoints are found, we do not create a target, so we can not connect to
- // it, so we will never reach this logic.
- const {configurationValue, usbInterfaceNumber, endpoints} =
- assertExists(interfaceAndEndpoint);
- this.usbInterfaceNumber = usbInterfaceNumber;
- this.usbReadEndpoint = this.findEndpointNumber(endpoints, 'in');
- this.usbWriteEpEndpoint = this.findEndpointNumber(endpoints, 'out');
- assertTrue(this.usbReadEndpoint >= 0 && this.usbWriteEpEndpoint >= 0);
- await this.device.selectConfiguration(configurationValue);
- return usbInterfaceNumber;
- }
-
- async streamClose(stream: AdbOverWebusbStream): Promise<void> {
- const otherStreamsQueue = this.writeQueue.filter(
- (queueElement) => queueElement.localStreamId !== stream.localStreamId,
- );
- const droppedPacketCount =
- this.writeQueue.length - otherStreamsQueue.length;
- if (droppedPacketCount > 0) {
- console.debug(
- `Dropping ${droppedPacketCount} queued messages due to stream closing.`,
- );
- this.writeQueue = otherStreamsQueue;
- }
-
- this.streams.delete(stream);
- if (this.streams.size === 0) {
- // We disconnect BEFORE calling `signalStreamClosed`. Otherwise, there can
- // be a race condition:
- // Stream A: streamA.onStreamClose
- // Stream B: device.open
- // Stream A: device.releaseInterface
- // Stream B: device.transferOut -> CRASH
- await this.disconnect();
- }
- stream.signalStreamClosed();
- }
-
- streamWrite(msg: string | Uint8Array, stream: AdbOverWebusbStream): void {
- const raw = isString(msg) ? utf8Encode(msg) : msg;
- if (this.writeInProgress) {
- this.writeQueue.push({message: raw, localStreamId: stream.localStreamId});
- return;
- }
- this.writeInProgress = true;
- this.sendMessage('WRTE', stream.localStreamId, stream.remoteStreamId, raw);
- }
-
- // We disconnect in 2 cases:
- // 1. When we close the last stream of the connection. This is to prevent the
- // browser holding onto the USB interface after having finished a trace
- // recording, which would make it impossible to use "adb shell" from the same
- // machine until the browser is closed.
- // 2. When we get a USB disconnect event. This happens for instance when the
- // device is unplugged.
- async disconnect(disconnectMessage?: string): Promise<void> {
- if (this.state === AdbState.DISCONNECTED) {
- return;
- }
- // Clear the resources in a synchronous method, because this can be used
- // for error handling callbacks as well.
- this.reachDisconnectState(disconnectMessage);
-
- // We have already disconnected so there is no need to pass a callback
- // which clears resources or notifies the user into 'wrapRecordingError'.
- await wrapRecordingError(
- this.device.releaseInterface(assertExists(this.usbInterfaceNumber)),
- () => {},
- );
- this.usbInterfaceNumber = undefined;
- }
-
- // This is a synchronous method which clears all resources.
- // It can be used as a callback for error handling.
- reachDisconnectState(disconnectMessage?: string): void {
- // We need to delete the streams BEFORE checking the Adb state because:
- //
- // We create streams before changing the Adb state from DISCONNECTED.
- // In case we can not claim the device, we will create a stream, but fail
- // to connect to the WebUSB device so the state will remain DISCONNECTED.
- const streamsToDelete = this.connectingStreams.entries();
- // Clear the streams before rejecting so we are not caught in a loop of
- // handling promise rejections.
- this.connectingStreams.clear();
- for (const [id, stream] of streamsToDelete) {
- stream.reject(
- `Failed to open stream with id ${id} because adb was disconnected.`,
- );
- }
-
- if (this.state === AdbState.DISCONNECTED) {
- return;
- }
-
- this.state = AdbState.DISCONNECTED;
- this.writeInProgress = false;
-
- this.writeQueue = [];
-
- this.streams.forEach((stream) => stream.close());
- this.onDisconnect(disconnectMessage);
- }
-
- private async startAdbAuth(): Promise<void> {
- const VERSION = this.useChecksum
- ? VERSION_WITH_CHECKSUM
- : VERSION_NO_CHECKSUM;
- this.state = AdbState.AUTH_STARTED;
- await this.sendMessage('CNXN', VERSION, this.maxPayload, 'host:1:UsbADB');
- }
-
- private findEndpointNumber(
- endpoints: USBEndpoint[],
- direction: 'out' | 'in',
- type = 'bulk',
- ): number {
- const ep = endpoints.find(
- (ep) => ep.type === type && ep.direction === direction,
- );
-
- if (ep) return ep.endpointNumber;
-
- throw new RecordingError(`Cannot find ${direction} endpoint`);
- }
-
- private async usbReceiveLoop(): Promise<void> {
- assertFalse(this.isUsbReceiveLoopRunning);
- this.isUsbReceiveLoopRunning = true;
- for (; this.state !== AdbState.DISCONNECTED; ) {
- const res = await this.wrapUsb(
- this.device.transferIn(this.usbReadEndpoint, ADB_MSG_SIZE),
- );
- if (!res) {
- this.isUsbReceiveLoopRunning = false;
- return;
- }
- if (res.status !== 'ok') {
- // Log and ignore messages with invalid status. These can occur
- // when the device is connected/disconnected repeatedly.
- console.error(
- `Received message with unexpected status '${res.status}'`,
- );
- continue;
- }
-
- const msg = AdbMsg.decodeHeader(res.data!);
- if (msg.dataLen > 0) {
- const resp = await this.wrapUsb(
- this.device.transferIn(this.usbReadEndpoint, msg.dataLen),
- );
- if (!resp) {
- this.isUsbReceiveLoopRunning = false;
- return;
- }
- msg.data = new Uint8Array(
- resp.data!.buffer,
- resp.data!.byteOffset,
- resp.data!.byteLength,
- );
- }
-
- if (this.useChecksum && generateChecksum(msg.data) !== msg.dataChecksum) {
- // We ignore messages with an invalid checksum. These sometimes appear
- // when the page is re-loaded in a middle of a recording.
- continue;
- }
- // The server can still send messages streams for previous streams.
- // This happens for instance if we record, reload the recording page and
- // then record again. We can also receive a 'WRTE' or 'OKAY' after
- // we have sent a 'CLSE' and marked the state as disconnected.
- if (
- (msg.cmd === 'CLSE' || msg.cmd === 'WRTE') &&
- !this.getStreamForLocalStreamId(msg.arg1)
- ) {
- continue;
- } else if (
- msg.cmd === 'OKAY' &&
- !this.connectingStreams.has(msg.arg1) &&
- !this.getStreamForLocalStreamId(msg.arg1)
- ) {
- continue;
- } else if (
- msg.cmd === 'AUTH' &&
- msg.arg0 === AuthCmd.TOKEN &&
- this.state === AdbState.AUTH_WITH_PUBLIC
- ) {
- // If we start a recording but fail because of a faulty physical
- // connection to the device, when we start a new recording, we will
- // received multiple AUTH tokens, of which we should ignore all but
- // one.
- continue;
- }
-
- // handle the ADB message from the device
- if (msg.cmd === 'CLSE') {
- assertExists(this.getStreamForLocalStreamId(msg.arg1)).close();
- } else if (msg.cmd === 'AUTH' && msg.arg0 === AuthCmd.TOKEN) {
- const key = await this.keyManager.getKey();
- if (this.state === AdbState.AUTH_STARTED) {
- // During this step, we send back the token received signed with our
- // private key. If the device has previously received our public key,
- // the dialog asking for user confirmation will not be displayed on
- // the device.
- this.state = AdbState.AUTH_WITH_PRIVATE;
- await this.sendMessage(
- 'AUTH',
- AuthCmd.SIGNATURE,
- 0,
- key.sign(msg.data),
- );
- } else {
- // If our signature with the private key is not accepted by the
- // device, we generate a new keypair and send the public key.
- this.state = AdbState.AUTH_WITH_PUBLIC;
- await this.sendMessage(
- 'AUTH',
- AuthCmd.RSAPUBLICKEY,
- 0,
- key.getPublicKey() + '\0',
- );
- this.onStatus(ALLOW_USB_DEBUGGING);
- await maybeStoreKey(key);
- }
- } else if (msg.cmd === 'CNXN') {
- assertTrue(
- [AdbState.AUTH_WITH_PRIVATE, AdbState.AUTH_WITH_PUBLIC].includes(
- this.state,
- ),
- );
- this.state = AdbState.CONNECTED;
- this.maxPayload = msg.arg1;
-
- const deviceVersion = msg.arg0;
-
- if (
- ![VERSION_WITH_CHECKSUM, VERSION_NO_CHECKSUM].includes(deviceVersion)
- ) {
- throw new RecordingError(`Version ${msg.arg0} not supported.`);
- }
- this.useChecksum = deviceVersion === VERSION_WITH_CHECKSUM;
- this.state = AdbState.CONNECTED;
-
- // This will resolve the promises awaited by
- // "ensureConnectionEstablished".
- this.pendingConnPromises.forEach((connPromise) =>
- connPromise.resolve(),
- );
- this.pendingConnPromises = [];
- } else if (msg.cmd === 'OKAY') {
- if (this.connectingStreams.has(msg.arg1)) {
- const connectingStream = assertExists(
- this.connectingStreams.get(msg.arg1),
- );
- const stream = new AdbOverWebusbStream(this, msg.arg1, msg.arg0);
- this.streams.add(stream);
- this.connectingStreams.delete(msg.arg1);
- connectingStream.resolve(stream);
- } else {
- assertTrue(this.writeInProgress);
- this.writeInProgress = false;
- for (; this.writeQueue.length; ) {
- // We go through the queued writes and choose the first one
- // corresponding to a stream that's still active.
- const queuedElement = assertExists(this.writeQueue.shift());
- const queuedStream = this.getStreamForLocalStreamId(
- queuedElement.localStreamId,
- );
- if (queuedStream) {
- queuedStream.write(queuedElement.message);
- break;
- }
- }
- }
- } else if (msg.cmd === 'WRTE') {
- const stream = assertExists(this.getStreamForLocalStreamId(msg.arg1));
- await this.sendMessage(
- 'OKAY',
- stream.localStreamId,
- stream.remoteStreamId,
- );
- stream.signalStreamData(msg.data);
- } else {
- this.isUsbReceiveLoopRunning = false;
- throw new RecordingError(
- `Unexpected message ${msg} in state ${this.state}`,
- );
- }
- }
- this.isUsbReceiveLoopRunning = false;
- }
-
- private getStreamForLocalStreamId(
- localStreamId: number,
- ): AdbOverWebusbStream | undefined {
- for (const stream of this.streams) {
- if (stream.localStreamId === localStreamId) {
- return stream;
- }
- }
- return undefined;
- }
-
- // The header and the message data must be sent consecutively. Using 2 awaits
- // Another message can interleave after the first header has been sent,
- // resulting in something like [header1] [header2] [data1] [data2];
- // In this way we are waiting both promises to be resolved before continuing.
- private async sendMessage(
- cmd: CmdType,
- arg0: number,
- arg1: number,
- data?: Uint8Array | string,
- ): Promise<void> {
- const msg = AdbMsg.create({
- cmd,
- arg0,
- arg1,
- data,
- useChecksum: this.useChecksum,
- });
-
- const msgHeader = msg.encodeHeader();
- const msgData = msg.data;
- assertTrue(
- msgHeader.length <= this.maxPayload && msgData.length <= this.maxPayload,
- );
-
- const sendPromises = [
- this.wrapUsb(
- this.device.transferOut(this.usbWriteEpEndpoint, msgHeader.buffer),
- ),
- ];
- if (msg.data.length > 0) {
- sendPromises.push(
- this.wrapUsb(
- this.device.transferOut(this.usbWriteEpEndpoint, msgData.buffer),
- ),
- );
- }
- await Promise.all(sendPromises);
- }
-
- private wrapUsb<T>(promise: Promise<T>): Promise<T | undefined> {
- return wrapRecordingError(promise, this.reachDisconnectState.bind(this));
- }
-}
-
-// An AdbOverWebusbStream is instantiated after the creation of a socket to the
-// device. Thanks to this, we can send commands and receive their output.
-// Messages are received in the main adb class, and are forwarded to an instance
-// of this class based on a stream id match.
-export class AdbOverWebusbStream implements ByteStream {
- private adbConnection: AdbConnectionOverWebusb;
- private _isConnected: boolean;
- private onStreamDataCallbacks: OnStreamDataCallback[] = [];
- private onStreamCloseCallbacks: OnStreamCloseCallback[] = [];
- localStreamId: number;
- remoteStreamId = -1;
-
- constructor(
- adb: AdbConnectionOverWebusb,
- localStreamId: number,
- remoteStreamId: number,
- ) {
- this.adbConnection = adb;
- this.localStreamId = localStreamId;
- this.remoteStreamId = remoteStreamId;
- // When the stream is created, the connection has been already established.
- this._isConnected = true;
- }
-
- addOnStreamDataCallback(onStreamData: OnStreamDataCallback): void {
- this.onStreamDataCallbacks.push(onStreamData);
- }
-
- addOnStreamCloseCallback(onStreamClose: OnStreamCloseCallback): void {
- this.onStreamCloseCallbacks.push(onStreamClose);
- }
-
- // Used by the connection object to signal newly received data, not exposed
- // in the interface.
- signalStreamData(data: Uint8Array): void {
- for (const onStreamData of this.onStreamDataCallbacks) {
- onStreamData(data);
- }
- }
-
- // Used by the connection object to signal the stream is closed, not exposed
- // in the interface.
- signalStreamClosed(): void {
- for (const onStreamClose of this.onStreamCloseCallbacks) {
- onStreamClose();
- }
- this.onStreamDataCallbacks = [];
- this.onStreamCloseCallbacks = [];
- }
-
- close(): void {
- this.closeAndWaitForTeardown();
- }
-
- async closeAndWaitForTeardown(): Promise<void> {
- this._isConnected = false;
- await this.adbConnection.streamClose(this);
- }
-
- write(msg: string | Uint8Array): void {
- this.adbConnection.streamWrite(msg, this);
- }
-
- isConnected(): boolean {
- return this._isConnected;
- }
-}
-
-const ADB_MSG_SIZE = 6 * 4; // 6 * int32.
-
-class AdbMsg {
- data: Uint8Array;
- readonly cmd: CmdType;
- readonly arg0: number;
- readonly arg1: number;
- readonly dataLen: number;
- readonly dataChecksum: number;
- readonly useChecksum: boolean;
-
- constructor(
- cmd: CmdType,
- arg0: number,
- arg1: number,
- dataLen: number,
- dataChecksum: number,
- useChecksum = false,
- ) {
- assertTrue(cmd.length === 4);
- this.cmd = cmd;
- this.arg0 = arg0;
- this.arg1 = arg1;
- this.dataLen = dataLen;
- this.data = new Uint8Array(dataLen);
- this.dataChecksum = dataChecksum;
- this.useChecksum = useChecksum;
- }
-
- static create({
- cmd,
- arg0,
- arg1,
- data,
- useChecksum = true,
- }: {
- cmd: CmdType;
- arg0: number;
- arg1: number;
- data?: Uint8Array | string;
- useChecksum?: boolean;
- }): AdbMsg {
- const encodedData = this.encodeData(data);
- const msg = new AdbMsg(cmd, arg0, arg1, encodedData.length, 0, useChecksum);
- msg.data = encodedData;
- return msg;
- }
-
- get dataStr() {
- return utf8Decode(this.data);
- }
-
- toString() {
- return `${this.cmd} [${this.arg0},${this.arg1}] ${this.dataStr}`;
- }
-
- // A brief description of the message can be found here:
- // https://android.googlesource.com/platform/system/core/+/main/adb/protocol.txt
- //
- // struct amessage {
- // uint32_t command; // command identifier constant
- // uint32_t arg0; // first argument
- // uint32_t arg1; // second argument
- // uint32_t data_length;// length of payload (0 is allowed)
- // uint32_t data_check; // checksum of data payload
- // uint32_t magic; // command ^ 0xffffffff
- // };
- static decodeHeader(dv: DataView): AdbMsg {
- assertTrue(dv.byteLength === ADB_MSG_SIZE);
- const cmd = utf8Decode(dv.buffer.slice(0, 4)) as CmdType;
- const cmdNum = dv.getUint32(0, true);
- const arg0 = dv.getUint32(4, true);
- const arg1 = dv.getUint32(8, true);
- const dataLen = dv.getUint32(12, true);
- const dataChecksum = dv.getUint32(16, true);
- const cmdChecksum = dv.getUint32(20, true);
- assertTrue(cmdNum === (cmdChecksum ^ 0xffffffff));
- return new AdbMsg(cmd, arg0, arg1, dataLen, dataChecksum);
- }
-
- encodeHeader(): Uint8Array {
- const buf = new Uint8Array(ADB_MSG_SIZE);
- const dv = new DataView(buf.buffer);
- const cmdBytes: Uint8Array = utf8Encode(this.cmd);
- const rawMsg = AdbMsg.encodeData(this.data);
- const checksum = this.useChecksum ? generateChecksum(rawMsg) : 0;
- for (let i = 0; i < 4; i++) dv.setUint8(i, cmdBytes[i]);
-
- dv.setUint32(4, this.arg0, true);
- dv.setUint32(8, this.arg1, true);
- dv.setUint32(12, rawMsg.byteLength, true);
- dv.setUint32(16, checksum, true);
- dv.setUint32(20, dv.getUint32(0, true) ^ 0xffffffff, true);
-
- return buf;
- }
-
- static encodeData(data?: Uint8Array | string): Uint8Array {
- if (data === undefined) return new Uint8Array([]);
- if (isString(data)) return utf8Encode(data + '\0');
- return data;
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts
deleted file mode 100644
index 078726f..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts
+++ /dev/null
@@ -1,125 +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 {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';
-
-// https://cs.android.com/android/platform/superproject/+/main:packages/
-// modules/adb/file_sync_protocol.h;l=144
-const MAX_SYNC_SEND_CHUNK_SIZE = 64 * 1024;
-
-// Adb does not accurately send some file permissions. If you need a special set
-// of permissions, do not rely on this value. Rather, send a shell command which
-// explicitly sets permissions, such as:
-// 'shell:chmod ${permissions} ${path}'
-const FILE_PERMISSIONS = 2 ** 15 + 0o644;
-
-// For details about the protocol, see:
-// https://cs.android.com/android/platform/superproject/+/main:packages/modules/adb/SYNC.TXT
-export class AdbFileHandler {
- private sentByteCount = 0;
- private isPushOngoing: boolean = false;
-
- constructor(private byteStream: ByteStream) {}
-
- async pushBinary(binary: Uint8Array, path: string): Promise<void> {
- // For a given byteStream, we only support pushing one binary at a time.
- assertFalse(this.isPushOngoing);
- this.isPushOngoing = true;
- const transferFinished = defer<void>();
-
- this.byteStream.addOnStreamDataCallback((data) =>
- this.onStreamData(data, transferFinished),
- );
- this.byteStream.addOnStreamCloseCallback(
- () => (this.isPushOngoing = false),
- );
-
- const sendMessage = new ArrayBufferBuilder();
- // 'SEND' is the API method used to send a file to device.
- sendMessage.append('SEND');
- // The remote file name is split into two parts separated by the last
- // comma (","). The first part is the actual path, while the second is a
- // decimal encoded file mode containing the permissions of the file on
- // device.
- sendMessage.append(path.length + 6);
- sendMessage.append(path);
- sendMessage.append(',');
- sendMessage.append(FILE_PERMISSIONS.toString());
- this.byteStream.write(new Uint8Array(sendMessage.toArrayBuffer()));
-
- while (!(await this.sendNextDataChunk(binary)));
-
- return transferFinished;
- }
-
- private onStreamData(data: Uint8Array, transferFinished: Deferred<void>) {
- this.sentByteCount = 0;
- const response = utf8Decode(data);
- if (response.split('\n')[0].includes('FAIL')) {
- // Sample failure response (when the file is transferred successfully
- // but the date is not formatted correctly):
- // 'OKAYFAIL\npath too long'
- transferFinished.reject(
- new RecordingError(`${BINARY_PUSH_FAILURE}: ${response}`),
- );
- } else if (utf8Decode(data).substring(0, 4) === 'OKAY') {
- // In case of success, the server responds to the last request with
- // 'OKAY'.
- transferFinished.resolve();
- } else {
- throw new RecordingError(`${BINARY_PUSH_UNKNOWN_RESPONSE}: ${response}`);
- }
- }
-
- private async sendNextDataChunk(binary: Uint8Array): Promise<boolean> {
- const endPosition = Math.min(
- this.sentByteCount + MAX_SYNC_SEND_CHUNK_SIZE,
- binary.byteLength,
- );
- const chunk = await binary.slice(this.sentByteCount, endPosition);
- // The file is sent in chunks. Each chunk is prefixed with "DATA" and the
- // chunk length. This is repeated until the entire file is transferred. Each
- // chunk must not be larger than 64k.
- const chunkLength = chunk.byteLength;
- const dataMessage = new ArrayBufferBuilder();
- dataMessage.append('DATA');
- dataMessage.append(chunkLength);
- dataMessage.append(
- new Uint8Array(chunk.buffer, chunk.byteOffset, chunkLength),
- );
-
- this.sentByteCount += chunkLength;
- const isDone = this.sentByteCount === binary.byteLength;
-
- if (isDone) {
- // When the file is transferred a sync request "DONE" is sent, together
- // with a timestamp, representing the last modified time for the file. The
- // server responds to this last request.
- dataMessage.append('DONE');
- // We send the date in seconds.
- dataMessage.append(Math.floor(Date.now() / 1000));
- }
- this.byteStream.write(new Uint8Array(dataMessage.toArrayBuffer()));
- return isDone;
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts
deleted file mode 100644
index 0ce297b..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts
+++ /dev/null
@@ -1,101 +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 {assetSrc} from '../../../../base/assets';
-import {AdbKey} from './adb_auth';
-
-function isPasswordCredential(
- cred: Credential | null,
-): cred is PasswordCredential {
- return cred !== null && cred.type === 'password';
-}
-
-function hasPasswordCredential() {
- return 'PasswordCredential' in window;
-}
-
-// how long we will store the key in memory
-const KEY_IN_MEMORY_TIMEOUT = 1000 * 60 * 30; // 30 minutes
-
-// Update credential store with the given key.
-export async function maybeStoreKey(key: AdbKey): Promise<void> {
- if (!hasPasswordCredential()) {
- return;
- }
- const credential = new PasswordCredential({
- id: 'webusb-adb-key',
- password: key.serializeKey(),
- name: 'WebUSB ADB Key',
- iconURL: assetSrc('assets/favicon.png'),
- });
- // The 'Save password?' Chrome dialogue only appears if the key is
- // not already stored in Chrome.
- await navigator.credentials.store(credential);
- // 'preventSilentAccess' guarantees the user is always notified when
- // credentials are accessed. Sometimes the user is asked to click a button
- // and other times only a notification is shown temporarily.
- await navigator.credentials.preventSilentAccess();
-}
-
-export class AdbKeyManager {
- private key?: AdbKey;
- // Id of timer used to expire the key kept in memory.
- private keyInMemoryTimerId?: ReturnType<typeof setTimeout>;
-
- // Finds a key, by priority:
- // - looking in memory (i.e. this.key)
- // - looking in the credential store
- // - and finally creating one from scratch if needed
- async getKey(): Promise<AdbKey> {
- // 1. If we have a private key in memory, we return it.
- if (this.key) {
- return this.key;
- }
-
- // 2. We try to get the private key from the browser.
- // The mediation is set as 'optional', because we use
- // 'preventSilentAccess', which sometimes requests the user to click
- // on a button to allow the auth, but sometimes only shows a
- // notification and does not require the user to click on anything.
- // If we had set mediation to 'required', the user would have been
- // asked to click on a button every time.
- if (hasPasswordCredential()) {
- const options: PasswordCredentialRequestOptions = {
- password: true,
- mediation: 'optional',
- };
- const credential = await navigator.credentials.get(options);
- if (isPasswordCredential(credential)) {
- return this.assignKey(AdbKey.DeserializeKey(credential.password));
- }
- }
-
- // 3. We generate a new key pair.
- return this.assignKey(await AdbKey.GenerateNewKeyPair());
- }
-
- // Assigns the key a new value, sets a timeout for storing the key in memory
- // and then returns the new key.
- private assignKey(key: AdbKey): AdbKey {
- this.key = key;
- if (this.keyInMemoryTimerId) {
- clearTimeout(this.keyInMemoryTimerId);
- }
- this.keyInMemoryTimerId = setTimeout(
- () => (this.key = undefined),
- KEY_IN_MEMORY_TIMEOUT,
- );
- return key;
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts
deleted file mode 100644
index 55f87ce..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts
+++ /dev/null
@@ -1,236 +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 {defer, Deferred} from '../../../base/deferred';
-import {assertExists, assertTrue} from '../../../base/logging';
-import {binaryDecode, binaryEncode} from '../../../base/string_utils';
-import {
- ChromeExtensionMessage,
- isChromeExtensionError,
- isChromeExtensionStatus,
- isGetCategoriesResponse,
-} from '../chrome_proxy_record_controller';
-import {
- isDisableTracingResponse,
- isEnableTracingResponse,
- isFreeBuffersResponse,
- isGetTraceStatsResponse,
- isReadBuffersResponse,
-} from '../consumer_port_types';
-import {
- EnableTracingRequest,
- IBufferStats,
- ISlice,
- TraceConfig,
-} from '../protos';
-import {RecordingError} from './recording_error_handling';
-import {
- TracingSession,
- TracingSessionListener,
-} from './recording_interfaces_v2';
-import {
- BUFFER_USAGE_INCORRECT_FORMAT,
- BUFFER_USAGE_NOT_ACCESSIBLE,
- EXTENSION_ID,
- MALFORMED_EXTENSION_MESSAGE,
-} from './recording_utils';
-
-// This class implements the protocol described in
-// https://perfetto.dev/docs/design-docs/api-and-abi#tracing-protocol-abi
-// However, with the Chrome extension we communicate using JSON messages.
-export class ChromeTracedTracingSession implements TracingSession {
- // Needed for ReadBufferResponse: all the trace packets are split into
- // several slices. |partialPacket| is the buffer for them. Once we receive a
- // slice with the flag |lastSliceForPacket|, a new packet is created.
- private partialPacket: ISlice[] = [];
-
- // For concurrent calls to 'GetCategories', we return the same value.
- private pendingGetCategoriesMessage?: Deferred<string[]>;
-
- private pendingStatsMessages = new Array<Deferred<IBufferStats[]>>();
-
- // Port through which we communicate with the extension.
- private chromePort: chrome.runtime.Port;
- // True when Perfetto is connected via the port to the tracing session.
- private isPortConnected: boolean;
-
- constructor(private tracingSessionListener: TracingSessionListener) {
- this.chromePort = chrome.runtime.connect(EXTENSION_ID);
- this.isPortConnected = true;
- }
-
- start(config: TraceConfig): void {
- if (!this.isPortConnected) return;
- const duration = config.durationMs;
- this.tracingSessionListener.onStatus(
- `Recording in progress${
- duration ? ' for ' + duration.toString() + ' ms' : ''
- }...`,
- );
-
- const enableTracingRequest = new EnableTracingRequest();
- enableTracingRequest.traceConfig = config;
- const enableTracingRequestProto = binaryEncode(
- EnableTracingRequest.encode(enableTracingRequest).finish(),
- );
- this.chromePort.postMessage({
- method: 'EnableTracing',
- requestData: enableTracingRequestProto,
- });
- }
-
- // The 'cancel' method will end the tracing session and will NOT return the
- // trace. Therefore, we do not need to keep the connection open.
- cancel(): void {
- if (!this.isPortConnected) return;
- this.terminateConnection();
- }
-
- // The 'stop' method will end the tracing session and cause the trace to be
- // returned via a callback. We maintain the connection to the target so we can
- // extract the trace.
- // See 'DisableTracing' in:
- // https://perfetto.dev/docs/design-docs/life-of-a-tracing-session
- stop(): void {
- if (!this.isPortConnected) return;
- this.chromePort.postMessage({method: 'DisableTracing'});
- }
-
- getCategories(): Promise<string[]> {
- if (!this.isPortConnected) {
- throw new RecordingError(
- 'Attempting to get categories from a ' +
- 'disconnected tracing session.',
- );
- }
- if (this.pendingGetCategoriesMessage) {
- return this.pendingGetCategoriesMessage;
- }
-
- this.chromePort.postMessage({method: 'GetCategories'});
- return (this.pendingGetCategoriesMessage = defer<string[]>());
- }
-
- async getTraceBufferUsage(): Promise<number> {
- if (!this.isPortConnected) return 0;
- const bufferStats = await this.getBufferStats();
- let percentageUsed = -1;
- for (const buffer of bufferStats) {
- const used = assertExists(buffer.bytesWritten);
- const total = assertExists(buffer.bufferSize);
- if (total >= 0) {
- percentageUsed = Math.max(percentageUsed, used / total);
- }
- }
-
- if (percentageUsed === -1) {
- throw new RecordingError(BUFFER_USAGE_INCORRECT_FORMAT);
- }
- return percentageUsed;
- }
-
- initConnection(): void {
- this.chromePort.onMessage.addListener((message: ChromeExtensionMessage) => {
- this.handleExtensionMessage(message);
- });
- }
-
- private getBufferStats(): Promise<IBufferStats[]> {
- this.chromePort.postMessage({method: 'GetTraceStats'});
-
- const statsMessage = defer<IBufferStats[]>();
- this.pendingStatsMessages.push(statsMessage);
- return statsMessage;
- }
-
- private terminateConnection(): void {
- this.chromePort.postMessage({method: 'FreeBuffers'});
- this.clearState();
- }
-
- private clearState() {
- this.chromePort.disconnect();
- this.isPortConnected = false;
- for (const statsMessage of this.pendingStatsMessages) {
- statsMessage.reject(new RecordingError(BUFFER_USAGE_NOT_ACCESSIBLE));
- }
- this.pendingStatsMessages = [];
- this.pendingGetCategoriesMessage = undefined;
- }
-
- private handleExtensionMessage(message: ChromeExtensionMessage) {
- if (isChromeExtensionError(message)) {
- this.terminateConnection();
- this.tracingSessionListener.onError(message.error);
- } else if (isChromeExtensionStatus(message)) {
- this.tracingSessionListener.onStatus(message.status);
- } else if (isReadBuffersResponse(message)) {
- if (!message.slices) {
- return;
- }
- for (const messageSlice of message.slices) {
- // The extension sends the binary data as a string.
- // see http://shortn/_oPmO2GT6Vb
- if (typeof messageSlice.data !== 'string') {
- throw new RecordingError(MALFORMED_EXTENSION_MESSAGE);
- }
- const decodedSlice = {
- data: binaryDecode(messageSlice.data),
- };
- this.partialPacket.push(decodedSlice);
- if (messageSlice.lastSliceForPacket) {
- let bufferSize = 0;
- for (const slice of this.partialPacket) {
- bufferSize += slice.data!.length;
- }
-
- const completeTrace = new Uint8Array(bufferSize);
- let written = 0;
- for (const slice of this.partialPacket) {
- const data = slice.data!;
- completeTrace.set(data, written);
- written += data.length;
- }
- // The trace already comes encoded as a proto.
- this.tracingSessionListener.onTraceData(completeTrace);
- this.terminateConnection();
- }
- }
- } else if (isGetCategoriesResponse(message)) {
- assertExists(this.pendingGetCategoriesMessage).resolve(
- message.categories,
- );
- this.pendingGetCategoriesMessage = undefined;
- } else if (isEnableTracingResponse(message)) {
- // Once the service notifies us that a tracing session is enabled,
- // we can start streaming the response using 'ReadBuffers'.
- this.chromePort.postMessage({method: 'ReadBuffers'});
- } else if (isGetTraceStatsResponse(message)) {
- const maybePendingStatsMessage = this.pendingStatsMessages.shift();
- if (maybePendingStatsMessage) {
- maybePendingStatsMessage.resolve(
- message?.traceStats?.bufferStats || [],
- );
- }
- } else if (isFreeBuffersResponse(message)) {
- // No action required. If we successfully read a whole trace,
- // we close the connection. Alternatively, if the tracing finishes
- // with an exception or if the user cancels it, we also close the
- // connection.
- } else {
- assertTrue(isDisableTracingResponse(message));
- // No action required. Same reasoning as for FreeBuffers.
- }
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts
deleted file mode 100644
index a03b791..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts
+++ /dev/null
@@ -1,83 +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 {defer} from '../../../base/deferred';
-import {
- ByteStream,
- OnStreamCloseCallback,
- OnStreamDataCallback,
-} from './recording_interfaces_v2';
-
-// A HostOsByteStream instantiates a websocket connection to the host OS.
-// It exposes an API to write commands to this websocket and read its output.
-export class HostOsByteStream implements ByteStream {
- // handshakeSignal will be resolved with the stream when the websocket
- // connection becomes open.
- private handshakeSignal = defer<HostOsByteStream>();
- private _isConnected: boolean = false;
- private websocket: WebSocket;
- private onStreamDataCallbacks: OnStreamDataCallback[] = [];
- private onStreamCloseCallbacks: OnStreamCloseCallback[] = [];
-
- private constructor(websocketUrl: string) {
- this.websocket = new WebSocket(websocketUrl);
- this.websocket.onmessage = this.onMessage.bind(this);
- this.websocket.onopen = this.onOpen.bind(this);
- }
-
- addOnStreamDataCallback(onStreamData: OnStreamDataCallback): void {
- this.onStreamDataCallbacks.push(onStreamData);
- }
-
- addOnStreamCloseCallback(onStreamClose: OnStreamCloseCallback): void {
- this.onStreamCloseCallbacks.push(onStreamClose);
- }
-
- close(): void {
- this.websocket.close();
- for (const onStreamClose of this.onStreamCloseCallbacks) {
- onStreamClose();
- }
- this.onStreamDataCallbacks = [];
- this.onStreamCloseCallbacks = [];
- }
-
- async closeAndWaitForTeardown(): Promise<void> {
- this.close();
- }
-
- isConnected(): boolean {
- return this._isConnected;
- }
-
- write(msg: string | Uint8Array): void {
- this.websocket.send(msg);
- }
-
- private async onMessage(evt: MessageEvent) {
- for (const onStreamData of this.onStreamDataCallbacks) {
- const arrayBufferResponse = await evt.data.arrayBuffer();
- onStreamData(new Uint8Array(arrayBufferResponse));
- }
- }
-
- private onOpen() {
- this._isConnected = true;
- this.handshakeSignal.resolve(this);
- }
-
- static create(websocketUrl: string): Promise<HostOsByteStream> {
- return new HostOsByteStream(websocketUrl).handshakeSignal;
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
index 471ebc8..d6d69ad 100644
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
@@ -15,15 +15,9 @@
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 '../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 {
RecordingTargetV2,
@@ -31,21 +25,8 @@
TracingSession,
TracingSessionListener,
} from './recording_interfaces_v2';
-import {
- BUFFER_USAGE_NOT_ACCESSIBLE,
- RECORDING_IN_PROGRESS,
-} from './recording_utils';
-import {
- ANDROID_WEBSOCKET_TARGET_FACTORY,
- AndroidWebsocketTargetFactory,
-} from './target_factories/android_websocket_target_factory';
-import {ANDROID_WEBUSB_TARGET_FACTORY} from './target_factories/android_webusb_target_factory';
-import {
- HOST_OS_TARGET_FACTORY,
- HostOsTargetFactory,
-} from './target_factories/host_os_target_factory';
+import {RECORDING_IN_PROGRESS} from './recording_utils';
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
@@ -194,57 +175,6 @@
);
}
}
-
- cancel() {
- if (this.tracingSession) {
- this.tracingSession.cancel();
- } else {
- // In some cases, the tracingSession may not be available to the
- // TracingSessionWrapper when the user cancels it.
- // For instance:
- // 1. The user clicked 'Start'.
- // 2. They clicked 'Stop' without authorizing on the device.
- // 3. They clicked 'Start'.
- // 4. They authorized on the device.
- // In these cases, we want to cancel the tracing session as soon as it
- // becomes available. Therefore, we keep the `isCancelled` boolean and
- // check it when we receive the tracing session.
- this.isCancelled = true;
- }
- this.controller.maybeClearRecordingState(this);
- }
-
- stop() {
- const stateGeneratioNr = this.controller.getStateGeneration();
- if (this.tracingSession) {
- this.tracingSession.stop();
- this.controller.maybeSetState(
- this,
- RecordingState.WAITING_FOR_TRACE_DISPLAY,
- stateGeneratioNr,
- );
- } else {
- // In some cases, the tracingSession may not be available to the
- // TracingSessionWrapper when the user stops it.
- // For instance:
- // 1. The user clicked 'Start'.
- // 2. They clicked 'Stop' without authorizing on the device.
- // 3. They clicked 'Start'.
- // 4. They authorized on the device.
- // In these cases, we want to cancel the tracing session as soon as it
- // becomes available. Therefore, we keep the `isCancelled` boolean and
- // check it when we receive the tracing session.
- this.isCancelled = true;
- this.controller.maybeClearRecordingState(this);
- }
- }
-
- getTraceBufferUsage(): Promise<number> {
- if (!this.tracingSession) {
- throw new RecordingError(BUFFER_USAGE_NOT_ACCESSIBLE);
- }
- return this.tracingSession.getTraceBufferUsage();
- }
}
// Keeps track of the state the Ui is in. Has methods which are executed on
@@ -262,8 +192,6 @@
// (Ex: Android) it is only created after we have succesfully authenticated
// with the target.
private tracingSessionWrapper?: TracingSessionWrapper = undefined;
- // How much of the buffer is used for the current tracing session.
- private bufferUsagePercentage: number = 0;
// A counter for state modifications. We use this to ensure that state
// transitions don't override one another in async functions.
private stateGeneration = 0;
@@ -273,14 +201,6 @@
this.recMgr = recMgr;
}
- getBufferUsagePercentage(): number {
- return this.bufferUsagePercentage;
- }
-
- getState(): RecordingState {
- return this.state;
- }
-
getStateGeneration(): number {
return this.stateGeneration;
}
@@ -298,7 +218,7 @@
}
this.setState(state);
this.recMgr.setRecordingStatus(undefined);
- scheduleFullRedraw();
+ this.app.raf.scheduleFullRedraw();
}
maybeClearRecordingState(tracingSessionWrapper: TracingSessionWrapper): void {
@@ -392,148 +312,22 @@
if (!this.target) {
this.setState(RecordingState.NO_TARGET);
- scheduleFullRedraw();
+ this.app.raf.scheduleFullRedraw();
return;
}
this.setState(RecordingState.TARGET_SELECTED);
- scheduleFullRedraw();
+ this.app.raf.scheduleFullRedraw();
this.tracingSessionWrapper = this.createTracingSessionWrapper(this.target);
this.tracingSessionWrapper.fetchTargetInfo();
}
- async addAndroidDevice(): Promise<void> {
- try {
- const target = await targetFactoryRegistry
- .get(ANDROID_WEBUSB_TARGET_FACTORY)
- .connectNewTarget();
- this.selectTarget(target);
- } catch (e) {
- if (e instanceof RecordingError) {
- showRecordingModal(e.message);
- } else {
- throw e;
- }
- }
- }
-
- onTargetSelection(targetName: string): void {
- assertTrue(
- RecordingState.NO_TARGET <= this.state &&
- this.state < RecordingState.RECORDING,
- );
- const allTargets = targetFactoryRegistry.listTargets();
- this.selectTarget(allTargets.find((t) => t.getInfo().name === targetName));
- }
-
- onStartRecordingPressed(): void {
- assertTrue(RecordingState.TARGET_INFO_DISPLAYED === this.state);
- location.href = '#!/record/instructions';
- autosaveConfigStore.save(this.recMgr.state.recordConfig);
-
- const target = this.getTarget();
- const targetInfo = target.getInfo();
- this.app.analytics.logEvent(
- 'Record Trace',
- `Record trace (${targetInfo.targetType})`,
- );
- const traceConfig = genTraceConfig(
- this.recMgr.state.recordConfig,
- targetInfo,
- );
-
- this.tracingSessionWrapper = this.createTracingSessionWrapper(target);
- this.tracingSessionWrapper.start(traceConfig);
- }
-
- onCancel() {
- assertTrue(
- RecordingState.AUTH_P2 <= this.state &&
- this.state <= RecordingState.RECORDING,
- );
- // The 'Cancel' button will only be shown after a `tracingSessionWrapper`
- // is created.
- this.getTracingSessionWrapper().cancel();
- }
-
- onStop() {
- assertTrue(
- RecordingState.AUTH_P2 <= this.state &&
- this.state <= RecordingState.RECORDING,
- );
- // The 'Stop' button will only be shown after a `tracingSessionWrapper`
- // is created.
- this.getTracingSessionWrapper().stop();
- }
-
- async fetchBufferUsage() {
- assertTrue(this.state >= RecordingState.AUTH_P2);
- if (!this.tracingSessionWrapper) return;
- const session = this.tracingSessionWrapper;
-
- try {
- const usage = await session.getTraceBufferUsage();
- if (this.tracingSessionWrapper === session) {
- this.bufferUsagePercentage = usage;
- }
- } catch (e) {
- // We ignore RecordingErrors because they are not necessary for the trace
- // to be successfully collected.
- if (!(e instanceof RecordingError)) {
- throw e;
- }
- }
- // We redraw if:
- // 1. We received a correct buffer usage value.
- // 2. We receive a RecordingError.
- scheduleFullRedraw();
- }
-
- initFactories() {
- assertTrue(this.state <= RecordingState.TARGET_INFO_DISPLAYED);
- for (const targetFactory of targetFactoryRegistry.listTargetFactories()) {
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
- if (targetFactory) {
- targetFactory.setOnTargetChange(this.onTargetChange.bind(this));
- }
- }
-
- if (targetFactoryRegistry.has(ANDROID_WEBSOCKET_TARGET_FACTORY)) {
- const websocketTargetFactory = targetFactoryRegistry.get(
- ANDROID_WEBSOCKET_TARGET_FACTORY,
- ) as AndroidWebsocketTargetFactory;
- websocketTargetFactory.tryEstablishWebsocket(DEFAULT_ADB_WEBSOCKET_URL);
- }
- if (targetFactoryRegistry.has(HOST_OS_TARGET_FACTORY)) {
- const websocketTargetFactory = targetFactoryRegistry.get(
- HOST_OS_TARGET_FACTORY,
- ) as HostOsTargetFactory;
- websocketTargetFactory.tryEstablishWebsocket(
- DEFAULT_TRACED_WEBSOCKET_URL,
- );
- }
- }
-
- shouldShowTargetSelection(): boolean {
- return (
- RecordingState.NO_TARGET <= this.state &&
- this.state < RecordingState.RECORDING
- );
- }
-
- shouldShowStopCancelButtons(): boolean {
- return (
- RecordingState.AUTH_P2 <= this.state &&
- this.state <= RecordingState.RECORDING
- );
- }
-
private onTargetChange() {
const allTargets = targetFactoryRegistry.listTargets();
// If the change happens for an existing target, the controller keeps the
// currently selected target in focus.
if (this.target && allTargets.includes(this.target)) {
- scheduleFullRedraw();
+ this.app.raf.scheduleFullRedraw();
return;
}
// If the change happens to a new target or the controller does not have a
@@ -548,30 +342,16 @@
}
private clearRecordingState(): void {
- this.bufferUsagePercentage = 0;
this.tracingSessionWrapper = undefined;
this.setState(RecordingState.TARGET_INFO_DISPLAYED);
this.recMgr.setRecordingStatus(undefined);
// Redrawing because this method has changed the RecordingState, which will
// affect the display of the record_page.
- scheduleFullRedraw();
+ this.app.raf.scheduleFullRedraw();
}
private setState(state: RecordingState) {
this.state = state;
this.stateGeneration += 1;
}
-
- private getTarget(): RecordingTargetV2 {
- assertTrue(RecordingState.TARGET_INFO_DISPLAYED === this.state);
- return assertExists(this.target);
- }
-
- private getTracingSessionWrapper(): TracingSessionWrapper {
- assertTrue(
- RecordingState.ASK_TO_FORCE_P2 <= this.state &&
- this.state <= RecordingState.RECORDING,
- );
- return assertExists(this.tracingSessionWrapper);
- }
}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts
deleted file mode 100644
index 03cda1f..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts
+++ /dev/null
@@ -1,268 +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 {
- OnTargetChangeCallback,
- RecordingTargetV2,
- TargetFactory,
-} from '../recording_interfaces_v2';
-import {
- buildAbdWebsocketCommand,
- WEBSOCKET_CLOSED_ABNORMALLY_CODE,
-} from '../recording_utils';
-import {AndroidWebsocketTarget} from '../targets/android_websocket_target';
-
-export const ANDROID_WEBSOCKET_TARGET_FACTORY = 'AndroidWebsocketTargetFactory';
-
-// https://cs.android.com/android/platform/superproject/+/main:packages/
-// modules/adb/SERVICES.TXT;l=135
-const PREFIX_LENGTH = 4;
-
-// information received over the websocket regarding a device
-// Ex: "${serialNumber} authorized"
-interface ListedDevice {
- serialNumber: string;
- // Full list of connection states can be seen at:
- // go/codesearch/android/packages/modules/adb/adb.cpp;l=115-139
- connectionState: string;
-}
-
-// Contains the result of parsing a message received over websocket.
-interface ParsingResult {
- listedDevices: ListedDevice[];
- messageRemainder: string;
-}
-
-// We issue the command 'track-devices' which will encode the short form
-// of the device:
-// see go/codesearch/android/packages/modules/adb/services.cpp;l=244-245
-// and go/codesearch/android/packages/modules/adb/transport.cpp;l=1417-1420
-// Therefore a line will contain solely the device serial number and the
-// connectionState (and no other properties).
-function parseListedDevice(line: string): ListedDevice | undefined {
- const parts = line.split('\t');
- if (parts.length === 2) {
- return {
- serialNumber: parts[0],
- connectionState: parts[1],
- };
- }
- return undefined;
-}
-
-export function parseWebsocketResponse(message: string): ParsingResult {
- // A response we receive on the websocket contains multiple messages:
- // "{m1.length}{m1.payload}{m2.length}{m2.payload}..."
- // where m1, m2 are messages
- // Each message has the form:
- // "{message.length}SN1\t${connectionState1}\nSN2\t${connectionState2}\n..."
- // where SN1, SN2 are device serial numbers
- // and connectionState1, connectionState2 are adb connection states, created
- // here: go/codesearch/android/packages/modules/adb/adb.cpp;l=115-139
- const latestStatusByDevice: Map<string, string> = new Map();
- while (message.length >= PREFIX_LENGTH) {
- const payloadLength = parseInt(message.substring(0, PREFIX_LENGTH), 16);
- const prefixAndPayloadLength = PREFIX_LENGTH + payloadLength;
- if (message.length < prefixAndPayloadLength) {
- break;
- }
-
- const payload = message.substring(PREFIX_LENGTH, prefixAndPayloadLength);
- for (const line of payload.split('\n')) {
- const listedDevice = parseListedDevice(line);
- if (listedDevice) {
- // We overwrite previous states for the same serial number.
- latestStatusByDevice.set(
- listedDevice.serialNumber,
- listedDevice.connectionState,
- );
- }
- }
- message = message.substring(prefixAndPayloadLength);
- }
- const listedDevices: ListedDevice[] = [];
- for (const [
- serialNumber,
- connectionState,
- ] of latestStatusByDevice.entries()) {
- listedDevices.push({serialNumber, connectionState});
- }
- return {listedDevices, messageRemainder: message};
-}
-
-export class WebsocketConnection {
- private targets: Map<string, AndroidWebsocketTarget> = new Map<
- string,
- AndroidWebsocketTarget
- >();
- private pendingData: string = '';
-
- constructor(
- private websocket: WebSocket,
- private maybeClearConnection: (connection: WebsocketConnection) => void,
- private onTargetChange: OnTargetChangeCallback,
- ) {
- this.initWebsocket();
- }
-
- listTargets(): RecordingTargetV2[] {
- return Array.from(this.targets.values());
- }
-
- // Setup websocket callbacks.
- initWebsocket(): void {
- this.websocket.onclose = (ev: CloseEvent) => {
- if (ev.code === WEBSOCKET_CLOSED_ABNORMALLY_CODE) {
- console.info(
- `It's safe to ignore the 'WebSocket connection to ${this.websocket.url} error above, if present. It occurs when ` +
- 'checking the connection to the local Websocket server.',
- );
- }
- this.maybeClearConnection(this);
- this.close();
- };
-
- // once the websocket is open, we start tracking the devices
- this.websocket.onopen = () => {
- this.websocket.send(buildAbdWebsocketCommand('host:track-devices'));
- };
-
- this.websocket.onmessage = async (evt: MessageEvent) => {
- let resp = await evt.data.text();
- if (resp.substr(0, 4) === 'OKAY') {
- resp = resp.substr(4);
- }
- const parsingResult = parseWebsocketResponse(this.pendingData + resp);
- this.pendingData = parsingResult.messageRemainder;
- this.trackDevices(parsingResult.listedDevices);
- };
- }
-
- close() {
- // The websocket connection may have already been closed by the websocket
- // server.
- if (this.websocket.readyState === this.websocket.OPEN) {
- this.websocket.close();
- }
- // Disconnect all the targets, to release all the websocket connections that
- // they hold and end their tracing sessions.
- for (const target of this.targets.values()) {
- target.disconnect();
- }
- this.targets.clear();
-
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
- if (this.onTargetChange) {
- this.onTargetChange();
- }
- }
-
- getUrl() {
- return this.websocket.url;
- }
-
- // Handle messages received over the websocket regarding devices connecting
- // or disconnecting.
- private trackDevices(listedDevices: ListedDevice[]) {
- // When a SN becomes offline, we should remove it from the list
- // of targets. Otherwise, we should check if it maps to a target. If the
- // SN does not map to a target, we should create one for it.
- let targetsUpdated = false;
- for (const listedDevice of listedDevices) {
- if (['offline', 'unknown'].includes(listedDevice.connectionState)) {
- const target = this.targets.get(listedDevice.serialNumber);
- if (target === undefined) {
- continue;
- }
- target.disconnect();
- this.targets.delete(listedDevice.serialNumber);
- targetsUpdated = true;
- } else if (!this.targets.has(listedDevice.serialNumber)) {
- this.targets.set(
- listedDevice.serialNumber,
- new AndroidWebsocketTarget(
- listedDevice.serialNumber,
- this.websocket.url,
- this.onTargetChange,
- ),
- );
- targetsUpdated = true;
- }
- }
-
- // Notify the calling code that the list of targets has been updated.
- if (targetsUpdated) {
- this.onTargetChange();
- }
- }
-}
-
-export class AndroidWebsocketTargetFactory implements TargetFactory {
- readonly kind = ANDROID_WEBSOCKET_TARGET_FACTORY;
- private onTargetChange: OnTargetChangeCallback = () => {};
- private websocketConnection?: WebsocketConnection;
-
- getName() {
- return 'Android Websocket';
- }
-
- listTargets(): RecordingTargetV2[] {
- return this.websocketConnection
- ? this.websocketConnection.listTargets()
- : [];
- }
-
- listRecordingProblems(): string[] {
- return [];
- }
-
- // This interface method can not return anything because a websocket target
- // can not be created on user input. It can only be created when the websocket
- // server detects a new target.
- connectNewTarget(): Promise<RecordingTargetV2> {
- return Promise.reject(
- new Error(
- 'The websocket can only automatically connect targets ' +
- 'when they become available.',
- ),
- );
- }
-
- tryEstablishWebsocket(websocketUrl: string) {
- if (this.websocketConnection) {
- if (this.websocketConnection.getUrl() === websocketUrl) {
- return;
- } else {
- this.websocketConnection.close();
- }
- }
-
- const websocket = new WebSocket(websocketUrl);
- this.websocketConnection = new WebsocketConnection(
- websocket,
- this.maybeClearConnection,
- this.onTargetChange,
- );
- }
-
- maybeClearConnection(connection: WebsocketConnection): void {
- if (this.websocketConnection === connection) {
- this.websocketConnection = undefined;
- }
- }
-
- setOnTargetChange(onTargetChange: OnTargetChangeCallback) {
- this.onTargetChange = onTargetChange;
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/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
deleted file mode 100644
index 80d3dcd..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory_unittest.ts
+++ /dev/null
@@ -1,56 +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 {parseWebsocketResponse} from './android_websocket_target_factory';
-
-test('parse device disconnection', () => {
- const message = '001702121FQC20XXXX\toffline\n';
- const response = parseWebsocketResponse(message);
- expect(response.messageRemainder).toEqual('');
- expect(response.listedDevices.length).toEqual(1);
- expect(response.listedDevices[0].serialNumber).toEqual('02121FQC20XXXX');
- expect(response.listedDevices[0].connectionState).toEqual('offline');
-});
-
-test('parse two devices connected in the same message', () => {
- const message = '003202121FQC20XXXX\tdevice\n06131FDD40YYYY\tunauthorized\n';
- const response = parseWebsocketResponse(message);
- expect(response.messageRemainder).toEqual('');
- expect(response.listedDevices.length).toEqual(2);
- expect(response.listedDevices[0].serialNumber).toEqual('02121FQC20XXXX');
- expect(response.listedDevices[0].connectionState).toEqual('device');
- expect(response.listedDevices[1].serialNumber).toEqual('06131FDD40YYYY');
- expect(response.listedDevices[1].connectionState).toEqual('unauthorized');
-});
-
-test('parse device connection in multiple messages', () => {
- const message =
- '001702121FQC20XXXX\toffline\n001602121FQC20XXXX\tdevice\n' +
- '001602121FQC20XXXX\tdevice\n';
- const response = parseWebsocketResponse(message);
- expect(response.messageRemainder).toEqual('');
- expect(response.listedDevices.length).toEqual(1);
- expect(response.listedDevices[0].serialNumber).toEqual('02121FQC20XXXX');
- expect(response.listedDevices[0].connectionState).toEqual('device');
-});
-
-test('parse with remainder', () => {
- const remainder = 'FFFFsome_other_stuff';
- const message = `001602121FQC20XXXX\tdevice\n${remainder}`;
- const response = parseWebsocketResponse(message);
- expect(response.messageRemainder).toEqual(remainder);
- expect(response.listedDevices.length).toEqual(1);
- expect(response.listedDevices[0].serialNumber).toEqual('02121FQC20XXXX');
- expect(response.listedDevices[0].connectionState).toEqual('device');
-});
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts
deleted file mode 100644
index a969c31..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts
+++ /dev/null
@@ -1,155 +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 {assertExists} from '../../../../base/logging';
-import {AdbKeyManager} from '../auth/adb_key_manager';
-import {RecordingError} from '../recording_error_handling';
-import {
- OnTargetChangeCallback,
- RecordingTargetV2,
- TargetFactory,
-} from '../recording_interfaces_v2';
-import {ADB_DEVICE_FILTER, findInterfaceAndEndpoint} from '../recording_utils';
-import {AndroidWebusbTarget} from '../targets/android_webusb_target';
-
-export const ANDROID_WEBUSB_TARGET_FACTORY = 'AndroidWebusbTargetFactory';
-const SERIAL_NUMBER_ISSUE = 'an invalid serial number';
-const ADB_INTERFACE_ISSUE = 'an incompatible adb interface';
-
-interface DeviceValidity {
- isValid: boolean;
- issues: string[];
-}
-
-function createDeviceErrorMessage(device: USBDevice, issue: string): string {
- const productName = device.productName;
- return `USB device${productName ? ' ' + productName : ''} has ${issue}`;
-}
-
-export class AndroidWebusbTargetFactory implements TargetFactory {
- readonly kind = ANDROID_WEBUSB_TARGET_FACTORY;
- onTargetChange: OnTargetChangeCallback = () => {};
- private recordingProblems: string[] = [];
- private targets: Map<string, AndroidWebusbTarget> = new Map<
- string,
- AndroidWebusbTarget
- >();
- // AdbKeyManager should only be instantiated once, so we can use the same key
- // for all devices.
- private keyManager: AdbKeyManager = new AdbKeyManager();
-
- constructor(private usb: USB) {
- this.init();
- }
-
- getName() {
- return 'Android WebUsb';
- }
-
- listTargets(): RecordingTargetV2[] {
- return Array.from(this.targets.values());
- }
-
- listRecordingProblems(): string[] {
- return this.recordingProblems;
- }
-
- async connectNewTarget(): Promise<RecordingTargetV2> {
- let device: USBDevice;
- try {
- device = await this.usb.requestDevice({filters: [ADB_DEVICE_FILTER]});
- } catch (e) {
- throw new RecordingError(getErrorMessage(e));
- }
-
- const deviceValid = this.checkDeviceValidity(device);
- if (!deviceValid.isValid) {
- throw new RecordingError(deviceValid.issues.join('\n'));
- }
-
- const androidTarget = new AndroidWebusbTarget(
- device,
- this.keyManager,
- this.onTargetChange,
- );
- this.targets.set(assertExists(device.serialNumber), androidTarget);
- return androidTarget;
- }
-
- setOnTargetChange(onTargetChange: OnTargetChangeCallback) {
- this.onTargetChange = onTargetChange;
- }
-
- private async init() {
- let devices: USBDevice[] = [];
- try {
- devices = await this.usb.getDevices();
- } catch (_) {
- return; // WebUSB not available or disallowed in iframe.
- }
-
- for (const device of devices) {
- if (this.checkDeviceValidity(device).isValid) {
- this.targets.set(
- assertExists(device.serialNumber),
- new AndroidWebusbTarget(device, this.keyManager, this.onTargetChange),
- );
- }
- }
-
- this.usb.addEventListener('connect', (ev: USBConnectionEvent) => {
- if (this.checkDeviceValidity(ev.device).isValid) {
- this.targets.set(
- assertExists(ev.device.serialNumber),
- new AndroidWebusbTarget(
- ev.device,
- this.keyManager,
- this.onTargetChange,
- ),
- );
- this.onTargetChange();
- }
- });
-
- this.usb.addEventListener('disconnect', async (ev: USBConnectionEvent) => {
- // We don't check device validity when disconnecting because if the device
- // is invalid we would not have connected in the first place.
- const serialNumber = assertExists(ev.device.serialNumber);
- await assertExists(this.targets.get(serialNumber)).disconnect(
- `Device with serial ${serialNumber} was disconnected.`,
- );
- this.targets.delete(serialNumber);
- this.onTargetChange();
- });
- }
-
- private checkDeviceValidity(device: USBDevice): DeviceValidity {
- const deviceValidity: DeviceValidity = {isValid: true, issues: []};
- if (!device.serialNumber) {
- deviceValidity.issues.push(
- createDeviceErrorMessage(device, SERIAL_NUMBER_ISSUE),
- );
- deviceValidity.isValid = false;
- }
- if (!findInterfaceAndEndpoint(device)) {
- deviceValidity.issues.push(
- createDeviceErrorMessage(device, ADB_INTERFACE_ISSUE),
- );
- deviceValidity.isValid = false;
- }
- this.recordingProblems.push(...deviceValidity.issues);
- return deviceValidity;
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory.ts
deleted file mode 100644
index 68630ee..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory.ts
+++ /dev/null
@@ -1,100 +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 {RecordingError} from '../recording_error_handling';
-import {
- OnTargetChangeCallback,
- RecordingTargetV2,
- TargetFactory,
-} from '../recording_interfaces_v2';
-import {
- EXTENSION_ID,
- EXTENSION_NOT_INSTALLED,
- isCrOS,
- isWindows,
-} from '../recording_utils';
-import {targetFactoryRegistry} from '../target_factory_registry';
-import {ChromeTarget} from '../targets/chrome_target';
-
-export const CHROME_TARGET_FACTORY = 'ChromeTargetFactory';
-
-export class ChromeTargetFactory implements TargetFactory {
- readonly kind = CHROME_TARGET_FACTORY;
- // We only check the connection once at the beginning to:
- // a) Avoid creating a 'Port' object every time 'getInfo' is called.
- // b) When a new Port is created, the extension starts communicating with it
- // and leaves aside the old Port objects, so creating a new Port would break
- // any ongoing tracing session.
- isExtensionInstalled: boolean = false;
- private targets: ChromeTarget[] = [];
-
- constructor() {
- this.init();
- }
-
- init() {
- const testPort = chrome.runtime.connect(EXTENSION_ID);
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
- this.isExtensionInstalled = !!testPort;
- testPort.disconnect();
-
- if (!this.isExtensionInstalled) {
- return;
- }
- this.targets.push(new ChromeTarget('Chrome', 'CHROME'));
- if (isCrOS(navigator.userAgent)) {
- this.targets.push(new ChromeTarget('ChromeOS', 'CHROME_OS'));
- }
- // Pass through the chrome target since it launches ETW on windows through
- // same path as when we start chrome tracing.
- if (isWindows(navigator.userAgent)) {
- this.targets.push(new ChromeTarget('Windows Desktop', 'WINDOWS'));
- }
- }
-
- connectNewTarget(): Promise<RecordingTargetV2> {
- throw new RecordingError(
- 'Can not create a new Chrome target.' +
- 'All Chrome targets are created at factory initialisation.',
- );
- }
-
- getName(): string {
- return 'Chrome';
- }
-
- listRecordingProblems(): string[] {
- const recordingProblems = [];
- if (!this.isExtensionInstalled) {
- recordingProblems.push(EXTENSION_NOT_INSTALLED);
- }
- return recordingProblems;
- }
-
- listTargets(): RecordingTargetV2[] {
- return this.targets;
- }
-
- setOnTargetChange(onTargetChange: OnTargetChangeCallback): void {
- for (const target of this.targets) {
- target.onTargetChange = onTargetChange;
- }
- }
-}
-
-// We only instantiate the factory if Perfetto UI is open in the Chrome browser.
-// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-if (globalThis.chrome && chrome.runtime) {
- targetFactoryRegistry.register(new ChromeTargetFactory());
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory_unittest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory_unittest.ts
deleted file mode 100644
index d238391..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory_unittest.ts
+++ /dev/null
@@ -1,43 +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 {isCrOS, isLinux, isMacOs} from '../recording_utils';
-
-test('parse Chrome on Chrome OS user agent', () => {
- const userAgent =
- 'Mozilla/5.0 (X11; CrOS x86_64 14816.99.0) ' +
- 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 ' +
- 'Safari/537.36';
- expect(isCrOS(userAgent)).toBe(true);
- expect(isMacOs(userAgent)).toBe(false);
- expect(isLinux(userAgent)).toBe(false);
-});
-
-test('parse Chrome on Mac user agent', () => {
- const userAgent =
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' +
- 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36';
- expect(isCrOS(userAgent)).toBe(false);
- expect(isMacOs(userAgent)).toBe(true);
- expect(isLinux(userAgent)).toBe(false);
-});
-
-test('parse Chrome on Linux user agent', () => {
- const userAgent =
- 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' +
- '(KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36';
- expect(isCrOS(userAgent)).toBe(false);
- expect(isMacOs(userAgent)).toBe(false);
- expect(isLinux(userAgent)).toBe(true);
-});
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/host_os_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/host_os_target_factory.ts
deleted file mode 100644
index 09e73e7..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/host_os_target_factory.ts
+++ /dev/null
@@ -1,85 +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 {RecordingError} from '../recording_error_handling';
-import {
- OnTargetChangeCallback,
- RecordingTargetV2,
- TargetFactory,
-} from '../recording_interfaces_v2';
-import {isLinux, isMacOs} from '../recording_utils';
-import {targetFactoryRegistry} from '../target_factory_registry';
-import {HostOsTarget} from '../targets/host_os_target';
-
-export const HOST_OS_TARGET_FACTORY = 'HostOsTargetFactory';
-
-export class HostOsTargetFactory implements TargetFactory {
- readonly kind = HOST_OS_TARGET_FACTORY;
- private target?: HostOsTarget;
- private onTargetChange: OnTargetChangeCallback = () => {};
-
- connectNewTarget(): Promise<RecordingTargetV2> {
- throw new RecordingError(
- 'Can not create a new Host OS target.' +
- 'The Host OS target is created at factory initialisation.',
- );
- }
-
- getName(): string {
- return 'HostOs';
- }
-
- listRecordingProblems(): string[] {
- return [];
- }
-
- listTargets(): RecordingTargetV2[] {
- if (this.target) {
- return [this.target];
- }
- return [];
- }
-
- tryEstablishWebsocket(websocketUrl: string) {
- if (this.target) {
- if (this.target.getUrl() === websocketUrl) {
- return;
- } else {
- this.target.disconnect();
- }
- }
- this.target = new HostOsTarget(
- websocketUrl,
- this.maybeClearTarget.bind(this),
- this.onTargetChange,
- );
- this.onTargetChange();
- }
-
- maybeClearTarget(target: HostOsTarget): void {
- if (this.target === target) {
- this.target = undefined;
- this.onTargetChange();
- }
- }
-
- setOnTargetChange(onTargetChange: OnTargetChangeCallback): void {
- this.onTargetChange = onTargetChange;
- }
-}
-
-// We instantiate the host target factory only on Mac, Linux, and Windows.
-if (isMacOs(navigator.userAgent) || isLinux(navigator.userAgent)) {
- targetFactoryRegistry.register(new HostOsTargetFactory());
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/index.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/index.ts
deleted file mode 100644
index 3c1e3af..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/index.ts
+++ /dev/null
@@ -1,19 +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 './android_webusb_target_factory';
-import './android_websocket_target_factory';
-import './chrome_target_factory';
-import './host_os_target_factory';
-import './virtual_target_factory';
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/virtual_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/virtual_target_factory.ts
deleted file mode 100644
index a6b98ca..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/virtual_target_factory.ts
+++ /dev/null
@@ -1,60 +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 {RecordingError} from '../recording_error_handling';
-import {
- OnTargetChangeCallback,
- RecordingTargetV2,
- TargetFactory,
-} from '../recording_interfaces_v2';
-import {targetFactoryRegistry} from '../target_factory_registry';
-import {AndroidVirtualTarget} from '../targets/android_virtual_target';
-
-const VIRTUAL_TARGET_FACTORY = 'VirtualTargetFactory';
-
-export class VirtualTargetFactory implements TargetFactory {
- readonly kind: string = VIRTUAL_TARGET_FACTORY;
- private targets: AndroidVirtualTarget[];
-
- constructor() {
- this.targets = [];
- this.targets.push(new AndroidVirtualTarget('Android Q', 29));
- this.targets.push(new AndroidVirtualTarget('Android P', 28));
- this.targets.push(new AndroidVirtualTarget('Android O-', 27));
- }
-
- connectNewTarget(): Promise<RecordingTargetV2> {
- throw new RecordingError(
- 'Can not create a new virtual target.' +
- 'All virtual targets are created at factory initialisation.',
- );
- }
-
- getName(): string {
- return 'Virtual';
- }
-
- listRecordingProblems(): string[] {
- return [];
- }
-
- listTargets(): RecordingTargetV2[] {
- return this.targets;
- }
-
- // Virtual targets won't change.
- setOnTargetChange(_: OnTargetChangeCallback): void {}
-}
-
-targetFactoryRegistry.register(new VirtualTargetFactory());
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts
deleted file mode 100644
index 0bac1e4..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts
+++ /dev/null
@@ -1,170 +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 {fetchWithTimeout} from '../../../../base/http_utils';
-import {exists} from '../../../../base/utils';
-import {VERSION} from '../../../../gen/perfetto_version';
-import {AdbConnectionImpl} from '../adb_connection_impl';
-import {
- DataSource,
- OnTargetChangeCallback,
- RecordingTargetV2,
- TargetInfo,
- TracingSession,
- TracingSessionListener,
-} from '../recording_interfaces_v2';
-import {
- CUSTOM_TRACED_CONSUMER_SOCKET_PATH,
- DEFAULT_TRACED_CONSUMER_SOCKET_PATH,
- TRACEBOX_DEVICE_PATH,
- TRACEBOX_FETCH_TIMEOUT,
-} from '../recording_utils';
-import {TracedTracingSession} from '../traced_tracing_session';
-
-export abstract class AndroidTarget implements RecordingTargetV2 {
- private consumerSocketPath = DEFAULT_TRACED_CONSUMER_SOCKET_PATH;
- protected androidApiLevel?: number;
- protected dataSources?: DataSource[];
-
- protected constructor(
- private adbConnection: AdbConnectionImpl,
- private onTargetChange: OnTargetChangeCallback,
- ) {}
-
- abstract getInfo(): TargetInfo;
-
- // This is called when a usb USBConnectionEvent of type 'disconnect' event is
- // emitted. This event is emitted when the USB connection is lost (example:
- // when the user unplugged the connecting cable).
- async disconnect(disconnectMessage?: string): Promise<void> {
- await this.adbConnection.disconnect(disconnectMessage);
- }
-
- // Starts a tracing session in order to fetch information such as apiLevel
- // and dataSources from the device. Then, it cancels the session.
- async fetchTargetInfo(listener: TracingSessionListener): Promise<void> {
- const tracingSession = await this.createTracingSession(listener);
- tracingSession.cancel();
- }
-
- // We do not support long tracing on Android.
- canCreateTracingSession(recordingMode: string): boolean {
- return recordingMode !== 'LONG_TRACE';
- }
-
- async createTracingSession(
- tracingSessionListener: TracingSessionListener,
- ): Promise<TracingSession> {
- this.adbConnection.onStatus = tracingSessionListener.onStatus;
- this.adbConnection.onDisconnect = tracingSessionListener.onDisconnect;
-
- if (!exists(this.androidApiLevel)) {
- // 1. Fetch the API version from the device.
- const version = await this.adbConnection.shellAndGetOutput(
- 'getprop ro.build.version.sdk',
- );
- this.androidApiLevel = Number(version);
-
- this.onTargetChange();
-
- // 2. For older OS versions we push the tracebox binary.
- if (this.androidApiLevel < 29) {
- await this.pushTracebox();
- this.consumerSocketPath = CUSTOM_TRACED_CONSUMER_SOCKET_PATH;
-
- await this.adbConnection.shellAndWaitCompletion(
- this.composeTraceboxCommand('traced'),
- );
- await this.adbConnection.shellAndWaitCompletion(
- this.composeTraceboxCommand('traced_probes'),
- );
- }
- }
-
- const adbStream = await this.adbConnection.connectSocket(
- this.consumerSocketPath,
- );
-
- // 3. Start a tracing session.
- const tracingSession = new TracedTracingSession(
- adbStream,
- tracingSessionListener,
- );
- await tracingSession.initConnection();
-
- if (!this.dataSources) {
- // 4. Fetch dataSources from QueryServiceState.
- this.dataSources = await tracingSession.queryServiceState();
-
- this.onTargetChange();
- }
- return tracingSession;
- }
-
- async pushTracebox() {
- const arch = await this.fetchArchitecture();
- const shortVersion = VERSION.split('-')[0];
- const requestUrl = `https://commondatastorage.googleapis.com/perfetto-luci-artifacts/${shortVersion}/${arch}/tracebox`;
- const fetchResponse = await fetchWithTimeout(
- requestUrl,
- {method: 'get'},
- TRACEBOX_FETCH_TIMEOUT,
- );
- const traceboxBin = await fetchResponse.arrayBuffer();
- await this.adbConnection.push(
- new Uint8Array(traceboxBin),
- TRACEBOX_DEVICE_PATH,
- );
-
- // We explicitly set the tracebox permissions because adb does not reliably
- // set permissions when uploading the binary.
- await this.adbConnection.shellAndWaitCompletion(
- `chmod 755 ${TRACEBOX_DEVICE_PATH}`,
- );
- }
-
- async fetchArchitecture() {
- const abiList = await this.adbConnection.shellAndGetOutput(
- 'getprop ro.vendor.product.cpu.abilist',
- );
- // If multiple ABIs are allowed, the 64bit ones should have higher priority.
- if (abiList.includes('arm64-v8a')) {
- return 'android-arm64';
- } else if (abiList.includes('x86')) {
- return 'android-x86';
- } else if (abiList.includes('armeabi-v7a') || abiList.includes('armeabi')) {
- return 'android-arm';
- } else if (abiList.includes('x86_64')) {
- return 'android-x64';
- }
- // Most devices have arm64 architectures, so we should return this if
- // nothing else is found.
- return 'android-arm64';
- }
-
- canConnectWithoutContention(): Promise<boolean> {
- return this.adbConnection.canConnectWithoutContention();
- }
-
- composeTraceboxCommand(applet: string) {
- // 1. Set the consumer socket.
- return (
- 'PERFETTO_CONSUMER_SOCK_NAME=@traced_consumer ' +
- // 2. Set the producer socket.
- 'PERFETTO_PRODUCER_SOCK_NAME=@traced_producer ' +
- // 3. Start the applet in the background.
- `/data/local/tmp/tracebox ${applet} --background`
- );
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_virtual_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_virtual_target.ts
deleted file mode 100644
index d18d075..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_virtual_target.ts
+++ /dev/null
@@ -1,59 +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 {RecordingError} from '../recording_error_handling';
-import {
- RecordingTargetV2,
- TargetInfo,
- TracingSession,
- TracingSessionListener,
-} from '../recording_interfaces_v2';
-
-export class AndroidVirtualTarget implements RecordingTargetV2 {
- constructor(
- private name: string,
- private androidApiLevel: number,
- ) {}
-
- canConnectWithoutContention(): Promise<boolean> {
- return Promise.resolve(true);
- }
-
- canCreateTracingSession(): boolean {
- return false;
- }
-
- createTracingSession(_: TracingSessionListener): Promise<TracingSession> {
- throw new RecordingError(
- 'Can not create tracing session for a virtual target',
- );
- }
-
- disconnect(_?: string): Promise<void> {
- throw new RecordingError('Can not disconnect from a virtual target');
- }
-
- fetchTargetInfo(_: TracingSessionListener): Promise<void> {
- return Promise.resolve();
- }
-
- getInfo(): TargetInfo {
- return {
- name: this.name,
- androidApiLevel: this.androidApiLevel,
- targetType: 'ANDROID',
- dataSources: [],
- };
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_websocket_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_websocket_target.ts
deleted file mode 100644
index 8959720..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_websocket_target.ts
+++ /dev/null
@@ -1,40 +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 {AdbConnectionOverWebsocket} from '../adb_connection_over_websocket';
-import {OnTargetChangeCallback, TargetInfo} from '../recording_interfaces_v2';
-import {AndroidTarget} from './android_target';
-
-export class AndroidWebsocketTarget extends AndroidTarget {
- constructor(
- private serialNumber: string,
- websocketUrl: string,
- onTargetChange: OnTargetChangeCallback,
- ) {
- super(
- new AdbConnectionOverWebsocket(serialNumber, websocketUrl),
- onTargetChange,
- );
- }
-
- getInfo(): TargetInfo {
- return {
- targetType: 'ANDROID',
- // 'androidApiLevel' will be populated after ADB authorization.
- androidApiLevel: this.androidApiLevel,
- dataSources: this.dataSources || [],
- name: this.serialNumber + ' WebSocket',
- };
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts
deleted file mode 100644
index dc6e64d..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts
+++ /dev/null
@@ -1,44 +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 {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';
-import {AndroidTarget} from './android_target';
-
-export class AndroidWebusbTarget extends AndroidTarget {
- constructor(
- private device: USBDevice,
- keyManager: AdbKeyManager,
- onTargetChange: OnTargetChangeCallback,
- ) {
- super(new AdbConnectionOverWebusb(device, keyManager), onTargetChange);
- }
-
- getInfo(): TargetInfo {
- const name =
- assertExists(this.device.productName) +
- ' ' +
- assertExists(this.device.serialNumber) +
- ' WebUsb';
- return {
- targetType: 'ANDROID',
- // 'androidApiLevel' will be populated after ADB authorization.
- androidApiLevel: this.androidApiLevel,
- dataSources: this.dataSources || [],
- name,
- };
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/chrome_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/chrome_target.ts
deleted file mode 100644
index 9baaf96..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/chrome_target.ts
+++ /dev/null
@@ -1,88 +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 {ChromeTracedTracingSession} from '../chrome_traced_tracing_session';
-import {
- ChromeTargetInfo,
- OnTargetChangeCallback,
- RecordingTargetV2,
- TracingSession,
- TracingSessionListener,
-} from '../recording_interfaces_v2';
-
-export class ChromeTarget implements RecordingTargetV2 {
- onTargetChange?: OnTargetChangeCallback;
- private chromeCategories?: string[];
-
- constructor(
- private name: string,
- private targetType: 'CHROME' | 'CHROME_OS' | 'WINDOWS',
- ) {}
-
- getInfo(): ChromeTargetInfo {
- return {
- targetType: this.targetType,
- name: this.name,
- dataSources: [
- {name: 'chromeCategories', descriptor: this.chromeCategories},
- ],
- };
- }
-
- // Chrome targets are created after we check that the extension is installed,
- // so they support tracing sessions.
- canCreateTracingSession(): boolean {
- return true;
- }
-
- async createTracingSession(
- tracingSessionListener: TracingSessionListener,
- ): Promise<TracingSession> {
- const tracingSession = new ChromeTracedTracingSession(
- tracingSessionListener,
- );
- tracingSession.initConnection();
-
- if (!this.chromeCategories) {
- // Fetch chrome categories from the extension.
- this.chromeCategories = await tracingSession.getCategories();
- if (this.onTargetChange) {
- this.onTargetChange();
- }
- }
-
- return tracingSession;
- }
-
- // Starts a tracing session in order to fetch chrome categories from the
- // device. Then, it cancels the session.
- async fetchTargetInfo(
- tracingSessionListener: TracingSessionListener,
- ): Promise<void> {
- const tracingSession = await this.createTracingSession(
- tracingSessionListener,
- );
- tracingSession.cancel();
- }
-
- disconnect(_disconnectMessage?: string): Promise<void> {
- return Promise.resolve(undefined);
- }
-
- // We can connect to the Chrome target without taking the connection away
- // from another process.
- async canConnectWithoutContention(): Promise<boolean> {
- return true;
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/host_os_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/host_os_target.ts
deleted file mode 100644
index 7b32fcc..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/host_os_target.ts
+++ /dev/null
@@ -1,144 +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 {HostOsByteStream} from '../host_os_byte_stream';
-import {RecordingError} from '../recording_error_handling';
-import {
- DataSource,
- HostOsTargetInfo,
- OnDisconnectCallback,
- OnTargetChangeCallback,
- RecordingTargetV2,
- TracingSession,
- TracingSessionListener,
-} from '../recording_interfaces_v2';
-import {
- isLinux,
- isMacOs,
- WEBSOCKET_CLOSED_ABNORMALLY_CODE,
-} from '../recording_utils';
-import {TracedTracingSession} from '../traced_tracing_session';
-
-export class HostOsTarget implements RecordingTargetV2 {
- private readonly targetType: 'LINUX' | 'MACOS';
- private readonly name: string;
- private websocket: WebSocket;
- private streams = new Set<HostOsByteStream>();
- private dataSources?: DataSource[];
- private onDisconnect: OnDisconnectCallback = (_) => {};
-
- constructor(
- websocketUrl: string,
- private maybeClearTarget: (target: HostOsTarget) => void,
- private onTargetChange: OnTargetChangeCallback,
- ) {
- if (isMacOs(navigator.userAgent)) {
- this.name = 'MacOS';
- this.targetType = 'MACOS';
- } else if (isLinux(navigator.userAgent)) {
- this.name = 'Linux';
- this.targetType = 'LINUX';
- } else {
- throw new RecordingError(
- 'Host OS target created on an unsupported operating system.',
- );
- }
-
- this.websocket = new WebSocket(websocketUrl);
- this.websocket.onclose = this.onClose.bind(this);
- // 'onError' gets called when the websocketURL where the UI tries to connect
- // is disallowed by the Content Security Policy. In this case, we disconnect
- // the target.
- this.websocket.onerror = this.disconnect.bind(this);
- }
-
- getInfo(): HostOsTargetInfo {
- return {
- targetType: this.targetType,
- name: this.name,
- dataSources: this.dataSources || [],
- };
- }
-
- canCreateTracingSession(): boolean {
- return true;
- }
-
- async createTracingSession(
- tracingSessionListener: TracingSessionListener,
- ): Promise<TracingSession> {
- this.onDisconnect = tracingSessionListener.onDisconnect;
-
- const osStream = await HostOsByteStream.create(this.getUrl());
- this.streams.add(osStream);
- const tracingSession = new TracedTracingSession(
- osStream,
- tracingSessionListener,
- );
- await tracingSession.initConnection();
-
- if (!this.dataSources) {
- this.dataSources = await tracingSession.queryServiceState();
- this.onTargetChange();
- }
- return tracingSession;
- }
-
- // Starts a tracing session in order to fetch data sources from the
- // device. Then, it cancels the session.
- async fetchTargetInfo(
- tracingSessionListener: TracingSessionListener,
- ): Promise<void> {
- const tracingSession = await this.createTracingSession(
- tracingSessionListener,
- );
- tracingSession.cancel();
- }
-
- async disconnect(): Promise<void> {
- if (this.websocket.readyState === this.websocket.OPEN) {
- this.websocket.close();
- // We remove the 'onclose' callback so the 'disconnect' method doesn't get
- // executed twice.
- this.websocket.onclose = null;
- }
- for (const stream of this.streams) {
- stream.close();
- }
- // We remove the existing target from the factory if present.
- this.maybeClearTarget(this);
- // We run the onDisconnect callback in case this target is used for tracing.
- this.onDisconnect();
- }
-
- // We can connect to the Host OS without taking the connection away from
- // another process.
- async canConnectWithoutContention(): Promise<boolean> {
- return true;
- }
-
- getUrl() {
- return this.websocket.url;
- }
-
- private onClose(ev: CloseEvent): void {
- if (ev.code === WEBSOCKET_CLOSED_ABNORMALLY_CODE) {
- console.info(
- `It's safe to ignore the 'WebSocket connection to ${this.getUrl()} error above, if present. It occurs when ` +
- 'checking the connection to the local Websocket server.',
- );
- }
- this.disconnect();
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts
deleted file mode 100644
index 33151bd..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts
+++ /dev/null
@@ -1,439 +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 protobuf from 'protobufjs/minimal';
-import {defer, Deferred} from '../../../base/deferred';
-import {assertExists, assertFalse, assertTrue} from '../../../base/logging';
-import {
- DisableTracingRequest,
- DisableTracingResponse,
- EnableTracingRequest,
- EnableTracingResponse,
- FreeBuffersRequest,
- FreeBuffersResponse,
- GetTraceStatsRequest,
- GetTraceStatsResponse,
- IBufferStats,
- IMethodInfo,
- IPCFrame,
- ISlice,
- QueryServiceStateRequest,
- QueryServiceStateResponse,
- ReadBuffersRequest,
- ReadBuffersResponse,
- TraceConfig,
-} from '../protos';
-import {RecordingError} from './recording_error_handling';
-import {
- ByteStream,
- DataSource,
- TracingSession,
- TracingSessionListener,
-} from './recording_interfaces_v2';
-import {
- BUFFER_USAGE_INCORRECT_FORMAT,
- BUFFER_USAGE_NOT_ACCESSIBLE,
- PARSING_UNABLE_TO_DECODE_METHOD,
- PARSING_UNKNWON_REQUEST_ID,
- PARSING_UNRECOGNIZED_MESSAGE,
- PARSING_UNRECOGNIZED_PORT,
- RECORDING_IN_PROGRESS,
-} from './recording_utils';
-import {exists} from '../../../base/utils';
-
-// See wire_protocol.proto for more details.
-const WIRE_PROTOCOL_HEADER_SIZE = 4;
-// See basic_types.h (kIPCBufferSize) for more details.
-const MAX_IPC_BUFFER_SIZE = 128 * 1024;
-
-const PROTO_LEN_DELIMITED_WIRE_TYPE = 2;
-const TRACE_PACKET_PROTO_ID = 1;
-const TRACE_PACKET_PROTO_TAG =
- (TRACE_PACKET_PROTO_ID << 3) | PROTO_LEN_DELIMITED_WIRE_TYPE;
-
-function parseMessageSize(buffer: Uint8Array) {
- const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.length);
- return dv.getUint32(0, true);
-}
-
-// This class implements the protocol described in
-// https://perfetto.dev/docs/design-docs/api-and-abi#tracing-protocol-abi
-export class TracedTracingSession implements TracingSession {
- // Buffers received wire protocol data.
- private incomingBuffer = new Uint8Array(MAX_IPC_BUFFER_SIZE);
- private bufferedPartLength = 0;
- private currentFrameLength?: number;
-
- private availableMethods: IMethodInfo[] = [];
- private serviceId = -1;
-
- private resolveBindingPromise!: Deferred<void>;
- private requestMethods = new Map<number, string>();
-
- // Needed for ReadBufferResponse: all the trace packets are split into
- // several slices. |partialPacket| is the buffer for them. Once we receive a
- // slice with the flag |lastSliceForPacket|, a new packet is created.
- private partialPacket: ISlice[] = [];
- // Accumulates trace packets into a proto trace file..
- private traceProtoWriter = protobuf.Writer.create();
-
- // Accumulates DataSource objects from QueryServiceStateResponse,
- // which can have >1 replies for each query
- // go/codesearch/android/external/perfetto/protos/
- // perfetto/ipc/consumer_port.proto;l=243-246
- private pendingDataSources: DataSource[] = [];
-
- // For concurrent calls to 'QueryServiceState', we return the same value.
- private pendingQssMessage?: Deferred<DataSource[]>;
-
- // Wire protocol request ID. After each request it is increased. It is needed
- // to keep track of the type of request, and parse the response correctly.
- private requestId = 1;
-
- private pendingStatsMessages = new Array<Deferred<IBufferStats[]>>();
-
- // The bytestream is obtained when creating a connection with a target.
- // For instance, the AdbStream is obtained from a connection with an Adb
- // device.
- constructor(
- private byteStream: ByteStream,
- private tracingSessionListener: TracingSessionListener,
- ) {
- this.byteStream.addOnStreamDataCallback((data) =>
- this.handleReceivedData(data),
- );
- this.byteStream.addOnStreamCloseCallback(() => this.clearState());
- }
-
- queryServiceState(): Promise<DataSource[]> {
- if (this.pendingQssMessage) {
- return this.pendingQssMessage;
- }
-
- const requestProto = QueryServiceStateRequest.encode(
- new QueryServiceStateRequest(),
- ).finish();
- this.rpcInvoke('QueryServiceState', requestProto);
-
- return (this.pendingQssMessage = defer<DataSource[]>());
- }
-
- start(config: TraceConfig): void {
- const duration = config.durationMs;
- this.tracingSessionListener.onStatus(
- `${RECORDING_IN_PROGRESS}${
- duration ? ' for ' + duration.toString() + ' ms' : ''
- }...`,
- );
-
- const enableTracingRequest = new EnableTracingRequest();
- enableTracingRequest.traceConfig = config;
- const enableTracingRequestProto =
- EnableTracingRequest.encode(enableTracingRequest).finish();
- this.rpcInvoke('EnableTracing', enableTracingRequestProto);
- }
-
- cancel(): void {
- this.terminateConnection();
- }
-
- stop(): void {
- const requestProto = DisableTracingRequest.encode(
- new DisableTracingRequest(),
- ).finish();
- this.rpcInvoke('DisableTracing', requestProto);
- }
-
- async getTraceBufferUsage(): Promise<number> {
- if (!this.byteStream.isConnected()) {
- // TODO(octaviant): make this more in line with the other trace buffer
- // error cases.
- return 0;
- }
- const bufferStats = await this.getBufferStats();
- let percentageUsed = -1;
- for (const buffer of bufferStats) {
- if (
- !Number.isFinite(buffer.bytesWritten) ||
- !Number.isFinite(buffer.bufferSize)
- ) {
- continue;
- }
- const used = assertExists(buffer.bytesWritten);
- const total = assertExists(buffer.bufferSize);
- if (total >= 0) {
- percentageUsed = Math.max(percentageUsed, used / total);
- }
- }
-
- if (percentageUsed === -1) {
- return Promise.reject(new RecordingError(BUFFER_USAGE_INCORRECT_FORMAT));
- }
- return percentageUsed;
- }
-
- initConnection(): Promise<void> {
- // bind IPC methods
- const requestId = this.requestId++;
- const frame = new IPCFrame({
- requestId,
- msgBindService: new IPCFrame.BindService({serviceName: 'ConsumerPort'}),
- });
- this.writeFrame(frame);
-
- // We shouldn't bind multiple times to the service in the same tracing
- // session.
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
- assertFalse(!!this.resolveBindingPromise);
- this.resolveBindingPromise = defer<void>();
- return this.resolveBindingPromise;
- }
-
- private getBufferStats(): Promise<IBufferStats[]> {
- const getTraceStatsRequestProto = GetTraceStatsRequest.encode(
- new GetTraceStatsRequest(),
- ).finish();
- try {
- this.rpcInvoke('GetTraceStats', getTraceStatsRequestProto);
- } catch (e) {
- // GetTraceStats was introduced only on Android 10.
- this.raiseError(e);
- }
-
- const statsMessage = defer<IBufferStats[]>();
- this.pendingStatsMessages.push(statsMessage);
- return statsMessage;
- }
-
- private terminateConnection(): void {
- this.clearState();
- const requestProto = FreeBuffersRequest.encode(
- new FreeBuffersRequest(),
- ).finish();
- this.rpcInvoke('FreeBuffers', requestProto);
- this.byteStream.close();
- }
-
- private clearState() {
- for (const statsMessage of this.pendingStatsMessages) {
- statsMessage.reject(new RecordingError(BUFFER_USAGE_NOT_ACCESSIBLE));
- }
- this.pendingStatsMessages = [];
- this.pendingDataSources = [];
- this.pendingQssMessage = undefined;
- }
-
- private rpcInvoke(methodName: string, argsProto: Uint8Array): void {
- if (!this.byteStream.isConnected()) {
- return;
- }
- const method = this.availableMethods.find((m) => m.name === methodName);
- if (!exists(method) || !exists(method.id)) {
- throw new RecordingError(
- `Method ${methodName} not supported by the target`,
- );
- }
- const requestId = this.requestId++;
- const frame = new IPCFrame({
- requestId,
- msgInvokeMethod: new IPCFrame.InvokeMethod({
- serviceId: this.serviceId,
- methodId: method.id,
- argsProto,
- }),
- });
- this.requestMethods.set(requestId, methodName);
- this.writeFrame(frame);
- }
-
- private writeFrame(frame: IPCFrame): void {
- const frameProto: Uint8Array = IPCFrame.encode(frame).finish();
- const frameLen = frameProto.length;
- const buf = new Uint8Array(WIRE_PROTOCOL_HEADER_SIZE + frameLen);
- const dv = new DataView(buf.buffer);
- dv.setUint32(0, frameProto.length, /* littleEndian */ true);
- for (let i = 0; i < frameLen; i++) {
- dv.setUint8(WIRE_PROTOCOL_HEADER_SIZE + i, frameProto[i]);
- }
- this.byteStream.write(buf);
- }
-
- private handleReceivedData(rawData: Uint8Array): void {
- // we parse the length of the next frame if it's available
- if (
- this.currentFrameLength === undefined &&
- this.canCompleteLengthHeader(rawData)
- ) {
- const remainingFrameBytes =
- WIRE_PROTOCOL_HEADER_SIZE - this.bufferedPartLength;
- this.appendToIncomingBuffer(rawData.subarray(0, remainingFrameBytes));
- rawData = rawData.subarray(remainingFrameBytes);
-
- this.currentFrameLength = parseMessageSize(this.incomingBuffer);
- this.bufferedPartLength = 0;
- }
-
- // Parse all complete frames.
- while (
- this.currentFrameLength !== undefined &&
- this.bufferedPartLength + rawData.length >= this.currentFrameLength
- ) {
- // Read the remaining part of this message.
- const bytesToCompleteMessage =
- this.currentFrameLength - this.bufferedPartLength;
- this.appendToIncomingBuffer(rawData.subarray(0, bytesToCompleteMessage));
- this.parseFrame(this.incomingBuffer.subarray(0, this.currentFrameLength));
- this.bufferedPartLength = 0;
- // Remove the data just parsed.
- rawData = rawData.subarray(bytesToCompleteMessage);
-
- if (!this.canCompleteLengthHeader(rawData)) {
- this.currentFrameLength = undefined;
- break;
- }
- this.currentFrameLength = parseMessageSize(rawData);
- rawData = rawData.subarray(WIRE_PROTOCOL_HEADER_SIZE);
- }
-
- // Buffer the remaining data (part of the next message).
- this.appendToIncomingBuffer(rawData);
- }
-
- private canCompleteLengthHeader(newData: Uint8Array): boolean {
- return newData.length + this.bufferedPartLength > WIRE_PROTOCOL_HEADER_SIZE;
- }
-
- private appendToIncomingBuffer(array: Uint8Array): void {
- this.incomingBuffer.set(array, this.bufferedPartLength);
- this.bufferedPartLength += array.length;
- }
-
- private parseFrame(frameBuffer: Uint8Array): void {
- // Get a copy of the ArrayBuffer to avoid the original being overriden.
- // See 170256902#comment21
- const frame = IPCFrame.decode(frameBuffer.slice());
- if (frame.msg === 'msgBindServiceReply') {
- const msgBindServiceReply = frame.msgBindServiceReply;
- if (
- exists(msgBindServiceReply) &&
- exists(msgBindServiceReply.methods) &&
- exists(msgBindServiceReply.serviceId)
- ) {
- assertTrue(msgBindServiceReply.success === true);
- this.availableMethods = msgBindServiceReply.methods;
- this.serviceId = msgBindServiceReply.serviceId;
- this.resolveBindingPromise.resolve();
- }
- } else if (frame.msg === 'msgInvokeMethodReply') {
- const msgInvokeMethodReply = frame.msgInvokeMethodReply;
- // We process messages without a `replyProto` field (for instance
- // `FreeBuffers` does not have `replyProto`). However, we ignore messages
- // without a valid 'success' field.
- if (msgInvokeMethodReply?.success !== true) {
- return;
- }
-
- const method = this.requestMethods.get(frame.requestId);
- if (!method) {
- this.raiseError(`${PARSING_UNKNWON_REQUEST_ID}: ${frame.requestId}`);
- return;
- }
- const decoder = decoders.get(method);
- if (decoder === undefined) {
- this.raiseError(`${PARSING_UNABLE_TO_DECODE_METHOD}: ${method}`);
- return;
- }
- const data = {...decoder(msgInvokeMethodReply.replyProto)};
-
- if (method === 'ReadBuffers') {
- for (const slice of data.slices ?? []) {
- this.partialPacket.push(slice);
- if (slice.lastSliceForPacket === true) {
- let bufferSize = 0;
- for (const slice of this.partialPacket) {
- bufferSize += slice.data!.length;
- }
- const tracePacket = new Uint8Array(bufferSize);
- let written = 0;
- for (const slice of this.partialPacket) {
- const data = slice.data!;
- tracePacket.set(data, written);
- written += data.length;
- }
- this.traceProtoWriter.uint32(TRACE_PACKET_PROTO_TAG);
- this.traceProtoWriter.bytes(tracePacket);
- this.partialPacket = [];
- }
- }
- if (msgInvokeMethodReply.hasMore === false) {
- this.tracingSessionListener.onTraceData(
- this.traceProtoWriter.finish(),
- );
- this.terminateConnection();
- }
- } else if (method === 'EnableTracing') {
- const readBuffersRequestProto = ReadBuffersRequest.encode(
- new ReadBuffersRequest(),
- ).finish();
- this.rpcInvoke('ReadBuffers', readBuffersRequestProto);
- } else if (method === 'GetTraceStats') {
- const maybePendingStatsMessage = this.pendingStatsMessages.shift();
- if (maybePendingStatsMessage) {
- maybePendingStatsMessage.resolve(data?.traceStats?.bufferStats ?? []);
- }
- } else if (method === 'FreeBuffers') {
- // No action required. If we successfully read a whole trace,
- // we close the connection. Alternatively, if the tracing finishes
- // with an exception or if the user cancels it, we also close the
- // connection.
- } else if (method === 'DisableTracing') {
- // No action required. Same reasoning as for FreeBuffers.
- } else if (method === 'QueryServiceState') {
- const dataSources =
- (data as QueryServiceStateResponse)?.serviceState?.dataSources || [];
- for (const dataSource of dataSources) {
- const name = dataSource?.dsDescriptor?.name;
- if (name) {
- this.pendingDataSources.push({
- name,
- descriptor: dataSource.dsDescriptor,
- });
- }
- }
- if (msgInvokeMethodReply.hasMore === false) {
- assertExists(this.pendingQssMessage).resolve(this.pendingDataSources);
- this.pendingDataSources = [];
- this.pendingQssMessage = undefined;
- }
- } else {
- this.raiseError(`${PARSING_UNRECOGNIZED_PORT}: ${method}`);
- }
- } else {
- this.raiseError(`${PARSING_UNRECOGNIZED_MESSAGE}: ${frame.msg}`);
- }
- }
-
- private raiseError(message: string): void {
- this.terminateConnection();
- this.tracingSessionListener.onError(message);
- }
-}
-
-const decoders = new Map<string, Function>()
- .set('EnableTracing', EnableTracingResponse.decode)
- .set('FreeBuffers', FreeBuffersResponse.decode)
- .set('ReadBuffers', ReadBuffersResponse.decode)
- .set('DisableTracing', DisableTracingResponse.decode)
- .set('GetTraceStats', GetTraceStatsResponse.decode)
- .set('QueryServiceState', QueryServiceStateResponse.decode);
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts
deleted file mode 100644
index 2da8f5b..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts
+++ /dev/null
@@ -1,74 +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 {
- ADB_ENDPOINT,
- DEFAULT_WEBSOCKET_URL,
- TRACED_ENDPOINT,
-} from '../recording_ui_utils';
-import {TargetFactory} from './recording_interfaces_v2';
-import {
- ANDROID_WEBSOCKET_TARGET_FACTORY,
- AndroidWebsocketTargetFactory,
-} from './target_factories/android_websocket_target_factory';
-import {
- HOST_OS_TARGET_FACTORY,
- HostOsTargetFactory,
-} from './target_factories/host_os_target_factory';
-import {targetFactoryRegistry} from './target_factory_registry';
-
-// The WebsocketMenuController will handle paths for all factories which
-// connect over websocket. At present, these are:
-// - adb websocket factory
-// - host OS websocket factory
-export class WebsocketMenuController {
- private path: string = DEFAULT_WEBSOCKET_URL;
-
- getPath(): string {
- return this.path;
- }
-
- setPath(path: string): void {
- this.path = path;
- }
-
- onPathChange(): void {
- if (targetFactoryRegistry.has(ANDROID_WEBSOCKET_TARGET_FACTORY)) {
- const androidTargetFactory = targetFactoryRegistry.get(
- ANDROID_WEBSOCKET_TARGET_FACTORY,
- ) as AndroidWebsocketTargetFactory;
- androidTargetFactory.tryEstablishWebsocket(this.path + ADB_ENDPOINT);
- }
-
- if (targetFactoryRegistry.has(HOST_OS_TARGET_FACTORY)) {
- const hostTargetFactory = targetFactoryRegistry.get(
- HOST_OS_TARGET_FACTORY,
- ) as HostOsTargetFactory;
- hostTargetFactory.tryEstablishWebsocket(this.path + TRACED_ENDPOINT);
- }
- }
-
- getTargetFactories(): TargetFactory[] {
- const targetFactories = [];
- if (targetFactoryRegistry.has(ANDROID_WEBSOCKET_TARGET_FACTORY)) {
- targetFactories.push(
- targetFactoryRegistry.get(ANDROID_WEBSOCKET_TARGET_FACTORY),
- );
- }
- if (targetFactoryRegistry.has(HOST_OS_TARGET_FACTORY)) {
- targetFactories.push(targetFactoryRegistry.get(HOST_OS_TARGET_FACTORY));
- }
- return targetFactories;
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
index be29691..7504ec9 100644
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
@@ -25,12 +25,7 @@
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';
@@ -41,31 +36,22 @@
private _state: RecordingState = createEmptyState();
private recCtl: RecordController;
- constructor(app: App, useRecordingV2: boolean) {
+ constructor(app: App) {
this.app = app;
const extensionLocalChannel = new MessageChannel();
this.recCtl = new RecordController(app, this, extensionLocalChannel.port1);
this.setupExtentionPort(extensionLocalChannel);
- if (useRecordingV2) {
- targetFactoryRegistry.register(new AndroidWebsocketTargetFactory());
- if (exists(navigator.usb)) {
- targetFactoryRegistry.register(
- new AndroidWebusbTargetFactory(navigator.usb),
- );
- }
- } else {
- this.updateAvailableAdbDevices();
- try {
- navigator.usb.addEventListener('connect', () =>
- this.updateAvailableAdbDevices(),
- );
- navigator.usb.addEventListener('disconnect', () =>
- this.updateAvailableAdbDevices(),
- );
- } catch (e) {
- console.error('WebUSB API not supported');
- }
+ this.updateAvailableAdbDevices();
+ try {
+ navigator.usb.addEventListener('connect', () =>
+ this.updateAvailableAdbDevices(),
+ );
+ navigator.usb.addEventListener('disconnect', () =>
+ this.updateAvailableAdbDevices(),
+ );
+ } catch (e) {
+ console.error('WebUSB API not supported');
}
}
@@ -158,7 +144,7 @@
(message: object, _port: chrome.runtime.Port) => {
if (isGetCategoriesResponse(message)) {
this._state.chromeCategories = message.categories;
- scheduleFullRedraw();
+ this.app.raf.scheduleFullRedraw();
return;
}
extensionLocalChannel.port2.postMessage(message);
@@ -193,7 +179,7 @@
this.setAvailableAdbDevices(availableAdbDevices);
this.selectAndroidDeviceIfAvailable(availableAdbDevices, recordingTarget);
- scheduleFullRedraw();
+ this.app.raf.scheduleFullRedraw();
return availableAdbDevices;
}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts
deleted file mode 100644
index 0e34f5c..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts
+++ /dev/null
@@ -1,115 +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 {
- RecordingTargetV2,
- TargetFactory,
-} 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 {
- targetFactories: TargetFactory[];
- // Reference to the controller which maintains the state of the recording
- // page.
- controller: RecordingPageController;
-}
-
-export class RecordingMultipleChoice
- implements m.ClassComponent<RecordingMultipleChoiceAttrs>
-{
- private selectedIndex: number = -1;
-
- targetSelection(
- targets: RecordingTargetV2[],
- controller: RecordingPageController,
- ): m.Vnode | undefined {
- const targetInfo = controller.getTargetInfo();
- const targetNames = [];
- this.selectedIndex = -1;
- for (let i = 0; i < targets.length; i++) {
- const targetName = targets[i].getInfo().name;
- targetNames.push(m('option', targetName));
- if (targetInfo && targetName === targetInfo.name) {
- this.selectedIndex = i;
- }
- }
-
- const selectedIndex = this.selectedIndex;
- return m(
- 'label',
- m(
- 'select',
- {
- selectedIndex,
- onchange: (e: Event) => {
- controller.onTargetSelection((e.target as HTMLSelectElement).value);
- },
- onupdate: (select) => {
- // Work around mithril bug
- // (https://github.com/MithrilJS/mithril.js/issues/2107): We
- // may update the select's options while also changing the
- // selectedIndex at the same time. The update of selectedIndex
- // may be applied before the new options are added to the
- // select element. Because the new selectedIndex may be
- // outside of the select's options at that time, we have to
- // reselect the correct index here after any new children were
- // added.
- (select.dom as HTMLSelectElement).selectedIndex =
- this.selectedIndex;
- },
- ...{size: targets.length, multiple: 'multiple'},
- },
- ...targetNames,
- ),
- );
- }
-
- view({attrs}: m.CVnode<RecordingMultipleChoiceAttrs>): m.Vnode[] | undefined {
- const controller = attrs.controller;
- if (!controller.shouldShowTargetSelection()) {
- return undefined;
- }
- const targets: RecordingTargetV2[] = [];
- for (const targetFactory of attrs.targetFactories) {
- for (const target of targetFactory.listTargets()) {
- targets.push(target);
- }
- }
- if (targets.length === 0) {
- return undefined;
- }
-
- return [
- m('text', 'Select target:'),
- m(
- '.record-modal-command',
- this.targetSelection(targets, controller),
- m(
- 'button.record-modal-button-high',
- {
- disabled: this.selectedIndex === -1,
- onclick: () => {
- closeModal(RECORDING_MODAL_DIALOG_KEY);
- controller.onStartRecordingPressed();
- },
- },
- 'Connect',
- ),
- ),
- ];
- }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts b/ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts
deleted file mode 100644
index 4d3d048..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts
+++ /dev/null
@@ -1,186 +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 {RecordingPageController} from './recordingV2/recording_page_controller';
-import {
- EXTENSION_URL,
- RECORDING_MODAL_DIALOG_KEY,
-} from './recordingV2/recording_utils';
-import {
- CHROME_TARGET_FACTORY,
- ChromeTargetFactory,
-} 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 {RecordingMultipleChoice} from './recording_multiple_choice';
-
-const RUN_WEBSOCKET_CMD =
- '# Get tracebox\n' +
- 'curl -LO https://get.perfetto.dev/tracebox\n' +
- 'chmod +x ./tracebox\n' +
- '# Option A - trace android devices\n' +
- 'adb start-server\n' +
- '# Option B - trace the host OS\n' +
- './tracebox traced --background\n' +
- './tracebox traced_probes --background\n' +
- '# Start the websocket server\n' +
- './tracebox websocket_bridge\n';
-
-export function showAddNewTargetModal(controller: RecordingPageController) {
- showModal({
- title: 'Add new recording target',
- key: RECORDING_MODAL_DIALOG_KEY,
- content: () =>
- m(
- '.record-modal',
- m('text', 'Select platform:'),
- assembleWebusbSection(controller),
- m('.line'),
- assembleWebsocketSection(controller),
- m('.line'),
- assembleChromeSection(controller),
- ),
- });
-}
-
-function assembleWebusbSection(
- recordingPageController: RecordingPageController,
-): m.Vnode {
- return m(
- '.record-modal-section',
- m('.logo-wrapping', m('i.material-icons', 'usb')),
- m(
- '.record-modal-description',
- m('h3', 'Android device over WebUSB'),
- m(
- 'text',
- 'Android developers: this option cannot co-operate ' +
- 'with the adb host on your machine. Only one entity between ' +
- 'the browser and adb can control the USB endpoint. If adb is ' +
- 'running, you will be prompted to re-assign the device to the ' +
- 'browser. Use the websocket option below to use both ' +
- 'simultaneously.',
- ),
- m(
- '.record-modal-button',
- {
- onclick: () => {
- closeModal(RECORDING_MODAL_DIALOG_KEY);
- recordingPageController.addAndroidDevice();
- },
- },
- 'Connect new WebUSB driver',
- ),
- ),
- );
-}
-
-function assembleWebsocketSection(
- recordingPageController: RecordingPageController,
-): m.Vnode {
- const websocketComponents = [];
- websocketComponents.push(
- m('h3', 'Android / Linux / MacOS device via Websocket'),
- );
- websocketComponents.push(
- m(
- 'text',
- 'This option assumes that the adb server is already ' +
- 'running on your machine.',
- ),
- m(
- '.record-modal-command',
- m(CodeSnippet, {
- text: RUN_WEBSOCKET_CMD,
- }),
- ),
- );
-
- websocketComponents.push(
- m(
- '.record-modal-command',
- m('text', 'Websocket bridge address: '),
- m('input[type=text]', {
- value: websocketMenuController.getPath(),
- oninput() {
- websocketMenuController.setPath(this.value);
- },
- }),
- m(
- '.record-modal-logo-button',
- {
- onclick: () => websocketMenuController.onPathChange(),
- },
- m('i.material-icons', 'refresh'),
- ),
- ),
- );
-
- websocketComponents.push(
- m(RecordingMultipleChoice, {
- controller: recordingPageController,
- targetFactories: websocketMenuController.getTargetFactories(),
- }),
- );
-
- return m(
- '.record-modal-section',
- m('.logo-wrapping', m('i.material-icons', 'settings_ethernet')),
- m('.record-modal-description', ...websocketComponents),
- );
-}
-
-function assembleChromeSection(
- recordingPageController: RecordingPageController,
-): m.Vnode | undefined {
- if (!targetFactoryRegistry.has(CHROME_TARGET_FACTORY)) {
- return undefined;
- }
-
- const chromeComponents = [];
- chromeComponents.push(m('h3', 'Chrome Browser instance or ChromeOS device'));
-
- const chromeFactory: ChromeTargetFactory = targetFactoryRegistry.get(
- CHROME_TARGET_FACTORY,
- ) as ChromeTargetFactory;
-
- if (!chromeFactory.isExtensionInstalled) {
- chromeComponents.push(
- m(
- 'text',
- 'Install the extension ',
- m('a', {href: EXTENSION_URL, target: '_blank'}, 'from this link '),
- 'and refresh the page.',
- ),
- );
- } else {
- chromeComponents.push(
- m(RecordingMultipleChoice, {
- controller: recordingPageController,
- targetFactories: [chromeFactory],
- }),
- );
- }
-
- return m(
- '.record-modal-section',
- m('.logo-wrapping', m('i.material-icons', 'web')),
- m('.record-modal-description', ...chromeComponents),
- );
-}
-
-const websocketMenuController = new WebsocketMenuController();
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/OWNERS b/ui/src/plugins/dev.perfetto.RecordTraceV2/OWNERS
new file mode 100644
index 0000000..ffd5543
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/OWNERS
@@ -0,0 +1 @@
+primiano@google.com
\ No newline at end of file
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_device.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_device.ts
new file mode 100644
index 0000000..28798d5
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_device.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 {defer} from '../../../base/deferred';
+import {ResizableArrayBuffer} from '../../../base/resizable_array_buffer';
+import {okResult, Result} from '../../../base/result';
+import {utf8Decode} from '../../../base/string_utils';
+import {ByteStream} from '../interfaces/byte_stream';
+
+/**
+ * A base abstraction that represents an Android ADB device, allowing to shell
+ * commands and create streams (e.g. connecting to a UNIX-socket).
+ * This abstraction exists so that AdbTracingSession can drive a tracing session
+ * regardless of the underlying Webusb Websocket connection.
+ * AdbWebusbDevice and AdbWebsocketDevice implement this.
+ * @see @class AdbWebusbDevice
+ * @see @class AdbWebsocketDevice
+ */
+export abstract class AdbDevice {
+ /**
+ * Opens an ADB Stream. Example services:
+ * - 'shell:command arg1 arg2 ...'
+ * - 'shell:' for interactive shell
+ * - 'localfilesystem:/dev/socket/xxx': for UNIX sockets.
+ * - 'localabstract:sock_name': for UNIX abstract sockets.
+ */
+ abstract createStream(svc: string): Promise<Result<ByteStream>>;
+
+ abstract close(): void;
+
+ /** Invoke a command and return its stdout+err. */
+ async shell(cmd: string): Promise<Result<string>> {
+ const cmdOut = new ResizableArrayBuffer();
+ const streamEndedPromise = defer<string>();
+ const status = await this.createStream(`shell:${cmd}`);
+ if (!status.ok) return status;
+ const stream = status.value;
+ stream.onData = (data: Uint8Array) => cmdOut.append(data);
+ stream.onClose = () => {
+ streamEndedPromise.resolve(utf8Decode(cmdOut.get()));
+ };
+ const outTxt = (await streamEndedPromise).trimEnd();
+ return okResult(outTxt);
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_msg.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_msg.ts
new file mode 100644
index 0000000..f98d141
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_msg.ts
@@ -0,0 +1,101 @@
+// 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 {assertTrue} from '../../../base/logging';
+import {isString} from '../../../base/object_utils';
+import {binaryEncode, utf8Decode, utf8Encode} from '../../../base/string_utils';
+
+const ADB_MSG_SIZE = 6 * 4; // 6 * int32.
+
+export interface AdbMsgHdr {
+ readonly cmd: string;
+ readonly arg0: number;
+ readonly arg1: number;
+ readonly dataLen: number;
+ readonly dataChecksum: number;
+}
+
+export interface AdbMsg extends AdbMsgHdr {
+ data: Uint8Array;
+}
+
+// A brief description of the message can be found here:
+// https://android.googlesource.com/platform/system/core/+/main/adb/protocol.txt
+//
+// struct amessage {
+// uint32_t command; // command identifier constant
+// uint32_t arg0; // first argument
+// uint32_t arg1; // second argument
+// uint32_t data_length;// length of payload (0 is allowed)
+// uint32_t data_check; // checksum of data payload
+// uint32_t magic; // command ^ 0xffffffff
+// };
+export function parseAdbMsgHdr(dv: DataView): AdbMsgHdr {
+ assertTrue(dv.byteLength === ADB_MSG_SIZE);
+ const cmd = utf8Decode(dv.buffer.slice(0, 4));
+ const cmdNum = dv.getUint32(0, true);
+ const arg0 = dv.getUint32(4, true);
+ const arg1 = dv.getUint32(8, true);
+ const dataLen = dv.getUint32(12, true);
+ const dataChecksum = dv.getUint32(16, true);
+ const cmdChecksum = dv.getUint32(20, true);
+ const magic = dv.getUint32(20, true);
+ assertTrue(magic === (cmdNum ^ 0xffffffff) >>> 0);
+ assertTrue(cmdNum === (cmdChecksum ^ 0xffffffff));
+ return {cmd, arg0, arg1, dataLen, dataChecksum};
+}
+
+export function encodeAdbMsg(
+ cmd: string,
+ arg0: number,
+ arg1: number,
+ data: Uint8Array,
+ useChecksum = false,
+) {
+ const checksum = useChecksum ? generateChecksum(data) : 0;
+ const buf = new Uint8Array(ADB_MSG_SIZE);
+ const dv = new DataView(buf.buffer);
+ for (let i = 0; i < 4; i++) {
+ dv.setUint8(i, cmd.charCodeAt(i));
+ }
+ dv.setUint32(4, arg0, true);
+ dv.setUint32(8, arg1, true);
+ dv.setUint32(12, data.byteLength, true);
+ dv.setUint32(16, checksum, true);
+ dv.setUint32(20, dv.getUint32(0, true) ^ 0xffffffff, true);
+
+ return buf;
+}
+
+export function encodeAdbData(data?: Uint8Array | string): Uint8Array {
+ if (data === undefined) return new Uint8Array([]);
+ if (isString(data)) return utf8Encode(data + '\0');
+ return data;
+}
+
+function generateChecksum(data: Uint8Array): number {
+ let res = 0;
+ for (let i = 0; i < data.byteLength; i++) res += data[i];
+ return res & 0xffffffff;
+}
+
+export function adbMsgToString(msg: AdbMsg | AdbMsgHdr) {
+ return (
+ `cmd=${msg.cmd}, arg0=${msg.arg0}, arg1=${msg.arg1}, ` +
+ `cksm=${msg.dataChecksum}, dlen=${msg.dataLen}` +
+ ('data' in msg && msg.data !== undefined
+ ? `, data=${binaryEncode(msg.data)}`
+ : '')
+ );
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_platform_checks.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_platform_checks.ts
new file mode 100644
index 0000000..8e79367
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_platform_checks.ts
@@ -0,0 +1,77 @@
+// 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 protos from '../../../protos';
+import {errResult, okResult, Result} from '../../../base/result';
+import {PreflightCheck} from '../interfaces/connection_check';
+import {AdbDevice} from './adb_device';
+import {getAdbTracingServiceState} from './adb_tracing_session';
+
+/**
+ * Common pre-flight checks for Android targets. This function is used by
+ * both the AdbWebusbTarget and AdbWebsocketTarget. In both cases we want to
+ * perform the same types of checks regardless of the transport.
+ * @yields a sequence of pre-flight checks.
+ */
+export async function* checkAndroidTarget(
+ adbDevice: AdbDevice,
+): AsyncGenerator<PreflightCheck> {
+ yield {
+ name: 'Android version',
+ status: await (async (): Promise<Result<string>> => {
+ const status = await adbDevice.shell('getprop ro.build.version.sdk');
+ if (!status.ok) return status;
+ const sdkVer = parseInt(status.value);
+ const minApi = 29;
+ if (sdkVer < minApi) {
+ return errResult(`Android API level ${minApi}+ (Q+) required`);
+ }
+ return okResult(`API level ${sdkVer} >= ${minApi}`);
+ })(),
+ };
+ yield {
+ name: 'traced running?',
+ status: await (async (): Promise<Result<string>> => {
+ const status = await adbDevice.shell('pidof traced');
+ if (!status.ok) return status;
+ if (isFinite(parseInt(status.value))) {
+ return okResult(`pid = ${status.value}`);
+ }
+ return errResult(
+ 'Not running. Try `adb shell setprop persist.traced.enable 1`',
+ );
+ })(),
+ };
+ const svcStatus = await getAdbTracingServiceState(adbDevice);
+ yield {
+ name: 'Traced version',
+ status: await (async (): Promise<Result<string>> => {
+ if (!svcStatus.ok) return svcStatus;
+ return okResult(svcStatus.value.tracingServiceVersion ?? 'N/A');
+ })(),
+ };
+ if (svcStatus === undefined) return;
+ yield {
+ name: 'Traced state',
+ status: await (async (): Promise<Result<string>> => {
+ if (!svcStatus.ok) return svcStatus;
+ const tss: protos.ITracingServiceState = svcStatus.value;
+ return okResult(
+ `#producers: ${tss.producers?.length ?? 'N/A'}, ` +
+ `#datasources: ${tss.dataSources?.length ?? 'N/A'}, ` +
+ `#sessions: ${tss.numSessionsStarted ?? 'N/A'}`,
+ );
+ })(),
+ };
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_tracing_session.ts
new file mode 100644
index 0000000..9e962d4
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_tracing_session.ts
@@ -0,0 +1,55 @@
+// 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 protos from '../../../protos';
+import {AdbDevice} from './adb_device';
+import {TracingProtocol} from '../tracing_protocol/tracing_protocol';
+import {errResult, okResult, Result} from '../../../base/result';
+import {exists} from '../../../base/utils';
+import {ConsumerIpcTracingSession} from '../tracing_protocol/consumer_ipc_tracing_session';
+
+export const CONSUMER_SOCKET = '/dev/socket/traced_consumer';
+
+export async function createAdbTracingSession(
+ adbDevice: AdbDevice,
+ traceConfig: protos.ITraceConfig,
+): Promise<Result<ConsumerIpcTracingSession>> {
+ const streamStatus = await adbDevice.createStream(
+ `localfilesystem:${CONSUMER_SOCKET}`,
+ );
+ if (!streamStatus.ok) return streamStatus;
+ const stream = streamStatus.value;
+ const consumerIpc = await TracingProtocol.create(stream);
+ const session = new ConsumerIpcTracingSession(consumerIpc, traceConfig);
+ return okResult(session);
+}
+
+export async function getAdbTracingServiceState(
+ adbDevice: AdbDevice,
+): Promise<Result<protos.ITracingServiceState>> {
+ const sock = CONSUMER_SOCKET;
+ const status = await adbDevice.createStream(`localfilesystem:${sock}`);
+ if (!status.ok) {
+ return errResult(`Failed to connect to ${sock}: ${status.error}`);
+ }
+ const stream = status.value;
+ using consumerPort = await TracingProtocol.create(stream);
+ const req = new protos.QueryServiceStateRequest({});
+ const rpcCall = consumerPort.invokeStreaming('QueryServiceState', req);
+ const resp = await rpcCall.promise;
+ if (!exists(resp.serviceState)) {
+ return errResult('Failed to decode QueryServiceStateResponse');
+ }
+ return okResult(resp.serviceState);
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_device.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_device.ts
new file mode 100644
index 0000000..a468103
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_device.ts
@@ -0,0 +1,89 @@
+// 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 {Result, errResult, okResult} from '../../../../base/result';
+import {WebSocketStream} from '../../websocket/websocket_stream';
+import {AdbDevice} from '../adb_device';
+import {adbCmdAndWait} from './adb_websocket_utils';
+import {AsyncWebsocket} from '../../websocket/async_websocket';
+
+/**
+ * This class implements the state machine required to communicate with an ADB
+ * device over WebSocket using the perfetto websocket_bridge.
+ * It takes a websocket url as input (which behind the scenes is a plain
+ * bridge to adbd TCP on 127.0.0.1:5037) and a device serial and returns an
+ * object suitable to run shell commands and create streams on it.
+ */
+export class AdbWebsocketDevice extends AdbDevice {
+ private streams = new Array<WebSocketStream>();
+
+ private constructor(
+ private wsUrl: string,
+ private deviceSerial: string,
+ // This socket is only used to tell if we are still connected or not.
+ // Each stream needs a new websocket because of the way the ADB TCP protocol
+ // works.
+ private transportSock: AsyncWebsocket,
+ ) {
+ super();
+ }
+
+ static async connect(
+ wsUrl: string,
+ deviceSerial: string,
+ ): Promise<Result<AdbWebsocketDevice>> {
+ const status = await this.connectToTransport(wsUrl, deviceSerial);
+ if (!status.ok) return status;
+ const sock = status.value;
+ return okResult(new AdbWebsocketDevice(wsUrl, deviceSerial, sock));
+ }
+
+ private static async connectToTransport(
+ wsUrl: string,
+ deviceSerial: string,
+ ): Promise<Result<AsyncWebsocket>> {
+ const sock = await AsyncWebsocket.connect(wsUrl);
+ if (sock === undefined) {
+ return errResult(`Connection to ${wsUrl} failed`);
+ }
+ const transport = `host:transport:${deviceSerial}`;
+ const status = await adbCmdAndWait(sock, transport, false);
+ if (!status.ok) return status;
+ return okResult(sock);
+ }
+
+ override async createStream(svc: string): Promise<Result<WebSocketStream>> {
+ const connRes = await AdbWebsocketDevice.connectToTransport(
+ this.wsUrl,
+ this.deviceSerial,
+ );
+ if (!connRes.ok) return connRes;
+ const sock = connRes.value;
+ const status = await adbCmdAndWait(sock, svc, false);
+ if (!status.ok) return status;
+ const stream = new WebSocketStream(sock.release());
+ this.streams.push(stream);
+ return okResult(stream);
+ }
+
+ get connected(): boolean {
+ return this.transportSock.connected;
+ }
+
+ override close(): void {
+ this.transportSock.close();
+ this.streams.forEach((s) => s.close());
+ this.streams.splice(0);
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target.ts
new file mode 100644
index 0000000..5bc6dd7
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target.ts
@@ -0,0 +1,95 @@
+// 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 protos from '../../../../protos';
+import {errResult, okResult, Result} from '../../../../base/result';
+import {PreflightCheck} from '../../interfaces/connection_check';
+import {RecordingTarget} from '../../interfaces/recording_target';
+import {ConsumerIpcTracingSession} from '../../tracing_protocol/consumer_ipc_tracing_session';
+import {checkAndroidTarget} from '../adb_platform_checks';
+import {
+ createAdbTracingSession,
+ getAdbTracingServiceState,
+} from '../adb_tracing_session';
+import {AdbWebsocketDevice} from './adb_websocket_device';
+import {AsyncLazy} from '../../../../base/async_lazy';
+
+export class AdbWebsocketTarget implements RecordingTarget {
+ readonly kind = 'LIVE_RECORDING';
+ readonly platform = 'ANDROID';
+ readonly transportType = 'WebSocket';
+
+ private adbDevice = new AsyncLazy<AdbWebsocketDevice>();
+
+ constructor(
+ private wsUrl: string,
+ private serial: string,
+ private model: string,
+ ) {}
+
+ get id(): string {
+ return this.serial;
+ }
+
+ get name(): string {
+ return `${this.model} [${this.serial}]`;
+ }
+
+ get connected(): boolean {
+ return this.adbDevice.value?.connected ?? false;
+ }
+
+ async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {
+ yield {
+ name: 'WebSocket connection',
+ status: await (async (): Promise<Result<string>> => {
+ const status = await this.connectIfNeeded();
+ if (!status.ok) return status;
+ return okResult('connected');
+ })(),
+ };
+ if (this.adbDevice.value === undefined) return;
+ yield* checkAndroidTarget(this.adbDevice.value);
+ }
+
+ private async connectIfNeeded(): Promise<Result<AdbWebsocketDevice>> {
+ return this.adbDevice.getOrCreate(() =>
+ AdbWebsocketDevice.connect(this.wsUrl, this.serial),
+ );
+ }
+
+ disconnect(): void {
+ // There isn't much to do in this case. If the device is disconnected,
+ // the per-stream sockets will be naturally closed by adb. In turn,
+ // websocket_bridge will propagate that as a closure of the per-stream
+ // WebSockets.
+ this.adbDevice.value?.close();
+ this.adbDevice.reset();
+ }
+
+ async getServiceState(): Promise<Result<protos.ITracingServiceState>> {
+ if (this.adbDevice.value === undefined) {
+ return errResult('WebSocket transport disconnected');
+ }
+ return getAdbTracingServiceState(this.adbDevice.value);
+ }
+
+ async startTracing(
+ traceConfig: protos.ITraceConfig,
+ ): Promise<Result<ConsumerIpcTracingSession>> {
+ const adbDeviceStatus = await this.connectIfNeeded();
+ if (!adbDeviceStatus.ok) return adbDeviceStatus;
+ return await createAdbTracingSession(adbDeviceStatus.value, traceConfig);
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target_provider.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target_provider.ts
new file mode 100644
index 0000000..54592bf
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target_provider.ts
@@ -0,0 +1,98 @@
+// 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 {errResult, okResult, Result} from '../../../../base/result';
+import {exists} from '../../../../base/utils';
+import {PreflightCheck} from '../../interfaces/connection_check';
+import {AsyncWebsocket} from '../../websocket/async_websocket';
+import {RecordingTargetProvider} from '../../interfaces/recording_target_provider';
+import {AdbWebsocketTarget} from './adb_websocket_target';
+import {adbCmdAndWait} from './adb_websocket_utils';
+import {EvtSource} from '../../../../base/events';
+import {websocketInstructions} from '../../websocket/websocket_utils';
+
+export class AdbWebsocketTargetProvider implements RecordingTargetProvider {
+ readonly id = 'adb_websocket';
+ readonly name = 'ADB + WebSocket';
+ readonly description =
+ 'This option uses the adbd server and can co-exist with other ' +
+ 'adb-based tools. Requires launching the websocket_bridge on the host.';
+ readonly icon = 'lan';
+ readonly supportedPlatforms = ['ANDROID'] as const;
+ private readonly wsHost = '127.0.0.1:8037';
+ readonly onTargetsChanged = new EvtSource<void>();
+ private targets = new Map<string, AdbWebsocketTarget>();
+
+ async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {
+ yield {
+ name: 'WebSocket connection',
+ status: await (async (): Promise<Result<string>> => {
+ using sock = await AsyncWebsocket.connect(this.wsUrl);
+ return sock
+ ? okResult('Connected')
+ : errResult(
+ `Failed to connect ${this.wsUrl}. ` +
+ websocketInstructions('ANDROID'),
+ );
+ })(),
+ };
+ }
+
+ async listTargets(): Promise<AdbWebsocketTarget[]> {
+ await this.refreshTargets();
+ return Array.from(this.targets.values());
+ }
+
+ private async refreshTargets() {
+ const adbDevices = await this.listAdbdDevices();
+ // Find and disconnected devices.
+ for (const [serial, target] of this.targets.entries()) {
+ if (!adbDevices.has(serial)) {
+ target.disconnect();
+ this.targets.delete(serial);
+ }
+ }
+ // Find new devices.
+ for (const [serial, model] of adbDevices.entries()) {
+ if (this.targets.has(serial)) continue; // We already have a target.
+ const newTarget = new AdbWebsocketTarget(this.wsUrl, serial, model);
+ this.targets.set(serial, newTarget);
+ }
+ }
+
+ // Returns a map of device serial -> product.
+ private async listAdbdDevices(): Promise<Map<string, string>> {
+ const devices = new Map<string, string>();
+ using sock = await AsyncWebsocket.connect(this.wsUrl);
+ if (!sock) return devices;
+ const status = await adbCmdAndWait(sock, 'host:devices-l', true);
+ if (!status.ok) return devices;
+ for (const line of status.value.trimEnd().split('\n')) {
+ if (line === '') continue;
+ const m = line.match(/^(\w+)\s+.*model:([^ ]+)/);
+ if (!exists(m)) {
+ console.warn('Could not parse ADB device', line);
+ continue;
+ }
+ const serial = m[1];
+ const model = m[2];
+ devices.set(serial, model);
+ }
+ return devices;
+ }
+
+ private get wsUrl(): string {
+ return `ws://${this.wsHost}/adb`;
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_utils.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_utils.ts
new file mode 100644
index 0000000..486802d
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_utils.ts
@@ -0,0 +1,49 @@
+// 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 {assertTrue} from '../../../../base/logging';
+import {Result, okResult, errResult} from '../../../../base/result';
+import {AsyncWebsocket} from '../../websocket/async_websocket';
+import {prefixWithHexLen} from '../../websocket/websocket_utils';
+
+/**
+ * Sends an ADB command over the websocket and waits for an OKAY or FAIL.
+ * If `wantResponse` == true, expects a payload after the OKAY.
+ * For all intents and purposes, the websocket here is the moral equivalent of
+ * talking directly to ADB on 127.0.0.1:5037.
+ * See //packages/modules/adb/docs/dev/services.md .
+ */
+export async function adbCmdAndWait(
+ ws: AsyncWebsocket,
+ cmd: string,
+ wantResponse: boolean,
+): Promise<Result<string>> {
+ ws.send(prefixWithHexLen(cmd));
+ const hdr = await ws.waitForString(4);
+ if (hdr === 'FAIL' || (hdr === 'OKAY' && wantResponse)) {
+ const hexLen = await ws.waitForString(4);
+ const len = parseInt(hexLen, 16);
+ assertTrue(!isNaN(len));
+ const payload = await ws.waitForString(len);
+ if (hdr === 'OKAY') {
+ return okResult(payload);
+ } else {
+ return errResult(payload);
+ }
+ } else if (hdr === 'OKAY') {
+ return okResult('');
+ } else {
+ return errResult(`ADB protocol error, hdr ${hdr}`);
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_key.ts
similarity index 95%
rename from ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts
rename to ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_key.ts
index 7ed275e..6f4d139 100644
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_key.ts
@@ -19,7 +19,6 @@
base64Encode,
hexEncode,
} from '../../../../base/string_utils';
-import {RecordingError} from '../recording_error_handling';
const WORD_SIZE = 4;
const MODULUS_SIZE_BITS = 2048;
@@ -54,36 +53,6 @@
qi: string;
}
-function isValidJsonWebKey(key: JsonWebKey): key is ValidJsonWebKey {
- return (
- key.n !== undefined &&
- key.e !== undefined &&
- key.d !== undefined &&
- key.p !== undefined &&
- key.q !== undefined &&
- key.dp !== undefined &&
- key.dq !== undefined &&
- key.qi !== undefined
- );
-}
-
-// Convert a BigInteger to an array of a specified size in bytes.
-function bigIntToFixedByteArray(bn: BigInteger, size: number): Uint8Array {
- const paddedBnBytes = bn.toByteArray();
- let firstNonZeroIndex = 0;
- while (
- firstNonZeroIndex < paddedBnBytes.length &&
- paddedBnBytes[firstNonZeroIndex] === 0
- ) {
- firstNonZeroIndex++;
- }
- const bnBytes = Uint8Array.from(paddedBnBytes.slice(firstNonZeroIndex));
- const res = new Uint8Array(size);
- assertTrue(bnBytes.length <= res.length);
- res.set(bnBytes, res.length - bnBytes.length);
- return res;
-}
-
export class AdbKey {
// We use this JsonWebKey to:
// - create a private key and sign with it
@@ -92,11 +61,15 @@
// from the device and deserialize)
jwkPrivate: ValidJsonWebKey;
+ static deserialize(serializedKey: string): AdbKey {
+ return new AdbKey(JSON.parse(serializedKey));
+ }
+
private constructor(jwkPrivate: ValidJsonWebKey) {
this.jwkPrivate = jwkPrivate;
}
- static async GenerateNewKeyPair(): Promise<AdbKey> {
+ static async generateNewKeyPair(): Promise<AdbKey> {
// Construct a new CryptoKeyPair and keep its private key in JWB format.
const keyPair = await crypto.subtle.generateKey(
ADB_WEB_CRYPTO_ALGORITHM,
@@ -105,15 +78,11 @@
);
const jwkPrivate = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
if (!isValidJsonWebKey(jwkPrivate)) {
- throw new RecordingError('Could not generate a valid private key.');
+ throw new Error('Could not generate a valid ADB private key');
}
return new AdbKey(jwkPrivate);
}
- static DeserializeKey(serializedKey: string): AdbKey {
- return new AdbKey(JSON.parse(serializedKey));
- }
-
// Perform an RSA signing operation for the ADB auth challenge.
//
// For the RSA signature, the token is expected to have already
@@ -193,7 +162,37 @@
return base64Encode(dvU8) + ' ui.perfetto.dev';
}
- serializeKey(): string {
+ serialize(): string {
return JSON.stringify(this.jwkPrivate);
}
}
+
+function isValidJsonWebKey(key: JsonWebKey): key is ValidJsonWebKey {
+ return (
+ key.n !== undefined &&
+ key.e !== undefined &&
+ key.d !== undefined &&
+ key.p !== undefined &&
+ key.q !== undefined &&
+ key.dp !== undefined &&
+ key.dq !== undefined &&
+ key.qi !== undefined
+ );
+}
+
+// Convert a BigInteger to an array of a specified size in bytes.
+function bigIntToFixedByteArray(bn: BigInteger, size: number): Uint8Array {
+ const paddedBnBytes = bn.toByteArray();
+ let firstNonZeroIndex = 0;
+ while (
+ firstNonZeroIndex < paddedBnBytes.length &&
+ paddedBnBytes[firstNonZeroIndex] === 0
+ ) {
+ firstNonZeroIndex++;
+ }
+ const bnBytes = Uint8Array.from(paddedBnBytes.slice(firstNonZeroIndex));
+ const res = new Uint8Array(size);
+ assertTrue(bnBytes.length <= res.length);
+ res.set(bnBytes, res.length - bnBytes.length);
+ return res;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_key_manager.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_key_manager.ts
new file mode 100644
index 0000000..957e1c6
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_key_manager.ts
@@ -0,0 +1,108 @@
+// 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 {assetSrc} from '../../../../base/assets';
+import {AsyncLazy} from '../../../../base/async_lazy';
+import {errResult, okResult, Result} from '../../../../base/result';
+import {exists} from '../../../../base/utils';
+import {AdbKey} from './adb_key';
+
+// How long we will store the key in memory
+const KEY_IN_MEMORY_TIMEOUT = 1000 * 60 * 30; // 30 minutes
+
+export class AdbKeyManager {
+ // private key?: AdbKey;
+ private expiryTimerId = -1;
+ private key = new AsyncLazy<AdbKey>();
+ // private asyncGuard = new AsyncGuard<AdbKey | undefined>();
+
+ // Finds a key, by priority:
+ // - Look in memory (i.e. this.key)
+ // - Look in the credential store.
+ // - Finally creates one from scratch if needed.
+ async getOrCreateKey(): Promise<Result<AdbKey>> {
+ this.refreshKeyExpiry();
+ return this.key.getOrCreate(async () => {
+ // 2. We try to get the private key from the browser.
+ // The mediation is set as 'optional', because we use
+ // 'preventSilentAccess', which sometimes requests the user to click
+ // on a button to allow the auth, but sometimes only shows a
+ // notification and does not require the user to click on anything.
+ // If we had set mediation to 'required', the user would have been
+ // asked to click on a button every time.
+ if (hasPasswordCredential()) {
+ const options: PasswordCredentialRequestOptions = {
+ password: true,
+ mediation: 'optional',
+ };
+ const credential = await navigator.credentials.get(options);
+ await navigator.credentials.preventSilentAccess();
+ if (exists(credential) && 'password' in credential) {
+ return okResult(AdbKey.deserialize(credential.password as string));
+ }
+ }
+
+ // This can happen in two cases:
+ // 1. The very first time when we have no credentials saved.
+ // 2. If the user (accidentally) dismisses the "sign in" dialog.
+ // We use this UX to prevent that if the user accidentally clicks Escape,
+ // we invalidate the key and generates a new one, which would be
+ // unauthorized.
+ if (!confirm("Couldn't load the ADB key. Generate a new key?")) {
+ return errResult(
+ "Couldn't load the ADB Key. " + 'Did you dismiss the sign-in dialog',
+ );
+ }
+
+ // 3. We generate a new key pair.
+ const newKey = await AdbKey.generateNewKeyPair();
+ await storeKeyInBrowserCredentials(newKey);
+ return okResult(newKey);
+ });
+ }
+
+ private refreshKeyExpiry() {
+ if (this.expiryTimerId >= 0) {
+ clearTimeout(this.expiryTimerId);
+ }
+ this.expiryTimerId = self.setTimeout(
+ () => this.key.reset(),
+ KEY_IN_MEMORY_TIMEOUT,
+ );
+ }
+}
+
+// Update credential store with the given key.
+async function storeKeyInBrowserCredentials(key: AdbKey): Promise<void> {
+ if (!hasPasswordCredential()) {
+ return;
+ }
+ const credential = new PasswordCredential({
+ id: 'webusb-adb-key',
+ password: key.serialize(),
+ name: 'WebUSB ADB Key',
+ iconURL: assetSrc('assets/favicon.png'),
+ });
+ // The 'Save password?' Chrome dialogue only appears if the key is
+ // not already stored in Chrome.
+ await navigator.credentials.store(credential);
+ // 'preventSilentAccess' guarantees the user is always notified when
+ // credentials are accessed. Sometimes the user is asked to click a button
+ // and other times only a notification is shown temporarily.
+ await navigator.credentials.preventSilentAccess();
+}
+
+function hasPasswordCredential() {
+ return 'PasswordCredential' in window;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_device.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_device.ts
new file mode 100644
index 0000000..22f19a9
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_device.ts
@@ -0,0 +1,435 @@
+// 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 {defer, Deferred} from '../../../../base/deferred';
+import {assertFalse, assertTrue} from '../../../../base/logging';
+import {isString} from '../../../../base/object_utils';
+import {hexEncode, utf8Decode, utf8Encode} from '../../../../base/string_utils';
+import {exists} from '../../../../base/utils';
+import {closeModal, showModal} from '../../../../widgets/modal';
+import {AdbKeyManager} from './adb_key_manager';
+import {AdbDevice} from '../adb_device';
+import {
+ encodeAdbMsg,
+ encodeAdbData,
+ parseAdbMsgHdr,
+ AdbMsg,
+ adbMsgToString,
+} from '../adb_msg';
+import {getAdbWebUsbInterface, AdbUsbInterface} from './adb_webusb_utils';
+import {errResult, okResult, Result} from '../../../../base/result';
+import {AdbWebusbStream} from './adb_webusb_stream';
+
+const ADB_MSG_SIZE = 6 * 4; // 6 * int32.
+const DEFAULT_MAX_PAYLOAD_BYTES = 256 * 1024;
+const VERSION_WITH_CHECKSUM = 0x01000000;
+const VERSION_NO_CHECKSUM = 0x01000001;
+
+/**
+ * This class implements the state machine required to communicate with an ADB
+ * device over WebUsb. It takes a {@link USBDevice} in input and returns an
+ * object suitable to run shell commands and create streams on it.
+ */
+export class AdbWebusbDevice extends AdbDevice {
+ private lastStreamId = 0;
+ private _connected = true;
+ private rxLoopRunning = false;
+ private streams = new Map<number, AdbWebusbStream>();
+ private pendingStreams = new Map<number, PendingStream>();
+ private txQueue = new Array<TxQueueEntry>();
+ private txPending = false;
+
+ /** Use {@link connect()} to obtain an instance of this class. */
+ private constructor(
+ private readonly usb: AdbUsbInterface,
+ private readonly maxPayload: number,
+ private readonly useChecksum: boolean,
+ ) {
+ super();
+ this.usb = usb;
+ // Deliberately not awaited, the rx looop will loop forever in the
+ // background until we disconnect.
+ this.usbRxLoop();
+ }
+
+ /**
+ * Creates a new instance of this class.
+ * @param usbdev the device obtained via {@link navigator.usb.requestDevice}.
+ * @param adbKeyMgr an instance of the key manager.
+ */
+ static async connect(
+ usbdev: USBDevice,
+ adbKeyMgr: AdbKeyManager,
+ ): Promise<Result<AdbWebusbDevice>> {
+ const usb = getAdbWebUsbInterface(usbdev);
+ if (usb === undefined) {
+ return errResult(
+ 'Could not find the USB Interface. ' +
+ 'Try disconnecting and reconnecting the device.',
+ );
+ }
+ if (usbdev.opened) {
+ await usbdev.close();
+ }
+ await usbdev.open();
+ using autoClose = new CloseDeviceWhenOutOfScope(usbdev);
+ await usbdev.selectConfiguration(usb.configurationValue);
+
+ try {
+ await usbdev.claimInterface(usb.usbInterfaceNumber);
+ } catch (err) {
+ console.error(err);
+ return errResult(
+ 'Failed to claim USB interface. Try `adb kill-server` or ' +
+ 'close other profiling tools and try again',
+ );
+ }
+
+ const keyRes = await adbKeyMgr.getOrCreateKey();
+ if (!keyRes.ok) return keyRes;
+ const key = keyRes.value;
+
+ await AdbWebusbDevice.send(
+ usb,
+ 'CNXN',
+ VERSION_NO_CHECKSUM,
+ DEFAULT_MAX_PAYLOAD_BYTES,
+ 'host:1:WebUsb',
+ );
+
+ // At this point there are two options:
+ // 1. The device accepts the key and responds with a CNXN msg.
+ // 2. The device doesn't recognize us, and responds with another AUTH msg.
+
+ // We need to have some tolerance from queued messages from previous
+ // sessions, hence the 10 attempts to deal with spurious messages.
+ let authAttempts = 0;
+ const modalKey = 'adbauth';
+ for (let attempt = 0; attempt < 10; attempt++) {
+ const msg = await this.recvMsg(usb);
+
+ if (msg.cmd === 'CNXN') {
+ // Success, the device authenticated us.
+ closeModal(modalKey);
+ const maxPayload = msg.arg1;
+ const ver = msg.arg0;
+ if (ver !== VERSION_WITH_CHECKSUM && ver !== VERSION_NO_CHECKSUM) {
+ return errResult(`ADB version ${ver} not supported`);
+ }
+ const useChecksum = ver === VERSION_WITH_CHECKSUM;
+ autoClose.keepOpen = true;
+ return okResult(new AdbWebusbDevice(usb, maxPayload, useChecksum));
+ }
+
+ if (msg.cmd !== 'AUTH') {
+ logSpuriousMsg(msg);
+ continue;
+ }
+
+ assertTrue(msg.arg0 === AuthCmd.TOKEN);
+ const authAttempt = authAttempts++;
+ if (authAttempt === 0) {
+ // Case 1: we are presented with a nonce to sign. If the device has
+ // previously received our public key, the dialog asking for user
+ // confirmation will NOT be displayed.
+ const signedNonce = key.sign(msg.data);
+ await this.send(usb, 'AUTH', AuthCmd.SIGNATURE, 0, signedNonce);
+ continue;
+ }
+ if (authAttempt === 1) {
+ // Case 2: present our public key. This will prompt the dialog.
+ await this.send(usb, 'AUTH', AuthCmd.PUBKEY, 0, key.getPublicKey());
+ showModal({
+ key: modalKey,
+ title: 'ADB Authorization required',
+ content: 'Please unlock the device and authorize the ADB connection',
+ });
+ continue;
+ }
+ break;
+ }
+ return errResult('ADB authorization failed');
+ }
+
+ override async createStream(svc: string): Promise<Result<AdbWebusbStream>> {
+ const ps: PendingStream = {
+ promise: defer<Result<AdbWebusbStream>>(),
+ localId: ++this.lastStreamId,
+ svc,
+ };
+ this.pendingStreams.set(ps.localId, ps);
+ this.send('OPEN', ps.localId, 0, svc);
+ return ps.promise;
+ }
+
+ override close(): void {
+ this._connected = false;
+ this.usb.dev.opened && this.usb.dev.close();
+ this.streams.forEach((stream) => this.streamClose(stream));
+ }
+
+ get connected() {
+ return this._connected;
+ }
+
+ streamWrite(
+ stream: AdbWebusbStream,
+ data: string | Uint8Array,
+ ): Promise<void> {
+ const promise = defer<void>();
+ const raw = isString(data) ? utf8Encode(data) : data;
+ let sent = 0;
+ while (sent < raw.byteLength) {
+ const chunkLen = Math.min(this.maxPayload, raw.byteLength - sent);
+ const chunk = raw.subarray(sent, sent + chunkLen);
+ sent += chunkLen;
+ const tx: TxQueueEntry = {
+ stream,
+ data: chunk,
+ // This is the last chunk. Attach the promise only to the last chunk.
+ promise: sent === raw.byteLength ? promise : undefined,
+ };
+ this.txQueue.push(tx);
+ if (!this.txPending) {
+ assertTrue(this.txQueue.length === 1);
+ this.streamWriteFromQueue(tx);
+ }
+ }
+ return promise;
+ }
+
+ streamClose(stream: AdbWebusbStream): void {
+ // Remove any pending entry from the tx queue.
+ this.txQueue = this.txQueue.filter((tx) => tx.stream !== stream);
+ this.send('CLSE', stream.localId, stream.remoteId);
+ this.streams.delete(stream.localId);
+ stream.notifyClose();
+ }
+
+ private streamWriteFromQueue(tx: TxQueueEntry) {
+ assertFalse(this.txPending);
+ this.txPending = true;
+ this.send('WRTE', tx.stream.localId, tx.stream.remoteId, tx.data);
+ }
+
+ private async usbRxLoop(): Promise<void> {
+ assertFalse(this.rxLoopRunning);
+ this.rxLoopRunning = true;
+ try {
+ while (this._connected) {
+ await this.usbRxLoopInner();
+ }
+ } catch (e) {
+ // We allow the transferIn() in recv() to fail if we disconnected. That
+ // will naturally happen in the [Symbol.dispose].
+ const transferInAborted =
+ e instanceof Error && e.message.includes('transfer was cancelled');
+ if (!(transferInAborted && !this._connected)) {
+ throw e;
+ }
+ } finally {
+ this.rxLoopRunning = false;
+ this._connected = false;
+ }
+ }
+
+ private async usbRxLoopInner(): Promise<void> {
+ const msg = await AdbWebusbDevice.recvMsg(this.usb);
+
+ if (msg.cmd === 'OKAY') {
+ // There are two cases here:
+ // 1) This is an ACK to an OPEN (new stream).
+ // 2) This is an ACK to a WRTE on an existing stream.
+ const remoteStreamId = msg.arg0;
+ const localStreamId = msg.arg1;
+ const pendingStream = this.pendingStreams.get(localStreamId);
+ if (pendingStream !== undefined) {
+ // Case 1.
+ this.pendingStreams.delete(localStreamId);
+ const stream = new AdbWebusbStream(this, localStreamId, remoteStreamId);
+ this.streams.set(localStreamId, stream);
+ pendingStream.promise.resolve(okResult(stream));
+ } else {
+ // Case 2.
+ const queuedEntry = this.popFromTxQueue(localStreamId, remoteStreamId);
+ if (queuedEntry === undefined) {
+ return logSpuriousMsg(msg);
+ }
+ this.txPending = false;
+ queuedEntry.promise?.resolve();
+ const next = this.txQueue[0];
+ next !== undefined && this.streamWriteFromQueue(next);
+ }
+ return;
+ } else if (msg.cmd === 'WRTE') {
+ const localStreamId = msg.arg1;
+ const stream = this.streams.get(localStreamId);
+ if (stream === undefined) {
+ return logSpuriousMsg(msg);
+ }
+ await this.send('OKAY', stream.localId, stream.remoteId);
+ stream.onData(msg.data);
+ } else if (msg.cmd === 'CLSE') {
+ // Close a stream.
+ const localStreamId = msg.arg1;
+
+ // If the stream has not been opened yet, this is a failure while opening.
+ const ps = this.pendingStreams.get(localStreamId);
+ if (ps !== undefined) {
+ this.pendingStreams.delete(localStreamId);
+ ps.promise.resolve(errResult(`Stream ${ps.svc} failed to connect`));
+ return;
+ }
+
+ // Otherwise the service is telling us about a stream getting closed from
+ // their end (e.g. the shell:xxx command terminated).
+ const stream = this.streams.get(localStreamId);
+ // If we initiate the closure, the stream entry is already removed.
+ if (stream !== undefined) {
+ this.streams.delete(localStreamId);
+ stream.notifyClose();
+ }
+ } else {
+ console.error(`Unexpected ADB cmd ${msg.cmd} ${msg.arg0} ${msg.arg1}`);
+ }
+ }
+
+ private popFromTxQueue(
+ localStreamId: number,
+ remoteStreamId: number,
+ ): TxQueueEntry {
+ for (let i = 0; i < this.txQueue.length; i++) {
+ const tx = this.txQueue[i];
+ if (tx.stream.localId !== localStreamId) continue;
+ if (tx.stream.remoteId !== remoteStreamId) continue;
+ return this.txQueue.splice(i, 1)[0];
+ }
+ throw new WebusbTransportError(
+ `Could not find ADB queue entry L=${localStreamId}, ` +
+ `R=${remoteStreamId}, TxLen=${this.txQueue.length}`,
+ );
+ }
+
+ private static async recv(
+ usb: AdbUsbInterface,
+ len: number,
+ ): Promise<DataView> {
+ const res = await usb.dev.transferIn(usb.rx, len);
+ if (!exists(res.data) || res.status !== 'ok') {
+ throw new WebusbTransportError(`res: ${res.status}, data: ${!!res.data}`);
+ }
+ return res.data;
+ }
+
+ private static async recvMsg(usb: AdbUsbInterface): Promise<AdbMsg> {
+ const hdrData = await this.recv(usb, ADB_MSG_SIZE);
+ if (hdrData.byteLength !== ADB_MSG_SIZE) {
+ const arr = new Uint8Array(hdrData.buffer);
+ throw new WebusbTransportError(
+ `RX spurious: ${hexEncode(arr)} ${utf8Decode(arr)}`,
+ );
+ }
+ const hdr = parseAdbMsgHdr(hdrData);
+ let payload = new Uint8Array();
+ if (hdr.dataLen > 0) {
+ const payloadData = await this.recv(usb, hdr.dataLen);
+ payload = new Uint8Array(
+ payloadData.buffer,
+ payloadData.byteOffset,
+ payloadData.byteLength,
+ ).slice();
+ }
+ return {...hdr, data: payload};
+ }
+
+ private send(
+ cmd: string,
+ arg0: number,
+ arg1: number,
+ data?: Uint8Array | string,
+ ): Promise<void> {
+ if (!this.connected) return Promise.resolve();
+ const useCksum = this.useChecksum;
+ return AdbWebusbDevice.send(this.usb, cmd, arg0, arg1, data, useCksum);
+ }
+
+ private static async send(
+ usb: AdbUsbInterface,
+ cmd: string,
+ arg0: number,
+ arg1: number,
+ data?: Uint8Array | string,
+ useChecksum = false,
+ ): Promise<void> {
+ const payload = encodeAdbData(data);
+ const header = encodeAdbMsg(cmd, arg0, arg1, payload, useChecksum);
+
+ // The header and the message data must be sent consecutively. In order to
+ // avoid interleaving ([hdr1] [hdr2] [data1] [data2]), we chain promises.
+ const sendPromises = [usb.dev.transferOut(usb.tx, header.buffer)];
+ if (payload.length > 0) {
+ sendPromises.push(usb.dev.transferOut(usb.tx, payload.buffer));
+ if (payload.length % usb.txPacketSize === 0) {
+ // if the number of bytes transferred fits exactly into packets then
+ // we need an extra zero length packet at the end.
+ sendPromises.push(usb.dev.transferOut(usb.tx, new Uint8Array(0)));
+ }
+ }
+ await Promise.all(sendPromises);
+ }
+}
+
+enum AuthCmd {
+ TOKEN = 1,
+ SIGNATURE = 2,
+ PUBKEY = 3,
+}
+
+interface TxQueueEntry {
+ stream: AdbWebusbStream;
+ data: Uint8Array;
+ promise?: Deferred<void>;
+}
+
+interface PendingStream {
+ promise: Deferred<Result<AdbWebusbStream>>;
+ localId: number;
+ svc: string; // The service being requested, e.g. 'shell:whoami'.
+}
+
+class WebusbTransportError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'WebusbTransportError';
+ }
+}
+
+// These log messages are non-fatal because we need to tolerate the fact that
+// adbd can buffer messages from previous connections (e.g. if reloading a tab)
+// and won't clear the queue when we restart the flow (as one would expect).
+function logSpuriousMsg(msg: AdbMsg): void {
+ console.log('Spurious ADB message', adbMsgToString(msg));
+}
+
+class CloseDeviceWhenOutOfScope {
+ constructor(private usbdev: USBDevice) {}
+ keepOpen = false;
+
+ [Symbol.dispose]() {
+ if (this.keepOpen) return;
+ if (this.usbdev.opened) {
+ this.usbdev.close();
+ }
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_stream.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_stream.ts
new file mode 100644
index 0000000..31357d5
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_stream.ts
@@ -0,0 +1,58 @@
+// 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 {ByteStream} from '../../interfaces/byte_stream';
+import {AdbWebusbDevice} from './adb_webusb_device';
+
+export class AdbWebusbStream extends ByteStream {
+ private state: 'CONNECTED' | 'CLOSING' | 'CLOSED' = 'CONNECTED';
+
+ constructor(
+ private adbWebusbDevice: AdbWebusbDevice,
+ readonly localId: number,
+ readonly remoteId: number,
+ ) {
+ super();
+ }
+
+ get connected(): boolean {
+ return this.state === 'CONNECTED';
+ }
+
+ write(data: string | Uint8Array): Promise<void> {
+ if (this.state !== 'CONNECTED') {
+ // Ignore writes queued once the stream is being closed.
+ return Promise.resolve();
+ }
+ return this.adbWebusbDevice.streamWrite(this, data);
+ }
+
+ // This is invoked by the user to request closure. This is the case when the
+ // closure is initiated by the caller (e.g. terminating a shell process).
+ close(): void {
+ if (this.state !== 'CONNECTED') return;
+ this.state = 'CLOSING';
+ this.adbWebusbDevice.streamClose(this);
+ }
+
+ // Called by AdbWebusbTransport in two cases:
+ // 1. To ACK a closure request, if we are in state = 'CLOSING'.
+ // 2. To inform us about device-side closure (e.g. the process terminated)
+ // if we are in state 'CONNECTED'.
+ notifyClose() {
+ if (this.state === 'CLOSED') return;
+ this.state = 'CLOSED';
+ this.onClose();
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target.ts
new file mode 100644
index 0000000..e9c5322
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target.ts
@@ -0,0 +1,94 @@
+// 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 protos from '../../../../protos';
+import {RecordingTarget} from '../../interfaces/recording_target';
+import {PreflightCheck} from '../../interfaces/connection_check';
+import {AdbKeyManager} from './adb_key_manager';
+import {
+ createAdbTracingSession,
+ getAdbTracingServiceState,
+} from '../adb_tracing_session';
+import {AdbWebusbDevice} from './adb_webusb_device';
+import {AdbUsbInterface, usbDeviceToStr} from './adb_webusb_utils';
+import {errResult, okResult, Result} from '../../../../base/result';
+import {checkAndroidTarget} from '../adb_platform_checks';
+import {ConsumerIpcTracingSession} from '../../tracing_protocol/consumer_ipc_tracing_session';
+import {AsyncLazy} from '../../../../base/async_lazy';
+
+export class AdbWebusbTarget implements RecordingTarget {
+ readonly kind = 'LIVE_RECORDING';
+ readonly platform = 'ANDROID';
+ readonly transportType = 'WebUSB';
+ private adbDevice = new AsyncLazy<AdbWebusbDevice>();
+
+ constructor(
+ private usbiface: AdbUsbInterface,
+ private adbKeyMgr: AdbKeyManager,
+ ) {}
+
+ async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {
+ const status = await this.connectIfNeeded();
+
+ yield {
+ name: 'WebUSB connection',
+ status: await (async (): Promise<Result<string>> => {
+ if (!status.ok) return status;
+ return okResult('connected');
+ })(),
+ };
+
+ if (this.adbDevice.value === undefined) return;
+ yield* checkAndroidTarget(this.adbDevice.value);
+ }
+
+ async connectIfNeeded(): Promise<Result<AdbWebusbDevice>> {
+ return this.adbDevice.getOrCreate(() =>
+ AdbWebusbDevice.connect(this.usbiface.dev, this.adbKeyMgr),
+ );
+ }
+
+ get connected(): boolean {
+ return this.adbDevice.value?.connected ?? false;
+ }
+
+ get id(): string {
+ return usbDeviceToStr(this.usbiface.dev);
+ }
+
+ get name(): string {
+ const dev = this.usbiface.dev;
+ return `${dev.productName} [${dev.serialNumber}]`;
+ }
+
+ async getServiceState(): Promise<Result<protos.ITracingServiceState>> {
+ if (this.adbDevice.value === undefined) {
+ return errResult('WebUSB transport disconnected');
+ }
+ return getAdbTracingServiceState(this.adbDevice.value);
+ }
+
+ async startTracing(
+ traceConfig: protos.ITraceConfig,
+ ): Promise<Result<ConsumerIpcTracingSession>> {
+ const adbDeviceStatus = await this.connectIfNeeded();
+ if (!adbDeviceStatus.ok) return adbDeviceStatus;
+ return await createAdbTracingSession(adbDeviceStatus.value, traceConfig);
+ }
+
+ disconnect(): void {
+ this.adbDevice.value?.close();
+ this.adbDevice.reset();
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target_provider.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target_provider.ts
new file mode 100644
index 0000000..c962a7b
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target_provider.ts
@@ -0,0 +1,132 @@
+// 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 {exists} from '../../../../base/utils';
+import {PreflightCheck} from '../../interfaces/connection_check';
+import {AdbKeyManager} from './adb_key_manager';
+import {
+ ADB_DEVICE_FILTER,
+ AdbUsbInterface,
+ getAdbWebUsbInterface,
+ usbDeviceToStr,
+} from './adb_webusb_utils';
+import {errResult} from '../../../../base/result';
+import {RecordingTargetProvider} from '../../interfaces/recording_target_provider';
+import {AdbWebusbTarget} from './adb_webusb_target';
+import {EvtSource} from '../../../../base/events';
+
+export class AdbWebusbTargetProvider implements RecordingTargetProvider {
+ readonly id = 'adb_webusb';
+ readonly name = 'WebUsb';
+ readonly icon = 'usb';
+ readonly supportedPlatforms = ['ANDROID'] as const;
+ readonly description =
+ 'This is the easiest option to use but requires exclusive access to the ' +
+ 'device. If you are an android developer and use ADB, you should use the ' +
+ 'websocket option instead.';
+
+ private adbKeyMgr = new AdbKeyManager();
+ private targets = new Map<string, AdbWebusbTarget>();
+ readonly onTargetsChanged = new EvtSource<void>();
+
+ constructor() {
+ if (!exists(navigator.usb)) return;
+ navigator.usb.addEventListener('disconnect', () => this.refreshTargets());
+ navigator.usb.addEventListener('connect', () => this.refreshTargets());
+ }
+
+ async listTargets(): Promise<AdbWebusbTarget[]> {
+ if (!exists(navigator.usb)) return [];
+ await this.refreshTargets();
+ return Array.from(this.targets.values());
+ }
+
+ async pairNewTarget(): Promise<AdbWebusbTarget | undefined> {
+ if (!exists(navigator.usb)) return undefined;
+ let usbdev: USBDevice;
+ try {
+ usbdev = await navigator.usb.requestDevice({
+ filters: [ADB_DEVICE_FILTER],
+ });
+ } catch (err) {
+ if (`${err.name}` === 'NotFoundError') {
+ return undefined; // The user just clicked cancel.
+ }
+ throw err;
+ }
+ const usbiface = getAdbWebUsbInterface(usbdev);
+ if (usbiface === undefined) return undefined;
+
+ const key = usbDeviceToStr(usbdev);
+ this.removeTarget(key);
+
+ // If the user re-pairs the same device, remove it from the list and keep
+ // the new one.
+ const newTarget = new AdbWebusbTarget(usbiface, this.adbKeyMgr);
+ this.targets.set(key, newTarget);
+ this.onTargetsChanged.notify();
+ return newTarget;
+ }
+
+ async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {
+ if (!exists(navigator.usb)) {
+ yield {
+ name: 'WebUSB support',
+ status: errResult(`Not supported`),
+ };
+ }
+ }
+
+ private async refreshTargets() {
+ let triggerOnTrgetsChanged = false;
+ const usbDevices = await this.listUsbDevices();
+ // Find and disconnected devices.
+ for (const key of this.targets.keys()) {
+ if (!usbDevices.has(key)) {
+ // Entry disconnected.
+ this.removeTarget(key);
+ triggerOnTrgetsChanged = true;
+ }
+ }
+ for (const [key, usbiface] of usbDevices.entries()) {
+ if (this.targets.has(key)) continue; // We already have this target.
+ const newTarget = new AdbWebusbTarget(usbiface, this.adbKeyMgr);
+ this.targets.set(key, newTarget);
+ triggerOnTrgetsChanged = true;
+ }
+ triggerOnTrgetsChanged && this.onTargetsChanged.notify();
+ }
+
+ private removeTarget(key: string) {
+ const target = this.targets.get(key);
+ if (target === undefined) return;
+ this.targets.delete(key);
+ target.disconnect();
+ }
+
+ private async listUsbDevices(): Promise<Map<string, AdbUsbInterface>> {
+ const devices = new Map<string, AdbUsbInterface>();
+ // NOTE: getDevices() only returns the previously paired devices. It will
+ // not list connected devices that never got paired. In order to discover
+ // those we need to call navigator.usb.requestDevices() which prompts the
+ // "pair device" dialog. See pairNewTarget().
+ for (const dev of await navigator.usb.getDevices()) {
+ const usbiface = getAdbWebUsbInterface(dev);
+ if (usbiface === undefined) continue;
+ const key = usbDeviceToStr(dev);
+ devices.set(key, usbiface);
+ }
+ return devices;
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_utils.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_utils.ts
new file mode 100644
index 0000000..cbf12db
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_utils.ts
@@ -0,0 +1,73 @@
+// 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 {exists} from '../../../../base/utils';
+
+export const ADB_DEVICE_FILTER = {
+ classCode: 255, // USB vendor specific code
+ subclassCode: 66, // Android vendor specific subclass
+ protocolCode: 1, // Adb protocol
+};
+
+export interface AdbUsbInterface {
+ readonly dev: USBDevice;
+ readonly configurationValue: number;
+ readonly usbInterfaceNumber: number;
+ readonly rx: number;
+ readonly tx: number;
+ readonly txPacketSize: number;
+}
+
+// Returns a key that can be used to index the device in a map for idempotency
+// checks.
+export function usbDeviceToStr(d: USBDevice): string {
+ const ver = `${d.deviceVersionMajor}.${d.deviceVersionMinor}`;
+ return `${d.vendorId}:${d.productId}:${ver}:${d.serialNumber}`;
+}
+
+export function getAdbWebUsbInterface(
+ device: USBDevice,
+): AdbUsbInterface | undefined {
+ if (!exists(device.serialNumber)) return undefined;
+ const adbDeviceFilter = ADB_DEVICE_FILTER;
+ for (const config of device.configurations) {
+ for (const iface of config.interfaces) {
+ for (const alt of iface.alternates) {
+ if (
+ alt.interfaceClass === adbDeviceFilter.classCode &&
+ alt.interfaceSubclass === adbDeviceFilter.subclassCode &&
+ alt.interfaceProtocol === adbDeviceFilter.protocolCode
+ ) {
+ const rxEndpoint = alt.endpoints.find(
+ (e) => e.type === 'bulk' && e.direction === 'in',
+ );
+ const txEndpoint = alt.endpoints.find(
+ (e) => e.type === 'bulk' && e.direction === 'out',
+ );
+ if (rxEndpoint === undefined || txEndpoint === undefined) continue;
+ return {
+ dev: device,
+ configurationValue: config.configurationValue,
+ usbInterfaceNumber: iface.interfaceNumber,
+ rx: rxEndpoint.endpointNumber,
+ tx: txEndpoint.endpointNumber,
+ txPacketSize: txEndpoint.packetSize,
+ };
+ } // if (alternate)
+ } // for (interface.alternates)
+ } // for (configuration.interfaces)
+ } // for (configurations)
+
+ return undefined;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/credentials_interfaces.d.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/credentials_interfaces.d.ts
similarity index 100%
rename from ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/credentials_interfaces.d.ts
rename to ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/credentials_interfaces.d.ts
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_target.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_target.ts
new file mode 100644
index 0000000..76b1a7d
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_target.ts
@@ -0,0 +1,200 @@
+// 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 protos from '../../../protos';
+import {defer, Deferred} from '../../../base/deferred';
+import {errResult, okResult, Result} from '../../../base/result';
+import {binaryEncode} from '../../../base/string_utils';
+import {exists} from '../../../base/utils';
+import {PreflightCheck} from '../interfaces/connection_check';
+import {RecordingTarget} from '../interfaces/recording_target';
+import {TargetPlatformId} from '../interfaces/target_platform';
+import {ChromeExtensionTracingSession} from './chrome_extension_tracing_session';
+
+const EXTENSION_ID = 'lfmkphfpdbjijhpomgecfikhfohaoine';
+const EXTENSION_URL = `g.co/chrome/tracing-extension`;
+
+export class ChromeExtensionTarget implements RecordingTarget {
+ readonly id = 'chrome_extension';
+ readonly kind = 'LIVE_RECORDING';
+ readonly transportType = 'Extension';
+ platform: TargetPlatformId = 'CHROME';
+ private port?: chrome.runtime.Port;
+ private _connected = false;
+ private _extensionVersion?: string;
+ private _connectPromise?: Deferred<void>;
+ private chromeCategories?: string[];
+ private chromeCategoriesPromise = defer<string[]>();
+ private session?: ChromeExtensionTracingSession;
+
+ async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {
+ yield {
+ name: 'Tracing Extension',
+ status: await (async (): Promise<Result<string>> => {
+ if (!exists(window.chrome) || !exists(window.chrome.runtime)) {
+ return errResult(
+ 'window.chrome.runtime not Available. ' +
+ 'The extension is supported only in the Chrome browser',
+ );
+ }
+ try {
+ await this.connectIfNeeded();
+ } catch {}
+ return this._connected
+ ? okResult(`Connected (version: ${this._extensionVersion})`)
+ : errResult(`Not found. Please install ${EXTENSION_URL}`);
+ })(),
+ };
+
+ if (this.platform === 'CHROME_OS') {
+ yield {
+ name: 'CrOS detection',
+ status: ((): Result<string> => {
+ const userAgent = navigator.userAgent;
+ const isChromeOS = /CrOS/.test(userAgent);
+ return isChromeOS ? okResult(userAgent) : errResult(userAgent);
+ })(),
+ };
+ }
+ }
+
+ async connectIfNeeded(): Promise<void> {
+ if (!exists(window.chrome) || !exists(window.chrome.runtime)) {
+ return;
+ }
+ if (this._connected) return;
+ this.port = window.chrome.runtime.connect(EXTENSION_ID);
+ this.port.onMessage.addListener(this.onExtensionMessage.bind(this));
+ this.port.onDisconnect.addListener(this.onExtensionDisconnect.bind(this));
+
+ // This promise is resolved once the extension replies with 'version'.
+ // Unfortunately the chrome.runtime API doesn't offer a way to tell if the
+ // extension exists or not. The port is always connected. If the extension
+ // doesn't exist, then we receive an onDisconnect soon after.
+ const retPromise = defer<void>();
+ this._connectPromise = retPromise;
+
+ // This will trigger a promise resolution once the extension replies with
+ // the version (in onExtensionMessage() below);
+ this.invokeExtensionMethod('ExtensionVersion');
+ await retPromise;
+ }
+
+ disconnect(): void {
+ this._connected = false;
+ this.port?.disconnect();
+ this.port = undefined;
+ }
+
+ get connected(): boolean {
+ return this._connected;
+ }
+
+ get name(): string {
+ return 'Chrome (this browser)';
+ }
+
+ get emitsCompressedtrace(): boolean {
+ return this.platform === 'CHROME';
+ }
+
+ async getServiceState(): Promise<Result<protos.ITracingServiceState>> {
+ const categories = await this.getChromeCategories();
+ return okResult(categoriesToServiceState(categories));
+ }
+
+ async getChromeCategories(): Promise<string[]> {
+ if (this.chromeCategories === undefined) {
+ await this.connectIfNeeded();
+ this.chromeCategories = await this.chromeCategoriesPromise;
+ }
+ return this.chromeCategories;
+ }
+
+ async startTracing(
+ traceConfig: protos.ITraceConfig,
+ ): Promise<Result<ChromeExtensionTracingSession>> {
+ await this.connectIfNeeded();
+ if (!this._connected) {
+ return errResult('Cannot connect to the Chrome Tracing extension');
+ }
+ this.session = new ChromeExtensionTracingSession(this, traceConfig);
+ return okResult(this.session);
+ }
+
+ private onExtensionMessage(msg: object): void {
+ if ('version' in msg) {
+ this._connected = true;
+ this._extensionVersion = `${msg.version}`;
+ const cp = this._connectPromise;
+ this._connectPromise = undefined;
+ cp?.resolve();
+ this.invokeExtensionMethod('GetCategories');
+ return;
+ }
+
+ if (!('type' in msg)) {
+ return;
+ }
+
+ if (msg.type === 'GetCategoriesResponse') {
+ const cats = (msg as {type: string; categories: string[]}).categories;
+ this.chromeCategoriesPromise.resolve(cats);
+ } else {
+ this.session?.onExtensionMessage(`${msg.type}`, msg);
+ }
+ }
+
+ invokeExtensionMethod(method: string, data?: Uint8Array) {
+ const requestData = binaryEncode(data ?? new Uint8Array());
+ this.port?.postMessage({method, requestData});
+ }
+
+ private onExtensionDisconnect() {
+ if (this._connected) {
+ console.log(
+ 'Chrome tracing extension disconnected',
+ chrome.runtime.lastError,
+ );
+ }
+ void chrome.runtime.lastError;
+ this.port = undefined;
+ this._connected = false;
+ if (this._connectPromise) {
+ this._connectPromise.reject('Chrome Tracing extension not found');
+ }
+ m.redraw();
+ }
+}
+
+function categoriesToServiceState(
+ categories: string[],
+): protos.ITracingServiceState {
+ return {
+ producers: [{id: 1, name: 'Chrome'}],
+ dataSources: [
+ {
+ producerId: 1,
+ dsDescriptor: {
+ name: 'track_event',
+ id: 1,
+ trackEventDescriptor: {
+ availableCategories: categories.map((cat) => ({name: cat})),
+ },
+ },
+ },
+ ],
+ };
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_target_provider.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_target_provider.ts
new file mode 100644
index 0000000..5e680c6
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_target_provider.ts
@@ -0,0 +1,43 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {EvtSource} from '../../../base/events';
+import {PreflightCheck} from '../interfaces/connection_check';
+import {RecordingTargetProvider} from '../interfaces/recording_target_provider';
+import {TargetPlatformId} from '../interfaces/target_platform';
+import {ChromeExtensionTarget} from './chrome_extension_target';
+
+export class ChromeExtensionTargetProvider implements RecordingTargetProvider {
+ readonly id = 'chrome_extension';
+ readonly name = 'Chrome Tracing extension';
+ readonly icon = 'extension';
+ readonly description = 'Chrome using extension';
+ readonly supportedPlatforms = ['CHROME', 'CHROME_OS'] as const;
+ readonly onTargetsChanged = new EvtSource<void>();
+
+ private target = new ChromeExtensionTarget();
+
+ async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {}
+
+ async listTargets(
+ platform: TargetPlatformId,
+ ): Promise<ChromeExtensionTarget[]> {
+ this.target.platform = platform;
+ return [this.target];
+ }
+
+ getChromeCategories(): Promise<string[]> {
+ return this.target.getChromeCategories();
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_tracing_session.ts
new file mode 100644
index 0000000..400bf3a
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_tracing_session.ts
@@ -0,0 +1,148 @@
+// 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 protos from '../../../protos';
+import {EvtSource} from '../../../base/events';
+import {ResizableArrayBuffer} from '../../../base/resizable_array_buffer';
+import {binaryDecode} from '../../../base/string_utils';
+import {
+ TracingSession,
+ TracingSessionLogEntry,
+ TracingSessionState,
+} from '../interfaces/tracing_session';
+import {ChromeExtensionTarget} from './chrome_extension_target';
+import {defer, Deferred} from '../../../base/deferred';
+
+export class ChromeExtensionTracingSession implements TracingSession {
+ private _state: TracingSessionState = 'RECORDING';
+ readonly logs = new Array<TracingSessionLogEntry>();
+ private traceBuf = new ResizableArrayBuffer(64 * 1024);
+ readonly onSessionUpdate = new EvtSource<void>();
+ private pendingBufferUsage = new Array<Deferred<number>>();
+
+ constructor(
+ private target: ChromeExtensionTarget,
+ traceConfig: protos.ITraceConfig,
+ ) {
+ this.start(traceConfig);
+ }
+
+ private async start(traceConfig: protos.ITraceConfig): Promise<void> {
+ const requestData = protos.EnableTracingRequest.encode({
+ traceConfig,
+ }).finish();
+ this.target.invokeExtensionMethod('EnableTracing', requestData);
+ }
+
+ async stop(): Promise<void> {
+ this.target.invokeExtensionMethod('DisableTracing');
+ this.setState('STOPPING');
+ }
+
+ async cancel(): Promise<void> {
+ this.target.invokeExtensionMethod('FreeBuffers');
+ this.setState('STOPPING');
+ }
+
+ async getBufferUsagePct(): Promise<number | undefined> {
+ if (this._state !== 'RECORDING') return undefined;
+ const promise = defer<number>();
+ this.pendingBufferUsage.push(promise);
+ this.target.invokeExtensionMethod('GetTraceStats');
+ return promise;
+ }
+
+ getTraceData(): Uint8Array | undefined {
+ if (this._state !== 'FINISHED') return undefined;
+ const buf = this.traceBuf.get();
+ return buf;
+ }
+
+ onExtensionMessage(msgType: string, msg: object) {
+ switch (msgType) {
+ case 'ChromeExtensionError':
+ const err = (msg as {type: string; error: string}).error;
+ this.log(`Tracing failed: ${err}`, /* isError */ true);
+ if (this._state !== 'FINISHED') {
+ // Ignore spurious errors that arrive after the session finishes.
+ this.setState('ERRORED');
+ this.target.disconnect();
+ }
+ break;
+
+ case 'ChromeExtensionStatus':
+ const status = (msg as {type: string; status: string}).status;
+ this.log(status);
+ break;
+
+ case 'EnableTracingResponse':
+ this.target.invokeExtensionMethod('ReadBuffers');
+ this.setState('STOPPING');
+ break;
+
+ case 'GetTraceStatsResponse':
+ const statResp = msg as {type: string} & protos.IGetTraceStatsResponse;
+ let totSize = 0;
+ let usedSize = 0;
+ for (const buf of statResp.traceStats?.bufferStats ?? []) {
+ totSize += buf.bufferSize ?? 0;
+ // bytesWritten can be >> bufferSize for ring buffer traces.
+ usedSize += Math.min(buf.bytesWritten ?? 0, buf.bufferSize ?? 0);
+ }
+ const pct = Math.min(Math.round((100 * usedSize) / totSize), 100);
+ for (const promise of this.pendingBufferUsage.splice(0)) {
+ promise.resolve(pct);
+ }
+ break;
+
+ case 'ReadBuffersResponse':
+ // The extension is really misusing the ReadBuffersResponse:
+ // - Data is a binary string, not a Uint8Array
+ // - The field 'lastSliceForPacket' is really 'lastPacketInTrace'.
+ // - Slices are really packets and don't need preambles.
+ // See http://shortn/_53WB8A1aIr.
+ const resp = msg as {type: string} & protos.IReadBuffersResponse;
+ let eof = false;
+ for (const slice of resp.slices ?? []) {
+ const data = binaryDecode(slice.data as unknown as string);
+ this.traceBuf.append(data);
+ eof = Boolean(slice.lastSliceForPacket);
+ if (eof) {
+ this.setState('FINISHED');
+ this.target.invokeExtensionMethod('FreeBuffers');
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ get state(): TracingSessionState {
+ return this._state;
+ }
+
+ private setState(newState: TracingSessionState) {
+ this._state = newState;
+ this.onSessionUpdate.notify();
+ }
+
+ private log(message: string, isError = false) {
+ this.logs.push({
+ message,
+ timestamp: new Date(),
+ isError,
+ });
+ this.onSessionUpdate.notify();
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/config/config_interfaces.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/config_interfaces.ts
new file mode 100644
index 0000000..41db0ac
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/config_interfaces.ts
@@ -0,0 +1,140 @@
+// 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 {TargetPlatformId} from '../interfaces/target_platform';
+import {TraceConfigBuilder} from './trace_config_builder';
+import {RecordPluginSchema, RecordSessionSchema} from '../serialization_schema';
+
+/**
+ * A sub-page of the Record page.
+ * Each section maps to an entry in the left sidebar of the recording page.
+ * There are three types of subpages. The last two are identical with exception
+ * of the serialization scope.
+ * 1. Probes pages: the ones that are a structured collection of probes that
+ * can be toggled.
+ * 2,3. Session and global pages: they care of their own rendering and
+ * de/serialization.
+ * 2. Session pages serialize their state in the per-session object (e.g. buffer
+ * sizes). This object can be shared with other people when using the share
+ * config feature.
+ * 3. Global pages instead hold onto the "global" state of the plugin, which is
+ * not tied to the specific config of the recording session (e.g. the target
+ * being recorded, the list of saved configs). This state is retained in
+ * localstorage but is NOT shared with others.
+ */
+export type RecordSubpage = {
+ /** A unique string. This becomes the subpage in the fragment #!/record/xxx */
+ readonly id: string;
+
+ /** The name of the material-design icon that is displayed on the sidebar. */
+ readonly icon: string;
+
+ /** The main text displayed in the left sidebar. */
+ readonly title: string;
+
+ /** The subtitle displayed when hovering over the entry of the sidebar. */
+ readonly subtitle: string;
+} & (
+ | {
+ kind: 'PROBES_PAGE';
+
+ /** The list of probes (togglable entries) for this section. */
+ readonly probes: ReadonlyArray<RecordProbe>;
+ }
+ | {
+ kind: 'SESSION_PAGE';
+ render(): m.Children;
+
+ // Save-restore the page state into the JSON object that is saved in
+ // localstorage and shared when sharing a config.
+ serialize(state: RecordSessionSchema): void;
+ deserialize(state: RecordSessionSchema): void;
+ }
+ | {
+ kind: 'GLOBAL_PAGE';
+ render(): m.Children;
+
+ // Save-restore the page state into the JSON object that is saved in
+ // localstorage.
+ serialize(state: RecordPluginSchema): void;
+ deserialize(state: RecordPluginSchema): void;
+ }
+);
+
+export interface RecordProbe {
+ /**
+ * lower_with_under id. Keep stable, is used for serialization.
+ * This id must be globally unique (not just per-section).
+ */
+ readonly id: string;
+
+ /** Human readable name. */
+ readonly title: string;
+
+ /** (optional) decription. */
+ readonly description?: string;
+
+ /** (optional) file name of a .png file under assets/. */
+ readonly image?: string;
+
+ /** (optional) Link to documentation (e.g. 'https://docs.perfetto.dev/...') */
+ readonly docsLink?: string;
+
+ /** (optional). If specified restricts the probe to the given platorms. */
+ readonly supportedPlatforms?: TargetPlatformId[];
+
+ /** (optional): a list of settings for the probe (e.g. polling interval). */
+ readonly settings?: Record<string, ProbeSetting>;
+
+ /**
+ * (optional): a list of probe IDs that will be force-enabled if this probe is
+ * also enabled.
+ */
+ readonly dependencies?: string[];
+
+ /**
+ * Generate the TraceConfig for the probe. This happens in vdom-style: every
+ * time we make a change to the probes the RecordingManager starts a blank
+ * TraceConfigBuilder and asks all probes to update its config invoking this
+ * method.
+ */
+ genConfig(tc: TraceConfigBuilder): void;
+}
+
+/**
+ * The interface to create widgets that change the state of a probe.
+ * The widget is maintains its own state and must be able to de/serialize it.
+ * Realistically you don't want to implment this interface yourself but use one
+ * of the pre-made widgets under ../pages/widgets/, e.g., Slider().
+ */
+export interface ProbeSetting {
+ readonly render: () => m.Children;
+
+ // The two methods below are supposed to save/restore the state of the setting
+ // in a JSON-serializable entity (object | number | string | boolean). This is
+ // to support saving configs into localstorage and sharing them.
+ serialize(): unknown;
+ deserialize(state: unknown): void;
+}
+
+export function supportsPlatform(
+ probe: RecordProbe,
+ platform: TargetPlatformId,
+): boolean {
+ return (
+ probe.supportedPlatforms === undefined ||
+ probe.supportedPlatforms.includes(platform)
+ );
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/config/config_manager.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/config_manager.ts
new file mode 100644
index 0000000..32c6af7
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/config_manager.ts
@@ -0,0 +1,180 @@
+// 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 protos from '../../../protos';
+import {assertExists, assertFalse, assertTrue} from '../../../base/logging';
+import {getOrCreate} from '../../../base/utils';
+import {ProbesSchema} from '../serialization_schema';
+import {TargetPlatformId} from '../interfaces/target_platform';
+import {RecordProbe, supportsPlatform} from './config_interfaces';
+import {DEFAULT_BUFFER_ID, TraceConfigBuilder} from './trace_config_builder';
+
+/**
+ * ConfigManager holds all the state required for config generation (everything
+ * in the record page that has nothing to do with the actual record over
+ * webusb/websocket).
+ * Recording is arranged as a set of Probes. A Probe is a slightly different
+ * concept than a DataSource, as in, it's a higher-level, more user-friendly
+ * concept to help the user configuring tracing behaviours with toggles.
+ * In some cases a Probe can just match 1:1 with a data source; in other cases
+ * N probes can contribute to the same data source (e.g. when they enable
+ * different ftrace events); In other cases a probe can enable 2+ data sources.
+ * At the end of the day, probe contribute to generating a TraceConfig proto.
+ * They do so in a react-style fashion (we start from blank and append entries
+ * every time there is a change). @see {@link TraceConfigBuilder}.
+ */
+export class ConfigManager {
+ readonly probesById = new Map<string, RecordProbe>();
+ private _traceConfig = new TraceConfigBuilder();
+ private enabledProbes = new Map<string, boolean>();
+ private indirectlyEnabledProbes = new Map<string, Set<string>>();
+
+ get traceConfig() {
+ return this._traceConfig;
+ }
+
+ registerProbes(probes: ReadonlyArray<RecordProbe>) {
+ for (const probe of probes) {
+ assertFalse(this.probesById.has(probe.id));
+ this.probesById.set(probe.id, probe);
+ }
+ }
+
+ setProbeEnabled(probeId: string, enabled: boolean) {
+ const probe = assertExists(this.probesById.get(probeId));
+ this.enabledProbes.set(probeId, enabled);
+ for (const depProbeId of probe.dependencies ?? []) {
+ assertTrue(this.probesById.has(depProbeId));
+ const depSet = getOrCreate(
+ this.indirectlyEnabledProbes,
+ depProbeId,
+ () => new Set<string>(),
+ );
+ if (enabled) {
+ depSet.add(probeId);
+ } else {
+ depSet.delete(probeId);
+ }
+ }
+ }
+
+ isProbeEnabled(probeId: string): boolean {
+ const directlyEnabled = this.enabledProbes.get(probeId) === true;
+ const enabledDueToDeps = Boolean(
+ this.indirectlyEnabledProbes.get(probeId)?.size,
+ );
+ return directlyEnabled || enabledDueToDeps;
+ }
+
+ /**
+ * Returns the human-friendly name for the probes that are enabled and depend
+ * on this probe. This is so we can tell the user: you cannot turn this probe
+ * off because another probe you enbled requires this one.
+ */
+ getProbeEnableDependants(probeId: string): string[] {
+ return Array.from(this.indirectlyEnabledProbes.get(probeId) ?? []).map(
+ (id) => assertExists(this.probesById.get(id)).title,
+ );
+ }
+
+ /**
+ * Generates the TraceConfig proto for the current configuration.
+ */
+ genTraceConfig(platform: TargetPlatformId): protos.TraceConfig {
+ // We approach trace config generation similar to vdom rendering: we start
+ // fresh all the time and let the various probes add things to the
+ // TraceConfigBuilder.
+
+ this._traceConfig.dataSources.clear();
+
+ // Clear all buffers other than the default one.
+ for (const bufId of this._traceConfig.buffers.keys()) {
+ if (bufId !== DEFAULT_BUFFER_ID) {
+ this._traceConfig.buffers.delete(bufId);
+ }
+ }
+
+ // Now regenerate the config. Go in probe registration order, but
+ // respect dependencies (deps come first).
+ const orderedProbes = this.getProbesOrderedByDep(/* enabledOnly */ true);
+
+ for (const probe of orderedProbes) {
+ if (!supportsPlatform(probe, platform)) continue;
+ probe.genConfig(this._traceConfig);
+ }
+ return this._traceConfig.toTraceConfig();
+ }
+
+ // For sharing and localstorage persistence.
+ serializeProbes(): ProbesSchema {
+ return Object.fromEntries(
+ this.getProbesOrderedByDep(/* enabledOnly */ true).map((probe) => [
+ probe.id,
+ {
+ settings: Object.fromEntries(
+ Object.entries(probe.settings ?? {}).map(([settingId, setting]) => [
+ settingId,
+ setting.serialize(),
+ ]),
+ ),
+ },
+ ]),
+ );
+ }
+
+ // For sharing and localstorage persistence.
+ deserializeProbes(state: ProbesSchema): void {
+ this.enabledProbes.clear();
+ this.indirectlyEnabledProbes.clear();
+ this.getProbesOrderedByDep().forEach((probe) => {
+ const probeState = state[probe.id];
+ if (probeState === undefined || probeState.settings === undefined) {
+ return;
+ }
+ this.setProbeEnabled(probe.id, true);
+ if (probe.settings === undefined) {
+ // The probe has no settings, there is nothing to restore.
+ // This return is theoretically redundant but is here to make tsc happy.
+ return;
+ }
+ for (const [key, settingState] of Object.entries(probeState.settings)) {
+ if (key in probe.settings) {
+ probe.settings[key].deserialize(settingState);
+ }
+ }
+ });
+ }
+
+ private getProbesOrderedByDep(enabledOnly = false): RecordProbe[] {
+ const orderedProbes: RecordProbe[] = [];
+ const seenIds = new Set<string>();
+ const queueProbe = (probeId: string) => {
+ if (enabledOnly && !this.isProbeEnabled(probeId)) return;
+ const probe = assertExists(this.probesById.get(probeId));
+ if (orderedProbes.includes(probe)) return; // Already added.
+ if (seenIds.has(probeId)) {
+ throw new Error('Cycle detected in probe ' + probeId);
+ }
+ seenIds.add(probeId);
+ for (const dep of probe.dependencies ?? []) {
+ queueProbe(dep);
+ }
+ orderedProbes.push(probe);
+ };
+ for (const probeId of this.probesById.keys()) {
+ queueProbe(probeId);
+ }
+ return orderedProbes;
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/config/trace_config_builder.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/trace_config_builder.ts
new file mode 100644
index 0000000..0491cb6
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/trace_config_builder.ts
@@ -0,0 +1,130 @@
+// 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 {assertExists, assertFalse} from '../../../base/logging';
+import {getOrCreate} from '../../../base/utils';
+import protos from '../../../protos';
+
+export const FTRACE_DS = 'linux.ftrace';
+export type RecordMode = 'STOP_WHEN_FULL' | 'RING_BUFFER' | 'LONG_TRACE';
+export type BufferMode = 'DISCARD' | 'RING_BUFFER';
+
+export const DEFAULT_BUFFER_ID = 'default';
+
+export class TraceConfigBuilder {
+ readonly buffers = new Map<string, BufferConfig>();
+ readonly dataSources = new Map<string, DataSource>();
+
+ // The default values here don't matter, they exist only to make the TS
+ // compiler happy. The actual defaults are defined by serialization_schema.ts.
+ mode: RecordMode = 'STOP_WHEN_FULL';
+ durationMs = 10_000;
+ maxFileSizeMb = 0;
+ fileWritePeriodMs = 0;
+ compression = false;
+
+ constructor() {
+ this.buffers.set(DEFAULT_BUFFER_ID, {sizeKb: 64 * 1024});
+ }
+
+ get defaultBuffer(): BufferConfig {
+ return assertExists(this.buffers.get(DEFAULT_BUFFER_ID));
+ }
+
+ // It has get-or-create semantics.
+ addDataSource(name: string, targetBufId?: string): protos.IDataSourceConfig {
+ return getOrCreate(this.dataSources, name, () => ({
+ targetBufId,
+ config: {name},
+ })).config;
+ }
+
+ addBuffer(id: string, sizeKb: number, mode?: BufferMode) {
+ assertFalse(this.buffers.has(id));
+ this.buffers.set(id, {sizeKb, mode});
+ }
+
+ addFtraceEvents(...ftraceEvents: string[]) {
+ const cfg = this.addDataSource('linux.ftrace');
+ cfg.ftraceConfig ??= {};
+ cfg.ftraceConfig.ftraceEvents ??= [];
+ cfg.ftraceConfig.ftraceEvents.push(...ftraceEvents);
+ }
+
+ addAtraceApps(...apps: string[]) {
+ const cfg = this.addDataSource('linux.ftrace');
+ cfg.ftraceConfig ??= {};
+ cfg.ftraceConfig.atraceApps ??= [];
+ cfg.ftraceConfig.atraceApps.push(...apps);
+ }
+
+ addAtraceCategories(...cats: string[]) {
+ const cfg = this.addDataSource('linux.ftrace');
+ cfg.ftraceConfig ??= {};
+ cfg.ftraceConfig.atraceCategories ??= [];
+ cfg.ftraceConfig.atraceCategories.push(...cats);
+ }
+
+ toTraceConfig(): protos.TraceConfig {
+ const traceCfg = new protos.TraceConfig();
+ traceCfg.durationMs = this.durationMs;
+ if (this.mode === 'LONG_TRACE') {
+ traceCfg.writeIntoFile = true;
+ traceCfg.fileWritePeriodMs = this.fileWritePeriodMs;
+ traceCfg.maxFileSizeBytes = this.maxFileSizeMb * 1_000_000;
+ }
+
+ if (this.compression) {
+ traceCfg.compressionType =
+ protos.TraceConfig.CompressionType.COMPRESSION_TYPE_DEFLATE;
+ }
+
+ const orderedBufIds = [];
+ for (const [id, buf] of this.buffers.entries()) {
+ const fillPolicy =
+ buf.mode === 'DISCARD' ||
+ (buf.mode === undefined && this.mode === 'STOP_WHEN_FULL')
+ ? protos.TraceConfig.BufferConfig.FillPolicy.DISCARD
+ : protos.TraceConfig.BufferConfig.FillPolicy.RING_BUFFER;
+ traceCfg.buffers.push({sizeKb: buf.sizeKb, fillPolicy});
+ orderedBufIds.push(id);
+ }
+ for (const ds of this.dataSources.values()) {
+ let targetBuffer: number | undefined = undefined;
+ if (ds.targetBufId !== undefined) {
+ targetBuffer = orderedBufIds.indexOf(ds.targetBufId);
+ if (targetBuffer < 0) {
+ throw new Error(
+ `DataSource ${ds.config.name} specified buffer id ` +
+ `${ds.targetBufId} but it doesn't exist. ` +
+ `Buffers: [${orderedBufIds.join(',')}]`,
+ );
+ }
+ }
+ traceCfg.dataSources.push({config: {...ds.config, targetBuffer}});
+ }
+ return traceCfg;
+ }
+}
+
+export interface DataSource {
+ config: protos.IDataSourceConfig;
+ targetBufId?: string;
+}
+
+export interface BufferConfig {
+ sizeKb: number;
+ // If omitted infers from the config-wide mode.
+ mode?: BufferMode;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/config/trace_config_utils_wasm.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/trace_config_utils_wasm.ts
new file mode 100644
index 0000000..6027ddf
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/trace_config_utils_wasm.ts
@@ -0,0 +1,113 @@
+// 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 protos from '../../../protos';
+import {assetSrc} from '../../../base/assets';
+import {defer} from '../../../base/deferred';
+import {assertTrue} from '../../../base/logging';
+import {errResult, okResult, Result} from '../../../base/result';
+import {utf8Decode, utf8Encode} from '../../../base/string_utils';
+import WasmModuleGen from '../../../gen/trace_config_utils';
+
+/**
+ * This file is the TS-equivalent of src/trace_config_utils.
+ * It exposes two functions to conver the TraceConfig proto from txt<>protobuf.
+ * It guarrantees to have the same behaviour of perfetto_cmd and trace_processor
+ * by using precisely the same code via WebAssembly.
+ */
+interface WasmModule {
+ module: WasmModuleGen.Module;
+ buf: Uint8Array;
+}
+
+let moduleInstance: WasmModule | undefined = undefined;
+
+/**
+ * Convert a binary-encoded protos.TracConfig to pbtxt (i.e. the text format
+ * that can be passed to perfetto --txt).
+ */
+export async function traceConfigToTxt(
+ config: Uint8Array | protos.ITraceConfig,
+): Promise<string> {
+ const wasm = await initWasmOnce();
+
+ const configU8: Uint8Array =
+ config instanceof Uint8Array
+ ? config
+ : protos.TraceConfig.encode(config).finish();
+ assertTrue(configU8.length <= wasm.buf.length);
+ wasm.buf.set(configU8);
+
+ const txtSize =
+ wasm.module.ccall(
+ 'trace_config_pb_to_txt',
+ 'number',
+ ['number'],
+ [configU8.length],
+ ) >>> 0;
+
+ const txt = utf8Decode(wasm.buf.subarray(0, txtSize));
+ return txt;
+}
+
+/** Convert a pbtxt (text-proto) text to a proto-encoded TraceConfig. */
+export async function traceConfigToPb(
+ configTxt: string,
+): Promise<Result<Uint8Array>> {
+ const wasm = await initWasmOnce();
+
+ const configUtf8 = utf8Encode(configTxt);
+ assertTrue(configUtf8.length <= wasm.buf.length);
+ wasm.buf.set(configUtf8);
+
+ const resSize =
+ wasm.module.ccall(
+ 'trace_config_txt_to_pb',
+ 'number',
+ ['number'],
+ [configUtf8.length],
+ ) >>> 0;
+
+ const success = wasm.buf.at(0) === 1;
+ const payload = wasm.buf.slice(1, 1 + resSize);
+ return success ? okResult(payload) : errResult(utf8Decode(payload));
+}
+
+async function initWasmOnce(): Promise<WasmModule> {
+ if (moduleInstance === undefined) {
+ // We have to fetch the .wasm file manually because the stub generated by
+ // emscripten uses sync-loading, which works only in Workers.
+ const resp = await fetch(assetSrc('trace_config_utils.wasm'));
+ const wasmBinary = await resp.arrayBuffer();
+ const deferredRuntimeInitialized = defer<void>();
+ const instance = WasmModuleGen({
+ noInitialRun: true,
+ locateFile: (s: string) => s,
+ print: (s: string) => console.log(s),
+ printErr: (s: string) => console.error(s),
+ onRuntimeInitialized: () => deferredRuntimeInitialized.resolve(),
+ wasmBinary,
+ } as WasmModuleGen.ModuleArgs);
+ await deferredRuntimeInitialized;
+ const bufAddr =
+ instance.ccall('trace_config_utils_buf', 'number', [], []) >>> 0;
+ const bufSize =
+ instance.ccall('trace_config_utils_buf_size', 'number', [], []) >>> 0;
+ moduleInstance = {
+ module: instance,
+ buf: instance.HEAPU8.subarray(bufAddr, bufAddr + bufSize),
+ };
+ }
+ return moduleInstance;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/index.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/index.ts
new file mode 100644
index 0000000..48ec477
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/index.ts
@@ -0,0 +1,92 @@
+// 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 {bindMithrilAttrs} from '../../base/mithril_utils';
+import {App} from '../../public/app';
+import {PerfettoPlugin} from '../../public/plugin';
+import RecordingV1Plugin from '../dev.perfetto.RecordTrace';
+import {AdbWebsocketTargetProvider} from './adb/websocket/adb_websocket_target_provider';
+import {AdbWebusbTargetProvider} from './adb/webusb/adb_webusb_target_provider';
+import {ChromeExtensionTargetProvider} from './chrome/chrome_extension_target_provider';
+import {advancedRecordSection} from './pages/advanced';
+import {androidRecordSection} from './pages/android';
+import {bufferConfigPage} from './pages/buffer_config_page';
+import {chromeRecordSection} from './pages/chrome';
+import {instructionsPage} from './pages/instructions_page';
+import {cpuRecordSection} from './pages/cpu';
+import {gpuRecordSection} from './pages/gpu';
+import {memoryRecordSection} from './pages/memory';
+import {powerRecordSection} from './pages/power';
+import {RecordPageV2} from './pages/record_page';
+import {stackSamplingRecordSection} from './pages/stack_sampling';
+import {targetSelectionPage} from './pages/target_selection_page';
+import {RecordingManager} from './recording_manager';
+import {TracedWebsocketTargetProvider} from './traced_over_websocket/traced_websocket_provider';
+import {savedConfigsPage} from './pages/saved_configs';
+
+export default class implements PerfettoPlugin {
+ static readonly id = 'dev.perfetto.RecordTraceV2';
+ static readonly dependencies = [RecordingV1Plugin];
+ private static recordingMgr?: RecordingManager;
+
+ static onActivate(app: App) {
+ if (!RecordingV1Plugin.useRecordingV2) return;
+ app.sidebar.addMenuItem({
+ section: 'navigation',
+ text: 'Record new trace',
+ href: '#!/record',
+ icon: 'fiber_smart_record',
+ sortOrder: 2,
+ });
+ app.pages.registerPage({
+ route: '/record',
+ traceless: true,
+ page: bindMithrilAttrs(RecordPageV2, {
+ getRecordingManager: this.getRecordingManager.bind(this, app),
+ }),
+ });
+ }
+
+ // Lazily initialize the RecordingManager at first call. This is to prevent
+ // providers to connect to sockets / devtools (which in turn can trigger
+ // security UX in the browser) before the user has even done anything.
+ private static getRecordingManager(app: App): RecordingManager {
+ if (this.recordingMgr === undefined) {
+ const recMgr = new RecordingManager(app);
+ this.recordingMgr = recMgr;
+ recMgr.registerProvider(new AdbWebusbTargetProvider());
+ recMgr.registerProvider(new AdbWebsocketTargetProvider());
+ const chromeProvider = new ChromeExtensionTargetProvider();
+ recMgr.registerProvider(chromeProvider);
+ recMgr.registerProvider(new TracedWebsocketTargetProvider());
+ recMgr.registerPage(
+ targetSelectionPage(recMgr),
+ bufferConfigPage(recMgr),
+ instructionsPage(recMgr),
+ savedConfigsPage(recMgr),
+
+ chromeRecordSection(() => chromeProvider.getChromeCategories()),
+ cpuRecordSection(),
+ gpuRecordSection(),
+ powerRecordSection(),
+ memoryRecordSection(),
+ androidRecordSection(),
+ stackSamplingRecordSection(),
+ advancedRecordSection(),
+ );
+ recMgr.restorePluginStateFromLocalstorage();
+ }
+ return this.recordingMgr;
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/byte_stream.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/byte_stream.ts
new file mode 100644
index 0000000..7e165fe
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/byte_stream.ts
@@ -0,0 +1,28 @@
+// 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.
+
+/**
+ * The base class for implementing byte streams. This is used both for
+ * implementing various layers of the ADB stack and for modelling data exhanges
+ * on the tracing protocol.
+ */
+export abstract class ByteStream {
+ // Event handlers
+ onData: (data: Uint8Array) => void = () => {};
+ onClose: () => void = () => {};
+
+ abstract get connected(): boolean;
+ abstract write(data: string | Uint8Array): Promise<void>;
+ abstract close(): void;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/connection_check.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/connection_check.ts
new file mode 100644
index 0000000..b226844
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/connection_check.ts
@@ -0,0 +1,47 @@
+// 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 {Result} from '../../../base/result';
+
+export interface WithPreflightChecks {
+ /**
+ * yields a sequence of diagnostic check that should be performed before
+ * starting the connection. Those checks provide actionable information about
+ * missed preconditions.
+ */
+ runPreflightChecks(): AsyncGenerator<PreflightCheck>;
+}
+
+export type PreflightCheckResult = Result<string>;
+
+export interface PreflightCheck {
+ /** E.g. "Check Android Version", "Check WebUSB Connection" */
+ readonly name: string;
+
+ /**
+ * 1. An OK status, if the check succeeds. In this case the value can carry a
+ * message (e.g. "Connected, version 1.2.3").
+ * 2. An Error status, alongside the message: (e.g. "Could not connect to
+ * 127.0.0.1:1234").
+ */
+ readonly status: PreflightCheckResult;
+
+ /**
+ * [Optional] A mithril component that shows instruction on how to remediate
+ * to the problem. For cases where the StatusOr error is not enough and we
+ * want to show something more interactive.
+ */
+ readonly remediation?: m.ComponentTypes;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target.ts
new file mode 100644
index 0000000..f66260f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target.ts
@@ -0,0 +1,50 @@
+// 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 protos from '../../../protos';
+import {Result} from '../../../base/result';
+import {PreflightCheck, WithPreflightChecks} from './connection_check';
+import {TargetPlatformId} from './target_platform';
+import {TracingSession} from './tracing_session';
+
+/**
+ * The interface that models a device that can be used for recording a trace.
+ * This is the contract that RecordingTargetProvider(s) must implement in order
+ * to support recording. The UI bits don't care about the specific
+ * implementation and only use this class.
+ * Conceptually a RecordingTarget maps to a connection to the Consumer socket
+ * to the tracing service.
+ */
+export interface RecordingTarget extends WithPreflightChecks {
+ readonly id: string;
+ readonly platform: TargetPlatformId;
+ readonly name: string;
+ readonly transportType: string;
+ readonly connected: boolean;
+
+ // If true, the output file is gzip-compressed as a whole (!= than setting
+ // deflate in the trace config). The chrome devtools protocol does this.
+ readonly emitsCompressedtrace?: boolean;
+
+ // Returns a list of debugging check to diagnose target connection failures.
+ runPreflightChecks(): AsyncGenerator<PreflightCheck>;
+
+ getServiceState(): Promise<Result<protos.ITracingServiceState>>;
+
+ disconnect(): void;
+
+ startTracing(
+ traceConfig: protos.ITraceConfig,
+ ): Promise<Result<TracingSession>>;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target_provider.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target_provider.ts
new file mode 100644
index 0000000..c41b45d
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target_provider.ts
@@ -0,0 +1,53 @@
+// 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 {Evt} from '../../../base/events';
+import {PreflightCheck, WithPreflightChecks} from './connection_check';
+import {RecordingTarget} from './recording_target';
+import {TargetPlatformId} from './target_platform';
+
+/**
+ * The interface to describe target providers. A target provider uses a specific
+ * transport (e.g., WebUsb, WebSocket, Chrome extension) and allows to find
+ * and obtain Targets.
+ */
+export interface RecordingTargetProvider extends WithPreflightChecks {
+ readonly id: string;
+ readonly name: string;
+ readonly icon: string;
+ readonly description: string;
+ readonly supportedPlatforms: ReadonlyArray<TargetPlatformId>;
+
+ /**
+ * Event listener raised when the target list changes.
+ * The caller is expected to call listTargets() in response to this.
+ */
+ readonly onTargetsChanged: Evt<void>;
+
+ /** Returns a list of debugging checks to diagnose connection failures. */
+ runPreflightChecks(): AsyncGenerator<PreflightCheck>;
+
+ /**
+ * Lists the targets that can be discovered. Note that some providers
+ * (notably WebUSB) can't discover devices that never got paired before and
+ * need a call to {@link pairNewTarget()} to pop up a pair dialog.
+ */
+ listTargets(platform: TargetPlatformId): Promise<RecordingTarget[]>;
+
+ /**
+ * Optional. Some transports can't discover all targets upfront and need
+ * some user interaction to add a new target.
+ */
+ pairNewTarget?: () => Promise<RecordingTarget | undefined>;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/target_platform.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/target_platform.ts
new file mode 100644
index 0000000..db56278
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/target_platform.ts
@@ -0,0 +1,44 @@
+// 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.
+
+export interface TargetPlatform {
+ id: string;
+ name: string;
+ icon: string;
+}
+
+export const TARGET_PLATFORMS = [
+ {
+ id: 'ANDROID',
+ name: 'Android',
+ icon: 'android',
+ },
+ {
+ id: 'CHROME',
+ name: 'Chrome',
+ icon: 'travel_explore',
+ },
+ {
+ id: 'CHROME_OS',
+ name: 'ChromeOS',
+ icon: 'laptop_chromebook',
+ },
+ {
+ id: 'LINUX',
+ name: 'Linux',
+ icon: 'dns',
+ },
+] as const;
+
+export type TargetPlatformId = (typeof TARGET_PLATFORMS)[number]['id'];
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/tracing_session.ts
new file mode 100644
index 0000000..e404608
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/tracing_session.ts
@@ -0,0 +1,49 @@
+// 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 {Evt} from '../../../base/events';
+import {RecordingTarget} from './recording_target';
+
+/**
+ * The contract for the object returned by {@link RecordingTarget.startTracing}.
+ */
+export interface TracingSession {
+ readonly state: TracingSessionState;
+ readonly logs: ReadonlyArray<TracingSessionLogEntry>;
+ readonly onSessionUpdate: Evt<void>;
+
+ /** Stop tracing and get the data captured so far. */
+ stop(): Promise<void>;
+
+ /** Stop tracing and discard the data. */
+ cancel(): Promise<void>;
+
+ /* Returns the percentage of the trace buffer that is currently used */
+ getBufferUsagePct(): Promise<number | undefined>;
+
+ /** Returns the trace file captured once state === 'FINISHED'. */
+ getTraceData(): Uint8Array | undefined;
+}
+
+export type TracingSessionState =
+ | 'RECORDING'
+ | 'STOPPING'
+ | 'FINISHED'
+ | 'ERRORED';
+
+export interface TracingSessionLogEntry {
+ readonly timestamp: Date;
+ readonly message: string;
+ readonly isError?: boolean;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/advanced.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/advanced.ts
new file mode 100644
index 0000000..93a9fa9
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/advanced.ts
@@ -0,0 +1,154 @@
+// 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 {RecordSubpage, RecordProbe} from '../config/config_interfaces';
+import {FTRACE_DS, TraceConfigBuilder} from '../config/trace_config_builder';
+import {TypedMultiselect} from './widgets/multiselect';
+import {Slider} from './widgets/slider';
+import {Toggle} from './widgets/toggle';
+
+export const ADV_PROC_ASSOC_PROBE_ID = 'adv_proc_thread_assoc';
+export const PROC_STATS_DS_NAME = 'linux.process_stats';
+export const ADV_FTRACE_PROBE_ID = 'advanced_ftrace';
+
+export function advancedRecordSection(): RecordSubpage {
+ return {
+ kind: 'PROBES_PAGE',
+ id: 'advanced',
+ title: 'Advanced settings',
+ subtitle: 'For ftrace wizards',
+ icon: 'settings',
+ probes: [ftraceCfg(), procThreadAssociation()],
+ };
+}
+
+function ftraceCfg(): RecordProbe {
+ const settings = {
+ ksyms: new Toggle({
+ title: 'Resolve kernel symbols',
+ default: true,
+ descr:
+ 'Enables lookup via /proc/kallsyms for workqueue, ' +
+ 'sched_blocked_reason and other events ' +
+ '(userdebug/eng builds only).',
+ }),
+ genericEvents: new Toggle({
+ title: 'Enable generic events (slow)',
+ descr:
+ 'Enables capture of ftrace events that are not known at build time ' +
+ 'by perfetto as key-value string pairs. This is slow and expensive.',
+ }),
+ bufSize: new Slider({
+ title: 'Buf size',
+ cssClass: '.thin',
+ values: [0, 512, 1024, 2 * 1024, 4 * 1024, 16 * 1024, 32 * 1024],
+ unit: 'KB',
+ zeroIsDefault: true,
+ }),
+ drainRate: new Slider({
+ title: 'trace_pipe_raw read interval',
+ cssClass: '.thin',
+ values: [0, 100, 250, 500, 1000, 2500, 5000],
+ unit: 'ms',
+ zeroIsDefault: true,
+ }),
+ groups: new TypedMultiselect<string>({
+ title: 'Event groups',
+ options: new Map(
+ Object.entries({
+ binder: 'binder/*',
+ block: 'block/*',
+ clk: 'clk/*',
+ ext4: 'ext4/*',
+ f2fs: 'f2fs/*',
+ i2c: 'i2c/*',
+ irq: 'irq/*',
+ kmem: 'kmem/*',
+ memory_bus: 'memory_bus/*',
+ mmc: 'mmc/*',
+ oom: 'oom/*',
+ power: 'power/*',
+ regulator: 'regulator/*',
+ sched: 'sched/*',
+ sync: 'sync/*',
+ task: 'task/*',
+ vmscan: 'vmscan/*',
+ fastrpc: 'fastrpc/*',
+ }),
+ ),
+ }),
+ };
+ return {
+ id: ADV_FTRACE_PROBE_ID,
+ title: 'Advanced ftrace config',
+ image: 'rec_ftrace.png',
+ description:
+ 'Enable individual events and tune the kernel-tracing (ftrace) ' +
+ 'module. The events enabled here are in addition to those from ' +
+ 'enabled by other probes.',
+ supportedPlatforms: ['ANDROID', 'CHROME_OS', 'LINUX'],
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ const ds = tc.addDataSource(FTRACE_DS);
+ const cfg = (ds.ftraceConfig ??= {});
+ cfg.bufferSizeKb = settings.bufSize.value || undefined;
+ cfg.drainPeriodMs = settings.drainRate.value || undefined;
+ cfg.symbolizeKsyms = settings.ksyms.enabled ? true : undefined;
+ cfg.disableGenericEvents = !settings.genericEvents.enabled;
+ cfg.ftraceEvents ??= [];
+ cfg.ftraceEvents.push(...settings.groups.selectedValues());
+ },
+ };
+}
+
+function procThreadAssociation(): RecordProbe {
+ const ftraceEvents = [
+ 'sched/sched_process_exit',
+ 'sched/sched_process_free',
+ 'task/task_newtask',
+ 'task/task_rename',
+ ];
+ const settings = {
+ initialScan: new Toggle({
+ title: 'Scan all processes at startup',
+ descr: 'Reports all /proc/* processes when starting',
+ default: true,
+ }),
+ };
+ return {
+ id: ADV_PROC_ASSOC_PROBE_ID,
+ title: 'Process<>thread association',
+ description:
+ 'A union of ftrace events and /proc scrapers to capture thread<>process' +
+ 'associations as soon as they are seen from the cpu_pipe_raw. This is ' +
+ 'to capture the information about the whole process (e.g., cmdline).',
+ supportedPlatforms: ['ANDROID', 'CHROME_OS', 'LINUX'],
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ tc.addFtraceEvents(...ftraceEvents);
+ const bufId = 'proc_assoc';
+ // Set to 1/16th of the main buffer size, with reasonable limits.
+ const minMax = [256, 8 * 1024];
+ const bufSizeKb = Math.min(
+ Math.max(tc.defaultBuffer.sizeKb / 16, minMax[0]),
+ minMax[1],
+ );
+ tc.addBuffer(bufId, bufSizeKb);
+
+ const ds = tc.addDataSource(PROC_STATS_DS_NAME);
+ const cfg = (ds.processStatsConfig ??= {});
+ cfg.scanAllProcessesOnStart = settings.initialScan.enabled || undefined;
+ },
+ };
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/android.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/android.ts
new file mode 100644
index 0000000..81b9d0e
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/android.ts
@@ -0,0 +1,259 @@
+// 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 {splitLinesNonEmpty} from '../../../base/string_utils';
+import protos from '../../../protos';
+import {RecordSubpage, RecordProbe} from '../config/config_interfaces';
+import {TraceConfigBuilder} from '../config/trace_config_builder';
+import {TypedMultiselect} from './widgets/multiselect';
+import {POLL_INTERVAL_SLIDER, Slider} from './widgets/slider';
+import {Textarea} from './widgets/textarea';
+import {Toggle} from './widgets/toggle';
+
+export function androidRecordSection(): RecordSubpage {
+ return {
+ kind: 'PROBES_PAGE',
+ id: 'android',
+ title: 'Android apps & svcs',
+ subtitle: 'Android-specific data sources',
+ icon: 'android',
+ probes: [
+ atrace(),
+ logcat(),
+ frameTimeline(),
+ gameInterventions(),
+ netTracing(),
+ statsdAtoms(),
+ ],
+ };
+}
+
+function atrace(): RecordProbe {
+ const settings = {
+ categories: new TypedMultiselect<string>({
+ options: new Map(
+ Object.entries(ATRACE_CATEGORIES).map(([id, name]) => [
+ `${id}: ${name}`,
+ id,
+ ]),
+ ),
+ }),
+ allApps: new Toggle({
+ title: 'Record events from all Android apps and services',
+ cssClass: '.thin',
+ }),
+ };
+ return {
+ id: 'atrace',
+ title: 'Atrace userspace annotations',
+ image: 'rec_atrace.png',
+ description:
+ 'Enables C++ / Java codebase annotations (ATRACE_BEGIN() / os.Trace())',
+ supportedPlatforms: ['ANDROID'],
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ tc.addAtraceCategories(...settings.categories.selectedValues());
+ if (settings.allApps.enabled) {
+ tc.addAtraceApps('*');
+ }
+ if (
+ settings.categories.selectedKeys().length > 0 ||
+ settings.allApps.enabled
+ ) {
+ tc.addFtraceEvents('ftrace/print');
+ }
+ },
+ };
+}
+
+function logcat(): RecordProbe {
+ const settings = {
+ buffers: new TypedMultiselect<protos.AndroidLogId>({
+ options: new Map(
+ Object.entries({
+ 'Crash': protos.AndroidLogId.LID_CRASH,
+ 'Main': protos.AndroidLogId.LID_DEFAULT,
+ 'Binary events': protos.AndroidLogId.LID_EVENTS,
+ 'Kernel': protos.AndroidLogId.LID_KERNEL,
+ 'Radio': protos.AndroidLogId.LID_RADIO,
+ 'Security': protos.AndroidLogId.LID_SECURITY,
+ 'Stats': protos.AndroidLogId.LID_STATS,
+ 'System': protos.AndroidLogId.LID_SYSTEM,
+ }),
+ ),
+ }),
+ };
+ return {
+ id: 'logcat',
+ title: 'Event log (logcat)',
+ image: 'rec_logcat.png',
+ description:
+ 'Streams the event log into the trace. If no buffer filter is ' +
+ 'specified, all buffers are selected.',
+ supportedPlatforms: ['ANDROID'],
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ const logIds = settings.buffers.selectedValues();
+ tc.addDataSource('android.log').androidLogConfig = {
+ logIds: logIds.length > 0 ? logIds : undefined,
+ };
+ },
+ };
+}
+
+function frameTimeline(): RecordProbe {
+ return {
+ id: 'android_frame_timeline',
+ title: 'Frame timeline',
+ description:
+ 'Records expected/actual frame timings from surface_flinger.' +
+ 'Requires Android 12 (S) or above.',
+ supportedPlatforms: ['ANDROID'],
+ docsLink: 'https://perfetto.dev/docs/data-sources/frametimeline',
+ genConfig: function (tc: TraceConfigBuilder) {
+ tc.addDataSource('android.surfaceflinger.frametimeline');
+ },
+ };
+}
+
+function gameInterventions(): RecordProbe {
+ return {
+ id: 'android_game_interventions',
+ title: 'Game intervention list',
+ description:
+ 'List game modes and interventions. Requires Android 13 (T) or above.',
+ supportedPlatforms: ['ANDROID'],
+ docsLink:
+ 'https://perfetto.dev/docs/data-sources/android-game-intervention-list',
+ genConfig: function (tc: TraceConfigBuilder) {
+ tc.addDataSource('android.game_interventions');
+ },
+ };
+}
+
+function netTracing(): RecordProbe {
+ const settings = {pollMs: new Slider(POLL_INTERVAL_SLIDER)};
+ return {
+ id: 'network_tracing',
+ title: 'Network Tracing',
+ description:
+ 'Records detailed information on network packets. ' +
+ 'Requires Android 14 (U) or above',
+ supportedPlatforms: ['ANDROID'],
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ tc.addDataSource('android.network_packets').networkPacketTraceConfig = {
+ pollMs: settings.pollMs.value,
+ };
+ },
+ };
+}
+
+function statsdAtoms(): RecordProbe {
+ const settings = {
+ pushAtoms: new TypedMultiselect<protos.AtomId>({
+ title: 'Push atoms',
+ options: new Map(
+ Object.entries(protos.AtomId)
+ .filter(([_, v]) => typeof v === 'number' && v > 2 && v < 9999)
+ .map(([k, v]) => [k, v as protos.AtomId]),
+ ),
+ }),
+ rawPushIds: new Textarea({
+ placeholder:
+ 'Add raw pushed atoms IDs, one per line, e.g.:\n' + '818\n' + '819',
+ }),
+ pullAtoms: new TypedMultiselect<protos.AtomId>({
+ title: 'Pull atoms',
+ options: new Map(
+ Object.entries(protos.AtomId)
+ .filter(([_, v]) => typeof v === 'number' && v > 10000 && v < 99999)
+ .map(([k, v]) => [k, v as protos.AtomId]),
+ ),
+ }),
+ rawPullIds: new Textarea({
+ placeholder:
+ 'Add raw pulled atom IDs, one per line, e.g.:\n10063\n10064\n',
+ }),
+ pullInterval: new Slider({...POLL_INTERVAL_SLIDER, default: 5000}),
+ pullPkg: new Textarea({
+ placeholder:
+ 'Add pulled atom packages, one per line, e.g.:\n' +
+ 'com.android.providers.telephony',
+ }),
+ };
+ return {
+ id: 'statsd',
+ title: 'Statsd atoms',
+ description: 'Record instances of statsd atoms to the Statsd Atoms track.',
+ supportedPlatforms: ['ANDROID'],
+ docsLink:
+ 'https://cs.android.com/android/platform/superproject/main/+/main:frameworks/proto_logging/stats/atoms.proto',
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ const pkg = splitLinesNonEmpty(settings.pullPkg.text);
+ const pullIds = settings.pullAtoms.selectedValues();
+ const rawPullIds = splitLinesNonEmpty(settings.rawPullIds.text).map((l) =>
+ parseInt(l.trim()),
+ );
+ const hasPull = pullIds.length > 0 && rawPullIds.length > 0;
+ tc.addDataSource('android.statsd').statsdTracingConfig = {
+ pushAtomId: settings.pushAtoms.selectedValues(),
+ rawPushAtomId: splitLinesNonEmpty(settings.rawPushIds.text).map((l) =>
+ parseInt(l.trim()),
+ ),
+ pullConfig: hasPull
+ ? [
+ {
+ pullAtomId: pullIds,
+ rawPullAtomId: rawPullIds,
+ pullFrequencyMs: settings.pullInterval.value,
+ packages: pkg.length > 0 ? pkg : undefined,
+ },
+ ]
+ : undefined,
+ };
+ },
+ };
+}
+
+const ATRACE_CATEGORIES = {
+ adb: 'ADB',
+ aidl: 'AIDL calls',
+ am: 'Activity Manager',
+ audio: 'Audio',
+ binder_driver: 'Binder Kernel driver',
+ binder_lock: 'Binder global lock trace',
+ bionic: 'Bionic C library',
+ camera: 'Camera',
+ dalvik: 'ART & Dalvik',
+ database: 'Database',
+ gfx: 'Graphics',
+ hal: 'Hardware Modules',
+ input: 'Input',
+ network: 'Network',
+ nnapi: 'Neural Network API',
+ pm: 'Package Manager',
+ power: 'Power Management',
+ res: 'Resource Loading',
+ rro: 'Resource Overlay',
+ rs: 'RenderScript',
+ sm: 'Sync Manager',
+ ss: 'System Server',
+ vibrator: 'Vibrator',
+ video: 'Video',
+ view: 'View System',
+ webview: 'WebView',
+ wm: 'Window Manager',
+};
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/buffer_config_page.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/buffer_config_page.ts
new file mode 100644
index 0000000..a432725
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/buffer_config_page.ts
@@ -0,0 +1,177 @@
+// 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 {RecordingManager} from '../recording_manager';
+import {assetSrc} from '../../../base/assets';
+import {Slider} from './widgets/slider';
+import {RecordMode, TraceConfigBuilder} from '../config/trace_config_builder';
+import {ConfigManager} from '../config/config_manager';
+import {RecordSubpage} from '../config/config_interfaces';
+import {RecordSessionSchema} from '../serialization_schema';
+import {Toggle} from './widgets/toggle';
+
+type RecMgrAttrs = {recMgr: RecordingManager};
+
+export function bufferConfigPage(recMgr: RecordingManager): RecordSubpage {
+ return {
+ kind: 'SESSION_PAGE',
+ id: 'config',
+ icon: 'tune',
+ title: 'Buffers and duration',
+ subtitle: 'Buffer mode, size and duration',
+ render() {
+ return m(BufferConfigPage, {recMgr});
+ },
+ serialize(state: RecordSessionSchema) {
+ const tc: TraceConfigBuilder = recMgr.recordConfig.traceConfig;
+ state.mode = tc.mode;
+ state.bufSizeKb = tc.defaultBuffer.sizeKb;
+ state.durationMs = tc.durationMs;
+ state.maxFileSizeMb = tc.maxFileSizeMb;
+ state.fileWritePeriodMs = tc.fileWritePeriodMs;
+ state.compression = tc.compression;
+ },
+ async deserialize(state: RecordSessionSchema) {
+ const tc: TraceConfigBuilder = recMgr.recordConfig.traceConfig;
+ tc.mode = state.mode;
+ tc.defaultBuffer.sizeKb = state.bufSizeKb;
+ tc.durationMs = state.durationMs;
+ tc.maxFileSizeMb = state.maxFileSizeMb;
+ tc.fileWritePeriodMs = state.fileWritePeriodMs;
+ tc.compression = state.compression;
+ },
+ };
+}
+
+class BufferConfigPage implements m.ClassComponent<RecMgrAttrs> {
+ private bufSize: Slider;
+ private maxDuration: Slider;
+ private maxFileSize: Slider;
+ private flushPeriod: Slider;
+ private compress?: Toggle;
+
+ constructor({attrs}: m.CVnode<RecMgrAttrs>) {
+ const traceCfg = attrs.recMgr.recordConfig.traceConfig;
+ this.bufSize = new Slider({
+ title: 'In-memory buffer size',
+ icon: '360',
+ values: [4, 8, 16, 32, 64, 128, 256, 512],
+ default: traceCfg.defaultBuffer.sizeKb / 1024,
+ unit: 'MB',
+ onChange: (v: number) => (traceCfg.defaultBuffer.sizeKb = v * 1024),
+ });
+ this.maxDuration = new Slider({
+ title: 'Max duration',
+ icon: 'timer',
+ values: [S(10), S(15), S(30), S(60), M(5), M(30), H(1), H(6), H(12)],
+ default: traceCfg.durationMs,
+ isTime: true,
+ unit: 'h:m:s',
+ onChange: (value: number) => (traceCfg.durationMs = value),
+ });
+ this.maxFileSize = new Slider({
+ title: 'Max file size',
+ icon: 'save',
+ values: [5, 25, 50, 100, 500, 1000, 1000 * 5, 1000 * 10],
+ default: traceCfg.maxFileSizeMb,
+ unit: 'MB',
+ onChange: (value: number) => (traceCfg.maxFileSizeMb = value),
+ });
+ this.flushPeriod = new Slider({
+ title: 'Flush on disk every',
+ icon: 'av_timer',
+ values: [100, 250, 500, 1000, 2500, 5000],
+ default: traceCfg.fileWritePeriodMs,
+ unit: 'ms',
+ onChange: (value: number) => (traceCfg.fileWritePeriodMs = value),
+ });
+ if (!attrs.recMgr.currentTarget?.emitsCompressedtrace) {
+ this.compress = new Toggle({
+ title: 'Deflate (gzip) compression ',
+ descr:
+ 'Generates smaller trace files at the cost of extra CPU cycles ' +
+ 'when stopping the trace. Compression happens only after the end of ' +
+ 'the trace and does not improve the ring-buffer efficiency.',
+ default: traceCfg.compression,
+ onChange: (enabled) => (traceCfg.compression = enabled),
+ });
+ }
+ }
+
+ view({attrs}: m.CVnode<RecMgrAttrs>) {
+ const recCfg = attrs.recMgr.recordConfig;
+ return [
+ m('header', 'Recording mode'),
+ m(
+ '.record-mode',
+ this.recButton(
+ recCfg,
+ 'STOP_WHEN_FULL',
+ 'Stop when full',
+ 'rec_one_shot.png',
+ ),
+ this.recButton(
+ recCfg,
+ 'RING_BUFFER',
+ 'Ring buffer',
+ 'rec_ring_buf.png',
+ ),
+ this.recButton(
+ recCfg,
+ 'LONG_TRACE',
+ 'Long trace',
+ 'rec_long_trace.png',
+ ),
+ ),
+ this.bufSize.render(),
+ this.maxDuration.render(),
+ recCfg.traceConfig.mode === 'LONG_TRACE' && this.maxFileSize.render(),
+ recCfg.traceConfig.mode === 'LONG_TRACE' && this.flushPeriod.render(),
+ this.compress?.render(),
+ ];
+ }
+
+ recButton(
+ recCfg: ConfigManager,
+ mode: RecordMode,
+ title: string,
+ img: string,
+ ) {
+ const checkboxArgs = {
+ checked: recCfg.traceConfig.mode === mode,
+ onchange: (e: InputEvent) => {
+ const checked = (e.target as HTMLInputElement).checked;
+ if (!checked) return;
+ recCfg.traceConfig.mode = mode;
+ if (
+ mode === 'LONG_TRACE' &&
+ this.maxDuration.value === this.maxDuration.attrs.default
+ ) {
+ this.maxDuration.setValue(H(6));
+ }
+ },
+ };
+ return m(
+ `label${recCfg.traceConfig.mode === mode ? '.selected' : ''}`,
+ m(`input[type=radio][name=rec_mode]`, checkboxArgs),
+ m(`img[src=${assetSrc(`assets/${img}`)}]`),
+ m('span', title),
+ );
+ }
+}
+
+const S = (x: number) => x * 1000;
+const M = (x: number) => x * 1000 * 60;
+const H = (x: number) => x * 1000 * 60 * 60;
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/chrome.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/chrome.ts
new file mode 100644
index 0000000..dc170b3
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/chrome.ts
@@ -0,0 +1,573 @@
+// 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 protos from '../../../protos';
+import {
+ RecordSubpage,
+ RecordProbe,
+ ProbeSetting,
+} from '../config/config_interfaces';
+import {TraceConfigBuilder} from '../config/trace_config_builder';
+import {Toggle} from './widgets/toggle';
+import {Section} from '../../../widgets/section';
+import {
+ MultiSelect,
+ MultiSelectDiff,
+ Option as MultiSelectOption,
+} from '../../../widgets/multiselect';
+
+type ChromeCatFunction = () => Promise<string[]>;
+
+export function chromeRecordSection(
+ chromeCategoryGetter: ChromeCatFunction,
+): RecordSubpage {
+ return {
+ kind: 'PROBES_PAGE',
+ id: 'chrome',
+ title: 'Chrome browser',
+ subtitle: 'Chrome tracing',
+ icon: 'laptop_chromebook',
+ probes: [chromeProbe(chromeCategoryGetter)],
+ };
+}
+
+function chromeProbe(chromeCategoryGetter: ChromeCatFunction): RecordProbe {
+ const groupToggles = Object.fromEntries(
+ Object.keys(GROUPS).map((groupName) => [
+ groupName,
+ new Toggle({
+ title: groupName,
+ }),
+ ]),
+ );
+ const settings = {
+ ...groupToggles,
+ privacy: new Toggle({
+ title: 'Remove untyped and sensitive data like URLs from the trace',
+ descr:
+ 'Not recommended unless you intend to share the trace' +
+ ' with third-parties.',
+ }),
+ categories: new ChromeCategoriesWidget(chromeCategoryGetter),
+ };
+ return {
+ id: 'chrome_tracing',
+ title: 'Chrome browser tracing',
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ const cats = new Set<string>();
+ settings.categories.getEnabledCategories().forEach((c) => cats.add(c));
+ for (const [group, groupCats] of Object.entries(GROUPS)) {
+ if ((groupToggles[group] as Toggle).enabled) {
+ groupCats.forEach((c) => cats.add(c));
+ }
+ }
+ const memoryInfra = cats.has('disabled-by-default-memory-infra');
+ const jsonStruct = {
+ record_mode:
+ tc.mode === 'STOP_WHEN_FULL'
+ ? 'record-until-full'
+ : 'record-continuously',
+ included_categories: [...cats],
+ excluded_categories: ['*'], // Only include categories explicitly
+ memory_dump_config: memoryInfra
+ ? {
+ allowed_dump_modes: ['background', 'light', 'detailed'],
+ triggers: [
+ {
+ min_time_between_dumps_ms: 10000,
+ mode: 'detailed',
+ type: 'periodic_interval',
+ },
+ ],
+ }
+ : undefined,
+ };
+ const privacyFilteringEnabled = settings.privacy.enabled;
+ const chromeConfig = {
+ clientPriority: protos.ChromeConfig.ClientPriority.USER_INITIATED,
+ privacyFilteringEnabled,
+ traceConfig: JSON.stringify(jsonStruct),
+ };
+
+ const trackEvent = tc.addDataSource('track_event');
+ trackEvent.chromeConfig = chromeConfig;
+ const trackEvtCfg = (trackEvent.trackEventConfig ??= {});
+ trackEvtCfg.disabledCategories ??= ['*'];
+ trackEvtCfg.enabledCategories ??= [];
+ trackEvtCfg.enabledCategories.push(...cats);
+ trackEvtCfg.enabledCategories.push('__metadata');
+ trackEvtCfg.enableThreadTimeSampling = true;
+ trackEvtCfg.timestampUnitMultiplier = 1000;
+ trackEvtCfg.filterDynamicEventNames = privacyFilteringEnabled;
+ trackEvtCfg.filterDebugAnnotations = privacyFilteringEnabled;
+
+ tc.addDataSource('org.chromium.trace_metadata').chromeConfig =
+ chromeConfig;
+
+ if (memoryInfra) {
+ tc.addDataSource('org.chromium.memory_instrumentation').chromeConfig =
+ chromeConfig;
+ tc.addDataSource('org.chromium.native_heap_profiler').chromeConfig =
+ chromeConfig;
+ }
+
+ if (
+ cats.has('disabled-by-default-cpu_profiler') ||
+ cats.has('disabled-by-default-cpu_profiler.debug')
+ ) {
+ tc.addDataSource('org.chromium.sampler_profiler').chromeConfig =
+ chromeConfig;
+ }
+ if (cats.has('disabled-by-default-system_metrics')) {
+ tc.addDataSource('org.chromium.system_metrics').chromeConfig =
+ chromeConfig;
+ }
+ },
+ };
+}
+
+const DISAB_PREFIX = 'disabled-by-default-';
+
+export class ChromeCategoriesWidget implements ProbeSetting {
+ private options = new Array<MultiSelectOption>();
+
+ constructor(private chromeCategoryGetter: ChromeCatFunction) {
+ this.initializeCategories(BUILTIN_CATEGORIES);
+ }
+
+ private initializeCategories(cats: string[]) {
+ this.options = cats
+ .map((cat) => ({
+ id: cat,
+ name: cat.replace(DISAB_PREFIX, ''),
+ checked: false,
+ }))
+ .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
+ }
+
+ getEnabledCategories(): string[] {
+ return this.options.filter((o) => o.checked).map((o) => o.id);
+ }
+
+ setEnabled(cat: string, enabled: boolean) {
+ for (const option of this.options) {
+ if (option.id !== cat) continue;
+ option.checked = enabled;
+ }
+ }
+
+ serialize() {
+ return this.options.filter((o) => o.checked).map((o) => o.id);
+ }
+
+ deserialize(state: unknown): void {
+ if (Array.isArray(state) && state.every((x) => typeof x === 'string')) {
+ this.options.forEach((o) => (o.checked = false));
+ for (const key of state) {
+ const opt = this.options.find((o) => o.id === key);
+ if (opt !== undefined) opt.checked = true;
+ }
+ }
+ }
+
+ render() {
+ return m(
+ 'div.chrome-categories',
+ {
+ oninit: async () =>
+ this.initializeCategories(await this.chromeCategoryGetter()),
+ },
+ m(
+ Section,
+ {title: 'Additional Categories'},
+ m(MultiSelect, {
+ options: this.options.filter((o) => !o.id.startsWith(DISAB_PREFIX)),
+ repeatCheckedItemsAtTop: false,
+ fixedSize: false,
+ onChange: (diffs: MultiSelectDiff[]) => {
+ diffs.forEach(({id, checked}) => this.setEnabled(id, checked));
+ },
+ }),
+ ),
+ m(
+ Section,
+ {title: 'High Overhead Categories'},
+ m(MultiSelect, {
+ options: this.options.filter((o) => o.id.startsWith(DISAB_PREFIX)),
+ repeatCheckedItemsAtTop: false,
+ fixedSize: false,
+ onChange: (diffs: MultiSelectDiff[]) => {
+ diffs.forEach(({id, checked}) => this.setEnabled(id, checked));
+ },
+ }),
+ ),
+ );
+ }
+}
+
+const GROUPS = {
+ 'Task Scheduling': [
+ 'toplevel',
+ 'toplevel.flow',
+ 'scheduler',
+ 'sequence_manager',
+ 'disabled-by-default-toplevel.flow',
+ ],
+ 'IPC Flows': [
+ 'toplevel',
+ 'toplevel.flow',
+ 'disabled-by-default-ipc.flow',
+ 'mojom',
+ ],
+ 'Javascript execution': ['toplevel', 'v8'],
+ 'Web content rendering, layout and compositing': [
+ 'toplevel',
+ 'blink',
+ 'cc',
+ 'gpu',
+ ],
+ 'UI rendering and surface compositing': [
+ 'toplevel',
+ 'cc',
+ 'gpu',
+ 'viz',
+ 'ui',
+ 'views',
+ ],
+ 'Input events': [
+ 'toplevel',
+ 'benchmark',
+ 'evdev',
+ 'input',
+ 'disabled-by-default-toplevel.flow',
+ ],
+ 'Navigation and loading': [
+ 'loading',
+ 'net',
+ 'netlog',
+ 'navigation',
+ 'browser',
+ ],
+ 'Audio': [
+ 'base',
+ 'disabled-by-default-audio',
+ 'disabled-by-default-webaudio',
+ 'disabled-by-default-webaudio.audionode',
+ 'disabled-by-default-webrtc',
+ 'disabled-by-default-audio-worklet',
+ 'disabled-by-default-mediastream',
+ 'disabled-by-default-v8.gc',
+ 'disabled-by-default-toplevel',
+ 'disabled-by-default-toplevel.flow',
+ 'disabled-by-default-wakeup.flow',
+ 'disabled-by-default-cpu_profiler',
+ 'disabled-by-default-scheduler',
+ 'disabled-by-default-p2p',
+ 'disabled-by-default-net',
+ ],
+ 'Video': [
+ 'base',
+ 'gpu',
+ 'gpu.capture',
+ 'media',
+ 'toplevel',
+ 'toplevel.flow',
+ 'scheduler',
+ 'wakeup.flow',
+ 'webrtc',
+ 'disabled-by-default-video_and_image_capture',
+ 'disabled-by-default-webrtc',
+ ],
+};
+
+// List of static Chrome categories, last updated at 2024-05-15 from HEAD of
+// Chromium's //base/trace_event/builtin_categories.h.
+const BUILTIN_CATEGORIES = [
+ 'accessibility',
+ 'AccountFetcherService',
+ 'android.adpf',
+ 'android.ui.jank',
+ 'android_webview',
+ 'android_webview.timeline',
+ 'aogh',
+ 'audio',
+ 'base',
+ 'benchmark',
+ 'blink',
+ 'blink.animations',
+ 'blink.bindings',
+ 'blink.console',
+ 'blink.net',
+ 'blink.resource',
+ 'blink.user_timing',
+ 'blink.worker',
+ 'blink_style',
+ 'Blob',
+ 'browser',
+ 'browsing_data',
+ 'CacheStorage',
+ 'Calculators',
+ 'CameraStream',
+ 'cppgc',
+ 'camera',
+ 'cast_app',
+ 'cast_perf_test',
+ 'cast.mdns',
+ 'cast.mdns.socket',
+ 'cast.stream',
+ 'cc',
+ 'cc.debug',
+ 'cdp.perf',
+ 'chromeos',
+ 'cma',
+ 'compositor',
+ 'content',
+ 'content_capture',
+ 'interactions',
+ 'delegated_ink_trails',
+ 'device',
+ 'devtools',
+ 'devtools.contrast',
+ 'devtools.timeline',
+ 'disk_cache',
+ 'download',
+ 'download_service',
+ 'drm',
+ 'drmcursor',
+ 'dwrite',
+ 'DXVA_Decoding',
+ 'evdev',
+ 'event',
+ 'event_latency',
+ 'exo',
+ 'extensions',
+ 'explore_sites',
+ 'FileSystem',
+ 'file_system_provider',
+ 'fledge',
+ 'fonts',
+ 'GAMEPAD',
+ 'gpu',
+ 'gpu.angle',
+ 'gpu.angle.texture_metrics',
+ 'gpu.capture',
+ 'graphics.pipeline',
+ 'headless',
+ 'history',
+ 'hwoverlays',
+ 'identity',
+ 'ime',
+ 'IndexedDB',
+ 'input',
+ 'input.scrolling',
+ 'io',
+ 'ipc',
+ 'Java',
+ 'jni',
+ 'jpeg',
+ 'latency',
+ 'latencyInfo',
+ 'leveldb',
+ 'loading',
+ 'log',
+ 'login',
+ 'media',
+ 'media_router',
+ 'memory',
+ 'midi',
+ 'mojom',
+ 'mus',
+ 'native',
+ 'navigation',
+ 'navigation.debug',
+ 'net',
+ 'network.scheduler',
+ 'netlog',
+ 'offline_pages',
+ 'omnibox',
+ 'oobe',
+ 'openscreen',
+ 'ozone',
+ 'partition_alloc',
+ 'passwords',
+ 'p2p',
+ 'page-serialization',
+ 'paint_preview',
+ 'pepper',
+ 'PlatformMalloc',
+ 'power',
+ 'ppapi',
+ 'ppapi_proxy',
+ 'print',
+ 'raf_investigation',
+ 'rail',
+ 'renderer',
+ 'renderer_host',
+ 'renderer.scheduler',
+ 'resources',
+ 'RLZ',
+ 'ServiceWorker',
+ 'SiteEngagement',
+ 'safe_browsing',
+ 'scheduler',
+ 'scheduler.long_tasks',
+ 'screenlock_monitor',
+ 'segmentation_platform',
+ 'sequence_manager',
+ 'service_manager',
+ 'sharing',
+ 'shell',
+ 'shortcut_viewer',
+ 'shutdown',
+ 'skia',
+ 'sql',
+ 'stadia_media',
+ 'stadia_rtc',
+ 'startup',
+ 'sync',
+ 'system_apps',
+ 'test_gpu',
+ 'toplevel',
+ 'toplevel.flow',
+ 'ui',
+ 'v8',
+ 'v8.execute',
+ 'v8.wasm',
+ 'ValueStoreFrontend::Backend',
+ 'views',
+ 'views.frame',
+ 'viz',
+ 'vk',
+ 'wakeup.flow',
+ 'wayland',
+ 'webaudio',
+ 'webengine.fidl',
+ 'weblayer',
+ 'WebCore',
+ 'webnn',
+ 'webrtc',
+ 'webrtc_stats',
+ 'xr',
+ 'disabled-by-default-android_view_hierarchy',
+ 'disabled-by-default-animation-worklet',
+ 'disabled-by-default-audio',
+ 'disabled-by-default-audio.latency',
+ 'disabled-by-default-audio-worklet',
+ 'disabled-by-default-base',
+ 'disabled-by-default-blink.debug',
+ 'disabled-by-default-blink.debug.display_lock',
+ 'disabled-by-default-blink.debug.layout',
+ 'disabled-by-default-blink.debug.layout.trees',
+ 'disabled-by-default-blink.feature_usage',
+ 'disabled-by-default-blink.image_decoding',
+ 'disabled-by-default-blink.invalidation',
+ 'disabled-by-default-identifiability',
+ 'disabled-by-default-identifiability.high_entropy_api',
+ 'disabled-by-default-cc',
+ 'disabled-by-default-cc.debug',
+ 'disabled-by-default-cc.debug.cdp-perf',
+ 'disabled-by-default-cc.debug.display_items',
+ 'disabled-by-default-cc.debug.lcd_text',
+ 'disabled-by-default-cc.debug.picture',
+ 'disabled-by-default-cc.debug.scheduler',
+ 'disabled-by-default-cc.debug.scheduler.frames',
+ 'disabled-by-default-cc.debug.scheduler.now',
+ 'disabled-by-default-content.verbose',
+ 'disabled-by-default-cpu_profiler',
+ 'disabled-by-default-cppgc',
+ 'disabled-by-default-cpu_profiler.debug',
+ 'disabled-by-default-devtools.screenshot',
+ 'disabled-by-default-devtools.timeline',
+ 'disabled-by-default-devtools.timeline.frame',
+ 'disabled-by-default-devtools.timeline.inputs',
+ 'disabled-by-default-devtools.timeline.invalidationTracking',
+ 'disabled-by-default-devtools.timeline.layers',
+ 'disabled-by-default-devtools.timeline.picture',
+ 'disabled-by-default-devtools.timeline.stack',
+ 'disabled-by-default-devtools.target-rundown',
+ 'disabled-by-default-devtools.v8-source-rundown',
+ 'disabled-by-default-devtools.v8-source-rundown-sources',
+ 'disabled-by-default-file',
+ 'disabled-by-default-fonts',
+ 'disabled-by-default-gpu_cmd_queue',
+ 'disabled-by-default-gpu.dawn',
+ 'disabled-by-default-gpu.debug',
+ 'disabled-by-default-gpu.decoder',
+ 'disabled-by-default-gpu.device',
+ 'disabled-by-default-gpu.graphite.dawn',
+ 'disabled-by-default-gpu.service',
+ 'disabled-by-default-gpu.vulkan.vma',
+ 'disabled-by-default-histogram_samples',
+ 'disabled-by-default-java-heap-profiler',
+ 'disabled-by-default-layer-element',
+ 'disabled-by-default-layout_shift.debug',
+ 'disabled-by-default-lifecycles',
+ 'disabled-by-default-loading',
+ 'disabled-by-default-mediastream',
+ 'disabled-by-default-memory-infra',
+ 'disabled-by-default-memory-infra.v8.code_stats',
+ 'disabled-by-default-mojom',
+ 'disabled-by-default-net',
+ 'disabled-by-default-network',
+ 'disabled-by-default-paint-worklet',
+ 'disabled-by-default-power',
+ 'disabled-by-default-renderer.scheduler',
+ 'disabled-by-default-renderer.scheduler.debug',
+ 'disabled-by-default-sequence_manager',
+ 'disabled-by-default-sequence_manager.debug',
+ 'disabled-by-default-sequence_manager.verbose_snapshots',
+ 'disabled-by-default-skia',
+ 'disabled-by-default-skia.gpu',
+ 'disabled-by-default-skia.gpu.cache',
+ 'disabled-by-default-skia.shaders',
+ 'disabled-by-default-skottie',
+ 'disabled-by-default-SyncFileSystem',
+ 'disabled-by-default-system_power',
+ 'disabled-by-default-system_stats',
+ 'disabled-by-default-thread_pool_diagnostics',
+ 'disabled-by-default-toplevel.ipc',
+ 'disabled-by-default-user_action_samples',
+ 'disabled-by-default-v8.compile',
+ 'disabled-by-default-v8.cpu_profiler',
+ 'disabled-by-default-v8.gc',
+ 'disabled-by-default-v8.gc_stats',
+ 'disabled-by-default-v8.ic_stats',
+ 'disabled-by-default-v8.inspector',
+ 'disabled-by-default-v8.runtime',
+ 'disabled-by-default-v8.runtime_stats',
+ 'disabled-by-default-v8.runtime_stats_sampling',
+ 'disabled-by-default-v8.stack_trace',
+ 'disabled-by-default-v8.turbofan',
+ 'disabled-by-default-v8.wasm.detailed',
+ 'disabled-by-default-v8.wasm.turbofan',
+ 'disabled-by-default-video_and_image_capture',
+ 'disabled-by-default-display.framedisplayed',
+ 'disabled-by-default-viz.gpu_composite_time',
+ 'disabled-by-default-viz.debug.overlay_planes',
+ 'disabled-by-default-viz.hit_testing_flow',
+ 'disabled-by-default-viz.overdraw',
+ 'disabled-by-default-viz.quads',
+ 'disabled-by-default-viz.surface_id_flow',
+ 'disabled-by-default-viz.surface_lifetime',
+ 'disabled-by-default-viz.triangles',
+ 'disabled-by-default-viz.visual_debugger',
+ 'disabled-by-default-webaudio.audionode',
+ 'disabled-by-default-webgpu',
+ 'disabled-by-default-webnn',
+ 'disabled-by-default-webrtc',
+ 'disabled-by-default-worker.scheduler',
+ 'disabled-by-default-xr.debug',
+];
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/cpu.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/cpu.ts
new file mode 100644
index 0000000..eac9429
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/cpu.ts
@@ -0,0 +1,123 @@
+// 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 protos from '../../../protos';
+import {ADV_FTRACE_PROBE_ID, ADV_PROC_ASSOC_PROBE_ID} from './advanced';
+import {RecordSubpage, RecordProbe} from '../config/config_interfaces';
+import {TraceConfigBuilder} from '../config/trace_config_builder';
+import {POLL_INTERVAL_SLIDER, Slider} from './widgets/slider';
+
+const PROC_POLL_DS = 'linux.sys_stats';
+
+export function cpuRecordSection(): RecordSubpage {
+ return {
+ kind: 'PROBES_PAGE',
+ id: 'cpu',
+ title: 'CPU',
+ subtitle: 'CPU usage, scheduling, wakeups',
+ icon: 'subtitles',
+ probes: [cpuUsage(), sched(), cpuFreq(), syscalls()],
+ };
+}
+
+function cpuUsage(): RecordProbe {
+ const settings = {pollMs: new Slider(POLL_INTERVAL_SLIDER)};
+ return {
+ id: 'cpu_usage',
+ image: 'rec_cpu_coarse.png',
+ title: 'Coarse CPU usage counter',
+ supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+ description:
+ 'Lightweight polling of CPU usage counters via /proc/stat. ' +
+ 'Allows to periodically monitor CPU usage.',
+ dependencies: [ADV_PROC_ASSOC_PROBE_ID],
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ const cfg = tc.addDataSource(PROC_POLL_DS);
+ cfg.sysStatsConfig ??= {};
+ cfg.sysStatsConfig.statPeriodMs = settings.pollMs.value;
+ cfg.sysStatsConfig.statCounters ??= [];
+ cfg.sysStatsConfig.statCounters.push(
+ protos.SysStatsConfig.StatCounters.STAT_CPU_TIMES,
+ protos.SysStatsConfig.StatCounters.STAT_FORK_COUNT,
+ );
+ },
+ };
+}
+
+function sched(): RecordProbe {
+ return {
+ id: 'cpu_sched',
+ image: 'rec_cpu_fine.png',
+ title: 'Scheduling details',
+ supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+ dependencies: [ADV_FTRACE_PROBE_ID, ADV_PROC_ASSOC_PROBE_ID],
+ description: 'Enables high-detailed tracking of scheduling events',
+ genConfig: function (tc: TraceConfigBuilder) {
+ tc.addFtraceEvents(
+ 'sched/sched_switch',
+ 'power/suspend_resume',
+ 'sched/sched_blocked_reason',
+ 'sched/sched_wakeup',
+ 'sched/sched_wakeup_new',
+ 'sched/sched_waking',
+ 'sched/sched_process_exit',
+ 'sched/sched_process_free',
+ 'task/task_newtask',
+ 'task/task_rename',
+ );
+ },
+ };
+}
+
+function cpuFreq(): RecordProbe {
+ const settings = {pollMs: new Slider(POLL_INTERVAL_SLIDER)};
+ return {
+ id: 'cpu_freq',
+ image: 'rec_cpu_freq.png',
+ title: 'CPU frequency and idle states',
+ description:
+ 'Records cpu frequency and idle state changes via ftrace and sysfs',
+ supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ const cfg = tc.addDataSource(PROC_POLL_DS);
+ cfg.sysStatsConfig ??= {};
+ cfg.sysStatsConfig.cpufreqPeriodMs = settings.pollMs.value;
+ tc.addFtraceEvents(
+ 'power/cpu_frequency',
+ 'power/cpu_idle',
+ 'power/suspend_resume',
+ );
+ },
+ };
+}
+
+function syscalls(): RecordProbe {
+ return {
+ id: 'cpu_syscalls',
+ image: 'rec_syscalls.png',
+ title: 'Syscalls',
+ description:
+ 'Tracks the enter and exit of all syscalls. On Android' +
+ 'requires a userdebug or eng build.',
+ supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+ genConfig: function (tc: TraceConfigBuilder) {
+ tc.addFtraceEvents(
+ 'raw_syscalls/sys_enter', //
+ 'raw_syscalls/raw_exit', //
+ );
+ },
+ };
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/gpu.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/gpu.ts
new file mode 100644
index 0000000..aee581b
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/gpu.ts
@@ -0,0 +1,70 @@
+// 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 {RecordProbe, RecordSubpage} from '../config/config_interfaces';
+import {TraceConfigBuilder} from '../config/trace_config_builder';
+
+export function gpuRecordSection(): RecordSubpage {
+ return {
+ kind: 'PROBES_PAGE',
+ id: 'gpu',
+ title: 'GPU',
+ subtitle: 'GPU Frequency, memory',
+ icon: 'aspect_ratio',
+ probes: [gpuFreq(), gpuMemory(), gpuWorkPeriod()],
+ };
+}
+
+function gpuFreq(): RecordProbe {
+ return {
+ id: 'gpu_frequency',
+ image: 'rec_cpu_freq.png',
+ title: 'GPU frequency',
+ description: 'Records gpu frequency via ftrace',
+ supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+ genConfig: function (tc: TraceConfigBuilder) {
+ tc.addFtraceEvents('power/gpu_frequency');
+ },
+ };
+}
+
+function gpuMemory(): RecordProbe {
+ return {
+ id: 'gpu_memory',
+ image: 'rec_gpu_mem_total.png',
+ title: 'GPU memory',
+ description:
+ 'Allows to track per process and global total GPU memory usages. ' +
+ '(Available on recent Android 12+ kernels)',
+ supportedPlatforms: ['ANDROID'],
+ genConfig: function (tc: TraceConfigBuilder) {
+ tc.addDataSource('android.gpu.memory');
+ tc.addFtraceEvents('gpu_mem/gpu_mem_total');
+ },
+ };
+}
+
+function gpuWorkPeriod(): RecordProbe {
+ return {
+ id: 'gpu_work_period',
+ title: 'GPU work period',
+ description:
+ 'Allows to track per package GPU work.' +
+ '(Available on recent Android 14+ kernels)',
+ supportedPlatforms: ['ANDROID'],
+ genConfig: function (tc: TraceConfigBuilder) {
+ tc.addFtraceEvents('power/gpu_work_period');
+ },
+ };
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/instructions_page.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/instructions_page.ts
new file mode 100644
index 0000000..ebe879f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/instructions_page.ts
@@ -0,0 +1,102 @@
+// 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 {RecordingManager} from '../recording_manager';
+import {copyToClipboard} from '../../../base/clipboard';
+import {traceConfigToTxt} from '../config/trace_config_utils_wasm';
+import protos from '../../../protos';
+import {RecordSubpage} from '../config/config_interfaces';
+import {Anchor} from '../../../widgets/anchor';
+
+export function instructionsPage(recMgr: RecordingManager): RecordSubpage {
+ return {
+ kind: 'GLOBAL_PAGE',
+ id: 'cmdline',
+ icon: 'terminal',
+ title: 'Cmdline instructions',
+ subtitle: 'Show cmdline instructions',
+ render() {
+ return m(InstructionsPage, {recMgr});
+ },
+ serialize() {},
+ deserialize() {},
+ };
+}
+
+type RecMgrAttrs = {recMgr: RecordingManager};
+class InstructionsPage implements m.ClassComponent<RecMgrAttrs> {
+ private configTxt = '';
+ private cmdline?: string;
+ private docsLink?: string;
+
+ constructor({attrs}: m.CVnode<RecMgrAttrs>) {
+ // Generate the config PBTX.
+ const cfg = attrs.recMgr.genTraceConfig();
+ const cfgBytes = protos.TraceConfig.encode(cfg).finish().slice();
+ traceConfigToTxt(cfgBytes).then((txt) => {
+ this.configTxt = txt;
+ m.redraw();
+ });
+
+ // Generate the cmdline instructions.
+ switch (attrs.recMgr.currentPlatform) {
+ case 'ANDROID':
+ this.cmdline =
+ 'cat config.pbtx | adb shell perfetto' +
+ ' -c - --txt -o /data/misc/perfetto-traces/trace.pftrace';
+ this.docsLink = 'https://perfetto.dev/docs/quickstart/android-tracing';
+ break;
+ case 'LINUX':
+ this.cmdline = 'perfetto -c config.pbtx --txt -o /tmp/trace.pftrace';
+ this.docsLink = 'https://perfetto.dev/docs/quickstart/linux-tracing';
+ break;
+ case 'CHROME':
+ case 'CHROME_OS':
+ this.docsLink = 'https://perfetto.dev/docs/quickstart/chrome-tracing';
+ this.cmdline =
+ 'There is no cmdline support for Chrome/CrOS.\n' +
+ 'You must use the recording UI via the extension to record traces.';
+ }
+ }
+
+ view() {
+ return [
+ this.docsLink &&
+ m(
+ 'p',
+ 'See the documentation on ',
+ m(
+ Anchor,
+ {href: this.docsLink, target: '_blank'},
+ this.docsLink.replace('https://', ''),
+ ),
+ ),
+ this.cmdline && m('.code-snippet', m('code', this.cmdline)),
+ m('p', 'Save the file below as: config.pbtx'),
+ m(
+ '.code-snippet',
+ m(
+ 'button',
+ {
+ title: 'Copy to clipboard',
+ onclick: () => copyToClipboard(this.configTxt),
+ },
+ m('i.material-icons', 'assignment'),
+ ),
+ m('code', this.configTxt),
+ ),
+ ];
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/memory.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/memory.ts
new file mode 100644
index 0000000..fe7a6fb
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/memory.ts
@@ -0,0 +1,384 @@
+// 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 {assertExists} from '../../../base/logging';
+import {splitLinesNonEmpty} from '../../../base/string_utils';
+import protos from '../../../protos';
+import {ADV_PROC_ASSOC_PROBE_ID, PROC_STATS_DS_NAME} from './advanced';
+import {RecordProbe, RecordSubpage} from '../config/config_interfaces';
+import {TraceConfigBuilder} from '../config/trace_config_builder';
+import {TypedMultiselect} from './widgets/multiselect';
+import {POLL_INTERVAL_SLIDER, Slider} from './widgets/slider';
+import {Textarea} from './widgets/textarea';
+import {Toggle} from './widgets/toggle';
+
+const SYS_STAT_DS = 'linux.sys_stats';
+
+export function memoryRecordSection(): RecordSubpage {
+ return {
+ kind: 'PROBES_PAGE',
+ id: 'memory',
+ title: 'Memory',
+ subtitle: 'Physical mem, VM, LMK',
+ icon: 'memory',
+ probes: [
+ heapProfiling(),
+ heapDumps(),
+ meminfo(),
+ vmstat(),
+ hifreq(),
+ lmk(),
+ polledProcStats(),
+ ],
+ };
+}
+
+function heapProfiling(): RecordProbe {
+ const settings = {
+ targetProcs: new Textarea({
+ title: 'Names or pids of the processes to track (required)',
+ docsLink:
+ 'https://perfetto.dev/docs/data-sources/native-heap-profiler#heapprofd-targets',
+ placeholder:
+ 'One per line, e.g.:\n' +
+ 'system_server\n' +
+ 'com.google.android.apps.photos\n' +
+ '1503',
+ }),
+ samplingBytes: new Slider({
+ title: 'Sampling interval',
+ description: 'Trades off accuracy vs overhead in the target process',
+ cssClass: '.thin',
+ default: 4096,
+ values: [
+ 1, 16, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536,
+ 131072, 262144, 524288, 1048576,
+ ],
+ unit: 'B',
+ min: 1,
+ }),
+ dumpInterval: new Slider({
+ title: 'Continuous dump interval',
+ description: 'Time between following dumps (0 = only dump at the end)',
+ values: SAMPLING_TIMES_MS,
+ cssClass: '.thin',
+ unit: 'ms',
+ min: 0,
+ }),
+ dumpPhase: new Slider({
+ title: 'Continuous dumps phase',
+ description: 'Time before first dump',
+ values: SAMPLING_TIMES_MS,
+ cssClass: '.thin',
+ unit: 'ms',
+ min: 0,
+ }),
+ shmemKB: new Slider({
+ title: 'Shared memory buffer',
+ values: SMB_VALUES_KB,
+ cssClass: '.thin',
+ unit: 'KB',
+ }),
+ blockClient: new Toggle({
+ title: 'Block client',
+ cssClass: '.thin',
+ default: true,
+ descr: `Slow down target application if profiler cannot keep up.`,
+ }),
+ allHeaps: new Toggle({
+ title: 'All custom allocators (Q+)',
+ cssClass: '.thin',
+ descr:
+ 'If the target application exposes custom allocators, also ' +
+ 'sample from those.',
+ }),
+ };
+ return {
+ id: 'mem_hprof',
+ title: 'Native heap profiling',
+ image: 'rec_native_heap_profiler.png',
+ description:
+ 'Track native heap allocations & deallocations of an Android ' +
+ 'process. (Available on Android 10+)',
+ supportedPlatforms: ['ANDROID', 'LINUX'],
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ const s = settings;
+ const [cmdlines, pids] = extractCmdlinesAndPids(s.targetProcs.text);
+ tc.addDataSource('android.heapprofd').heapprofdConfig = {
+ samplingIntervalBytes: s.samplingBytes.value,
+ shmemSizeBytes: s.shmemKB.value * 1024,
+ blockClient: s.blockClient.enabled,
+ allHeaps: s.allHeaps.enabled,
+ processCmdline: cmdlines.length > 0 ? cmdlines : undefined,
+ pid: pids.length > 0 ? pids : undefined,
+ continuousDumpConfig:
+ s.dumpInterval.value == 0
+ ? undefined
+ : {
+ dumpIntervalMs: s.dumpInterval.value,
+ dumpPhaseMs: s.dumpPhase.value,
+ },
+ };
+ },
+ };
+}
+
+function heapDumps(): RecordProbe {
+ const settings = {
+ targetProcs: new Textarea({
+ title: 'Names or pids of the processes to track (required)',
+ docsLink: 'https://perfetto.dev/docs/data-sources/java-heap-profiler',
+ placeholder:
+ 'One per line, e.g.:\n' +
+ 'system_server\n' +
+ 'com.google.android.apps.photos\n' +
+ '1503',
+ }),
+ dumpInterval: new Slider({
+ title: 'Continuous dump interval',
+ description: 'Time between following dumps (0 = only dump at the end)',
+ values: SAMPLING_TIMES_MS,
+ cssClass: '.thin',
+ unit: 'ms',
+ min: 0,
+ }),
+ dumpPhase: new Slider({
+ title: 'Continuous dumps phase',
+ description: 'Time before first dump',
+ values: SAMPLING_TIMES_MS,
+ cssClass: '.thin',
+ unit: 'ms',
+ min: 0,
+ }),
+ };
+ return {
+ id: 'mem_heapdumps',
+ title: 'Java heap dumps',
+ image: 'rec_java_heap_dump.png',
+ description:
+ 'Dump information about the Java object graph of an ' +
+ 'Android app. (Available on Android 11+)',
+ supportedPlatforms: ['ANDROID'],
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ const s = settings;
+ const [cmdlines, pids] = extractCmdlinesAndPids(s.targetProcs.text);
+ tc.addDataSource('android.java_hprof').javaHprofConfig = {
+ processCmdline: cmdlines.length > 0 ? cmdlines : undefined,
+ pid: pids.length > 0 ? pids : undefined,
+ continuousDumpConfig:
+ s.dumpInterval.value == 0
+ ? undefined
+ : {
+ dumpIntervalMs: s.dumpInterval.value,
+ dumpPhaseMs: s.dumpPhase.value,
+ },
+ };
+ },
+ };
+}
+
+function meminfo(): RecordProbe {
+ const meminfoCounters = new Map<string, protos.MeminfoCounters>();
+ for (const x in protos.MeminfoCounters) {
+ if (
+ typeof protos.MeminfoCounters[x] === 'number' &&
+ !`${x}`.endsWith('_UNSPECIFIED')
+ ) {
+ meminfoCounters.set(
+ x.replace('MEMINFO_', '').toLowerCase(),
+ protos.MeminfoCounters[x],
+ );
+ }
+ }
+ const settings = {
+ pollMs: new Slider(POLL_INTERVAL_SLIDER),
+ counters: new TypedMultiselect<protos.MeminfoCounters>({
+ options: meminfoCounters,
+ }),
+ };
+ return {
+ id: 'mem_meminfo',
+ image: 'rec_meminfo.png',
+ title: 'Kernel meminfo',
+ description: 'Polling of /proc/meminfo',
+ supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ const ds = tc.addDataSource(SYS_STAT_DS);
+ // sysStatsConfig is shared with other probes, don't clobber.
+ const cfg = (ds.sysStatsConfig ??= {});
+ cfg.meminfoPeriodMs = settings.pollMs.value;
+ cfg.meminfoCounters = settings.counters.selectedValues();
+ },
+ };
+}
+
+function vmstat(): RecordProbe {
+ const vmstatCounters = new Map<string, protos.VmstatCounters>();
+ for (const x in protos.VmstatCounters) {
+ if (
+ typeof protos.VmstatCounters[x] === 'number' &&
+ !`${x}`.endsWith('_UNSPECIFIED')
+ ) {
+ vmstatCounters.set(
+ x.replace('VMSTAT_', '').toLowerCase(),
+ protos.VmstatCounters[x],
+ );
+ }
+ }
+ const settings = {
+ pollMs: new Slider(POLL_INTERVAL_SLIDER),
+ counters: new TypedMultiselect<protos.VmstatCounters>({
+ options: vmstatCounters,
+ }),
+ };
+ return {
+ id: 'mem_vmstat',
+ title: 'Virtual memory stats',
+ image: 'rec_vmstat.png',
+ description:
+ 'Periodically polls virtual memory stats from /proc/vmstat. ' +
+ 'Allows to gather statistics about swap, eviction, ' +
+ 'compression and pagecache efficiency',
+ supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ const ds = tc.addDataSource(SYS_STAT_DS);
+ // sysStatsConfig is shared with other probes, don't clobber.
+ const cfg = (ds.sysStatsConfig ??= {});
+ cfg.vmstatPeriodMs = settings.pollMs.value;
+ cfg.vmstatCounters = settings.counters.selectedValues();
+ },
+ };
+}
+
+function hifreq(): RecordProbe {
+ return {
+ id: 'mem_hifreq',
+ title: 'High-frequency memory events',
+ image: 'rec_mem_hifreq.png',
+ dependencies: [ADV_PROC_ASSOC_PROBE_ID],
+ description:
+ 'Allows to track short memory spikes and transitories through ' +
+ "ftrace's mm_event, rss_stat and ion events. Available only " +
+ 'on recent Android Q+ kernels',
+ supportedPlatforms: ['ANDROID'],
+ genConfig: function (tc: TraceConfigBuilder) {
+ tc.addFtraceEvents(
+ 'mm_event/mm_event_record',
+ 'kmem/rss_stat',
+ 'ion/ion_stat',
+ 'dmabuf_heap/dma_heap_stat',
+ 'kmem/ion_heap_grow',
+ 'kmem/ion_heap_shrink',
+ );
+ },
+ };
+}
+
+function lmk(): RecordProbe {
+ return {
+ id: 'mem_lmk',
+ title: 'Low memory killer',
+ image: 'rec_lmk.png',
+ dependencies: [ADV_PROC_ASSOC_PROBE_ID],
+ description:
+ 'Record LMK events. Works both with the old in-kernel LMK ' +
+ 'and the newer userspace lmkd. It also tracks OOM score adjustments.',
+ supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+ genConfig: function (tc: TraceConfigBuilder) {
+ tc.addFtraceEvents(
+ // For in-kernel LMK (roughly older devices until Go and Pixel 3).
+ 'lowmemorykiller/lowmemory_kill',
+ 'oom/oom_score_adj_update',
+ );
+
+ // For userspace LMKd (newer devices).
+ // 'lmkd' is not really required because the code in lmkd.c emits events
+ // with ATRACE_TAG_ALWAYS. We need something just to ensure that the final
+ // config will enable atrace userspace events.
+ tc.addAtraceApps('lmkd');
+ },
+ };
+}
+
+function polledProcStats(): RecordProbe {
+ const settings = {
+ pollMs: new Slider(POLL_INTERVAL_SLIDER),
+ procAge: new Toggle({title: 'Record process age'}),
+ procRuntime: new Toggle({title: 'Record process runtime'}),
+ };
+ return {
+ id: 'mem_proc_stat',
+ title: 'Per process /proc/ stat polling',
+ image: 'rec_ps_stats.png',
+ dependencies: [ADV_PROC_ASSOC_PROBE_ID],
+ description:
+ 'Periodically samples all processes in the system tracking: ' +
+ 'their thread list, memory counters (RSS, swap and other ' +
+ '/proc/status counters) and oom_score_adj.',
+ supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ const ds = tc.addDataSource(PROC_STATS_DS_NAME);
+ // Because of the dependency on ADV_PROC_ASSOC_PROBE_ID, we expect
+ // procThreadAssociation() to create the config first.
+ const cfg = assertExists(ds.processStatsConfig);
+ cfg.procStatsPollMs = settings.pollMs.value || undefined;
+ cfg.recordProcessAge = settings.procAge.enabled || undefined;
+ cfg.recordProcessRuntime = settings.procRuntime.enabled || undefined;
+ },
+ };
+}
+
+const SAMPLING_TIMES_MS = [
+ 0,
+ 1000,
+ 10 * 1000,
+ 30 * 1000,
+ 60 * 1000,
+ 5 * 60 * 1000,
+ 10 * 60 * 1000,
+ 30 * 60 * 1000,
+ 60 * 60 * 1000,
+];
+
+const SMB_VALUES_KB = [
+ 16,
+ 32,
+ 64,
+ 128,
+ 512,
+ 1024,
+ 4096,
+ 16 * 1024,
+ 32 * 1024,
+ 128 * 1024,
+];
+
+function extractCmdlinesAndPids(text: string): [string[], number[]] {
+ const cmdlines = [];
+ const pids = [];
+ for (const line of splitLinesNonEmpty(text)) {
+ const num = parseInt(line);
+ if (isNaN(num)) {
+ cmdlines.push(line);
+ } else {
+ pids.push(num);
+ }
+ }
+ return [cmdlines, pids];
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/power.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/power.ts
new file mode 100644
index 0000000..ee3d232
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/power.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 protos from '../../../protos';
+import {RecordProbe, RecordSubpage} from '../config/config_interfaces';
+import {TraceConfigBuilder} from '../config/trace_config_builder';
+import {POLL_INTERVAL_SLIDER, Slider} from './widgets/slider';
+
+export function powerRecordSection(): RecordSubpage {
+ return {
+ kind: 'PROBES_PAGE',
+ id: 'power',
+ title: 'Power',
+ subtitle: 'Battery and other energy counters',
+ icon: 'battery_charging_full',
+ probes: [powerRails(), powerVoltages()],
+ };
+}
+
+function powerRails(): RecordProbe {
+ const ANDROID_POWER_DS = 'android.power';
+ const settings = {pollMs: new Slider(POLL_INTERVAL_SLIDER)};
+ return {
+ id: 'power_rails',
+ image: 'rec_battery_counters.png',
+ title: 'Battery drain & power rails',
+ description:
+ 'Polls charge counters and instantaneous power draw from ' +
+ 'the battery power management IC and the power rails from ' +
+ 'the PowerStats HAL.',
+ docsLink: 'https://perfetto.dev/docs/data-sources/battery-counters',
+ supportedPlatforms: ['ANDROID'],
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ tc.addDataSource(ANDROID_POWER_DS).androidPowerConfig = {
+ batteryPollMs: settings.pollMs.value,
+ collectPowerRails: true,
+ batteryCounters: [
+ protos.AndroidPowerConfig.BatteryCounters
+ .BATTERY_COUNTER_CAPACITY_PERCENT,
+ protos.AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CHARGE,
+ protos.AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CURRENT,
+ ],
+ };
+ },
+ };
+}
+
+function powerVoltages(): RecordProbe {
+ return {
+ id: 'power_voltages',
+ image: 'rec_board_voltage.png',
+ title: 'Board voltages & frequencies',
+ description: 'Tracks voltage and frequency changes from board sensors',
+ supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+ genConfig: function (tc: TraceConfigBuilder) {
+ tc.addFtraceEvents(
+ 'regulator/regulator_set_voltage',
+ 'regulator/regulator_set_voltage_complete',
+ 'power/clock_enable',
+ 'power/clock_disable',
+ 'power/clock_set_rate',
+ 'power/suspend_resume',
+ );
+ },
+ };
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/preflight_check_renderer.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/preflight_check_renderer.ts
new file mode 100644
index 0000000..289c1a6
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/preflight_check_renderer.ts
@@ -0,0 +1,82 @@
+// 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 {
+ PreflightCheck,
+ PreflightCheckResult,
+ WithPreflightChecks,
+} from '../interfaces/connection_check';
+import {Spinner} from '../../../widgets/spinner';
+import {Icon} from '../../../widgets/icon';
+
+type PreflightCheckWithResult = PreflightCheck & {
+ result?: PreflightCheckResult;
+};
+
+export class PreflightCheckRenderer {
+ private results = new Array<PreflightCheckWithResult>();
+ private allChecksCompleted = false;
+ private numChecksFailed = 0;
+
+ constructor(private testTarget: WithPreflightChecks) {}
+
+ async runPreflightChecks(): Promise<boolean> {
+ this.allChecksCompleted = false;
+ this.numChecksFailed = 0;
+ for await (const check of this.testTarget.runPreflightChecks()) {
+ const entry: PreflightCheckWithResult = {...check, result: check.status};
+ this.results.push(entry);
+ this.numChecksFailed += check.status.ok ? 0 : 1;
+ m.redraw();
+ }
+ this.allChecksCompleted = true;
+ m.redraw();
+ return this.numChecksFailed === 0;
+ }
+
+ renderIcon(): m.Children {
+ const attrs = {filled: true, className: 'preflight-checks-icon'};
+ if (!this.allChecksCompleted) {
+ return m(Spinner);
+ }
+ if (this.numChecksFailed > 0) {
+ attrs.className += ' ok';
+ return m(Icon, {icon: 'report', ...attrs});
+ }
+ attrs.className += ' error';
+ return m(Icon, {icon: 'check_circle', ...attrs});
+ }
+
+ renderTable(): m.Children {
+ return m(
+ 'table.preflight-checks-table',
+ this.results.map((res) =>
+ m(
+ 'tr',
+ m('td', res.name),
+ m(
+ 'td',
+ res.result === undefined
+ ? m(Spinner)
+ : res.result.ok
+ ? m('span.ok', res.result.value)
+ : m('span.error', res.result.error),
+ res.remediation && m('div', m(res.remediation)),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/probe_renderer.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/probe_renderer.ts
new file mode 100644
index 0000000..1089c3f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/probe_renderer.ts
@@ -0,0 +1,86 @@
+// 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 {assetSrc} from '../../../base/assets';
+import {ConfigManager} from '../config/config_manager';
+import {RecordProbe} from '../config/config_interfaces';
+import {exists} from '../../../base/utils';
+import {DocsChip} from './widgets/docs_chip';
+import {classNames} from '../../../base/classnames';
+
+export interface ProbeAttrs {
+ cfgMgr: ConfigManager;
+ probe: RecordProbe;
+}
+
+export class Probe implements m.ClassComponent<ProbeAttrs> {
+ view({attrs}: m.CVnode<ProbeAttrs>) {
+ const onToggle = (enabled: boolean) => {
+ attrs.cfgMgr.setProbeEnabled(attrs.probe.id, enabled);
+ };
+
+ const probe = attrs.probe;
+ const forceEnabledDeps = attrs.cfgMgr.getProbeEnableDependants(
+ attrs.probe.id,
+ );
+ const enabled = attrs.cfgMgr.isProbeEnabled(attrs.probe.id);
+ const compact =
+ !exists(probe.description) &&
+ !exists(probe.image) &&
+ (probe.settings ?? []).length === 0;
+ return m(
+ '.probe',
+ {
+ className: classNames(enabled && 'enabled', compact && 'compact'),
+ },
+ probe.image &&
+ m('img', {
+ src: assetSrc(`assets/${probe.image}`),
+ onclick: () => onToggle(!enabled),
+ }),
+ m(
+ 'label',
+ m(`input[type=checkbox]`, {
+ checked: enabled,
+ disabled: forceEnabledDeps.length > 0,
+ title:
+ forceEnabledDeps.length > 0
+ ? 'Force-enabled due to ' + forceEnabledDeps.join(',')
+ : '',
+ oninput: (e: InputEvent) => {
+ onToggle((e.target as HTMLInputElement).checked);
+ },
+ }),
+ m('span', probe.title),
+ ),
+ compact
+ ? ''
+ : m(
+ `div${probe.image ? '' : '.extended-desc'}`,
+ m(
+ 'div',
+ probe.description,
+ probe.docsLink && m(DocsChip, {href: probe.docsLink}),
+ ),
+ m(
+ '.probe-config',
+ Object.values(attrs.probe.settings ?? {}).map((widget) =>
+ widget.render(),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/record_page.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/record_page.ts
new file mode 100644
index 0000000..7f44beb
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/record_page.ts
@@ -0,0 +1,282 @@
+// 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 {PageAttrs} from '../../../public/page';
+import {RecordingManager} from '../recording_manager';
+import {Icon} from '../../../widgets/icon';
+import {RecordSubpage, supportsPlatform} from '../config/config_interfaces';
+import {Probe} from './probe_renderer';
+import {Button} from '../../../widgets/button';
+import {classNames} from '../../../base/classnames';
+import {showModal} from '../../../widgets/modal';
+import {CopyableLink} from '../../../widgets/copyable_link';
+import {assertExists} from '../../../base/logging';
+import {BUCKET_NAME} from '../../../base/gcs_uploader';
+import {RecordingTarget} from '../interfaces/recording_target';
+import {exists} from '../../../base/utils';
+
+export type RecordPageAttrs = PageAttrs & {
+ getRecordingManager: () => RecordingManager;
+};
+
+const DEFAULT_SUBPAGE = 'target';
+const PERSIST_EVERY_MS = 1000;
+const SHARE_SUBPAGE = 'share';
+
+// By design this interface overlaps with RecordConfigSection so we can use the
+// same for custom subpages (record, config) and the probe settings.
+interface MenuEntry {
+ readonly id: string;
+ readonly icon: string;
+ readonly title: string;
+ readonly subtitle: string;
+}
+
+export class RecordPageV2 implements m.ClassComponent<RecordPageAttrs> {
+ private recMgr: RecordingManager;
+ private subpage: string = DEFAULT_SUBPAGE;
+ private persistTimer: number | undefined = undefined;
+
+ constructor({attrs}: m.CVnode<RecordPageAttrs>) {
+ this.recMgr = attrs.getRecordingManager();
+ if (attrs.subpage && attrs.subpage.startsWith('/' + SHARE_SUBPAGE)) {
+ this.loadShared(attrs.subpage.substring(SHARE_SUBPAGE.length + 2));
+ }
+ }
+
+ view({attrs}: m.CVnode<RecordPageAttrs>) {
+ if (this.persistTimer === undefined) {
+ this.persistTimer = window.setTimeout(() => {
+ this.recMgr.persistIntoLocalStorage();
+ this.persistTimer = undefined;
+ }, PERSIST_EVERY_MS);
+ }
+ this.subpage =
+ exists(attrs.subpage) && attrs.subpage.length > 0
+ ? attrs.subpage.substring(1)
+ : DEFAULT_SUBPAGE;
+ return m(
+ '.record-page',
+ m(
+ '.record-container',
+ m(
+ '.record-container-content',
+ this.renderMenu(), //
+ this.renderSubPage(), //
+ ),
+ ),
+ );
+ }
+
+ onremove() {
+ window.clearTimeout(this.persistTimer);
+ this.recMgr.persistIntoLocalStorage();
+ }
+
+ private renderSubPage(): m.Children {
+ const page = this.recMgr.pages.get(this.subpage);
+ if (page === undefined) {
+ return m(
+ '.record-section.active',
+ m('header', `Invalid subpage /record/${this.subpage}`),
+ );
+ }
+ return [
+ m(
+ '.record-section.active',
+ {id: page.id, key: page.id},
+ this.renderSubpage(page),
+ ),
+ ];
+ }
+
+ private renderSubpage(page: RecordSubpage): m.Children {
+ switch (page.kind) {
+ case 'PROBES_PAGE':
+ return page.probes
+ .filter((p) => supportsPlatform(p, this.recMgr.currentPlatform))
+ .map((probe) => m(Probe, {cfgMgr: this.recMgr.recordConfig, probe}));
+ case 'GLOBAL_PAGE':
+ case 'SESSION_PAGE':
+ return page.render();
+ }
+ }
+
+ private renderMenu() {
+ const pages = Array.from(this.recMgr.pages.values());
+ return m(
+ '.record-menu',
+ m(RecordingCtl, {recMgr: this.recMgr}),
+ m(
+ 'header',
+ 'Record settings',
+ m(Button, {
+ icon: 'share',
+ title: 'Share current config',
+ onclick: () => this.share(),
+ }),
+ ),
+ m(
+ 'ul',
+ pages
+ .filter((p) => ['SESSION_PAGE', 'GLOBAL_PAGE'].includes(p.kind))
+ .map((rc) => this.renderMenuEntry(rc)),
+ ),
+ m(
+ 'header',
+ 'Probes',
+ m(Button, {
+ icon: 'delete_sweep',
+ title: 'Clear current configuration',
+ onclick: () => {
+ if (confirm('The current config will be cleared. Are you sure?')) {
+ this.recMgr.clearSession();
+ }
+ },
+ }),
+ ),
+ m(
+ 'ul',
+ pages
+ .filter((p) => p.kind === 'PROBES_PAGE')
+ .map((rc) => this.renderMenuEntry(rc)),
+ ),
+ );
+ }
+
+ private renderMenuEntry(rc: MenuEntry) {
+ let enabledProbes = 0;
+ let availProbes = 0;
+ let probeCountTxt = '';
+ const probePage = this.recMgr.pages.get(rc.id);
+ if (probePage?.kind === 'PROBES_PAGE') {
+ for (const probe of probePage.probes) {
+ if (!supportsPlatform(probe, this.recMgr.currentPlatform)) continue;
+ ++availProbes;
+ if (!this.recMgr.recordConfig.isProbeEnabled(probe.id)) continue;
+ ++enabledProbes;
+ }
+ probeCountTxt = `${enabledProbes > 0 ? enabledProbes : ''}`;
+ }
+ const disabled = availProbes === 0 && probePage?.kind === 'PROBES_PAGE';
+ const className = classNames(
+ this.subpage === rc.id && 'active',
+ disabled && 'disabled',
+ );
+ return m(
+ 'a',
+ {href: disabled ? undefined : `#!/record/${rc.id}`},
+ m(
+ 'li',
+ {className},
+ m(Icon, {icon: rc.icon}),
+ m('.title', rc.title, m('.probe-count', probeCountTxt)),
+ m('.sub', rc.subtitle),
+ ),
+ );
+ }
+
+ private async share() {
+ const msg =
+ 'This will generate a publicly-readable link to the ' +
+ 'current config which cannot be deleted. Continue?';
+ if (!confirm(msg)) return;
+ const url = await this.recMgr.share();
+ const hash = assertExists(url.split('/').pop());
+ showModal({
+ title: 'Permalink',
+ content: m(CopyableLink, {
+ url: `${self.location.origin}/#!/record/${SHARE_SUBPAGE}/${hash}`,
+ }),
+ });
+ }
+
+ private async loadShared(hash: string) {
+ const url = `https://storage.googleapis.com/${BUCKET_NAME}/${hash}`;
+ const fetchData = await fetch(url);
+ const json = await fetchData.text();
+ const res = this.recMgr.restoreSessionFromJson(json);
+ if (!res.ok) {
+ showModal({title: 'Restore error', content: res.error});
+ return;
+ }
+ this.recMgr.app.navigate('#!/record/cmdline');
+ }
+}
+
+interface RecCtlAttrs {
+ recMgr: RecordingManager;
+}
+
+class RecordingCtl implements m.ClassComponent<RecCtlAttrs> {
+ private recMgr: RecordingManager;
+ private lastTarget?: RecordingTarget;
+
+ constructor({attrs}: m.CVnode<RecCtlAttrs>) {
+ this.recMgr = attrs.recMgr;
+ }
+
+ view() {
+ const target = this.recMgr.currentTarget;
+ if (this.lastTarget !== target) {
+ this.lastTarget = target;
+ }
+
+ const currentSession = this.recMgr.currentSession;
+ const recordingInProgress = currentSession?.inProgress;
+ if (recordingInProgress) {
+ // Update the ETA if the recording is in progress.
+ setTimeout(() => m.redraw(), 1000);
+ }
+ const eta: string | undefined = currentSession?.eta;
+ return m(
+ '.record-ctl',
+ m(Button, {
+ icon: 'cable',
+ title: 'Click to select another target',
+ onclick: () => this.recMgr.app.navigate('#!/record/target'),
+ }),
+ m(
+ '.record-target',
+ recordingInProgress
+ ? `Recording${eta ? ', ETA ' + eta : ''}`
+ : target?.name ?? 'No target selected',
+ ),
+ recordingInProgress
+ ? m(Button, {
+ icon: 'stop',
+ disabled: currentSession.state !== 'RECORDING',
+ iconFilled: true,
+ title: 'Stop',
+ className: 'rec',
+ onclick: () => {
+ currentSession.session?.stop();
+ this.recMgr.app.navigate('#!/record/target');
+ },
+ })
+ : m(Button, {
+ icon: 'not_started',
+ disabled: target === undefined,
+ iconFilled: true,
+ title: 'Start tracing',
+ className: 'rec',
+ onclick: () => {
+ this.recMgr.startTracing();
+ this.recMgr.app.navigate('#!/record/target');
+ },
+ }),
+ );
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/saved_configs.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/saved_configs.ts
new file mode 100644
index 0000000..e936abf
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/saved_configs.ts
@@ -0,0 +1,143 @@
+// 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 {RecordingManager} from '../recording_manager';
+import {RecordSubpage} from '../config/config_interfaces';
+import {SavedSessionSchema, RecordPluginSchema} from '../serialization_schema';
+import {assertExists} from '../../../base/logging';
+
+export function savedConfigsPage(recMgr: RecordingManager): RecordSubpage {
+ const savedConfigs = new Array<SavedSessionSchema>();
+
+ return {
+ kind: 'GLOBAL_PAGE',
+ id: 'configs',
+ icon: 'save',
+ title: 'Saved configs',
+ subtitle: 'Save, restore and export configs',
+ render() {
+ return m(SavedConfigsPage, {recMgr, savedConfigs});
+ },
+ serialize(state: RecordPluginSchema) {
+ state.savedSessions = [...savedConfigs];
+ },
+ deserialize(state: RecordPluginSchema) {
+ savedConfigs.splice(0);
+ savedConfigs.push(...state.savedSessions);
+ },
+ };
+}
+
+type RecMgrAttrs = {
+ recMgr: RecordingManager;
+ savedConfigs: Array<SavedSessionSchema>;
+};
+
+class SavedConfigsPage implements m.ClassComponent<RecMgrAttrs> {
+ private newConfigName = '';
+ private recMgr: RecordingManager;
+ private savedConfigs: Array<SavedSessionSchema>;
+
+ constructor({attrs}: m.CVnode<RecMgrAttrs>) {
+ this.recMgr = attrs.recMgr;
+ this.savedConfigs = attrs.savedConfigs;
+ }
+
+ view() {
+ const canSave =
+ this.newConfigName.length > 0 &&
+ this.savedConfigs.every((s) => s.name !== this.newConfigName);
+ return [
+ m('header', 'Save and load configurations'),
+ m('.input-config', [
+ m('input', {
+ value: this.newConfigName,
+ placeholder: 'Title for config',
+ oninput: (e: Event) => {
+ this.newConfigName = (e.target as HTMLInputElement).value;
+ },
+ }),
+ m(
+ 'button',
+ {
+ class: 'config-button',
+ disabled: !canSave,
+ title: canSave
+ ? 'Save current config'
+ : 'Duplicate name, saving disabled',
+ onclick: () => {
+ this.savedConfigs.push({
+ name: this.newConfigName,
+ config: this.recMgr.serializeSession(),
+ });
+ this.newConfigName = '';
+ },
+ },
+ m('i.material-icons', 'save'),
+ ),
+ ]),
+ this.savedConfigs.map((s) => this.renderSavedSessions(s)),
+ ];
+ }
+
+ private renderSavedSessions(item: SavedSessionSchema) {
+ const self = this;
+ return m('.config', [
+ m('span.title-config', item.name),
+ m(
+ 'button',
+ {
+ class: 'config-button',
+ title: 'Apply configuration settings',
+ onclick: () => {
+ this.recMgr.loadSession(item.config);
+ },
+ },
+ m('i.material-icons', 'file_upload'),
+ ),
+ m(
+ 'button',
+ {
+ class: 'config-button',
+ title: 'Overwrite configuration with current settings',
+ onclick: () => {
+ const msg = `Overwrite config "${item.name}" with current settings?`;
+ if (!confirm(msg)) return;
+ const savedCfg = assertExists(
+ this.savedConfigs.find((s) => s.name === item.name),
+ );
+ savedCfg.config = this.recMgr.serializeSession();
+ },
+ },
+ m('i.material-icons', 'save'),
+ ),
+ m(
+ 'button',
+ {
+ class: 'config-button',
+ title: 'Remove configuration',
+ onclick: () => {
+ const idx = this.savedConfigs.findIndex(
+ (s) => s.name === item.name,
+ );
+ if (idx < 0) return;
+ self.savedConfigs.splice(idx, 1);
+ },
+ },
+ m('i.material-icons', 'delete'),
+ ),
+ ]);
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/stack_sampling.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/stack_sampling.ts
new file mode 100644
index 0000000..a30069a
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/stack_sampling.ts
@@ -0,0 +1,76 @@
+// 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 {splitLinesNonEmpty} from '../../../base/string_utils';
+import protos from '../../../protos';
+import {RecordProbe, RecordSubpage} from '../config/config_interfaces';
+import {TraceConfigBuilder} from '../config/trace_config_builder';
+import {Slider} from './widgets/slider';
+import {Textarea} from './widgets/textarea';
+
+export function stackSamplingRecordSection(): RecordSubpage {
+ return {
+ kind: 'PROBES_PAGE',
+ id: 'stack_sampling',
+ title: 'Stack sampling',
+ subtitle: 'Lightweight cpu profiling',
+ icon: 'full_stacked_bar_chart',
+ probes: [tracedPerf()],
+ };
+}
+
+function tracedPerf(): RecordProbe {
+ const settings = {
+ samplingFreq: new Slider({
+ title: 'Sampling frequency',
+ cssClass: '.thin',
+ default: 100,
+ values: [1, 10, 50, 100, 250, 500, 1000],
+ unit: 'Hz',
+ }),
+ procs: new Textarea({
+ placeholder:
+ 'Filters for processes to profile, one per line e.g.' +
+ 'com.android.phone\nlmkd\ncom.android.webview:sandboxed_process*',
+ }),
+ };
+ return {
+ id: 'traced_perf',
+ title: 'Callstack sampling',
+ image: 'rec_profiling.png',
+ description:
+ 'Periodically records the current callstack (chain of ' +
+ 'function calls) of processes.',
+ supportedPlatforms: ['ANDROID', 'LINUX'],
+ settings,
+ genConfig: function (tc: TraceConfigBuilder) {
+ const s = settings;
+ const pkgs = splitLinesNonEmpty(s.procs.text);
+ tc.addDataSource('linux.perf').perfEventConfig = {
+ timebase: {
+ frequency: s.samplingFreq.value,
+ timestampClock: protos.PerfEvents.PerfClock.PERF_CLOCK_MONOTONIC,
+ },
+ callstackSampling: {
+ scope:
+ pkgs.length > 0
+ ? {
+ targetCmdline: pkgs,
+ }
+ : undefined,
+ },
+ };
+ },
+ };
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/target_selection_page.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/target_selection_page.ts
new file mode 100644
index 0000000..20b86fb
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/target_selection_page.ts
@@ -0,0 +1,415 @@
+// 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 {RecordingTarget} from '../interfaces/recording_target';
+import {SegmentedButtons} from '../../../widgets/segmented_buttons';
+import {TARGET_PLATFORMS} from '../interfaces/target_platform';
+import {RecordingTargetProvider} from '../interfaces/recording_target_provider';
+import {Icon} from '../../../widgets/icon';
+import {Button} from '../../../widgets/button';
+import {Intent} from '../../../widgets/common';
+import {getOrCreate} from '../../../base/utils';
+import {PreflightCheckRenderer} from './preflight_check_renderer';
+import {Select} from '../../../widgets/select';
+import {DisposableStack} from '../../../base/disposable_stack';
+import {CurrentTracingSession, RecordingManager} from '../recording_manager';
+import {downloadData} from '../../../base/download_utils';
+import {RecordSubpage} from '../config/config_interfaces';
+import {RecordPluginSchema} from '../serialization_schema';
+import {Checkbox} from '../../../widgets/checkbox';
+
+type RecMgrAttrs = {recMgr: RecordingManager};
+
+export function targetSelectionPage(recMgr: RecordingManager): RecordSubpage {
+ return {
+ kind: 'GLOBAL_PAGE',
+ id: 'target',
+ icon: 'cable',
+ title: 'Target device',
+ subtitle: 'Live recording via USB/WebSocket',
+ render() {
+ return m(TargetSelectionPage, {recMgr});
+ },
+ serialize(state: RecordPluginSchema) {
+ state.target = {
+ platformId: recMgr.currentPlatform,
+ transportId: recMgr.currentProvider?.id,
+ targetId: recMgr.currentTarget?.id,
+ };
+ state.autoOpenTrace = recMgr.autoOpenTraceWhenTracingEnds;
+ },
+ async deserialize(state: RecordPluginSchema) {
+ recMgr.autoOpenTraceWhenTracingEnds = state.autoOpenTrace;
+ if (state.target.platformId === undefined) return;
+ recMgr.setPlatform(state.target.platformId);
+ const prov = recMgr.getProvider(state.target.transportId ?? '');
+ if (prov === undefined) return;
+ await recMgr.setProvider(prov);
+ if (state.target.targetId === undefined) return;
+ for (const target of await recMgr.listTargets()) {
+ if (target.id === state.target.targetId) {
+ await recMgr.setTarget(target);
+ }
+ }
+ },
+ };
+}
+
+class TargetSelectionPage implements m.ClassComponent<RecMgrAttrs> {
+ view({attrs}: m.CVnode<RecMgrAttrs>) {
+ return [
+ m('header', 'Select platform'),
+ m(SegmentedButtons, {
+ className: 'platform-selector',
+ options: TARGET_PLATFORMS.map((p) => ({label: p.name, icon: p.icon})),
+ selectedOption: TARGET_PLATFORMS.findIndex(
+ (p) => p.id === attrs.recMgr.currentPlatform,
+ ),
+ onOptionSelected: (num) => {
+ attrs.recMgr.setPlatform(TARGET_PLATFORMS[num].id);
+ // m.redraw();
+ },
+ }),
+ [
+ m(TransportSelector, {
+ recMgr: attrs.recMgr,
+ key: attrs.recMgr.currentPlatform,
+ }),
+ ],
+ ];
+ }
+}
+
+class TransportSelector implements m.ClassComponent<RecMgrAttrs> {
+ private transportKeys = new ObjToId();
+
+ view({attrs}: m.CVnode<RecMgrAttrs>) {
+ const options = [];
+ for (const provider of attrs.recMgr.listProvidersForCurrentPlatform()) {
+ const id = this.transportKeys.getKey(provider);
+ options.push([
+ m(`input[type=radio][name=recordingProvider][id=${id}]`, {
+ onchange: async () => {
+ await attrs.recMgr.setProvider(provider);
+ m.redraw();
+ },
+ checked: attrs.recMgr.currentProvider === provider,
+ }),
+ m(
+ `label[for=${id}]`,
+ m(Icon, {icon: provider.icon}),
+ m('.title', provider.name),
+ m('.description', provider.description),
+ ),
+ ]);
+ }
+ return [
+ m('header', 'Select transport'),
+ m('fieldset.record-transports', ...options),
+ attrs.recMgr.currentProvider && [
+ m(TargetSelector, {
+ recMgr: attrs.recMgr,
+ provider: attrs.recMgr.currentProvider,
+ key: this.transportKeys.getKey(attrs.recMgr.currentProvider),
+ }),
+ ],
+ ];
+ }
+}
+
+type TargetSelectorAttrs = {
+ recMgr: RecordingManager;
+ provider: RecordingTargetProvider;
+};
+class TargetSelector implements m.ClassComponent<TargetSelectorAttrs> {
+ private targetIdMap = new ObjToId();
+ private checksRenderer: PreflightCheckRenderer;
+ private trash = new DisposableStack();
+ private targets: RecordingTarget[] = [];
+ private provider: RecordingTargetProvider;
+ private recMgr: RecordingManager;
+
+ constructor({attrs}: m.CVnode<TargetSelectorAttrs>) {
+ this.recMgr = attrs.recMgr;
+ this.provider = attrs.provider;
+ this.checksRenderer = new PreflightCheckRenderer(attrs.provider);
+ this.trash.use(
+ attrs.provider.onTargetsChanged.addListener(() => this.refreshTargets()),
+ );
+ this.checksRenderer
+ .runPreflightChecks() //
+ .then(() => this.refreshTargets());
+ this.recMgr.listTargets().then((targets) => {
+ this.targets = targets;
+ m.redraw();
+ });
+ }
+
+ view({attrs}: m.CVnode<TargetSelectorAttrs>) {
+ const recMgr = attrs.recMgr;
+ return [
+ this.checksRenderer.renderTable(),
+ m('header', 'Select target device'),
+
+ m(
+ '.record-targets',
+ m(
+ Select,
+ {
+ onchange: (e: Event) => {
+ const idx = (e.target as HTMLSelectElement).selectedIndex;
+ recMgr.setTarget(this.targets[idx]);
+ // m.redraw();
+ },
+ },
+ ...this.targets.map((target) =>
+ m(
+ 'option',
+ {selected: recMgr.currentTarget === target},
+ target.name,
+ ),
+ ),
+ ),
+ m(Button, {
+ icon: 'refresh',
+ title: 'Refresh devices',
+ onclick: () => {
+ // This forces the TargetDetails component to be re-initialized,
+ // in turn causing the pre-flight checks to be repeated. UX-wise
+ // we want the refresh button to both reload the target list and
+ // also reload the current target.
+ this.targetIdMap.clear();
+ this.refreshTargets();
+ },
+ }),
+ recMgr.currentTarget &&
+ m(Button, {
+ icon: recMgr.currentTarget.connected ? 'cancel' : 'power_off',
+ iconFilled: true,
+ disabled: !recMgr.currentTarget.connected,
+ title: recMgr.currentTarget.connected
+ ? 'Disconnect the current device'
+ : 'Device disconnected',
+ onclick: () => recMgr.currentTarget?.disconnect(),
+ }),
+ attrs.provider.pairNewTarget &&
+ m(Button, {
+ label: 'Connect new device',
+ icon: 'add',
+ intent: Intent.Primary,
+ onclick: async () => {
+ const target = await attrs.provider.pairNewTarget!();
+ target && recMgr.setTarget(target);
+ await this.refreshTargets();
+ },
+ }),
+ ),
+ recMgr.currentTarget && [
+ m(TargetDetails, {
+ recMgr: attrs.recMgr,
+ target: recMgr.currentTarget,
+ key: this.targetIdMap.getKey(recMgr.currentTarget),
+ }),
+ ],
+ ];
+ }
+
+ onremove() {
+ this.trash.dispose();
+ }
+
+ private async refreshTargets() {
+ // Re-triggers refresh and auto-select first valid target.
+ this.recMgr.setProvider(this.provider);
+ this.targets = await this.recMgr.listTargets();
+ m.redraw();
+ }
+}
+
+type TargetDetailsAttrs = {recMgr: RecordingManager; target: RecordingTarget};
+class TargetDetails implements m.ClassComponent<TargetDetailsAttrs> {
+ private checksRenderer?: PreflightCheckRenderer;
+
+ constructor({attrs}: m.CVnode<TargetDetailsAttrs>) {
+ this.checksRenderer = new PreflightCheckRenderer(attrs.target);
+ this.checksRenderer.runPreflightChecks();
+ }
+
+ view({attrs}: m.CVnode<TargetDetailsAttrs>) {
+ return [
+ this.checksRenderer?.renderTable(),
+ m(SessionMgmtRenderer, {recMgr: attrs.recMgr, target: attrs.target}),
+ ];
+ }
+}
+
+type SessionMgmtAttrs = {recMgr: RecordingManager; target: RecordingTarget};
+class SessionMgmtRenderer implements m.ClassComponent<SessionMgmtAttrs> {
+ view({attrs}: m.CVnode<SessionMgmtAttrs>) {
+ const session = attrs.recMgr.currentSession;
+ const isRecording = session?.state === 'RECORDING';
+ return [
+ m('header', 'Tracing session'),
+ m(
+ 'div',
+ m(Button, {
+ label: 'Start tracing',
+ icon: 'not_started',
+ iconFilled: true,
+ className: 'start',
+ disabled: isRecording,
+ onclick: () => attrs.recMgr.startTracing().then(() => m.redraw()),
+ }),
+ m(Button, {
+ label: 'Stop',
+ icon: 'stop',
+ className: 'stop',
+ iconFilled: true,
+ disabled: !isRecording,
+ onclick: () => session?.session?.stop().then(() => m.redraw()),
+ }),
+ m(Button, {
+ label: 'Cancel',
+ icon: 'cancel',
+ className: 'cancel',
+ iconFilled: true,
+ disabled: !isRecording,
+ onclick: () => session?.session?.cancel().then(() => m.redraw()),
+ }),
+ m(Checkbox, {
+ label: 'Open trace when done',
+ checked: attrs.recMgr.autoOpenTraceWhenTracingEnds,
+ onchange: (e) => {
+ attrs.recMgr.autoOpenTraceWhenTracingEnds = Boolean(
+ (e.target as HTMLInputElement).checked,
+ );
+ },
+ }),
+ ),
+ session?.error && m('div', session.error),
+ session && [
+ m(SessionStateRenderer, {
+ session,
+ key: session.uuid,
+ }),
+ ],
+ ];
+ }
+}
+
+type SessionStateAttrs = {
+ session: CurrentTracingSession;
+};
+class SessionStateRenderer implements m.ClassComponent<SessionStateAttrs> {
+ private session: CurrentTracingSession;
+ private trash = new DisposableStack();
+ private bufferUsagePct = 'N/A';
+
+ constructor({attrs}: m.CVnode<SessionStateAttrs>) {
+ this.session = attrs.session;
+ this.trash.use(this.pollBufferState());
+ }
+
+ private pollBufferState(): Disposable {
+ const timerId = window.setInterval(async () => {
+ const bufferUsagePct = await this.session.session?.getBufferUsagePct();
+ if (bufferUsagePct !== undefined) {
+ // Retain the last valid buffer usage in the dialog, so the user can
+ // get a sense of overruns even after the trace ends.
+ this.bufferUsagePct = `${bufferUsagePct} %`;
+ }
+ m.redraw();
+ }, 1000);
+ return {
+ [Symbol.dispose]() {
+ window.clearInterval(timerId);
+ },
+ };
+ }
+
+ view() {
+ const traceData = this.session.isCompleted
+ ? this.session.session?.getTraceData()
+ : undefined;
+ const logs = this.getLogs();
+ const eta = this.session.eta;
+ return m(
+ 'table.session-status',
+ m('tr', m('td', 'State'), m('td', this.session.state)),
+ m('tr', m('td', 'Buffer usage'), m('td', this.bufferUsagePct)),
+ eta && m('tr', m('td', 'ETA'), m('td', eta)),
+ traceData &&
+ m(
+ 'tr',
+ m('td', 'Trace file'),
+ m(
+ 'td',
+ `${Math.round(traceData.length / 1e3).toLocaleString()} KB`,
+ this.session.isCompressed && ' (compressed)',
+ m(Button, {
+ label: 'Open',
+ icon: 'file_open',
+ onclick: () => this.session.openTrace(),
+ }),
+ m(Button, {
+ label: 'Download',
+ icon: 'download',
+ onclick: () => downloadData(this.session.fileName, traceData),
+ }),
+ ),
+ ),
+ logs != '' && m('tr', m('td', 'Logs'), m('td', m('pre.logs', logs))),
+ );
+ }
+
+ onremove() {
+ this.trash.dispose();
+ }
+
+ private getLogs() {
+ let log = '';
+ for (const l of this.session.session?.logs ?? []) {
+ const timestamp = l.timestamp.toTimeString().substring(0, 8);
+ log += `${timestamp}: ${l.message}\n`;
+ }
+ return log;
+ }
+}
+
+/**
+ * A utility class to assign unique string IDs to object instances.
+ * This is used to generate the key: attr for mithril, for components that take
+ * an object instance as attr, to ensure that mithril instantiates a new
+ * component when the input object changes.
+ * Example:
+ * let obj = new MyFoo();
+ * const map = new ObjId();
+ * console.log(map.getKey(obj)); // Prints 'obj_1'.
+ * console.log(map.getKey(obj)); // Prints 'obj_1'.
+ * obj = new MyFoo();
+ * console.log(map.getKey(obj)); // Prints 'obj_2'.
+ */
+export class ObjToId {
+ private map = new WeakMap<object, string>();
+ private lastId = 0;
+
+ getKey(obj: object): string {
+ return getOrCreate(this.map, obj, () => `obj_${++this.lastId}`);
+ }
+
+ clear() {
+ this.map = new WeakMap<object, string>();
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/docs_chip.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/docs_chip.ts
new file mode 100644
index 0000000..cc0c258
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/docs_chip.ts
@@ -0,0 +1,30 @@
+// 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';
+
+interface DocsChipAttrs {
+ href: string;
+}
+
+export class DocsChip implements m.ClassComponent<DocsChipAttrs> {
+ view({attrs}: m.CVnode<DocsChipAttrs>) {
+ return m(
+ 'a.inline-chip',
+ {href: attrs.href, title: 'Open docs in new tab', target: '_blank'},
+ m('i.material-icons', 'info'),
+ ' Docs',
+ );
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/multiselect.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/multiselect.ts
new file mode 100644
index 0000000..c3feefc
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/multiselect.ts
@@ -0,0 +1,84 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {ProbeSetting} from '../../config/config_interfaces';
+import {MultiSelect, MultiSelectDiff} from '../../../../widgets/multiselect';
+
+export interface CheckboxesAttrs<T> {
+ title?: string;
+ options: Map<string, T>;
+ onChange?: (options: string[]) => void;
+}
+
+export class TypedMultiselect<T> implements ProbeSetting {
+ private _selectedKeys = new Set<string>();
+
+ constructor(readonly attrs: CheckboxesAttrs<T>) {}
+
+ setEnabled(key: string, enabled: boolean) {
+ if (enabled) {
+ this._selectedKeys.add(key);
+ } else {
+ this._selectedKeys.delete(key);
+ }
+ }
+
+ selectedKeys(): string[] {
+ return Array.from(this._selectedKeys);
+ }
+
+ selectedValues(): T[] {
+ const values = [];
+ for (const [key, value] of this.attrs.options.entries()) {
+ if (this._selectedKeys.has(key)) {
+ values.push(value);
+ }
+ }
+ return values;
+ }
+
+ serialize() {
+ return Array.from(this._selectedKeys);
+ }
+
+ deserialize(state: unknown): void {
+ if (Array.isArray(state) && state.every((x) => typeof x === 'string')) {
+ this._selectedKeys.clear();
+ for (const key of state) {
+ this.attrs.options.has(key) && this._selectedKeys.add(key);
+ }
+ }
+ }
+
+ render() {
+ return [
+ this.attrs.title && m('header', this.attrs.title),
+ m(MultiSelect, {
+ fixedSize: true,
+ options: Array.from(this.attrs.options.keys()).map((key) => ({
+ id: key,
+ name: key,
+ checked: this._selectedKeys.has(key),
+ })),
+ onChange: (diffs: MultiSelectDiff[]) => {
+ for (const diff of diffs) {
+ this.setEnabled(diff.id, diff.checked);
+ }
+ this.attrs.onChange?.(Array.from(this._selectedKeys.values()));
+ },
+ }),
+ ];
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/slider.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/slider.ts
new file mode 100644
index 0000000..1d063ce
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/slider.ts
@@ -0,0 +1,142 @@
+// 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 {ProbeSetting} from '../../config/config_interfaces';
+import {assertTrue} from '../../../../base/logging';
+import {exists} from '../../../../base/utils';
+
+export interface SliderAttrs {
+ title: string;
+ values: number[];
+ default?: number;
+ icon?: string;
+ cssClass?: string;
+ isTime?: boolean;
+ unit: string;
+ min?: number;
+ description?: string;
+ disabled?: boolean;
+ zeroIsDefault?: boolean;
+ onChange?: (value: number) => void;
+}
+
+export class Slider implements ProbeSetting {
+ private _value: number;
+
+ constructor(readonly attrs: SliderAttrs) {
+ assertTrue(attrs.values.length > 0);
+ this._value = this.setValue(undefined);
+ }
+
+ serialize() {
+ return this._value;
+ }
+
+ deserialize(state: unknown): void {
+ if (typeof state === 'number') {
+ this._value = state;
+ }
+ }
+
+ get value(): number {
+ return this._value;
+ }
+
+ setValue(value: number | null | undefined) {
+ // Logic if value is null/undefined: try first the .default, if provided,
+ // otherwise fall back on the first value of the fixed range... otherwise 0.
+ this._value = exists(value)
+ ? value
+ : this.attrs.default ?? this.attrs.values[0] ?? 0;
+ return this._value;
+ }
+
+ private onValueChange(newVal: number) {
+ this._value = newVal;
+ this.attrs.onChange?.(newVal);
+ }
+
+ onTimeValueChange(hms: string) {
+ try {
+ const date = new Date(`1970-01-01T${hms}.000Z`);
+ if (isNaN(date.getTime())) return;
+ this.onValueChange(date.getTime());
+ } catch {}
+ }
+
+ onSliderChange(newIdx: number) {
+ this.onValueChange(this.attrs.values[newIdx]);
+ }
+
+ render() {
+ const attrs = this.attrs;
+ const id = attrs.title.replace(/[^a-z0-9]/gim, '_').toLowerCase();
+ const maxIdx = attrs.values.length - 1;
+ const val = this._value;
+ let min = attrs.min ?? 1;
+ if (attrs.zeroIsDefault) {
+ min = Math.min(0, min);
+ }
+ const description = attrs.description;
+ const disabled = attrs.disabled;
+
+ // Find the index of the closest value in the slider.
+ let idx = 0;
+ for (; idx < attrs.values.length && attrs.values[idx] < val; idx++) {}
+
+ let spinnerCfg = {};
+ if (attrs.isTime) {
+ spinnerCfg = {
+ type: 'text',
+ pattern: '(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}', // hh:mm:ss
+ value: new Date(val).toISOString().substring(11, 11 + 8),
+ oninput: (e: InputEvent) => {
+ this.onTimeValueChange((e.target as HTMLInputElement).value);
+ },
+ };
+ } else {
+ const isDefault = attrs.zeroIsDefault && val === 0;
+ spinnerCfg = {
+ type: 'number',
+ value: isDefault ? '' : val,
+ placeholder: isDefault ? '(default)' : '',
+ oninput: (e: InputEvent) => {
+ this.onValueChange(+(e.target as HTMLInputElement).value);
+ },
+ };
+ }
+ return m(
+ '.slider' + (attrs.cssClass ?? ''),
+ m('header', attrs.title),
+ description ? m('header.descr', attrs.description) : '',
+ attrs.icon !== undefined ? m('i.material-icons', attrs.icon) : [],
+ m(`input[id="${id}"][type=range][min=0][max=${maxIdx}][value=${idx}]`, {
+ disabled,
+ oninput: (e: InputEvent) => {
+ this.onSliderChange(+(e.target as HTMLInputElement).value);
+ },
+ }),
+ m(`input.spinner[min=${min}][for=${id}]`, spinnerCfg),
+ m('.unit', attrs.unit),
+ );
+ }
+}
+
+export const POLL_INTERVAL_SLIDER: SliderAttrs = {
+ title: 'Poll interval',
+ values: [250, 500, 1000, 2500, 5000, 30000, 60000],
+ cssClass: '.thin',
+ unit: 'ms',
+};
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/textarea.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/textarea.ts
new file mode 100644
index 0000000..18eb496
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/textarea.ts
@@ -0,0 +1,72 @@
+// 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 {ProbeSetting} from '../../config/config_interfaces';
+import {DocsChip} from './docs_chip';
+
+export interface TextareaAttrs {
+ placeholder: string;
+ title?: string;
+ docsLink?: string;
+ cssClass?: string;
+ default?: string;
+ onChange?: (text: string) => void;
+}
+
+export class Textarea implements ProbeSetting {
+ private _text: string;
+
+ constructor(readonly attrs: TextareaAttrs) {
+ this._text = this.setText(attrs.default); // re-assignment to make tsc happy.
+ }
+
+ setText(text: string | undefined) {
+ this._text = text ?? '';
+ return this._text;
+ }
+
+ get text(): string {
+ return this._text;
+ }
+
+ serialize() {
+ return this._text;
+ }
+
+ deserialize(state: unknown): void {
+ if (typeof state === 'string') {
+ this._text = state;
+ }
+ }
+
+ render() {
+ return m(
+ '.textarea-holder',
+ m(
+ 'header',
+ this.attrs.title,
+ this.attrs.docsLink && [' ', m(DocsChip, {href: this.attrs.docsLink})],
+ ),
+ m(`textarea.extra-input${this.attrs.cssClass ?? ''}`, {
+ onchange: (e: Event) => {
+ this.setText((e.target as HTMLTextAreaElement).value);
+ this.attrs.onChange?.(this._text);
+ },
+ placeholder: this.attrs.placeholder,
+ value: this._text,
+ }),
+ );
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/toggle.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/toggle.ts
new file mode 100644
index 0000000..94f2c38
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/toggle.ts
@@ -0,0 +1,69 @@
+// 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 {ProbeSetting} from '../../config/config_interfaces';
+
+export interface ToggleAttrs {
+ title: string;
+ descr?: string;
+ default?: boolean;
+ cssClass?: string;
+ onChange?: (enabled: boolean) => void;
+}
+
+export class Toggle implements ProbeSetting {
+ private _enabled: boolean;
+
+ constructor(readonly attrs: ToggleAttrs) {
+ this._enabled = this.setEnabled(undefined);
+ }
+
+ setEnabled(enabled: boolean | undefined) {
+ this._enabled = enabled ?? this.attrs.default ?? false;
+ return this._enabled;
+ }
+
+ get enabled(): boolean {
+ return this._enabled;
+ }
+
+ serialize() {
+ return this._enabled;
+ }
+
+ deserialize(state: unknown): void {
+ if (state === true || state === false) {
+ this._enabled = state;
+ }
+ }
+
+ render() {
+ return m(
+ `.toggle${this._enabled ? '.enabled' : ''}${this.attrs.cssClass ?? ''}`,
+ m(
+ 'label',
+ m(`input[type=checkbox]`, {
+ checked: this._enabled,
+ oninput: (e: InputEvent) => {
+ this.setEnabled((e.target as HTMLInputElement).checked);
+ this.attrs.onChange?.(this._enabled);
+ },
+ }),
+ m('span', this.attrs.title),
+ ),
+ m('.descr', this.attrs.descr),
+ );
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/recording_manager.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/recording_manager.ts
new file mode 100644
index 0000000..6803066
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/recording_manager.ts
@@ -0,0 +1,323 @@
+// 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 protos from '../../protos';
+import {assertFalse, assertTrue} from '../../base/logging';
+import {errResult, okResult, Result} from '../../base/result';
+import {App} from '../../public/app';
+import {RecordSubpage} from './config/config_interfaces';
+import {ConfigManager} from './config/config_manager';
+import {RecordingTarget} from './interfaces/recording_target';
+import {RecordingTargetProvider} from './interfaces/recording_target_provider';
+import {
+ RECORD_PLUGIN_SCHEMA,
+ RECORD_SESSION_SCHEMA,
+ RecordPluginSchema,
+ RecordSessionSchema,
+} from './serialization_schema';
+import {TargetPlatformId} from './interfaces/target_platform';
+import {TracingSession} from './interfaces/tracing_session';
+import {GcsUploader} from '../../base/gcs_uploader';
+import {uuidv4} from '../../base/uuid';
+import {Time, Timecode} from '../../base/time';
+
+const LOCALSTORAGE_KEY = 'recordPlugin';
+
+export class RecordingManager {
+ readonly pages = new Map<string, RecordSubpage>();
+
+ private providers = new Array<RecordingTargetProvider>();
+ private platform: TargetPlatformId = 'ANDROID';
+ private provider?: RecordingTargetProvider;
+ private target?: RecordingTarget;
+ private _tracingSession?: CurrentTracingSession;
+ readonly recordConfig = new ConfigManager();
+ autoOpenTraceWhenTracingEnds = true;
+
+ constructor(readonly app: App) {}
+
+ registerPage(...pages: RecordSubpage[]) {
+ for (const page of pages) {
+ assertTrue(!this.pages.has(page.id) || this.pages.get(page.id) === page);
+ this.pages.set(page.id, page);
+ if (page.kind === 'PROBES_PAGE') {
+ this.recordConfig.registerProbes(page.probes);
+ }
+ }
+ }
+
+ registerProvider(provider: RecordingTargetProvider) {
+ assertFalse(this.providers.includes(provider));
+ this.providers.push(provider);
+ }
+
+ get currentPlatform(): TargetPlatformId {
+ return this.platform;
+ }
+
+ setPlatform(platform: TargetPlatformId) {
+ this.platform = platform;
+ this.provider = undefined;
+ this.target = undefined;
+ // If there is only one provider for the platform, auto-select that.
+ const filteredProviders = this.listProvidersForCurrentPlatform();
+ if (filteredProviders.length === 1) {
+ this.provider = filteredProviders[0];
+ }
+ }
+
+ listProvidersForCurrentPlatform(): RecordingTargetProvider[] {
+ return this.providers.filter((p) =>
+ p.supportedPlatforms.includes(this.platform),
+ );
+ }
+
+ get currentProvider(): RecordingTargetProvider | undefined {
+ return this.provider;
+ }
+
+ getProvider(id: string): RecordingTargetProvider | undefined {
+ return this.providers.find((p) => p.id === id);
+ }
+
+ async setProvider(provider: RecordingTargetProvider) {
+ if (!provider.supportedPlatforms.includes(this.currentPlatform)) {
+ // This can happen if the promise that calls refreshTargets() completes
+ // after the user has switched to a different platform.
+ return;
+ }
+ this.provider = provider;
+ const targets = await provider.listTargets(this.currentPlatform);
+ if (this.target && targets.includes(this.target)) {
+ return; // The currently selected target is still valid, retain it.
+ }
+ this.target = targets.length > 0 ? targets[0] : undefined;
+ this.app.raf.scheduleFullRedraw();
+ }
+
+ async listTargets(): Promise<RecordingTarget[]> {
+ if (this.provider === undefined) return [];
+ return await this.provider.listTargets(this.currentPlatform);
+ }
+
+ get currentSession() {
+ return this._tracingSession;
+ }
+
+ setTarget(target: RecordingTarget) {
+ this.target = target;
+ }
+
+ get currentTarget(): RecordingTarget | undefined {
+ return this.target;
+ }
+
+ genTraceConfig(): protos.TraceConfig {
+ return this.recordConfig.genTraceConfig(this.currentPlatform);
+ }
+
+ async startTracing(): Promise<CurrentTracingSession> {
+ if (this._tracingSession !== undefined) {
+ this._tracingSession.session?.cancel();
+ this._tracingSession = undefined;
+ }
+ const traceCfg = this.genTraceConfig();
+ const wrappedSession = new CurrentTracingSession(this, traceCfg);
+ this._tracingSession = wrappedSession;
+ return wrappedSession;
+ }
+
+ async share(): Promise<string> {
+ const config = this.serializeSession();
+ const json = JSON.stringify(config);
+ const uploader = new GcsUploader(json, {mimeType: 'application/json'});
+ await uploader.waitForCompletion();
+ return uploader.uploadedUrl;
+ }
+
+ serializeSession(): RecordSessionSchema {
+ // Initialize with default values.
+ const state: RecordSessionSchema = RECORD_SESSION_SCHEMA.parse({});
+ for (const page of this.pages.values()) {
+ if (page.kind === 'SESSION_PAGE') {
+ page.serialize(state);
+ }
+ }
+ // Serialize the state of each probe page and their settings.
+ state.probes = this.recordConfig.serializeProbes();
+ return state;
+ }
+
+ loadSession(state: RecordSessionSchema): void {
+ for (const page of this.pages.values()) {
+ if (page.kind === 'SESSION_PAGE') {
+ page.deserialize(state);
+ }
+ }
+ this.recordConfig.deserializeProbes(state.probes);
+ }
+
+ persistIntoLocalStorage(): void {
+ const state: RecordPluginSchema = RECORD_PLUGIN_SCHEMA.parse({});
+ state.lastSession = this.serializeSession();
+ for (const page of this.pages.values()) {
+ if (page.kind === 'GLOBAL_PAGE') {
+ page.serialize(state);
+ }
+ }
+ const json = JSON.stringify(state);
+ localStorage.setItem(LOCALSTORAGE_KEY, json);
+ }
+
+ restorePluginStateFromLocalstorage(): void {
+ const stateJson = localStorage.getItem(LOCALSTORAGE_KEY) ?? '{}';
+ let parsedJson: unknown;
+ try {
+ parsedJson = JSON.parse(stateJson);
+ } catch (e) {
+ console.error('Record plugin: JSON parse failed', e);
+ parsedJson = {};
+ }
+ const res = RECORD_PLUGIN_SCHEMA.safeParse(parsedJson);
+ if (!res.success) {
+ throw new Error('Record plugin: deserialization failed', res.error);
+ }
+ const state = res.data;
+ for (const page of this.pages.values()) {
+ if (page.kind === 'GLOBAL_PAGE') {
+ page.deserialize(state);
+ }
+ }
+ if (state.lastSession !== undefined) {
+ this.loadSession(state.lastSession);
+ }
+ }
+
+ restoreSessionFromJson(json: string): Result<void> {
+ let parsedJson: unknown;
+ try {
+ parsedJson = JSON.parse(json);
+ } catch (e) {
+ return errResult(`JSON parser error: ${e.message}`);
+ }
+ const res = RECORD_SESSION_SCHEMA.safeParse(parsedJson);
+ if (!res.success) {
+ return errResult(`Deserialization error: ${res.error}`);
+ }
+ this.loadSession(res.data);
+ return okResult(undefined);
+ }
+
+ clearSession() {
+ const emptySession = RECORD_SESSION_SCHEMA.parse({});
+ return this.loadSession(emptySession);
+ }
+}
+
+export class CurrentTracingSession {
+ error?: string;
+ session?: TracingSession;
+ readonly uuid = uuidv4();
+ readonly fileName: string;
+ readonly isCompressed: boolean;
+ private _expectedEndTime: number | undefined;
+ private recMgr: RecordingManager;
+ private autoOpenedTriggered = false;
+
+ constructor(recMgr: RecordingManager, traceCfg: protos.TraceConfig) {
+ this.recMgr = recMgr;
+ const now = new Date();
+ const ymd = `${now.getFullYear()}${now.getMonth()}${now.getDay()}`;
+ const hms = `${now.getHours()}${now.getMinutes()}${now.getSeconds()}`;
+ const platLowerCase = recMgr.currentPlatform.toLowerCase();
+ this.fileName = `${platLowerCase}-${ymd}-${hms}.pftrace`;
+ this.isCompressed = traceCfg.compressionType !== 0;
+ if (recMgr.currentTarget === undefined) {
+ this.error = 'No target selected';
+ return;
+ }
+ if (recMgr.currentTarget.emitsCompressedtrace) {
+ this.fileName += '.gz';
+ this.isCompressed = true;
+ }
+ this.start(traceCfg, recMgr.currentTarget);
+ }
+
+ async start(traceCfg: protos.TraceConfig, target: RecordingTarget) {
+ const res = await target.startTracing(traceCfg);
+ this.recMgr.app.raf.scheduleFullRedraw();
+ if (!res.ok) {
+ this.error = res.error;
+ return;
+ }
+ const session = (this.session = res.value);
+
+ if (traceCfg.durationMs > 0) {
+ this._expectedEndTime = performance.now() + traceCfg.durationMs;
+ }
+
+ session.onSessionUpdate.addListener(() => {
+ this.recMgr.app.raf.scheduleFullRedraw();
+ if (
+ session.state === 'FINISHED' &&
+ this.recMgr.autoOpenTraceWhenTracingEnds &&
+ !this.autoOpenedTriggered
+ ) {
+ this.autoOpenedTriggered = true;
+ this.openTrace();
+ }
+ });
+ }
+
+ get state(): string {
+ if (this.error !== undefined) {
+ return `Error: ${this.error}`;
+ }
+ if (this.session === undefined) {
+ return 'Initializing';
+ }
+ return this.session.state;
+ }
+
+ get eta(): string | undefined {
+ if (this._expectedEndTime === undefined) return undefined;
+ let remainingMs = Math.max(this._expectedEndTime - performance.now(), 0);
+ if (['FINISHED', 'ERRORED'].includes(this.session?.state ?? '')) {
+ remainingMs = 0;
+ }
+ return new Timecode(Time.fromMillis(remainingMs)).dhhmmss;
+ }
+
+ openTrace() {
+ const traceData: Uint8Array | undefined = this.session?.getTraceData();
+ if (traceData === undefined) return;
+ this.recMgr.app.openTraceFromBuffer({
+ buffer: traceData,
+ title: this.fileName,
+ fileName: this.fileName,
+ });
+ }
+
+ get isCompleted(): boolean {
+ return this.session?.state === 'FINISHED';
+ }
+
+ get inProgress(): boolean {
+ return (
+ (this.session === undefined && this.error === undefined) ||
+ this.session?.state === 'RECORDING' ||
+ this.session?.state === 'STOPPING'
+ );
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/serialization_schema.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/serialization_schema.ts
new file mode 100644
index 0000000..82ec10f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/serialization_schema.ts
@@ -0,0 +1,94 @@
+// 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 {z} from 'zod';
+import {TARGET_PLATFORMS, TargetPlatformId} from './interfaces/target_platform';
+
+// Overall view
+// RECORD_PLUGIN_SCHEMA:
+// target: TARGET_SCHEMA
+// lastSession: RECORD_SESSION_SCHEMA
+// probes: PROBES_SCHEMA{}
+// savedSessions: Array<RECORD_SESSION_SCHEMA>
+// probes: PROBES_SCHEMA{}
+
+// Holds the state of the PROBES_PAGE subpages (e.g., Memory).
+// We don't define a strongly-typed schema for each probes as they are
+// changed frequently. Each probe is modelled as:
+// - An enable/disable boolean (the presence of the key)
+// - A map of "settings". Each setting widget (Slider, Textarea, Toggle)
+// takes care of its own de/serialization.
+export const PROBES_SCHEMA = z
+ .record(
+ z.string(), // key: the RecordProbe.id (it's globally unique).
+ z.object({
+ settings: z
+ .record(
+ z.string(), // key: the key in the RecordProbe.settings map.
+ z.unknown(), // value: The result of ProbeSetting.serialize().
+ )
+ .default({}),
+ }),
+ )
+ .default({});
+export type ProbesSchema = z.infer<typeof PROBES_SCHEMA>;
+
+// The schema that holds the settings for a recording session, that is, the
+// state of the probes and the buffer size & type.
+// This does NOT include the state of the other recording pages (e.g. the
+// Target device selector, the "saved sessions", etc)
+export const RECORD_SESSION_SCHEMA = z
+ .object({
+ mode: z
+ .enum(['STOP_WHEN_FULL', 'RING_BUFFER', 'LONG_TRACE'])
+ .default('STOP_WHEN_FULL'),
+ bufSizeKb: z.number().default(64 * 1024),
+ durationMs: z.number().default(10_000),
+ maxFileSizeMb: z.number().default(500),
+ fileWritePeriodMs: z.number().default(2500),
+ compression: z.boolean().default(false),
+ probes: PROBES_SCHEMA,
+ })
+ .default({});
+export type RecordSessionSchema = z.infer<typeof RECORD_SESSION_SCHEMA>;
+
+// The schema for the target selection page.
+export const TARGET_SCHEMA = z
+ .object({
+ platformId: z
+ .enum(TARGET_PLATFORMS.map((p) => p.id) as [TargetPlatformId])
+ .optional(),
+ transportId: z.string().optional(),
+ targetId: z.string().optional(),
+ })
+ .default({});
+export type TargetSchema = z.infer<typeof TARGET_SCHEMA>;
+
+export const SAVED_SESSION_SCHEMA = z.object({
+ name: z.string(),
+ config: RECORD_SESSION_SCHEMA,
+});
+export type SavedSessionSchema = z.infer<typeof SAVED_SESSION_SCHEMA>;
+
+// The schema for the root object that holds the whole state of the record
+// plugin.
+export const RECORD_PLUGIN_SCHEMA = z
+ .object({
+ target: TARGET_SCHEMA,
+ autoOpenTrace: z.boolean().default(true),
+ lastSession: RECORD_SESSION_SCHEMA.default({}),
+ savedSessions: z.array(SAVED_SESSION_SCHEMA).default([]),
+ })
+ .default({});
+export type RecordPluginSchema = z.infer<typeof RECORD_PLUGIN_SCHEMA>;
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/target_connection_management_dialog.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/target_connection_management_dialog.ts
new file mode 100644
index 0000000..fdcbffa
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/target_connection_management_dialog.ts
@@ -0,0 +1,126 @@
+// 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 {TracedWebsocketTarget} from './traced_websocket_target';
+import {PreflightCheckRenderer} from '../pages/preflight_check_renderer';
+import {closeModal, showModal} from '../../../widgets/modal';
+import {Button} from '../../../widgets/button';
+import {TracedWebsocketTargetProvider} from './traced_websocket_provider';
+import {defer, Deferred} from '../../../base/deferred';
+
+/**
+ * Shows a dialog that allows to add a connection to another websocket endpoint
+ * other than the default 127.0.0.1:8037. This dialog is displayed when the user
+ * clicks on "connect new device" in the "Target Device" page.
+ */
+export async function showTracedConnectionManagementDialog(
+ provider: TracedWebsocketTargetProvider,
+): Promise<TracedWebsocketTarget | undefined> {
+ const resultPromise = defer<TracedWebsocketTarget | undefined>();
+ const key = 'TracedConnectioManagementDialog';
+ showModal({
+ key,
+ title: 'Connect to remote tracing service',
+ content: () =>
+ m(TracedConnectioManagementDialog, {provider, resultPromise}),
+ }).then(() => resultPromise.resolve(undefined));
+ const targetOrUndefined = await resultPromise;
+ closeModal(key);
+ return targetOrUndefined;
+}
+
+interface DialogAttrs {
+ provider: TracedWebsocketTargetProvider;
+ resultPromise: Deferred<TracedWebsocketTarget | undefined>;
+}
+class TracedConnectioManagementDialog implements m.ClassComponent<DialogAttrs> {
+ private target?: TracedWebsocketTarget;
+ private checks?: PreflightCheckRenderer;
+
+ view({attrs}: m.CVnode<DialogAttrs>) {
+ const provider = attrs.provider;
+ return m(
+ '.record-page',
+ m(
+ 'div',
+ 'Forward port 8037 with ssh from the local host to the ' +
+ 'remote host where traced is running and invoke websocket_bridge.',
+ ),
+ m('br'),
+ m('code', 'ssh -L8037:remote_machine:8037 websocket_bridge'),
+ m('header', 'Connect a new target'),
+ m(
+ 'div',
+ m('input', {
+ placeholder: 'remote_machine:8037',
+ onchange: (e: Event) =>
+ this.testConnection((e.target as HTMLInputElement).value ?? ''),
+ }),
+ m(Button, {
+ icon: 'add',
+ onclick: () => {
+ if (this.target !== undefined) {
+ provider.targets.set(this.target.wsUrl, this.target);
+ }
+ attrs.resultPromise.resolve(this.target);
+ },
+ }),
+ ),
+ this.checks && this.checks.renderTable(),
+ m('header', 'Manage targets'),
+ m(
+ 'table',
+ ...Array.from(provider.targets.entries()).map(([wsUrl, target]) =>
+ m(
+ 'tr',
+ m(
+ 'td',
+ m(Button, {
+ icon: 'delete',
+ onclick: () => {
+ target.disconnect();
+ provider.targets.delete(wsUrl);
+ provider.onTargetsChanged.notify();
+ },
+ }),
+ ),
+ m('td', m('code', wsUrl)),
+ ),
+ ),
+ ),
+ );
+ }
+
+ private testConnection(userInput: string) {
+ this.target && this.target.disconnect();
+ this.target = undefined;
+ this.checks = undefined;
+
+ let wsUrl: string;
+ if (userInput.match(/^ws(s?):\/\//)) {
+ wsUrl = userInput;
+ } else if (userInput.match(/^[^:/]+:\d+$/)) {
+ wsUrl = `ws://${userInput}/traced`;
+ } else if (userInput.match(/^[^:/]+$/)) {
+ wsUrl = `ws://${userInput}:8037/traced`;
+ } else {
+ return;
+ }
+
+ this.target = new TracedWebsocketTarget(wsUrl);
+ this.checks = new PreflightCheckRenderer(this.target);
+ this.checks.runPreflightChecks();
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_provider.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_provider.ts
new file mode 100644
index 0000000..9c357e9
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_provider.ts
@@ -0,0 +1,49 @@
+// 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 {EvtSource} from '../../../base/events';
+import {PreflightCheck} from '../interfaces/connection_check';
+import {RecordingTarget} from '../interfaces/recording_target';
+import {RecordingTargetProvider} from '../interfaces/recording_target_provider';
+import {showTracedConnectionManagementDialog} from './target_connection_management_dialog';
+import {TracedWebsocketTarget} from './traced_websocket_target';
+
+export class TracedWebsocketTargetProvider implements RecordingTargetProvider {
+ readonly id = 'traced_websocket';
+ readonly name = 'WebSocket';
+ readonly description =
+ 'Allows to talk to the traced service UNIX socket via a WebSocket. ' +
+ 'Requires launching the websocket_bridge on the host';
+ readonly icon = 'lan';
+ readonly supportedPlatforms = ['LINUX'] as const;
+ readonly onTargetsChanged = new EvtSource<void>();
+
+ readonly targets = new Map<string, TracedWebsocketTarget>();
+
+ constructor() {
+ // Add the default target.
+ const defaultWsUrl = 'ws://127.0.0.1:8037/traced';
+ this.targets.set(defaultWsUrl, new TracedWebsocketTarget(defaultWsUrl));
+ }
+
+ async listTargets(): Promise<TracedWebsocketTarget[]> {
+ return Array.from(this.targets.values());
+ }
+
+ pairNewTarget(): Promise<RecordingTarget | undefined> {
+ return showTracedConnectionManagementDialog(this);
+ }
+
+ async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {}
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_target.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_target.ts
new file mode 100644
index 0000000..5fb9b5c
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_target.ts
@@ -0,0 +1,135 @@
+// 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 protos from '../../../protos';
+import {errResult, okResult, Result} from '../../../base/result';
+import {RecordingTarget} from '../interfaces/recording_target';
+import {PreflightCheck} from '../interfaces/connection_check';
+import {AsyncWebsocket} from '../websocket/async_websocket';
+import {websocketInstructions} from '../websocket/websocket_utils';
+import {ConsumerIpcTracingSession} from '../tracing_protocol/consumer_ipc_tracing_session';
+import {WebSocketStream} from '../websocket/websocket_stream';
+import {TracingProtocol} from '../tracing_protocol/tracing_protocol';
+import {exists} from '../../../base/utils';
+import {AsyncLazy} from '../../../base/async_lazy';
+
+export class TracedWebsocketTarget implements RecordingTarget {
+ readonly kind = 'LIVE_RECORDING';
+ readonly platform = 'LINUX';
+ readonly transportType = 'WebSocket';
+
+ // This Consumer connection is only used to detect the connection state and
+ // to query servce state. each new tracing session creates a new instance,
+ // because consumer connections in traced are single-use.
+ private mgmtConsumer = new AsyncLazy<TracingProtocol>();
+
+ /**
+ * @param wsUrl 'ws://127.0.0.1:8037/traced'
+ */
+ constructor(readonly wsUrl: string) {}
+
+ get id(): string {
+ return this.wsUrl;
+ }
+
+ get name(): string {
+ return this.wsUrl;
+ }
+
+ get connected(): boolean {
+ return this.mgmtConsumer.value?.connected ?? false;
+ }
+
+ async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {
+ const status = await this.connectIfNeeded();
+
+ yield {
+ name: 'WebSocket connection',
+ status: ((): Result<string> => {
+ if (!status.ok) return status;
+ return okResult('Connected');
+ })(),
+ };
+
+ if (!this.connected) return;
+ const svcStatus = await this.getServiceState();
+
+ yield {
+ name: 'Traced version',
+ status: ((): Result<string> => {
+ if (!svcStatus.ok) return svcStatus;
+ return okResult(svcStatus.value.tracingServiceVersion ?? 'N/A');
+ })(),
+ };
+
+ if (svcStatus === undefined) return;
+
+ yield {
+ name: 'Traced state',
+ status: ((): Result<string> => {
+ if (!svcStatus.ok) return svcStatus;
+ const tss = svcStatus.value;
+ return okResult(
+ `#producers: ${tss.producers?.length ?? 'N/A'}, ` +
+ `#datasources: ${tss.dataSources?.length ?? 'N/A'}, ` +
+ `#sessions: ${tss.numSessionsStarted ?? 'N/A'}`,
+ );
+ })(),
+ };
+ }
+
+ private async connectIfNeeded(): Promise<Result<TracingProtocol>> {
+ return this.mgmtConsumer.getOrCreate(() => this.createConsumerIpcChannel());
+ }
+
+ disconnect(): void {
+ this.mgmtConsumer.value?.close();
+ this.mgmtConsumer.reset();
+ }
+
+ async getServiceState(): Promise<Result<protos.ITracingServiceState>> {
+ const ipcStatus = await this.connectIfNeeded();
+ if (!ipcStatus.ok) return ipcStatus;
+ const consumerIpc = ipcStatus.value;
+ const req = new protos.QueryServiceStateRequest({});
+ const rpcCall = consumerIpc.invokeStreaming('QueryServiceState', req);
+ const resp = await rpcCall.promise;
+ if (!exists(resp.serviceState)) {
+ return errResult('Failed to decode QueryServiceStateResponse');
+ }
+ return okResult(resp.serviceState);
+ }
+
+ async startTracing(
+ traceConfig: protos.ITraceConfig,
+ ): Promise<Result<ConsumerIpcTracingSession>> {
+ const ipcStatus = await this.createConsumerIpcChannel();
+ if (!ipcStatus.ok) return ipcStatus;
+ const consumerIpc = ipcStatus.value;
+ const session = new ConsumerIpcTracingSession(consumerIpc, traceConfig);
+ return okResult(session);
+ }
+
+ private async createConsumerIpcChannel(): Promise<Result<TracingProtocol>> {
+ const maybeSock = await AsyncWebsocket.connect(this.wsUrl);
+ if (maybeSock == undefined) {
+ return errResult(
+ `Failed to connect ${this.wsUrl}. ${websocketInstructions()}`,
+ );
+ }
+ const stream = new WebSocketStream(maybeSock.release());
+ const consumerIpc = await TracingProtocol.create(stream);
+ return okResult(consumerIpc);
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/consumer_ipc_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/consumer_ipc_tracing_session.ts
new file mode 100644
index 0000000..4f2b246
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/consumer_ipc_tracing_session.ts
@@ -0,0 +1,150 @@
+// 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 protos from '../../../protos';
+import {EvtSource} from '../../../base/events';
+import {ResizableArrayBuffer} from '../../../base/resizable_array_buffer';
+import {
+ TracingSession,
+ TracingSessionLogEntry,
+ TracingSessionState,
+} from '../interfaces/tracing_session';
+import {TracingProtocol} from './tracing_protocol';
+
+/**
+ * A concrete implementation of {@link TracingSession} over a
+ * Perfetto IPC Tracing Procol. This class is suitable for all cases where we
+ * are able to obtain, in a way or another, a byte stream to talk to the traced
+ * consumer socket.
+ */
+export class ConsumerIpcTracingSession implements TracingSession {
+ private consumerIpc: TracingProtocol;
+ private _state: TracingSessionState = 'RECORDING';
+ readonly logs = new Array<TracingSessionLogEntry>();
+ private traceBuf = new ResizableArrayBuffer(64 * 1024);
+ readonly onSessionUpdate = new EvtSource<void>();
+
+ constructor(consumerIpc: TracingProtocol, traceConfig: protos.ITraceConfig) {
+ this.consumerIpc = consumerIpc;
+ this.consumerIpc.onClose = this.onProtocolClose.bind(this);
+ this.start(traceConfig);
+ }
+
+ get state(): TracingSessionState {
+ return this._state;
+ }
+
+ private async start(traceConfig: protos.ITraceConfig): Promise<void> {
+ const req = new protos.EnableTracingRequest({traceConfig});
+ this.log(`Starting trace, durationMs: ${traceConfig.durationMs}`);
+ const resp = await this.consumerIpc.invoke('EnableTracing', req);
+ this.onTraceStopped(resp.error);
+ }
+
+ async stop(): Promise<void> {
+ if (this._state !== 'RECORDING') return;
+ this.setState('STOPPING');
+ // Initiator=kPerfettoCmd, Reason=kTraceStop. See flush_flags.h.
+ const flags = (2 << 4) | 2;
+ this.log('Flushing data sources');
+ await this.consumerIpc.invoke('Flush', new protos.FlushRequest({flags}));
+ this.log('Flush complete, stopping trace');
+ const disReq = new protos.DisableTracingRequest({});
+ await this.consumerIpc.invoke('DisableTracing', disReq);
+ }
+
+ async cancel(): Promise<void> {
+ if (!['RECORDING', 'STOPPING'].includes(this._state)) return;
+ const req = new protos.FreeBuffersRequest({});
+ await this.consumerIpc.invoke('FreeBuffers', req);
+ this.fail('Trace cancelled');
+ }
+
+ async getBufferUsagePct(): Promise<number | undefined> {
+ if (this._state !== 'RECORDING') return undefined;
+ const req = new protos.GetTraceStatsRequest({});
+ const resp = await this.consumerIpc.invoke('GetTraceStats', req);
+ let totSize = 0;
+ let usedSize = 0;
+ for (const buf of resp.traceStats?.bufferStats ?? []) {
+ totSize += buf.bufferSize ?? 0;
+ // bytesWritten can be >> bufferSize for ring buffer traces.
+ usedSize += Math.min(buf.bytesWritten ?? 0, buf.bufferSize ?? 0);
+ }
+ return Math.min(Math.round((100 * usedSize) / totSize), 100);
+ }
+
+ private onTraceStopped(error: string) {
+ if (error !== '') {
+ this.fail(error);
+ return;
+ }
+ if (this.consumerIpc === undefined) {
+ return; // Spurious event after we failed.
+ }
+ // There is nothing more to do if we arrive here via cancel() or an error.
+ if (!['STOPPING', 'RECORDING'].includes(this._state)) return;
+
+ // We reach this point either:
+ // 1. In state == 'RECORDING', if the durationMs expired and the
+ // EnableTracing request is resolved.
+ // 2. In state == 'STOPPING', if the user has pressed stop().
+ this.setState('STOPPING');
+ this.log('Tracing stopped. Reading back data');
+ const rbreq = new protos.ReadBuffersRequest({});
+ const stream = this.consumerIpc.invokeStreaming('ReadBuffers', rbreq);
+ stream.onTraceData = this.onTraceData.bind(this);
+ }
+
+ getTraceData(): Uint8Array | undefined {
+ if (this._state !== 'FINISHED') return undefined;
+ const buf = this.traceBuf.get();
+ return buf;
+ }
+
+ private onTraceData(packets: Uint8Array, hasMore: boolean) {
+ this.traceBuf.append(packets);
+ if (hasMore) return;
+
+ this.setState('FINISHED');
+ this.consumerIpc?.close();
+ }
+
+ private onProtocolClose() {
+ if (this._state === 'RECORDING') {
+ this.setState('ERRORED');
+ this.fail('Protocol disconnected');
+ }
+ }
+
+ private setState(newState: TracingSessionState) {
+ this._state = newState;
+ this.onSessionUpdate.notify();
+ }
+
+ private log(message: string, isError = false) {
+ this.logs.push({
+ message,
+ timestamp: new Date(),
+ isError,
+ });
+ this.onSessionUpdate.notify();
+ }
+
+ fail(error: string) {
+ this.log(`Tracing failed: ${error}`, /* isError */ true);
+ this.setState('ERRORED');
+ this.consumerIpc.close();
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/packet_assembler.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/packet_assembler.ts
new file mode 100644
index 0000000..7b9d860
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/packet_assembler.ts
@@ -0,0 +1,75 @@
+// 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 protos from '../../../protos';
+import {ResizableArrayBuffer} from '../../../base/resizable_array_buffer';
+import {exists} from '../../../base/utils';
+
+/**
+ * Utility class to re-assemble trace packets from slice fragments.
+ * This is needed to deal with ReadBuffersResponse. Each ReadBuffersResponse
+ * provies an array of slices. A slice can be == a packet, or a fragment of it.
+ * Furthermore each ReadBufferResponse can provide slices for >1 packet (or for
+ * a packet and a bit). This class deals with the reassembly.
+ */
+export class PacketAssembler {
+ // Buffers the incoming slices until we see a full packet.
+ private curPacketSlices = new Array<Uint8Array>();
+
+ /**
+ * @param rdResp a ReadBufferResponse containing an array of slices.
+ * @returns A protos.perfetto.Trace protobuf-encoded buffer containing a
+ * sequence of whole packets. This buffer is suitable to be pushed into
+ * TraceProcessor, traceconv or other perfetto tools.
+ */
+ pushSlices(rdResp: protos.IReadBuffersResponse): Uint8Array {
+ const traceBuf = new ResizableArrayBuffer(4096);
+ for (const slice of rdResp.slices ?? []) {
+ if (!exists(slice.data)) continue;
+ this.curPacketSlices.push(slice.data);
+ if (!Boolean(slice.lastSliceForPacket)) {
+ continue;
+ }
+
+ // We received all the slices for the current packet.
+ // Below we assemble all the slices for each packet together and
+ // prepend them with the proto preamble.
+ const slices = this.curPacketSlices.splice(0); // ps = std::move(this.ps).
+
+ // We receive 1+ slices per packet. The slices contain only the payload
+ // of the packet, but not the packet preamble itself. We have to write
+ // the packet proto preamble ourselves. In order to do so we need to first
+ // compute the total packet size.
+ const totLen = slices.reduce((a, buf) => a + buf.length, 0);
+
+ // Becuase the packet size is varint-encoded, we don't know how many bytes
+ // the premable is going to take. Allow for 10 bytes of preamble. We will
+ // subarray() to the actual length at the end of this function.
+ const preamble: number[] = [TRACE_PACKET_PROTO_TAG];
+ let lenVarint = totLen;
+ do {
+ preamble.push((lenVarint & 0x7f) | (lenVarint > 0x7f ? 0x80 : 0));
+ lenVarint >>>= 7;
+ } while (lenVarint > 0);
+ traceBuf.append(preamble);
+ slices.forEach((slice) => traceBuf.append(slice));
+ } // for(slices)
+ return traceBuf.get();
+ }
+}
+
+const PROTO_LEN_DELIMITED_WIRE_TYPE = 2;
+const TRACE_PACKET_PROTO_ID = 1;
+const TRACE_PACKET_PROTO_TAG =
+ (TRACE_PACKET_PROTO_ID << 3) | PROTO_LEN_DELIMITED_WIRE_TYPE;
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/tracing_protocol.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/tracing_protocol.ts
new file mode 100644
index 0000000..33c9380
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/tracing_protocol.ts
@@ -0,0 +1,326 @@
+// 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 protobuf from 'protobufjs/minimal';
+import protos from '../../../protos';
+
+import {ByteStream} from '../interfaces/byte_stream';
+import {ProtoRingBuffer} from '../../../trace_processor/proto_ring_buffer';
+import {defer} from '../../../base/deferred';
+import {exists} from '../../../base/utils';
+import {assertExists, assertFalse, assertTrue} from '../../../base/logging';
+import {PacketAssembler} from './packet_assembler';
+import {ResizableArrayBuffer} from '../../../base/resizable_array_buffer';
+
+/**
+ * Implements the Consumer side of the Perfetto Tracing Protocol.
+ * https://perfetto.dev/docs/design-docs/api-and-abi#socket-protocol
+ *
+ * The passed stream must be a byte stream to the traced consumer port,
+ * e.g. obatained by connecting adb to the /dev/socket/traced_consumer.
+ */
+export class TracingProtocol {
+ private rxBuf = new ProtoRingBuffer('FIXED_SIZE');
+
+ private pendingInvokes = new Map<number, PendingInvoke>();
+
+ // Wire protocol request ID. After each request it is increased. It is needed
+ // to keep track of the type of request, and parse the response correctly.
+ // We start from 2 because the static create() method takes the first one
+ // for binding the service.
+ private requestId = 2;
+
+ onClose = () => {};
+
+ // We have a separate factory method to await the initial service binding, so
+ // we can return an object that is functional (methods can be invoked) and
+ // avoid buffering.
+ static async create(stream: ByteStream): Promise<TracingProtocol> {
+ // Send the bindService request. This is a one-off request to connect to the
+ // consumer port and list the RPC methods available.
+ const requestId = 1;
+ const txFrame = new protos.IPCFrame({
+ requestId,
+ msgBindService: new protos.IPCFrame.BindService({
+ serviceName: 'ConsumerPort',
+ }),
+ });
+ const repsponsePromise = defer<Uint8Array>();
+ const rxFrameBuf = new ProtoRingBuffer('FIXED_SIZE');
+ stream.onData = (data) => {
+ rxFrameBuf.append(data);
+ const rxFrame = rxFrameBuf.readMessage();
+ rxFrame && repsponsePromise.resolve(rxFrame);
+ };
+ TracingProtocol.sendFrame(stream, txFrame);
+
+ // Wait for the IPC reply. There is no state machine or queueing needed at
+ // this point (not just yet) because this is 1 req -> 1 reply.
+ const frameData = await repsponsePromise;
+ const rxFrame = protos.IPCFrame.decode(frameData);
+ assertTrue(rxFrame.msg === 'msgBindServiceReply');
+ const replyMsg = assertExists(rxFrame.msgBindServiceReply);
+ const boundMethods = new Map<string, number>();
+ assertTrue(replyMsg.success === true);
+ const serviceId = assertExists(replyMsg.serviceId);
+ for (const m of assertExists(replyMsg.methods)) {
+ boundMethods.set(assertExists(m.name), assertExists(m.id));
+ }
+ // Now that the details of the RPC methods are known, build and return the
+ // TracingProtocol object, so the caller can finally make calls.
+ return new TracingProtocol(stream, serviceId, boundMethods);
+ }
+
+ private constructor(
+ private stream: ByteStream,
+ private serviceId: number,
+ private boundMethods: Map<string, number>,
+ ) {
+ stream.onData = this.onStreamData.bind(this);
+ stream.onClose = () => this.close();
+ }
+
+ async invoke<T extends RpcMethodName>(
+ methodName: T,
+ req: RequestType<T>,
+ ): Promise<ResponseType<T>> {
+ const method = RPC_METHODS[methodName];
+ const resultPromise = defer<ResponseType<T>>();
+ const pendingInvoke: PendingInvoke = {
+ onResponse: (data: Uint8Array | undefined, hasMore: boolean) => {
+ assertFalse(hasMore); // Should have used invokeStreaming instead.
+ const response = exists(data)
+ ? method.respType.decode(data)
+ : method.respType.create();
+ resultPromise.resolve(response as ResponseType<T>);
+ },
+ };
+ this.beginInvoke(methodName, req, pendingInvoke);
+ return resultPromise;
+ }
+
+ invokeStreaming<T extends RpcStreamingMethodName>(
+ methodName: T,
+ req: RequestType<T>,
+ ): StreamingResponseType<T> {
+ const method = RPC_STREAMING_METHODS[methodName];
+ const streamDecoder = method.respType.createStreamingDecoder();
+
+ const pendingInvoke: PendingInvoke = {
+ onResponse: (data: Uint8Array | undefined, hasMore: boolean) => {
+ streamDecoder.decode(data, hasMore);
+ },
+ };
+ this.beginInvoke(methodName, req, pendingInvoke);
+ return streamDecoder as StreamingResponseType<T>;
+ }
+
+ // This call can arrive from two plaes:
+ // 1. The user clicking on Stop/Cancel. In this case ConsumerIpcTracingSession
+ // calls this.consumerIpc.close().
+ // 2. Stream disconnected is detected (e.g. the user pulls the cable). In this
+ // case we get here via stream.onClose = () => this.close().
+ close() {
+ if (this.stream.connected) {
+ this.stream.close();
+ }
+ this.pendingInvokes.clear();
+ this.onClose();
+ }
+
+ get connected() {
+ return this.stream.connected;
+ }
+
+ [Symbol.dispose]() {
+ this.close();
+ }
+
+ private beginInvoke<T extends RpcMethodName | RpcStreamingMethodName>(
+ methodName: T,
+ req: RequestType<T>,
+ pendingInvoke: PendingInvoke,
+ ): void {
+ const methodId = this.boundMethods.get(methodName);
+ if (methodId === undefined) {
+ throw new Error(`RPC Error: method ${methodName} not supported`);
+ }
+ const requestId = this.requestId++;
+ const argType =
+ methodName in RPC_METHODS
+ ? RPC_METHODS[methodName as RpcMethodName].argType
+ : RPC_STREAMING_METHODS[methodName as RpcStreamingMethodName].argType;
+ const argsProto: Uint8Array = argType.encode(req).finish();
+ const frame = new protos.IPCFrame({
+ requestId,
+ msgInvokeMethod: new protos.IPCFrame.InvokeMethod({
+ serviceId: this.serviceId,
+ methodId: methodId,
+ argsProto,
+ }),
+ });
+ TracingProtocol.sendFrame(this.stream, frame);
+ this.pendingInvokes.set(requestId, pendingInvoke);
+ }
+
+ private onStreamData(data: Uint8Array): void {
+ this.rxBuf.append(data);
+ for (;;) {
+ const frameData = this.rxBuf.readMessage();
+ if (frameData === undefined) break;
+ this.parseFrame(frameData);
+ }
+ }
+
+ private parseFrame(frameData: Uint8Array): void {
+ // Get a copy of the ArrayBuffer to avoid the original being overriden.
+ // See 170256902#comment21
+ const frame = protos.IPCFrame.decode(frameData.slice());
+ if (frame.msg === 'msgInvokeMethodReply') {
+ const reply = assertExists(frame.msgInvokeMethodReply);
+ // We process messages without a `replyProto` field (for instance
+ // `FreeBuffers` does not have `replyProto`). However, we ignore messages
+ // without a valid 'success' field.
+ assertTrue(Boolean(reply.success));
+
+ const pendInvoke = assertExists(this.pendingInvokes.get(frame.requestId));
+ pendInvoke.onResponse(
+ reply.replyProto ?? undefined,
+ Boolean(reply.hasMore),
+ );
+ if (!reply.hasMore) {
+ this.pendingInvokes.delete(frame.requestId);
+ }
+ } else {
+ throw new Error(`Tracing protocol: unrecognized frame ${frame.msg}`);
+ }
+ }
+
+ private static sendFrame(
+ stream: ByteStream,
+ frame: protos.IPCFrame,
+ ): Promise<void> {
+ const writer = protobuf.Writer.create();
+ writer.fixed32(0); // Reserve space for the 4 bytes header (frame len).
+ const frameData = protos.IPCFrame.encode(frame, writer).finish().slice();
+ const frameLen = frameData.length - 4;
+ const dv = new DataView(frameData.buffer);
+ dv.setUint32(0, frameLen, /* littleEndian */ true); // Write the header.
+ return stream.write(frameData);
+ }
+}
+
+export class PacketStream {
+ static createStreamingDecoder(): PacketStream {
+ return new PacketStream();
+ }
+
+ private traceBuf = new PacketAssembler();
+
+ onTraceData: (packets: Uint8Array, hasMore: boolean) => void = () => {};
+
+ decode(data: Uint8Array | undefined, hasMore: boolean) {
+ if (data === undefined) {
+ this.onTraceData(new Uint8Array(), hasMore);
+ return;
+ }
+
+ // ReadBuffers returns 1+ slices. They can form 1 packet (usually),
+ // >1 packet, or a fraction of a packet.
+ const rdresp = protos.ReadBuffersResponse.decode(data);
+ const packets: Uint8Array = this.traceBuf.pushSlices(rdresp);
+ this.onTraceData(packets, hasMore);
+ }
+}
+
+// QueryServiceStateResponse can be split in several chunks if the service state
+// exceeds the 128KB ipc limit. This class simply merges them and exposes the
+// merged result once hasMore = false.
+class ServiceStateMerger {
+ static createStreamingDecoder(): ServiceStateMerger {
+ return new ServiceStateMerger();
+ }
+
+ private rxBuf = new ResizableArrayBuffer();
+ readonly promise = defer<protos.QueryServiceStateResponse>();
+
+ decode(data: Uint8Array | undefined, hasMore: boolean) {
+ if (data !== undefined) {
+ this.rxBuf.append(data);
+ }
+
+ if (!hasMore) {
+ const msg = protos.QueryServiceStateResponse.decode(this.rxBuf.get());
+ this.rxBuf.clear();
+ this.promise.resolve(msg);
+ }
+ }
+}
+
+const RPC_METHODS = {
+ EnableTracing: {
+ argType: protos.EnableTracingRequest,
+ respType: protos.EnableTracingResponse,
+ },
+ DisableTracing: {
+ argType: protos.DisableTracingRequest,
+ respType: protos.DisableTracingResponse,
+ },
+ Flush: {
+ argType: protos.FlushRequest,
+ respType: protos.FlushResponse,
+ },
+ FreeBuffers: {
+ argType: protos.FreeBuffersRequest,
+ respType: protos.FreeBuffersResponse,
+ },
+ GetTraceStats: {
+ argType: protos.GetTraceStatsRequest,
+ respType: protos.GetTraceStatsResponse,
+ },
+};
+
+const RPC_STREAMING_METHODS = {
+ ReadBuffers: {
+ argType: protos.ReadBuffersRequest,
+ respType: PacketStream,
+ },
+ QueryServiceState: {
+ argType: protos.QueryServiceStateRequest,
+ respType: ServiceStateMerger,
+ },
+};
+
+type RpcMethods = typeof RPC_METHODS;
+type RpcStreamingMethods = typeof RPC_STREAMING_METHODS;
+
+export type RpcMethodName = keyof RpcMethods & string;
+export type RpcStreamingMethodName = keyof RpcStreamingMethods & string;
+export type RpcAllMethodName = RpcMethodName | RpcStreamingMethodName;
+
+type RequestType<T extends RpcAllMethodName> = InstanceType<
+ (RpcMethods & RpcStreamingMethods)[T]['argType']
+>;
+
+type ResponseType<T extends RpcMethodName> = InstanceType<
+ RpcMethods[T]['respType']
+>;
+
+type StreamingResponseType<T extends RpcStreamingMethodName> = InstanceType<
+ RpcStreamingMethods[T]['respType']
+>;
+
+interface PendingInvoke {
+ onResponse: (data: Uint8Array | undefined, hasMore: boolean) => void;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/async_websocket.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/async_websocket.ts
new file mode 100644
index 0000000..c871191
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/async_websocket.ts
@@ -0,0 +1,143 @@
+// 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 {defer, Deferred} from '../../../base/deferred';
+import {assertExists, assertTrue} from '../../../base/logging';
+import {ResizableArrayBuffer} from '../../../base/resizable_array_buffer';
+import {utf8Decode} from '../../../base/string_utils';
+
+/**
+ * A wrapper around WebSocket with async methods.
+ * It allows nicer usage with await, allowing to write async sequential code.
+ * E.g.:
+ * const sock = await AsyncWebsocket.connect('ws://...');
+ * sock.send('command');
+ * const response = await sock.waitForData(42); // Wait to receive 42 bytes.
+ * sock.send('command2');
+ * const response2 = await sock.waitForData(10); // Wait to receive 10 bytes.
+ */
+export class AsyncWebsocket {
+ private sock?: WebSocket;
+ private rxBuf = new ResizableArrayBuffer(128);
+ private rxBufRead = 0;
+ private rxPromise?: Deferred<Uint8Array>;
+ private rxPromiseBytes = 0;
+
+ static async connect(url: string): Promise<AsyncWebsocket | undefined> {
+ const sock = new WebSocket(url);
+ sock.binaryType = 'arraybuffer';
+
+ // In case of a connection failure, there are two possible scenarios:
+ // 1. The failure is immediate (e.g. due to CSP blocking access). In this
+ // case the onclose() is NOT triggered because happens before we get a
+ // chance to register the handler.
+ // 2. The connection failure happens in the near future. In this case we
+ // infer a connection failure by observing on onclose().
+ const readyState = sock.readyState;
+ if (readyState === WebSocket.CLOSED || readyState === WebSocket.CLOSED) {
+ return undefined; // Case 1.
+ }
+ const connectPromise = defer<AsyncWebsocket | undefined>();
+ const resolveConnectPromise = (success: boolean) => {
+ sock.onclose = null;
+ sock.onopen = null;
+ if (success) {
+ connectPromise.resolve(new AsyncWebsocket(sock));
+ } else {
+ connectPromise.resolve(undefined);
+ }
+ };
+ sock.onopen = () => resolveConnectPromise(true);
+ sock.onclose = () => resolveConnectPromise(false);
+ return connectPromise;
+ }
+
+ private constructor(sock: WebSocket) {
+ this.sock = sock;
+ sock.onmessage = this.onSocketMessage.bind(this);
+ }
+
+ /** Turns this back into a standard WebSocket. */
+ release(): WebSocket {
+ const sock = assertExists(this.sock);
+ this.sock = undefined;
+ sock.onmessage = null;
+ sock.onopen = null;
+ sock.onclose = null;
+ sock.onerror = null;
+ return sock;
+ }
+
+ send(data: string | ArrayBufferLike) {
+ assertExists(this.sock).send(data);
+ }
+
+ waitForData(numBytes: number): Promise<Uint8Array> {
+ if (this.rxPromise !== undefined) {
+ throw new Error('Another unresolved waitForData() is pending already');
+ }
+ const rxPromise = defer<Uint8Array>();
+ if (numBytes === 0) {
+ rxPromise.resolve(new Uint8Array());
+ return rxPromise;
+ }
+ this.rxPromise = rxPromise;
+ this.rxPromiseBytes = numBytes;
+ this.resolveRxPromiseIfEnoughDataAvail();
+ return rxPromise;
+ }
+
+ close() {
+ this.sock?.close();
+ }
+
+ get connected(): boolean {
+ return this.sock?.readyState === WebSocket.OPEN;
+ }
+
+ [Symbol.dispose]() {
+ this.close();
+ }
+
+ async waitForString(numBytes: number): Promise<string> {
+ const data = await this.waitForData(numBytes);
+ assertTrue(data.length === numBytes);
+ return utf8Decode(data);
+ }
+
+ private async onSocketMessage(e: MessageEvent) {
+ assertTrue(e.data instanceof ArrayBuffer);
+ const buf = new Uint8Array(e.data as ArrayBuffer);
+ this.rxBuf.append(buf);
+ this.resolveRxPromiseIfEnoughDataAvail();
+ }
+
+ private resolveRxPromiseIfEnoughDataAvail() {
+ if (this.rxPromise === undefined) return; // Nobody is waiting any data.
+ assertTrue(this.rxPromiseBytes > 0);
+ const bytesWanted = this.rxPromiseBytes;
+ const bytesAvail = this.rxBuf.size - this.rxBufRead;
+ if (bytesWanted > bytesAvail) return; // Not enough data.
+ const buf = this.rxBuf
+ .get()
+ .slice(this.rxBufRead, this.rxBufRead + this.rxPromiseBytes);
+ assertTrue(buf.length === bytesWanted);
+ this.rxBufRead += bytesWanted;
+
+ const rxPromise = this.rxPromise;
+ this.rxPromise = undefined;
+ this.rxPromiseBytes = 0;
+ rxPromise.resolve(buf);
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/websocket_stream.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/websocket_stream.ts
new file mode 100644
index 0000000..fd544e1
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/websocket_stream.ts
@@ -0,0 +1,40 @@
+// 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 {assertTrue} from '../../../base/logging';
+import {ByteStream} from '../interfaces/byte_stream';
+
+export class WebSocketStream extends ByteStream {
+ constructor(private sock: WebSocket) {
+ super();
+ sock.binaryType = 'arraybuffer';
+ sock.onclose = () => this.onClose();
+ sock.onmessage = async (e: MessageEvent) => {
+ assertTrue(e.data instanceof ArrayBuffer);
+ this.onData(new Uint8Array(e.data as ArrayBuffer));
+ };
+ }
+
+ get connected(): boolean {
+ return this.sock.readyState === WebSocket.OPEN;
+ }
+
+ async write(data: string | Uint8Array): Promise<void> {
+ this.sock.send(data);
+ }
+
+ close(): void {
+ this.sock.close();
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/websocket_utils.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/websocket_utils.ts
new file mode 100644
index 0000000..0246092
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/websocket_utils.ts
@@ -0,0 +1,42 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The messages read by the adb server have their length prepended in hex.
+// This method adds the length at the beginning of the message.
+// Example: 'host:track-devices' -> '0012host:track-devices'
+// go/codesearch/aosp-android11/system/core/adb/SERVICES.TXT
+export function prefixWithHexLen(cmd: string) {
+ const hdr = cmd.length.toString(16).padStart(4, '0');
+ return hdr + cmd;
+}
+
+export function websocketInstructions(os?: 'ANDROID') {
+ return (
+ 'Instructions:\n' +
+ (os === 'ANDROID' ? 'adb start-server\n' : '') +
+ 'curl -LO https://get.perfetto.dev/tracebox\n' +
+ 'chmod +x ./tracebox\n' +
+ './tracebox websocket_bridge\n'
+ );
+}
+
+export function disposeWebsocket(ws: WebSocket) {
+ ws.onclose = null;
+ ws.onerror = null;
+ ws.onmessage = null;
+ ws.onopen = null;
+ try {
+ ws.close();
+ } catch {}
+}
diff --git a/ui/src/plugins/dev.perfetto.Sched/active_cpu_count.ts b/ui/src/plugins/dev.perfetto.Sched/active_cpu_count.ts
index f2af628..601517c 100644
--- a/ui/src/plugins/dev.perfetto.Sched/active_cpu_count.ts
+++ b/ui/src/plugins/dev.perfetto.Sched/active_cpu_count.ts
@@ -40,7 +40,7 @@
getTrackShellButtons(): m.Children {
return m(Button, {
onclick: () => {
- this.trace.workspace.findTrackByUri(this.uri)?.remove();
+ this.trace.workspace.getTrackByUri(this.uri)?.remove();
},
icon: Icons.Close,
title: 'Close',
diff --git a/ui/src/plugins/dev.perfetto.SqlModules/sql_modules.ts b/ui/src/plugins/dev.perfetto.SqlModules/sql_modules.ts
index 7154cd6..a4bf549 100644
--- a/ui/src/plugins/dev.perfetto.SqlModules/sql_modules.ts
+++ b/ui/src/plugins/dev.perfetto.SqlModules/sql_modules.ts
@@ -101,7 +101,7 @@
export interface SqlColumn {
readonly name: string;
readonly description: string;
- readonly type: string;
+ readonly type: SqlType;
// Translates this column to SimpleColumn.
asSimpleColumn(tableName: string): SimpleColumn;
@@ -114,3 +114,10 @@
readonly description: string;
readonly type: string;
}
+
+export interface SqlType {
+ readonly name: string;
+ readonly shortName: string;
+ readonly table: string | undefined;
+ readonly column: string | undefined;
+}
diff --git a/ui/src/plugins/dev.perfetto.SqlModules/sql_modules_impl.ts b/ui/src/plugins/dev.perfetto.SqlModules/sql_modules_impl.ts
index 6b9739b..8796665 100644
--- a/ui/src/plugins/dev.perfetto.SqlModules/sql_modules_impl.ts
+++ b/ui/src/plugins/dev.perfetto.SqlModules/sql_modules_impl.ts
@@ -23,6 +23,7 @@
SqlPackage,
SqlTable,
SqlTableFunction,
+ SqlType,
} from './sql_modules';
import {SqlTableDescription} from '../../components/widgets/sql/legacy_table/table_description';
import {
@@ -219,57 +220,62 @@
class StdlibColumnImpl implements SqlColumn {
name: string;
- type: string;
+ type: SqlType;
description: string;
constructor(docs: DocsArgOrColSchemaType) {
- this.type = docs.type;
+ this.type = {
+ name: docs.type,
+ shortName: docs.name.split('(')[0],
+ table: docs.table ? docs.table : undefined,
+ column: docs.column ? docs.column : undefined,
+ };
this.description = docs.desc;
this.name = docs.name;
}
asSimpleColumn(tableName: string): SimpleColumn {
- if (this.type === 'TIMESTAMP') {
+ if (this.type.shortName === 'TIMESTAMP') {
return createTimestampColumn(this.name);
}
- if (this.type === 'DURATION') {
+ if (this.type.shortName === 'DURATION') {
return createDurationColumn(this.name);
}
- if (this.name === 'ID') {
- if (tableName === 'slice') {
- return createSliceIdColumn(this.name);
- }
- if (tableName === 'thread') {
- return createThreadIdColumn(this.name);
- }
- if (tableName === 'process') {
- return createProcessIdColumn(this.name);
- }
- if (tableName === 'thread_state') {
- return createThreadStateIdColumn(this.name);
- }
- if (tableName === 'sched') {
- return createSchedIdColumn(this.name);
+ if (this.type.shortName === 'ID') {
+ switch (tableName.toLowerCase()) {
+ case 'slice':
+ return createSliceIdColumn(this.name);
+ case 'thread':
+ return createThreadIdColumn(this.name);
+ case 'process':
+ return createProcessIdColumn(this.name);
+ case 'thread_state':
+ return createThreadStateIdColumn(this.name);
+ case 'sched':
+ return createSchedIdColumn(this.name);
}
return createStandardColumn(this.name);
}
- if (this.type === 'JOINID(slice.id)') {
- return createSliceIdColumn(this.name);
+ if (this.type.shortName === 'JOINID') {
+ if (this.type.table === undefined) {
+ return createStandardColumn(this.name);
+ }
+ switch (this.type.table.toLowerCase()) {
+ case 'slice':
+ return createSliceIdColumn(this.name);
+ case 'thread':
+ return createThreadIdColumn(this.name);
+ case 'process':
+ return createProcessIdColumn(this.name);
+ case 'thread_state':
+ return createThreadStateIdColumn(this.name);
+ case 'sched':
+ return createSchedIdColumn(this.name);
+ }
}
- if (this.type === 'JOINID(thread.id)') {
- return createThreadIdColumn(this.name);
- }
- if (this.type === 'JOINID(process.id)') {
- return createProcessIdColumn(this.name);
- }
- if (this.type === 'JOINID(thread_state.id)') {
- return createThreadStateIdColumn(this.name);
- }
- if (this.type === 'JOINID(sched.id)') {
- return createSchedIdColumn(this.name);
- }
+
return createStandardColumn(this.name);
}
}
@@ -290,6 +296,8 @@
name: z.string(),
type: z.string(),
desc: z.string(),
+ table: z.string().nullable(),
+ column: z.string().nullable(),
});
type DocsArgOrColSchemaType = z.infer<typeof ARG_OR_COL_SCHEMA>;
diff --git a/ui/src/plugins/dev.perfetto.StandardGroups/index.ts b/ui/src/plugins/dev.perfetto.StandardGroups/index.ts
new file mode 100644
index 0000000..ae684a7
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.StandardGroups/index.ts
@@ -0,0 +1,104 @@
+// 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 {PerfettoPlugin} from '../../public/plugin';
+import {TrackNode, Workspace} from '../../public/workspace';
+
+// Type indicating the standard groups supported by this plugin.
+export type StandardGroup =
+ | 'USER_INTERACTION'
+ | 'THERMALS'
+ | 'POWER'
+ | 'IO'
+ | 'MEMORY'
+ | 'HARDWARE'
+ | 'CPU'
+ | 'GPU'
+ | 'NETWORK'
+ | 'SYSTEM';
+
+export default class implements PerfettoPlugin {
+ static readonly id = 'dev.perfetto.StandardGroups';
+
+ async onTraceLoad() {}
+
+ /**
+ * Gets or creates a standard group to place tracks into.
+ *
+ * @param workspace - The workspace on which to create the group.
+ */
+ getOrCreateStandardGroup(
+ workspace: Workspace,
+ group: StandardGroup,
+ ): TrackNode {
+ switch (group) {
+ case 'USER_INTERACTION':
+ // Expand this by default
+ return getOrCreateGroup(
+ workspace,
+ '/standard_group_user_interaction',
+ 'User Interaction',
+ false,
+ );
+ case 'THERMALS':
+ return getOrCreateGroup(
+ workspace,
+ '/standard_group_thermal',
+ 'Thermals',
+ );
+ case 'POWER':
+ return getOrCreateGroup(workspace, '/standard_group_power', 'Power');
+ case 'CPU':
+ return getOrCreateGroup(workspace, '/standard_group_cpu', 'CPU');
+ case 'GPU':
+ return getOrCreateGroup(workspace, '/standard_group_gpu', 'GPU');
+ case 'HARDWARE':
+ return getOrCreateGroup(
+ workspace,
+ '/standard_group_hardware',
+ 'Hardware',
+ );
+ case 'IO':
+ return getOrCreateGroup(workspace, '/standard_group_io', 'IO');
+ case 'MEMORY':
+ return getOrCreateGroup(workspace, '/standard_group_memory', 'Memory');
+ case 'NETWORK':
+ return getOrCreateGroup(
+ workspace,
+ '/standard_group_network',
+ 'Network',
+ );
+ case 'SYSTEM':
+ return getOrCreateGroup(workspace, '/standard_group_system', 'System');
+ }
+ }
+}
+
+// Internal utility function to avoid duplicating the logic to get or create a
+// group by ID.
+function getOrCreateGroup(
+ workspace: Workspace,
+ id: string,
+ title: string,
+ collapsed: boolean = true,
+): TrackNode {
+ const group = workspace.getTrackById(id);
+ if (group) {
+ return group;
+ } else {
+ const group = new TrackNode({id, title, isSummary: true, collapsed});
+ workspace.addChildInOrder(group);
+ return group;
+ }
+}
diff --git a/ui/src/plugins/dev.perfetto.SysUIWorkspace/OWNERS b/ui/src/plugins/dev.perfetto.SysUIWorkspace/OWNERS
new file mode 100644
index 0000000..9f37949
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.SysUIWorkspace/OWNERS
@@ -0,0 +1 @@
+nicomazz@google.com
diff --git a/ui/src/plugins/dev.perfetto.SysUIWorkspace/index.ts b/ui/src/plugins/dev.perfetto.SysUIWorkspace/index.ts
new file mode 100644
index 0000000..8a5b1d7
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.SysUIWorkspace/index.ts
@@ -0,0 +1,263 @@
+// Copyright (C) 2025 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 {NUM, STR} from '../../trace_processor/query_result';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {TrackNode, Workspace} from '../../public/workspace';
+
+const TRACKS_TO_COPY: string[] = [
+ 'L<',
+ 'UI Events',
+ 'IKeyguardService',
+ 'Transition:',
+];
+const SYSTEM_UI_PROCESS: string = 'com.android.systemui';
+
+// Plugin that creates an opinionated Workspace specific for SysUI
+export default class implements PerfettoPlugin {
+ static readonly id = 'dev.perfetto.SysUIWorkspace';
+
+ async onTraceLoad(ctx: Trace): Promise<void> {
+ ctx.commands.registerCommand({
+ id: 'dev.perfetto.SysUIWorkspace#CreateSysUIWorkspace',
+ name: 'Create System UI workspace',
+ callback: () =>
+ ProcessWorkspaceFactory.create(
+ ctx,
+ SYSTEM_UI_PROCESS,
+ 'System UI',
+ TRACKS_TO_COPY,
+ ),
+ });
+ }
+}
+
+/**
+ * Creates a workspace for a process with the following tracks:
+ * - timelines
+ * - main thread and render thread
+ * - All other ui threads in a group
+ * - List of tracks having name manually provided to this class constructor
+ * - groups tracks having the "/(?<groupName>.*)##(?<trackName>.*)/" format
+ * (e.g. "notifications##visible" will create a "visible" track inside the
+ * "notification" group)
+ *
+ * This is useful to reduce the clutter when focusing on a single process, and
+ * organizing tracks related to the same area in groups.
+ */
+class ProcessWorkspaceFactory {
+ private readonly ws: Workspace;
+ private readonly processTracks: TrackNode[];
+
+ constructor(
+ private readonly trace: Trace,
+ private readonly process: ProcessIdentifier,
+ private readonly workspaceName: string,
+ private readonly topLevelTracksToPin: string[] = [],
+ ) {
+ // We're going to iterate them often: let's filter the process ones.
+ this.processTracks = this.findProcessTracks();
+ this.ws = this.trace.workspaces.createEmptyWorkspace(this.workspaceName);
+ }
+
+ /**
+ * Creates a new workspace for a specific process in a trace.
+ *
+ * No workspace is created if it was there already.
+ * This is expected to be called from the default workspace.
+ *
+ * @param trace
+ * @param packageName Name of the Android package to create the workspace for.
+ * @param workspaceName Desired name for the new workspace.
+ * @param tracksToCopy - An optional list of track names to be added to
+ * the new workspace
+ * @returns A `Promise` that resolves when the workspace has been created.
+ */
+ public static async create(
+ trace: Trace,
+ packageName: string,
+ workspaceName: string,
+ tracksToCopy: string[] = [],
+ ) {
+ const exists = trace.workspaces.all.find(
+ (ws) => ws.title === workspaceName,
+ );
+ if (exists) return;
+
+ const process = await getProcessInfo(trace, packageName);
+ if (!process) return;
+ const factory = new ProcessWorkspaceFactory(
+ trace,
+ process,
+ workspaceName,
+ tracksToCopy,
+ );
+ await factory.createWorkspace();
+ }
+
+ private async createWorkspace() {
+ this.pinTracksContaining('Actual Timeline', 'Expected Timeline');
+ this.pinMainThread();
+ this.pinFirstRenderThread();
+ await this.pinUiThreads();
+ this.topLevelTracksToPin.forEach((s) =>
+ this.pinTracksContainingInGroupIfNeeded(s),
+ );
+ this.createGroups();
+ this.trace.workspaces.switchWorkspace(this.ws);
+ }
+
+ private findProcessTracks(): TrackNode[] {
+ return this.trace.workspace.flatTracks.filter((track) => {
+ if (!track.uri) return false;
+ const descriptor = this.trace.tracks.getTrack(track.uri);
+ return descriptor?.tags?.upid === this.process.upid;
+ });
+ }
+
+ private pinTracksContaining(...args: string[]) {
+ args.forEach((s) => this.pinTrackContaining(s));
+ }
+
+ private pinTrackContaining(titleSubstring: string) {
+ this.getTracksContaining(titleSubstring).forEach((track) =>
+ this.ws.addChildLast(track.clone()),
+ );
+ }
+
+ private pinTracksContainingInGroupIfNeeded(
+ titleSubstring: string,
+ minSizeToGroup: number = 2,
+ ) {
+ const tracks = this.getTracksContaining(titleSubstring);
+ if (tracks.length == 0) return;
+ if (tracks.length >= minSizeToGroup) {
+ const newGroup = new TrackNode({title: titleSubstring, isSummary: true});
+ this.ws.addChildLast(newGroup);
+ tracks.forEach((track) => newGroup.addChildLast(track.clone()));
+ } else {
+ tracks.forEach((track) => this.ws.addChildLast(track.clone()));
+ }
+ }
+
+ private getTracksContaining(titleSubstring: string): TrackNode[] {
+ return this.processTracks.filter((track) =>
+ track.title.includes(titleSubstring),
+ );
+ }
+
+ private pinMainThread() {
+ const tracks = this.processTracks.filter((track) => {
+ return this.getTrackUtid(track) == this.process.upid;
+ });
+ tracks.forEach((track) => this.ws.addChildLast(track.clone()));
+ }
+
+ // In traces there might be many short-lived threads called "render thread"
+ // used to allocate stuff. We don't care about them, but only of the first one
+ // (that has lower thread id)
+ private pinFirstRenderThread() {
+ const tracks = this.getTracksContaining('RenderThread');
+ const utids = tracks
+ .map((t) => this.getTrackUtid(t))
+ .filter((utid): utid is number => utid !== undefined);
+ const minUtid = Math.min(...utids);
+
+ const toPin = tracks.filter((track) => this.getTrackUtid(track) == minUtid);
+ toPin.forEach((track) => this.ws.addChildLast(track.clone()));
+ }
+
+ private async pinUiThreads() {
+ const result = await this.trace.engine.query(`
+ INCLUDE PERFETTO MODULE slices.slices;
+ SELECT DISTINCT utid FROM _slice_with_thread_and_process_info
+ WHERE upid = ${this.process.upid}
+ AND upid != utid -- main thread excluded
+ AND name GLOB "Choreographer#doFrame*"
+ `);
+ if (result.numRows() === 0) {
+ return;
+ }
+ const uiThreadUtidsSet = new Set<number>();
+ const it = result.iter({utid: NUM});
+ for (; it.valid(); it.next()) {
+ uiThreadUtidsSet.add(it.utid);
+ }
+
+ const toPin = this.processTracks.filter((track) => {
+ const utid = this.getTrackUtid(track);
+ return utid != undefined && uiThreadUtidsSet.has(utid);
+ });
+ toPin.sort((a, b) => {
+ return a.title.localeCompare(b.title);
+ });
+ const uiThreadTrack = new TrackNode({title: 'UI Threads', isSummary: true});
+ this.ws.addChildLast(uiThreadTrack);
+ toPin.forEach((track) => uiThreadTrack.addChildLast(track.clone()));
+ }
+
+ private getTrackUtid(node: TrackNode): number | undefined {
+ return this.trace.tracks.getTrack(node.uri!)?.tags?.utid;
+ }
+
+ private createGroups() {
+ const groupRegex = /(?<groupName>.*)##(?<trackName>.*)/;
+ const trackGroups = new Map<string, TrackNode>();
+
+ this.processTracks.forEach((track) => {
+ const match = track.title.match(groupRegex);
+ if (!match?.groups) return;
+
+ const {groupName, trackName} = match.groups;
+
+ const newTrack = track.clone();
+ newTrack.title = trackName;
+
+ if (!trackGroups.has(groupName)) {
+ const newGroup = new TrackNode({title: groupName, isSummary: true});
+ this.ws.addChildLast(newGroup);
+ trackGroups.set(groupName, newGroup);
+ }
+ trackGroups.get(groupName)!.addChildLast(newTrack);
+ });
+ }
+}
+
+type ProcessIdentifier = {
+ upid: number;
+ name: string;
+};
+
+async function getProcessInfo(
+ ctx: Trace,
+ processName: string,
+): Promise<ProcessIdentifier | undefined> {
+ const result = await ctx.engine.query(`
+ INCLUDE PERFETTO MODULE android.process_metadata;
+ select
+ _process_available_info_summary.upid,
+ process.name
+ from _process_available_info_summary
+ join process using(upid)
+ where process.name = '${processName}';
+ `);
+ if (result.numRows() === 0) {
+ return undefined;
+ }
+ return result.firstRow({
+ upid: NUM,
+ name: STR,
+ });
+}
diff --git a/ui/src/plugins/dev.perfetto.ThreadState/thread_state_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.ThreadState/thread_state_selection_aggregator.ts
index b220405..33c73e7 100644
--- a/ui/src/plugins/dev.perfetto.ThreadState/thread_state_selection_aggregator.ts
+++ b/ui/src/plugins/dev.perfetto.ThreadState/thread_state_selection_aggregator.ts
@@ -23,15 +23,24 @@
STR_NULL,
} from '../../trace_processor/query_result';
import {AreaSelectionAggregator} from '../../public/selection';
-import {UnionDataset} from '../../trace_processor/dataset';
+import {Dataset} from '../../trace_processor/dataset';
import {translateState} from '../../components/sql_utils/thread_state';
-import {TrackDescriptor} from '../../public/track';
export class ThreadStateSelectionAggregator implements AreaSelectionAggregator {
readonly id = 'thread_state_aggregation';
- async createAggregateView(engine: Engine, area: AreaSelection) {
- const dataset = this.getDatasetFromTracks(area.tracks);
+ readonly schema = {
+ dur: LONG,
+ io_wait: NUM_NULL,
+ state: STR,
+ utid: NUM,
+ } as const;
+
+ async createAggregateView(
+ engine: Engine,
+ area: AreaSelection,
+ dataset?: Dataset,
+ ) {
if (dataset === undefined) return false;
await engine.query(`
@@ -60,8 +69,8 @@
async getExtra(
engine: Engine,
area: AreaSelection,
+ dataset?: Dataset,
): Promise<ThreadStateExtra | void> {
- const dataset = this.getDatasetFromTracks(area.tracks);
if (dataset === undefined) return;
const query = `
@@ -164,24 +173,4 @@
getDefaultSorting(): Sorting {
return {column: 'total_dur', direction: 'DESC'};
}
-
- // Creates an optimized dataset containing the thread state events within a
- // given list of tracks, or returns undefined if no compatible tracks are
- // present in the list.
- private getDatasetFromTracks(tracks: ReadonlyArray<TrackDescriptor>) {
- const desiredSchema = {
- dur: LONG,
- io_wait: NUM_NULL,
- state: STR,
- utid: NUM,
- };
- const validDatasets = tracks
- .map((track) => track.track.getDataset?.())
- .filter((ds) => ds !== undefined)
- .filter((ds) => ds.implements(desiredSchema));
- if (validDatasets.length === 0) {
- return undefined;
- }
- return new UnionDataset(validDatasets).optimize();
- }
}
diff --git a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
index 37206a3..e415816 100644
--- a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
+++ b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
@@ -272,7 +272,7 @@
private onmessage(msg: MessageEvent) {
if (this._ctx === undefined) return; // Trace unloaded
if (!('perfettoSync' in msg.data)) return;
- this._ctx.scheduleFullRedraw('force');
+ this._ctx.raf.scheduleFullRedraw();
const msgData = msg.data as SyncMessage;
const sync = msgData.perfettoSync;
switch (sync.cmd) {
diff --git a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
index 3e12b1c..4fed517 100644
--- a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
+++ b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
@@ -17,9 +17,12 @@
import {PerfettoPlugin} from '../../public/plugin';
import {createQuerySliceTrack} from '../../components/tracks/query_slice_track';
import {TrackNode} from '../../public/workspace';
+import StandardGroupsPlugin from '../dev.perfetto.StandardGroups';
export default class implements PerfettoPlugin {
static readonly id = 'dev.perfetto.TraceMetadata';
+ static readonly dependencies = [StandardGroupsPlugin];
+
async onTraceLoad(ctx: Trace): Promise<void> {
const res = await ctx.engine.query(`
select count() as cnt from (select 1 from clock_snapshot limit 1)
@@ -46,6 +49,9 @@
track,
});
const trackNode = new TrackNode({uri, title});
- ctx.workspace.addChildInOrder(trackNode);
+ const group = ctx.plugins
+ .getPlugin(StandardGroupsPlugin)
+ .getOrCreateStandardGroup(ctx.workspace, 'SYSTEM');
+ group.addChildInOrder(trackNode);
}
}
diff --git a/ui/src/plugins/dev.perfetto.Counter/counter_details_panel.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_details_panel.ts
similarity index 88%
rename from ui/src/plugins/dev.perfetto.Counter/counter_details_panel.ts
rename to ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_details_panel.ts
index 97de057..6d06231 100644
--- a/ui/src/plugins/dev.perfetto.Counter/counter_details_panel.ts
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_details_panel.ts
@@ -140,18 +140,39 @@
rootTable: string,
): Promise<CounterDetails> {
const query = `
- WITH CTE AS (
+ WITH CURRENT AS (
SELECT
id,
- ts as leftTs,
+ ts,
value,
- LAG(value) OVER (ORDER BY ts) AS prevValue,
- LEAD(ts) OVER (ORDER BY ts) AS rightTs,
- arg_set_id AS argSetId
+ arg_set_id
FROM ${rootTable}
- WHERE track_id = ${trackId}
+ WHERE track_id = ${trackId} and id = ${id}
+ ),
+ PREV as (
+ SELECT
+ value
+ FROM ${rootTable}
+ WHERE track_id = ${trackId} AND ts < (select ts from CURRENT)
+ ORDER BY ts DESC
+ LIMIT 1
+ ),
+ NEXT as (
+ SELECT
+ ts
+ FROM ${rootTable}
+ WHERE track_id = ${trackId} AND ts > (select ts from CURRENT)
+ ORDER BY ts ASC
+ LIMIT 1
)
- SELECT * FROM CTE WHERE id = ${id}
+ SELECT
+ id,
+ ts as leftTs,
+ value,
+ arg_set_id as argSetId,
+ (SELECT value FROM PREV) as prevValue,
+ (SELECT ts FROM NEXT) as rightTs
+ FROM CURRENT
`;
const counter = await engine.query(query);
diff --git a/ui/src/plugins/dev.perfetto.Counter/counter_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_selection_aggregator.ts
similarity index 68%
rename from ui/src/plugins/dev.perfetto.Counter/counter_selection_aggregator.ts
rename to ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_selection_aggregator.ts
index 6a2bd3f..d098821 100644
--- a/ui/src/plugins/dev.perfetto.Counter/counter_selection_aggregator.ts
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_selection_aggregator.ts
@@ -33,32 +33,43 @@
const duration = area.end - area.start;
const durationSec = Duration.toSeconds(duration);
+ await engine.query(`include perfetto module counters.intervals`);
+
// TODO(lalitm): Rewrite this query in a way that is both simpler and faster
let query;
if (trackIds.length === 1) {
// Optimized query for the special case where there is only 1 track id.
query = `CREATE OR REPLACE PERFETTO TABLE ${this.id} AS
- WITH aggregated AS (
- SELECT
- COUNT(1) AS count,
- ROUND(SUM(
- (MIN(ts + dur, ${area.end}) - MAX(ts,${area.start}))*value)/${duration},
- 2
- ) AS avg_value,
- (SELECT value FROM experimental_counter_dur WHERE track_id = ${trackIds[0]}
- AND ts + dur >= ${area.start}
- AND ts <= ${area.end} ORDER BY ts DESC LIMIT 1)
- AS last_value,
- (SELECT value FROM experimental_counter_dur WHERE track_id = ${trackIds[0]}
- AND ts + dur >= ${area.start}
- AND ts <= ${area.end} ORDER BY ts ASC LIMIT 1)
- AS first_value,
- MIN(value) AS min_value,
- MAX(value) AS max_value
- FROM experimental_counter_dur
- WHERE track_id = ${trackIds[0]}
- AND ts + dur >= ${area.start}
- AND ts <= ${area.end})
+ WITH
+ res AS (
+ select c.*
+ from counter_leading_intervals!((
+ SELECT counter.*
+ FROM counter
+ WHERE counter.track_id = ${trackIds[0]}
+ AND counter.ts <= ${area.end}
+ )) c
+ WHERE c.ts + c.dur >= ${area.start}
+ ),
+ aggregated AS (
+ SELECT
+ COUNT(1) AS count,
+ ROUND(SUM(
+ (MIN(ts + dur, ${area.end}) - MAX(ts,${area.start}))*value)/${duration},
+ 2
+ ) AS avg_value,
+ (SELECT value FROM counter WHERE track_id = ${trackIds[0]}
+ AND ts + dur >= ${area.start}
+ AND ts <= ${area.end} ORDER BY ts DESC LIMIT 1)
+ AS last_value,
+ (SELECT value FROM counter WHERE track_id = ${trackIds[0]}
+ AND ts + dur >= ${area.start}
+ AND ts <= ${area.end} ORDER BY ts ASC LIMIT 1)
+ AS first_value,
+ MIN(value) AS min_value,
+ MAX(value) AS max_value
+ FROM res
+ )
SELECT
(SELECT name FROM counter_track WHERE id = ${trackIds[0]}) AS name,
*,
@@ -68,23 +79,31 @@
} else {
// Slower, but general purspose query that can aggregate multiple tracks
query = `CREATE OR REPLACE PERFETTO TABLE ${this.id} AS
- WITH aggregated AS (
- SELECT track_id,
- COUNT(1) AS count,
- ROUND(SUM(
- (MIN(ts + dur, ${area.end}) - MAX(ts,${area.start}))*value)/${duration},
- 2
- ) AS avg_value,
- value_at_max_ts(-ts, value) AS first,
- value_at_max_ts(ts, value) AS last,
- MIN(value) AS min_value,
- MAX(value) AS max_value
- FROM experimental_counter_dur
- WHERE track_id IN (${trackIds})
- AND ts + dur >= ${area.start} AND
- ts <= ${area.end}
- GROUP BY track_id
- )
+ WITH
+ res AS (
+ select c.*
+ from counter_leading_intervals!((
+ SELECT counter.*
+ FROM counter
+ WHERE counter.track_id in (${trackIds})
+ AND counter.ts <= ${area.end}
+ )) c
+ where c.ts + c.dur >= ${area.start}
+ ),
+ aggregated AS (
+ SELECT track_id,
+ COUNT(1) AS count,
+ ROUND(SUM(
+ (MIN(ts + dur, ${area.end}) - MAX(ts,${area.start}))*value)/${duration},
+ 2
+ ) AS avg_value,
+ value_at_max_ts(-ts, value) AS first,
+ value_at_max_ts(ts, value) AS last,
+ MIN(value) AS min_value,
+ MAX(value) AS max_value
+ FROM res
+ GROUP BY track_id
+ )
SELECT
name,
count,
diff --git a/ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_tracks.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_tracks.ts
new file mode 100644
index 0000000..6bdfe65
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_tracks.ts
@@ -0,0 +1,426 @@
+// Copyright (C) 2025 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 {CounterOptions} from '../../components/tracks/base_counter_track';
+import {TopLevelTrackGroup, TrackGroupSchema} from './types';
+
+type CounterMode = CounterOptions['yMode'];
+
+interface CounterTrackTypeSchema {
+ type: string;
+ topLevelGroup: TopLevelTrackGroup;
+ group: string | TrackGroupSchema | undefined;
+ shareYAxis?: true;
+ mode?: CounterMode;
+}
+
+export const COUNTER_TRACK_SCHEMAS: ReadonlyArray<CounterTrackTypeSchema> = [
+ {
+ type: 'acpm_cooling_device_counter',
+ topLevelGroup: 'THERMALS',
+ group: 'ACPM Cooling Devices',
+ },
+ {
+ type: 'acpm_thermal_temperature',
+ topLevelGroup: 'THERMALS',
+ group: 'ACPM Temperature',
+ },
+ {
+ type: 'android_energy_estimation_breakdown_per_uid',
+ topLevelGroup: 'POWER',
+ group: 'Android Energy Estimates (per uid)',
+ },
+ {
+ type: 'android_energy_estimation_breakdown',
+ topLevelGroup: 'POWER',
+ group: 'Android Energy Estimates',
+ },
+ {
+ type: 'atrace_counter',
+ topLevelGroup: 'PROCESS',
+ group: undefined,
+ },
+ {
+ type: 'battery_counter',
+ topLevelGroup: 'POWER',
+ group: 'Battery Counters',
+ },
+ {
+ type: 'battery_stats',
+ topLevelGroup: 'POWER',
+ group: 'Battery Stats',
+ },
+ {
+ type: 'bcl_irq',
+ topLevelGroup: undefined,
+ group: 'BCL IRQ',
+ },
+ {
+ type: 'block_io',
+ topLevelGroup: 'IO',
+ group: 'Block IO',
+ },
+ {
+ type: 'buddyinfo',
+ topLevelGroup: 'MEMORY',
+ group: 'Buddyinfo',
+ },
+ {
+ type: 'chrome_process_stats',
+ topLevelGroup: 'PROCESS',
+ group: undefined,
+ },
+ {
+ type: 'clock_frequency',
+ topLevelGroup: 'HARDWARE',
+ group: 'Clock Frequency',
+ },
+ {
+ type: 'clock_state',
+ topLevelGroup: 'HARDWARE',
+ group: 'Clock State',
+ },
+ {
+ type: 'cooling_device_counter',
+ topLevelGroup: 'THERMALS',
+ group: 'Cooling Devices',
+ },
+ {
+ type: 'cpu_capacity',
+ topLevelGroup: 'CPU',
+ group: 'CPU Capacity',
+ },
+ {
+ type: 'cpu_frequency_throttle',
+ topLevelGroup: 'CPU',
+ group: 'CPU Frequency Throttling',
+ },
+ {
+ type: 'cpu_idle_state',
+ topLevelGroup: 'CPU',
+ group: 'CPU Idle State',
+ },
+ {
+ type: 'cpu_max_frequency_limit',
+ topLevelGroup: 'CPU',
+ group: 'CPU Max Frequency',
+ },
+ {
+ type: 'cpu_min_frequency_limit',
+ topLevelGroup: 'CPU',
+ group: 'CPU Min Frequency',
+ },
+ {
+ type: 'cpu_nr_running',
+ topLevelGroup: 'CPU',
+ group: 'CPU Number Running',
+ },
+ {
+ type: 'cpu_utilization',
+ topLevelGroup: 'CPU',
+ group: 'CPU Utilization',
+ },
+ {
+ type: 'cpustat',
+ topLevelGroup: 'CPU',
+ group: 'CPU Stat',
+ },
+ {
+ type: 'cros_ec_sensorhub_data',
+ topLevelGroup: 'HARDWARE',
+ group: 'ChromeOS EC Sensorhub',
+ },
+ {
+ type: 'diskstat',
+ topLevelGroup: 'IO',
+ group: 'Diskstat',
+ },
+ {
+ type: 'entity_state',
+ topLevelGroup: 'POWER',
+ group: 'Entity Residency',
+ shareYAxis: true,
+ mode: 'rate',
+ },
+ {
+ type: 'f2fs_iostat_latency',
+ topLevelGroup: 'IO',
+ group: 'F2FS IOStat Latency',
+ },
+ {
+ type: 'f2fs_iostat',
+ topLevelGroup: 'IO',
+ group: 'F2FS IOStat',
+ },
+ {
+ type: 'fastrpc_change',
+ topLevelGroup: 'PROCESS',
+ group: 'Fastrpc',
+ },
+ {
+ type: 'fastrpc',
+ topLevelGroup: 'HARDWARE',
+ group: 'Fastrpc',
+ },
+ {
+ type: 'fuchsia_counter',
+ topLevelGroup: 'PROCESS',
+ group: undefined,
+ },
+ {
+ type: 'gpu_counter',
+ topLevelGroup: 'GPU',
+ group: 'GPU Counters',
+ },
+ {
+ type: 'gpu_memory',
+ topLevelGroup: 'GPU',
+ group: undefined,
+ },
+ {
+ type: 'ion_change',
+ topLevelGroup: 'THREAD',
+ group: undefined,
+ },
+ {
+ type: 'ion',
+ topLevelGroup: 'MEMORY',
+ group: undefined,
+ },
+ {
+ type: 'json_counter_thread_fallback',
+ topLevelGroup: 'THREAD',
+ group: undefined,
+ },
+ {
+ type: 'json_counter',
+ topLevelGroup: 'PROCESS',
+ group: undefined,
+ },
+ {
+ type: 'linux_device_frequency',
+ topLevelGroup: 'HARDWARE',
+ group: 'Linux Device Frequency',
+ },
+ {
+ type: 'linux_rpm',
+ topLevelGroup: 'HARDWARE',
+ group: 'Linux RPM',
+ },
+ {
+ type: 'meminfo',
+ topLevelGroup: 'MEMORY',
+ group: 'Meminfo',
+ },
+ {
+ type: 'metatrace_counter',
+ topLevelGroup: 'THREAD',
+ group: undefined,
+ },
+ {
+ type: 'mm_event_thread_fallback',
+ topLevelGroup: 'THREAD',
+ group: 'MM Event',
+ },
+ {
+ type: 'mm_event',
+ topLevelGroup: 'PROCESS',
+ group: 'MM Event',
+ },
+ {
+ type: 'net_kfree_skb',
+ topLevelGroup: 'NETWORK',
+ group: 'Network Packet Frees',
+ },
+ {
+ type: 'net_receive',
+ topLevelGroup: 'NETWORK',
+ group: 'Network Receive',
+ mode: 'rate',
+ },
+ {
+ type: 'net_transmit',
+ topLevelGroup: 'NETWORK',
+ group: 'Network Send',
+ mode: 'rate',
+ },
+ {
+ type: 'num_forks',
+ topLevelGroup: 'SYSTEM',
+ group: undefined,
+ },
+ {
+ type: 'num_irq_total',
+ topLevelGroup: 'SYSTEM',
+ group: undefined,
+ },
+ {
+ type: 'num_irq',
+ topLevelGroup: 'SYSTEM',
+ group: 'IRQ Count',
+ },
+ {
+ type: 'num_softirq_total',
+ topLevelGroup: 'SYSTEM',
+ group: undefined,
+ },
+ {
+ type: 'num_softirq',
+ topLevelGroup: 'SYSTEM',
+ group: 'Softirq Count',
+ },
+ {
+ type: 'oom_score_adj_thread_fallback',
+ topLevelGroup: 'THREAD',
+ group: undefined,
+ },
+ {
+ type: 'oom_score_adj',
+ topLevelGroup: 'PROCESS',
+ group: undefined,
+ },
+ {
+ type: 'perf_counter',
+ topLevelGroup: 'HARDWARE',
+ group: 'perf counters',
+ },
+ {
+ type: 'pixel_cpm_counters',
+ topLevelGroup: 'THERMALS',
+ group: 'CPM Counters',
+ },
+ {
+ type: 'power_rails',
+ group: 'Power Rails',
+ topLevelGroup: 'POWER',
+ shareYAxis: true,
+ mode: 'rate',
+ },
+ {
+ type: 'proc_stat_runtime',
+ topLevelGroup: 'PROCESS',
+ group: undefined,
+ },
+ {
+ type: 'process_gpu_memory',
+ topLevelGroup: 'PROCESS',
+ group: undefined,
+ },
+ {
+ type: 'process_memory_thread_fallback',
+ topLevelGroup: 'THREAD',
+ group: undefined,
+ },
+ {
+ type: 'process_memory',
+ topLevelGroup: 'PROCESS',
+ group: undefined,
+ },
+ {
+ type: 'psi',
+ group: 'PSI',
+ topLevelGroup: 'SYSTEM',
+ mode: 'rate',
+ },
+ {
+ type: 'screen_state',
+ topLevelGroup: 'SYSTEM',
+ group: 'Screen State',
+ },
+ {
+ type: 'smaps',
+ topLevelGroup: 'MEMORY',
+ group: 'smaps',
+ },
+ {
+ type: 'sysprop_counter',
+ topLevelGroup: 'SYSTEM',
+ group: undefined,
+ },
+ {
+ type: 'thermal_temperature_sys',
+ topLevelGroup: 'THERMALS',
+ group: 'Temperature (/sys)',
+ },
+ {
+ type: 'thermal_temperature',
+ topLevelGroup: 'THERMALS',
+ group: 'Temperature',
+ },
+ {
+ type: 'ufs_clkgating',
+ topLevelGroup: 'IO',
+ group: undefined,
+ },
+ {
+ type: 'ufs_command_count',
+ topLevelGroup: 'IO',
+ group: undefined,
+ },
+ {
+ type: 'virtgpu_latency',
+ topLevelGroup: 'GPU',
+ group: 'Virtgpu Latency',
+ },
+ {
+ type: 'virtgpu_num_free',
+ topLevelGroup: 'GPU',
+ group: 'Virtgpu num_free',
+ },
+ {
+ type: 'vmstat',
+ topLevelGroup: 'MEMORY',
+ group: 'vmstat',
+ },
+ {
+ type: 'vulkan_device_mem_allocation',
+ topLevelGroup: 'GPU',
+ group: 'Vulkan Allocations',
+ },
+ {
+ type: 'vulkan_device_mem_bind',
+ topLevelGroup: 'GPU',
+ group: 'Vulkan Binds',
+ },
+ {
+ type: 'vulkan_driver_mem',
+ topLevelGroup: 'GPU',
+ group: 'Vulkan Driver Memory',
+ },
+ {
+ type: 'battery_status',
+ topLevelGroup: 'POWER',
+ group: undefined,
+ },
+ {
+ type: 'battery_plugged_status',
+ topLevelGroup: 'POWER',
+ group: undefined,
+ },
+ {
+ type: 'ion',
+ topLevelGroup: 'MEMORY',
+ group: undefined,
+ },
+ {
+ type: 'ion_change',
+ topLevelGroup: 'MEMORY',
+ group: undefined,
+ },
+ {
+ type: 'android_dma_heap_change',
+ topLevelGroup: 'THREAD',
+ group: undefined,
+ },
+];
diff --git a/ui/src/plugins/dev.perfetto.TraceProcessorTrack/index.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/index.ts
new file mode 100644
index 0000000..afb7e9f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/index.ts
@@ -0,0 +1,382 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {assertExists} from '../../base/logging';
+import {PerfettoPlugin} from '../../public/plugin';
+import {Trace} from '../../public/trace';
+import {COUNTER_TRACK_KIND, SLICE_TRACK_KIND} from '../../public/track_kinds';
+import {getTrackName} from '../../public/utils';
+import {TrackNode} from '../../public/workspace';
+import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
+import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
+import StandardGroupsPlugin from '../dev.perfetto.StandardGroups';
+import {CounterSelectionAggregator} from './counter_selection_aggregator';
+import {SLICE_TRACK_SCHEMAS} from './slice_tracks';
+import {TraceProcessorCounterTrack} from './trace_processor_counter_track';
+import {COUNTER_TRACK_SCHEMAS} from './counter_tracks';
+import {SliceSelectionAggregator} from './slice_selection_aggregator';
+import {TraceProcessorSliceTrack} from './trace_processor_slice_track';
+import {TopLevelTrackGroup, TrackGroupSchema} from './types';
+import {removeFalsyValues} from '../../base/array_utils';
+
+export default class implements PerfettoPlugin {
+ static readonly id = 'dev.perfetto.TraceProcessorTrack';
+ static readonly dependencies = [
+ ProcessThreadGroupsPlugin,
+ StandardGroupsPlugin,
+ ];
+
+ async onTraceLoad(ctx: Trace): Promise<void> {
+ await this.addCounters(ctx);
+ await this.addSlices(ctx);
+
+ ctx.selection.registerAreaSelectionAggregator(
+ new CounterSelectionAggregator(),
+ );
+
+ ctx.selection.registerSqlSelectionResolver({
+ sqlTableName: 'slice',
+ callback: async (id: number) => {
+ const compatibleTypes = SLICE_TRACK_SCHEMAS.map(
+ (schema) => `'${schema.type}'`,
+ ).join(',');
+
+ // Locate the track for a given id in the slice table
+ const result = await ctx.engine.query(`
+ select
+ slice.track_id as trackId
+ from slice
+ join track on slice.track_id = track.id
+ where slice.id = ${id} and track.type in (${compatibleTypes})
+ `);
+
+ if (result.numRows() === 0) {
+ return undefined;
+ }
+ const {trackId} = result.firstRow({
+ trackId: NUM,
+ });
+ return {
+ trackUri: `/slice_${trackId}`,
+ eventId: id,
+ };
+ },
+ });
+
+ ctx.selection.registerAreaSelectionAggregator(
+ new SliceSelectionAggregator(),
+ );
+ }
+
+ private async addCounters(ctx: Trace) {
+ const result = await ctx.engine.query(`
+ include perfetto module viz.threads;
+
+ with tracks_summary as (
+ select
+ ct.type,
+ ct.name,
+ ct.id,
+ ct.unit,
+ extract_arg(ct.dimension_arg_set_id, 'utid') as utid,
+ extract_arg(ct.dimension_arg_set_id, 'upid') as upid
+ from counter_track ct
+ join _counter_track_summary using (id)
+ order by ct.name
+ )
+ select
+ s.*,
+ thread.tid,
+ thread.name as threadName,
+ ifnull(p.pid, tp.pid) as pid,
+ ifnull(p.name, tp.name) as processName,
+ ifnull(thread.is_main_thread, 0) as isMainThread,
+ ifnull(k.is_kernel_thread, 0) AS isKernelThread
+ from tracks_summary s
+ left join process p on s.upid = p.upid
+ left join thread using (utid)
+ left join _threads_with_kernel_flag k using (utid)
+ left join process tp on thread.upid = tp.upid
+ order by lower(s.name)
+ `);
+
+ const schemas = new Map(COUNTER_TRACK_SCHEMAS.map((x) => [x.type, x]));
+ const it = result.iter({
+ id: NUM,
+ type: STR,
+ name: STR_NULL,
+ unit: STR_NULL,
+ utid: NUM_NULL,
+ upid: NUM_NULL,
+ threadName: STR_NULL,
+ processName: STR_NULL,
+ tid: NUM_NULL,
+ pid: NUM_NULL,
+ isMainThread: NUM,
+ isKernelThread: NUM,
+ });
+ for (; it.valid(); it.next()) {
+ const {
+ type,
+ id: trackId,
+ name,
+ unit,
+ utid,
+ upid,
+ threadName,
+ processName,
+ tid,
+ pid,
+ isMainThread,
+ isKernelThread,
+ } = it;
+ const schema = schemas.get(type);
+ if (schema === undefined) {
+ continue;
+ }
+ const {group, topLevelGroup} = schema;
+ const title = getTrackName({
+ name,
+ tid,
+ threadName,
+ pid,
+ processName,
+ upid,
+ utid,
+ kind: COUNTER_TRACK_KIND,
+ threadTrack: utid !== undefined,
+ });
+ const uri = `/counter_${trackId}`;
+ ctx.tracks.registerTrack({
+ uri,
+ title,
+ tags: {
+ kind: COUNTER_TRACK_KIND,
+ trackIds: [trackId],
+ upid: upid ?? undefined,
+ utid: utid ?? undefined,
+ ...(isKernelThread === 1 && {kernelThread: true}),
+ },
+ chips: removeFalsyValues([
+ isKernelThread === 0 && isMainThread === 1 && 'main thread',
+ ]),
+ track: new TraceProcessorCounterTrack(
+ ctx,
+ uri,
+ {
+ yMode: schema.mode,
+ yRangeSharingKey: schema.shareYAxis ? it.type : undefined,
+ unit: unit ?? undefined,
+ },
+ trackId,
+ title,
+ ),
+ });
+ addTrack(
+ ctx,
+ topLevelGroup,
+ group,
+ upid,
+ utid,
+ new TrackNode({
+ uri,
+ title,
+ sortOrder: utid !== undefined || upid !== undefined ? 30 : 0,
+ }),
+ );
+ }
+ }
+
+ private async addSlices(ctx: Trace) {
+ const result = await ctx.engine.query(`
+ include perfetto module viz.threads;
+
+ with grouped as materialized (
+ select
+ t.type,
+ t.name,
+ extract_arg(t.dimension_arg_set_id, 'utid') as utid,
+ extract_arg(t.dimension_arg_set_id, 'upid') as upid,
+ group_concat(t.id) as trackIds,
+ count() as trackCount
+ from _slice_track_summary s
+ join track t using (id)
+ group by type, upid, utid, name
+ )
+ select
+ s.type,
+ s.name,
+ s.utid,
+ ifnull(s.upid, tp.upid) as upid,
+ s.trackIds as trackIds,
+ __max_layout_depth(s.trackCount, s.trackIds) as maxDepth,
+ thread.tid,
+ thread.name as threadName,
+ ifnull(p.pid, tp.pid) as pid,
+ ifnull(p.name, tp.name) as processName,
+ ifnull(thread.is_main_thread, 0) as isMainThread,
+ ifnull(k.is_kernel_thread, 0) AS isKernelThread
+ from grouped s
+ left join process p on s.upid = p.upid
+ left join thread using (utid)
+ left join _threads_with_kernel_flag k using (utid)
+ left join process tp on thread.upid = tp.upid
+ order by lower(s.name)
+ `);
+
+ const schemas = new Map(SLICE_TRACK_SCHEMAS.map((x) => [x.type, x]));
+ const it = result.iter({
+ type: STR,
+ name: STR_NULL,
+ utid: NUM_NULL,
+ upid: NUM_NULL,
+ trackIds: STR,
+ maxDepth: NUM,
+ tid: NUM_NULL,
+ threadName: STR_NULL,
+ pid: NUM_NULL,
+ processName: STR_NULL,
+ isMainThread: NUM,
+ isKernelThread: NUM,
+ });
+ for (; it.valid(); it.next()) {
+ const {
+ trackIds: rawTrackIds,
+ type,
+ name,
+ maxDepth,
+ utid,
+ upid,
+ threadName,
+ processName,
+ tid,
+ pid,
+ isMainThread,
+ isKernelThread,
+ } = it;
+ const schema = schemas.get(type);
+ if (schema === undefined) {
+ continue;
+ }
+ const trackIds = rawTrackIds.split(',').map((v) => Number(v));
+ const {group, topLevelGroup} = schema;
+ const title = getTrackName({
+ name,
+ tid,
+ threadName,
+ pid,
+ processName,
+ upid,
+ utid,
+ kind: SLICE_TRACK_KIND,
+ threadTrack: utid !== undefined,
+ });
+ const uri = `/slice_${trackIds[0]}`;
+ ctx.tracks.registerTrack({
+ uri,
+ title,
+ tags: {
+ kind: SLICE_TRACK_KIND,
+ trackIds: trackIds,
+ upid: upid ?? undefined,
+ utid: utid ?? undefined,
+ ...(isKernelThread === 1 && {kernelThread: true}),
+ },
+ chips: removeFalsyValues([
+ isKernelThread === 0 && isMainThread === 1 && 'main thread',
+ ]),
+ track: new TraceProcessorSliceTrack(ctx, uri, maxDepth, trackIds),
+ });
+ addTrack(
+ ctx,
+ topLevelGroup,
+ group,
+ upid,
+ utid,
+ new TrackNode({
+ uri,
+ title,
+ sortOrder: utid !== undefined || upid !== undefined ? 20 : 0,
+ }),
+ );
+ }
+ }
+}
+
+function addTrack(
+ ctx: Trace,
+ topLevelGroup: TopLevelTrackGroup,
+ group: string | TrackGroupSchema | undefined,
+ upid: number | null,
+ utid: number | null,
+ track: TrackNode,
+) {
+ switch (topLevelGroup) {
+ case 'PROCESS': {
+ const process = assertExists(
+ ctx.plugins
+ .getPlugin(ProcessThreadGroupsPlugin)
+ .getGroupForProcess(assertExists(upid)),
+ );
+ getGroupByName(process, group, upid).addChildInOrder(track);
+ break;
+ }
+ case 'THREAD': {
+ const thread = assertExists(
+ ctx.plugins
+ .getPlugin(ProcessThreadGroupsPlugin)
+ .getGroupForThread(assertExists(utid)),
+ );
+ getGroupByName(thread, group, utid).addChildInOrder(track);
+ break;
+ }
+ case undefined: {
+ getGroupByName(ctx.workspace.tracks, group, upid).addChildInOrder(track);
+ break;
+ }
+ default: {
+ const standardGroup = ctx.plugins
+ .getPlugin(StandardGroupsPlugin)
+ .getOrCreateStandardGroup(ctx.workspace, topLevelGroup);
+ getGroupByName(standardGroup, group, null).addChildInOrder(track);
+ break;
+ }
+ }
+}
+
+function getGroupByName(
+ node: TrackNode,
+ group: string | TrackGroupSchema | undefined,
+ scopeId: number | null,
+) {
+ if (group === undefined) {
+ return node;
+ }
+ const name = typeof group === 'string' ? group : group.name;
+ const expanded = typeof group === 'string' ? false : group.expanded ?? false;
+ const groupId = `tp_group_${scopeId}_${name.toLowerCase().replace(' ', '_')}`;
+ const groupNode = node.getTrackById(groupId);
+ if (groupNode) {
+ return groupNode;
+ }
+ const newGroup = new TrackNode({
+ uri: `/${group}`,
+ id: groupId,
+ isSummary: true,
+ title: name,
+ collapsed: !expanded,
+ });
+ node.addChildInOrder(newGroup);
+ return newGroup;
+}
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/slice_selection_aggregator.ts
similarity index 74%
rename from ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts
rename to ui/src/plugins/dev.perfetto.TraceProcessorTrack/slice_selection_aggregator.ts
index 23226bc..5561533 100644
--- a/ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/slice_selection_aggregator.ts
@@ -13,30 +13,28 @@
// limitations under the License.
import {ColumnDef, Sorting} from '../../public/aggregation';
-import {AreaSelection} from '../../public/selection';
+import {AreaSelection, AreaSelectionAggregator} from '../../public/selection';
+import {Dataset} from '../../trace_processor/dataset';
import {Engine} from '../../trace_processor/engine';
-import {AreaSelectionAggregator} from '../../public/selection';
-import {UnionDataset} from '../../trace_processor/dataset';
import {LONG, NUM, STR} from '../../trace_processor/query_result';
export class SliceSelectionAggregator implements AreaSelectionAggregator {
readonly id = 'slice_aggregation';
- async createAggregateView(engine: Engine, area: AreaSelection) {
- const desiredSchema = {
- id: NUM,
- name: STR,
- ts: LONG,
- dur: LONG,
- };
- const validDatasets = area.tracks
- .map((track) => track.track.getDataset?.())
- .filter((ds) => ds !== undefined)
- .filter((ds) => ds.implements(desiredSchema));
- if (validDatasets.length === 0) {
- return false;
- }
- const unionDataset = new UnionDataset(validDatasets);
+ readonly schema = {
+ id: NUM,
+ name: STR,
+ ts: LONG,
+ dur: LONG,
+ } as const;
+
+ async createAggregateView(
+ engine: Engine,
+ area: AreaSelection,
+ dataset?: Dataset,
+ ) {
+ if (!dataset) return false;
+
await engine.query(`
create or replace perfetto table ${this.id} as
select
@@ -44,7 +42,7 @@
sum(dur) AS total_dur,
sum(dur)/count() as avg_dur,
count() as occurrences
- from (${unionDataset.optimize().query()})
+ from (${dataset.query()})
where
ts + dur > ${area.start}
and ts < ${area.end}
diff --git a/ui/src/plugins/dev.perfetto.TraceProcessorTrack/slice_tracks.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/slice_tracks.ts
new file mode 100644
index 0000000..bbc9aa9
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/slice_tracks.ts
@@ -0,0 +1,229 @@
+// Copyright (C) 2025 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 {StandardGroup} from '../dev.perfetto.StandardGroups';
+
+export interface SliceTrackGroupSchema {
+ name: string;
+ expanded?: true;
+}
+
+interface SliceTrackTypeSchema {
+ type: string;
+ group: string | SliceTrackGroupSchema | undefined;
+ topLevelGroup: 'PROCESS' | 'THREAD' | StandardGroup | undefined;
+}
+
+export const SLICE_TRACK_SCHEMAS: ReadonlyArray<SliceTrackTypeSchema> = [
+ {
+ type: 'battery_stats',
+ topLevelGroup: 'POWER',
+ group: 'Battery Stats',
+ },
+ {
+ type: 'legacy_async_process_slice',
+ topLevelGroup: 'PROCESS',
+ group: undefined,
+ },
+ {
+ type: 'legacy_async_global_slice',
+ topLevelGroup: undefined,
+ group: 'Global Legacy Events',
+ },
+ {
+ type: 'legacy_chrome_global_instants',
+ group: undefined,
+ topLevelGroup: undefined,
+ },
+ {
+ type: 'android_device_state',
+ topLevelGroup: 'SYSTEM',
+ group: undefined,
+ },
+ {
+ type: 'android_lmk',
+ topLevelGroup: 'PROCESS',
+ group: undefined,
+ },
+ {
+ type: 'chrome_process_instant',
+ topLevelGroup: 'PROCESS',
+ group: undefined,
+ },
+ {
+ type: 'drm_vblank',
+ topLevelGroup: 'HARDWARE',
+ group: 'DRM VBlank',
+ },
+ {
+ type: 'drm_sched_ring',
+ topLevelGroup: 'HARDWARE',
+ group: 'DRM Sched Ring',
+ },
+ {
+ type: 'drm_fence',
+ topLevelGroup: 'HARDWARE',
+ group: 'DRM Fence',
+ },
+ {
+ type: 'interconnect_events',
+ topLevelGroup: 'HARDWARE',
+ group: undefined,
+ },
+ {
+ type: 'cpu_irq',
+ topLevelGroup: 'CPU',
+ group: 'IRQs',
+ },
+ {
+ type: 'cpu_softirq',
+ topLevelGroup: 'CPU',
+ group: 'Softirqs',
+ },
+ {
+ type: 'net_socket_set_state',
+ topLevelGroup: 'NETWORK',
+ group: 'Socket Set State',
+ },
+ {
+ type: 'net_tcp_retransmit_skb',
+ topLevelGroup: 'NETWORK',
+ group: 'TCP Retransmit SKB',
+ },
+ {
+ type: 'cpu_napi_gro',
+ topLevelGroup: 'CPU',
+ group: 'NAPI GRO',
+ },
+ {
+ type: 'ufs_command_tag',
+ topLevelGroup: 'IO',
+ group: 'UFS Command Tag',
+ },
+ {
+ type: 'wakesource_wakelock',
+ topLevelGroup: 'POWER',
+ group: 'Kernel Wakelocks',
+ },
+ {
+ type: 'dumpstate_wakelocks',
+ topLevelGroup: 'POWER',
+ group: 'Kernel Wakelocks',
+ },
+ {
+ type: 'cpu_funcgraph',
+ topLevelGroup: 'CPU',
+ group: 'Funcgraph',
+ },
+ {
+ type: 'android_ion_allocations',
+ topLevelGroup: 'MEMORY',
+ group: 'ION',
+ },
+ {
+ type: 'android_fs',
+ topLevelGroup: 'IO',
+ group: undefined,
+ },
+ {
+ type: 'cpu_mali_irq',
+ topLevelGroup: 'CPU',
+ group: undefined,
+ },
+ {
+ type: 'mali_mcu_state',
+ topLevelGroup: 'GPU',
+ group: undefined,
+ },
+ {
+ type: 'pkvm_hypervisor',
+ topLevelGroup: 'SYSTEM',
+ group: undefined,
+ },
+ {
+ type: 'virtgpu_queue_event',
+ topLevelGroup: 'GPU',
+ group: 'Virtio GPU Events',
+ },
+ {
+ type: 'virtio_video_queue_event',
+ topLevelGroup: 'SYSTEM',
+ group: 'Virtio Video Queue Events',
+ },
+ {
+ type: 'virtio_video_command',
+ topLevelGroup: 'SYSTEM',
+ group: 'Virtio Video Command Events',
+ },
+ {
+ type: 'android_camera_event',
+ topLevelGroup: 'HARDWARE',
+ group: undefined,
+ },
+ {
+ type: 'gpu_render_stage',
+ topLevelGroup: 'GPU',
+ group: 'Render Stage',
+ },
+ {
+ type: 'vulkan_events',
+ topLevelGroup: 'GPU',
+ group: undefined,
+ },
+ {
+ type: 'gpu_log',
+ topLevelGroup: 'GPU',
+ group: undefined,
+ },
+ {
+ type: 'graphics_frame_event',
+ topLevelGroup: 'GPU',
+ group: undefined,
+ },
+ {
+ type: 'triggers',
+ topLevelGroup: 'SYSTEM',
+ group: undefined,
+ },
+ {
+ type: 'network_packets',
+ topLevelGroup: 'NETWORK',
+ group: undefined,
+ },
+ {
+ type: 'pixel_modem_event',
+ topLevelGroup: 'HARDWARE',
+ group: undefined,
+ },
+ {
+ type: 'statsd_atoms',
+ topLevelGroup: 'SYSTEM',
+ group: undefined,
+ },
+ {
+ type: 'atrace_async_slice',
+ topLevelGroup: 'PROCESS',
+ group: undefined,
+ },
+ {
+ type: 'atrace_async_slice_for_track',
+ topLevelGroup: 'PROCESS',
+ group: undefined,
+ },
+ {
+ type: 'thread_execution',
+ topLevelGroup: 'THREAD',
+ group: undefined,
+ },
+];
diff --git a/ui/src/plugins/dev.perfetto.Counter/trace_processor_counter_track.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_counter_track.ts
similarity index 89%
rename from ui/src/plugins/dev.perfetto.Counter/trace_processor_counter_track.ts
rename to ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_counter_track.ts
index 61f257e..e0be4ac 100644
--- a/ui/src/plugins/dev.perfetto.Counter/trace_processor_counter_track.ts
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_counter_track.ts
@@ -81,16 +81,24 @@
// SHOULD assume `rootTable` has an id column is another matter...
async getSelectionDetails(id: number): Promise<TrackEventDetails> {
const query = `
- WITH
- CTE AS (
+ WITH CTE AS (
+ SELECT
+ id,
+ ts as leftTs
+ FROM ${this.rootTable}
+ WHERE track_id = ${this.trackId} AND id = ${id}
+ )
+ SELECT
+ *,
+ (
SELECT
- id,
- ts as leftTs,
- LEAD(ts) OVER (ORDER BY ts) AS rightTs
+ ts
FROM ${this.rootTable}
- WHERE track_id = ${this.trackId}
- )
- SELECT * FROM CTE WHERE id = ${id}
+ WHERE track_id = ${this.trackId} AND ts > leftTs
+ ORDER BY ts ASC
+ LIMIT 1
+ ) as rightTs
+ FROM CTE
`;
const counter = await this.engine.query(query);
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_slice_track.ts
similarity index 95%
rename from ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts
rename to ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_slice_track.ts
index 46db758..6794d93 100644
--- a/ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_slice_track.ts
@@ -40,11 +40,14 @@
};
export type ThreadSliceRow = typeof THREAD_SLICE_ROW;
-export class AsyncSliceTrack extends NamedSliceTrack<Slice, ThreadSliceRow> {
+export class TraceProcessorSliceTrack extends NamedSliceTrack<
+ Slice,
+ ThreadSliceRow
+> {
constructor(
trace: Trace,
uri: string,
- maxDepth: number,
+ maxDepth: number | undefined,
private readonly trackIds: number[],
) {
super(trace, uri);
diff --git a/ui/src/widgets/raf.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/types.ts
similarity index 66%
rename from ui/src/widgets/raf.ts
rename to ui/src/plugins/dev.perfetto.TraceProcessorTrack/types.ts
index dc0d3ab..b2956b3 100644
--- a/ui/src/widgets/raf.ts
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/types.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 The Android Open Source Project
+// Copyright (C) 2025 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.
@@ -12,12 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-let FULL_REDRAW_FUNCTION = (_force?: 'force') => {};
+import {StandardGroup} from '../dev.perfetto.StandardGroups';
-export function setScheduleFullRedraw(func: () => void) {
- FULL_REDRAW_FUNCTION = func;
-}
+export type TopLevelTrackGroup =
+ | 'PROCESS'
+ | 'THREAD'
+ | StandardGroup
+ | undefined;
-export function scheduleFullRedraw(force?: 'force') {
- FULL_REDRAW_FUNCTION(force);
+export interface TrackGroupSchema {
+ name: string;
+ expanded?: true;
}
diff --git a/ui/src/plugins/dev.perfetto.TrackEvent/index.ts b/ui/src/plugins/dev.perfetto.TrackEvent/index.ts
new file mode 100644
index 0000000..a72f765
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.TrackEvent/index.ts
@@ -0,0 +1,236 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
+import TraceProcessorTrackPlugin from '../dev.perfetto.TraceProcessorTrack';
+import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
+import {TrackNode} from '../../public/workspace';
+import {assertExists, assertTrue} from '../../base/logging';
+import {COUNTER_TRACK_KIND, SLICE_TRACK_KIND} from '../../public/track_kinds';
+import {TraceProcessorSliceTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_slice_track';
+import {TraceProcessorCounterTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_counter_track';
+import {getTrackName} from '../../public/utils';
+
+export default class implements PerfettoPlugin {
+ static readonly id = 'dev.perfetto.TrackEvent';
+ static readonly dependencies = [
+ ProcessThreadGroupsPlugin,
+ TraceProcessorTrackPlugin,
+ ];
+
+ private readonly trackIdToUri = new Map<number, string>();
+
+ async onTraceLoad(ctx: Trace): Promise<void> {
+ const res = await ctx.engine.query(`
+ include perfetto module viz.summary.track_event;
+ select
+ ifnull(g.upid, t.upid) as upid,
+ g.utid,
+ g.parent_id as parentId,
+ g.is_counter AS isCounter,
+ g.name,
+ g.unit,
+ g.builtin_counter_type as builtinCounterType,
+ g.has_data AS hasData,
+ g.has_children AS hasChildren,
+ g.track_ids as trackIds,
+ g.order_id as orderId,
+ t.name as threadName,
+ t.tid as tid,
+ ifnull(p.pid, tp.pid) as pid,
+ ifnull(p.name, tp.name) as processName
+ from _track_event_tracks_ordered_groups g
+ left join process p using (upid)
+ left join thread t using (utid)
+ left join process tp on tp.upid = t.upid
+ `);
+ const it = res.iter({
+ upid: NUM_NULL,
+ utid: NUM_NULL,
+ parentId: NUM_NULL,
+ isCounter: NUM,
+ name: STR_NULL,
+ unit: STR_NULL,
+ builtinCounterType: STR_NULL,
+ hasData: NUM,
+ hasChildren: NUM,
+ trackIds: STR,
+ orderId: NUM,
+ threadName: STR_NULL,
+ tid: NUM_NULL,
+ pid: NUM_NULL,
+ processName: STR_NULL,
+ });
+ const processGroupsPlugin = ctx.plugins.getPlugin(
+ ProcessThreadGroupsPlugin,
+ );
+ const trackIdToTrackNode = new Map<number, TrackNode>();
+ for (; it.valid(); it.next()) {
+ const {
+ upid,
+ utid,
+ parentId,
+ isCounter,
+ name,
+ unit,
+ builtinCounterType,
+ hasData,
+ hasChildren,
+ trackIds: rawTrackIds,
+ orderId,
+ threadName,
+ tid,
+ pid,
+ processName,
+ } = it;
+
+ // Don't add track_event tracks which don't have any data and don't have
+ // any children.
+ if (!hasData && !hasChildren) {
+ continue;
+ }
+
+ const kind = isCounter ? COUNTER_TRACK_KIND : SLICE_TRACK_KIND;
+ const trackIds = rawTrackIds.split(',').map((v) => Number(v));
+ const title = getTrackName({
+ name,
+ utid,
+ upid,
+ kind,
+ threadTrack: utid !== null,
+ threadName,
+ processName,
+ tid,
+ pid,
+ });
+ const uri = `/track_event_${trackIds[0]}`;
+ if (hasData && isCounter) {
+ // Don't show any builtin counter.
+ if (builtinCounterType !== null) {
+ continue;
+ }
+ assertTrue(trackIds.length === 1);
+ const trackId = trackIds[0];
+ this.trackIdToUri.set(trackId, uri);
+ ctx.tracks.registerTrack({
+ uri,
+ title,
+ tags: {
+ kind,
+ trackIds: [trackIds[0]],
+ upid: upid ?? undefined,
+ utid: utid ?? undefined,
+ },
+ track: new TraceProcessorCounterTrack(
+ ctx,
+ uri,
+ {
+ unit: unit ?? undefined,
+ },
+ trackId,
+ title,
+ ),
+ });
+ } else if (hasData) {
+ for (const trackId of trackIds) {
+ this.trackIdToUri.set(trackId, uri);
+ }
+ ctx.tracks.registerTrack({
+ uri,
+ title,
+ tags: {
+ kind,
+ trackIds: trackIds,
+ upid: upid ?? undefined,
+ utid: utid ?? undefined,
+ },
+ track: new TraceProcessorSliceTrack(ctx, uri, undefined, trackIds),
+ });
+ }
+ const parent = findParentTrackNode(
+ ctx,
+ processGroupsPlugin,
+ trackIdToTrackNode,
+ parentId ?? undefined,
+ upid ?? undefined,
+ utid ?? undefined,
+ hasChildren,
+ );
+ const node = new TrackNode({
+ title,
+ sortOrder: orderId,
+ isSummary: hasData === 0,
+ uri: uri,
+ });
+ parent.addChildInOrder(node);
+ trackIdToTrackNode.set(trackIds[0], node);
+ }
+
+ ctx.selection.registerSqlSelectionResolver({
+ sqlTableName: 'slice',
+ callback: async (eventId: number) => {
+ const res = await ctx.engine.query(`
+ select
+ track_id as trackId
+ from slice
+ where slice.id = ${eventId}
+ `);
+ const firstRow = res.maybeFirstRow({
+ trackId: NUM,
+ });
+ if (!firstRow) return undefined;
+ const trackId = firstRow.trackId;
+ const trackUri = this.trackIdToUri.get(trackId);
+ if (!trackUri) return undefined;
+ return {trackUri, eventId};
+ },
+ });
+ }
+}
+
+function findParentTrackNode(
+ ctx: Trace,
+ processGroupsPlugin: ProcessThreadGroupsPlugin,
+ trackIdToTrackNode: Map<number, TrackNode>,
+ parentId: number | undefined,
+ upid: number | undefined,
+ utid: number | undefined,
+ hasChildren: number,
+): TrackNode {
+ if (parentId !== undefined) {
+ return assertExists(trackIdToTrackNode.get(parentId));
+ }
+ if (utid !== undefined) {
+ return assertExists(processGroupsPlugin.getGroupForThread(utid));
+ }
+ if (upid !== undefined) {
+ return assertExists(processGroupsPlugin.getGroupForProcess(upid));
+ }
+ if (hasChildren) {
+ return ctx.workspace.tracks;
+ }
+ const id = `/track_event_root`;
+ let node = ctx.workspace.getTrackById(id);
+ if (node === undefined) {
+ node = new TrackNode({
+ id,
+ title: 'Global Track Events',
+ isSummary: true,
+ });
+ ctx.workspace.addChildInOrder(node);
+ }
+ return node;
+}
diff --git a/ui/src/plugins/dev.perfetto.VizPage/viz_page.ts b/ui/src/plugins/dev.perfetto.VizPage/viz_page.ts
index 7a76d76..97b6ada 100644
--- a/ui/src/plugins/dev.perfetto.VizPage/viz_page.ts
+++ b/ui/src/plugins/dev.perfetto.VizPage/viz_page.ts
@@ -41,7 +41,6 @@
initialText: attrs.spec,
onUpdate: (text: string) => {
attrs.setSpec(text);
- attrs.trace.scheduleFullRedraw();
},
}),
);
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
index c1306d6..57b1018 100644
--- a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
@@ -55,10 +55,9 @@
import {SegmentedButtons} from '../../widgets/segmented_buttons';
import {MiddleEllipsis} from '../../widgets/middle_ellipsis';
import {Chip, ChipBar} from '../../widgets/chip';
-import {TrackWidget} from '../../widgets/track_widget';
-import {scheduleFullRedraw} from '../../widgets/raf';
+import {TrackShell} from '../../widgets/track_shell';
import {CopyableLink} from '../../widgets/copyable_link';
-import {VirtualOverlayCanvas} from '../../widgets/virtual_overlay_canvas';
+import {VirtualOverlayCanvas} from '../../components/widgets/virtual_overlay_canvas';
import {SplitPanel} from '../../widgets/split_panel';
import {TabbedSplitPanel} from '../../widgets/tabbed_split_panel';
@@ -313,7 +312,6 @@
intent: Intent.Primary,
onclick: () => {
portalOpen = !portalOpen;
- scheduleFullRedraw();
},
}),
portalOpen &&
@@ -365,7 +363,6 @@
label: 'Close Popup',
onclick: () => {
popupOpen = !popupOpen;
- scheduleFullRedraw();
},
}),
);
@@ -501,7 +498,6 @@
label: key,
onchange: () => {
this.optValues[key] = !Boolean(this.optValues[key]);
- scheduleFullRedraw();
},
});
}
@@ -515,7 +511,6 @@
value: this.optValues[key],
oninput: (e: Event) => {
this.optValues[key] = (e.target as HTMLInputElement).value;
- scheduleFullRedraw();
},
}),
);
@@ -533,7 +528,6 @@
this.optValues[key] = Number.parseInt(
(e.target as HTMLInputElement).value,
);
- scheduleFullRedraw();
},
}),
);
@@ -553,7 +547,6 @@
onchange: (e: Event) => {
const el = e.target as HTMLSelectElement;
this.optValues[key] = el.value;
- scheduleFullRedraw();
},
},
optionElements,
@@ -645,14 +638,12 @@
onTagAdd: (tag) => {
tags.push(tag);
tagInputValue = '';
- scheduleFullRedraw();
},
onChange: (value) => {
tagInputValue = value;
},
onTagRemove: (index) => {
tags.splice(index, 1);
- scheduleFullRedraw();
},
});
},
@@ -669,7 +660,6 @@
selectedOption: selectedIdx,
onOptionSelected: (num) => {
selectedIdx = num;
- scheduleFullRedraw();
},
});
},
@@ -869,7 +859,6 @@
diffs.forEach(({id, checked}) => {
options[id] = checked;
});
- scheduleFullRedraw();
},
...rest,
}),
@@ -896,7 +885,6 @@
diffs.forEach(({id, checked}) => {
options[id] = checked;
});
- scheduleFullRedraw();
},
...rest,
}),
@@ -1028,7 +1016,7 @@
{
icon: Icons.ContextMenu,
},
- 'SELECT * FROM raw WHERE id = 123',
+ 'SELECT * FROM ftrace_event WHERE id = 123',
),
},
m(MenuItem, {
@@ -1276,7 +1264,6 @@
offset: rowOffset,
rows,
};
- scheduleFullRedraw();
},
};
return m(VirtualTable, attrs);
@@ -1333,22 +1320,29 @@
},
}),
m(WidgetShowcase, {
- label: 'Track',
- description: `The shell and content DOM elements of a track.`,
+ label: 'TrackShell',
+ description: `The Mithril parts of a track (the shell, mainly).`,
renderWidget: (opts) => {
- const {buttons, chips, multipleTracks, ...rest} = opts;
+ const {buttons, chips, multipleTracks, error, ...rest} = opts;
const dummyButtons = () => [
m(Button, {icon: 'info', compact: true}),
m(Button, {icon: 'settings', compact: true}),
];
const dummyChips = () => ['foo', 'bar'];
- const renderTrack = () =>
- m(TrackWidget, {
- buttons: Boolean(buttons) ? dummyButtons() : undefined,
- chips: Boolean(chips) ? dummyChips() : undefined,
- ...rest,
- });
+ const renderTrack = (children?: m.Children) =>
+ m(
+ TrackShell,
+ {
+ buttons: Boolean(buttons) ? dummyButtons() : undefined,
+ chips: Boolean(chips) ? dummyChips() : undefined,
+ error: Boolean(error)
+ ? new Error('An error has occurred')
+ : undefined,
+ ...rest,
+ },
+ children,
+ );
return m(
'',
@@ -1366,14 +1360,15 @@
buttons: true,
chips: true,
heightPx: 32,
- indentationLevel: 3,
collapsible: true,
collapsed: true,
- isSummary: false,
+ summary: false,
highlight: false,
error: false,
multipleTracks: false,
reorderable: false,
+ depth: 0,
+ lite: false,
},
}),
m(WidgetShowcase, {
@@ -1510,7 +1505,6 @@
},
view: function (vnode: m.Vnode<{}, {progress: number}>) {
vnode.state.progress = (vnode.state.progress + 1) % 100;
- scheduleFullRedraw();
return m(
'div',
m('div', 'You should see an animating progress bar'),
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/event_latency_details_panel.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/event_latency_details_panel.ts
index 5f026db..5eeacc8 100644
--- a/ui/src/plugins/org.chromium.ChromeScrollJank/event_latency_details_panel.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/event_latency_details_panel.ts
@@ -45,7 +45,7 @@
} from './scroll_jank_cause_link_utils';
import {ScrollJankCauseMap} from './scroll_jank_cause_map';
import {sliceRef} from '../../components/widgets/slice';
-import {JANKS_TRACK_URI, renderSliceRef} from './selection_utils';
+import {JANKS_TRACK_URI, renderSliceRef} from './utils';
import {TrackEventDetailsPanel} from '../../public/details_panel';
import {Trace} from '../../public/trace';
@@ -161,7 +161,6 @@
this.trace.engine,
asSliceSqlId(this.id),
);
- this.trace.scheduleFullRedraw();
}
async loadJankSlice() {
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/index.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/index.ts
index cc5affc..fd1edf5 100644
--- a/ui/src/plugins/org.chromium.ChromeScrollJank/index.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/index.ts
@@ -35,7 +35,7 @@
await this.addEventLatencyTrack(ctx, group);
await this.addScrollJankV3ScrollTrack(ctx, group);
await ScrollJankCauseMap.initialize(ctx.engine);
- this.addScrollTimelineTrack(ctx, group);
+ await this.addScrollTimelineTrack(ctx, group);
ctx.workspace.addChildInOrder(group);
group.expand();
}
@@ -193,14 +193,21 @@
group.addChildInOrder(track);
}
- private addScrollTimelineTrack(ctx: Trace, group: TrackNode) {
+ private async addScrollTimelineTrack(
+ ctx: Trace,
+ group: TrackNode,
+ ): Promise<void> {
const uri = 'org.chromium.ChromeScrollJank#scrollTimeline';
const title = 'Chrome Scroll Timeline';
+ const tableName =
+ 'scrolltimelinetrack_org_chromium_ChromeScrollJank_scrollTimeline';
+ await ScrollTimelineTrack.createTableForTrack(ctx, tableName);
+
ctx.tracks.registerTrack({
uri,
title,
- track: new ScrollTimelineTrack(ctx, uri),
+ track: new ScrollTimelineTrack(ctx, uri, tableName),
});
const track = new TrackNode({uri, title});
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/jank_colors.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/jank_colors.ts
index 5f572f5..fad569d 100644
--- a/ui/src/plugins/org.chromium.ChromeScrollJank/jank_colors.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/jank_colors.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {HSLColor} from '../../public/color';
+import {HSLColor} from '../../base/color';
import {makeColorScheme} from '../../components/colorizer';
export const JANK_COLOR = makeColorScheme(new HSLColor([343, 100, 43]));
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_details_panel.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_details_panel.ts
index e68e58b..4ef92f0 100644
--- a/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_details_panel.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_details_panel.ts
@@ -43,7 +43,7 @@
getPredictorJankDeltas,
getPresentedScrollDeltas,
} from './scroll_delta_graph';
-import {JANKS_TRACK_URI, renderSliceRef} from './selection_utils';
+import {JANKS_TRACK_URI, renderSliceRef} from './utils';
import {TrackEventDetailsPanel} from '../../public/details_panel';
import {Trace} from '../../public/trace';
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_v3_details_panel.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_v3_details_panel.ts
index 410e635..2e007ef 100644
--- a/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_v3_details_panel.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_v3_details_panel.ts
@@ -27,7 +27,7 @@
import {SqlRef} from '../../widgets/sql_ref';
import {MultiParagraphText, TextParagraph} from '../../widgets/text_paragraph';
import {dictToTreeNodes, Tree, TreeNode} from '../../widgets/tree';
-import {EVENT_LATENCY_TRACK_URI, renderSliceRef} from './selection_utils';
+import {EVENT_LATENCY_TRACK_URI, renderSliceRef} from './utils';
import {TrackEventDetailsPanel} from '../../public/details_panel';
import {Trace} from '../../public/trace';
@@ -137,9 +137,7 @@
};
await this.loadJankyFrames();
-
await this.loadSlices();
- this.trace.scheduleFullRedraw();
}
private hasCause(): boolean {
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_timeline_details_panel.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_timeline_details_panel.ts
new file mode 100644
index 0000000..6f9a75d
--- /dev/null
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_timeline_details_panel.ts
@@ -0,0 +1,219 @@
+// Copyright (C) 2025 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 {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
+import {LONG, NUM_NULL, STR} from '../../trace_processor/query_result';
+import {DetailsShell} from '../../widgets/details_shell';
+import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
+import {Duration, duration, Time, time} from '../../base/time';
+import {assertExists, assertTrue} from '../../base/logging';
+import {Section} from '../../widgets/section';
+import {Tree, TreeNode} from '../../widgets/tree';
+import {Timestamp} from '../../components/widgets/timestamp';
+import {DurationWidget} from '../../components/widgets/duration';
+import {SqlRef} from '../../widgets/sql_ref';
+import {asSliceSqlId} from '../../components/sql_utils/core_types';
+import {fromSqlBool} from './utils';
+
+export class ScrollTimelineDetailsPanel implements TrackEventDetailsPanel {
+ // Information about the scroll update *slice*, which was emitted by
+ // ScrollTimelineTrack.
+ // Source: this.tableName[id=this.id]
+ private sliceData?: {
+ name: string;
+ ts: time;
+ dur: duration;
+ // ID of the scroll update in chrome_scroll_update_info.
+ scrollUpdateId: bigint;
+ };
+
+ // Information about the scroll *update*, which comes from the Chrome tracing
+ // stdlib.
+ // Source: chrome_scroll_update_info[id=this.sliceData.scrollUpdateId]
+ private scrollData?: {
+ vsyncInterval: duration | undefined;
+ isPresented: boolean | undefined;
+ isJanky: boolean | undefined;
+ isInertial: boolean | undefined;
+ isFirstScrollUpdateInScroll: boolean | undefined;
+ isFirstScrollUpdateInFrame: boolean | undefined;
+ };
+
+ constructor(
+ private readonly trace: Trace,
+ private readonly tableName: string,
+ // ID of the slice in tableName.
+ private readonly id: number,
+ ) {}
+
+ async load(): Promise<void> {
+ await this.querySliceData();
+ await this.queryScrollData();
+ }
+
+ private async querySliceData(): Promise<void> {
+ assertTrue(this.sliceData === undefined);
+ const queryResult = await this.trace.engine.query(`
+ SELECT
+ name,
+ ts,
+ dur,
+ scroll_update_id
+ FROM ${this.tableName}
+ WHERE id = ${this.id}`);
+ const row = queryResult.firstRow({
+ name: STR,
+ ts: LONG,
+ dur: LONG,
+ scroll_update_id: LONG,
+ });
+ this.sliceData = {
+ name: row.name,
+ ts: Time.fromRaw(row.ts),
+ dur: Duration.fromRaw(row.dur),
+ scrollUpdateId: row.scroll_update_id,
+ };
+ }
+
+ private async queryScrollData(): Promise<void> {
+ assertExists(this.sliceData);
+ assertTrue(this.scrollData === undefined);
+ const queryResult = await this.trace.engine.query(`
+ INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
+ SELECT
+ vsync_interval_ms,
+ is_presented,
+ is_janky,
+ is_inertial,
+ is_first_scroll_update_in_scroll,
+ is_first_scroll_update_in_frame
+ FROM chrome_scroll_update_info
+ WHERE id = ${this.sliceData!.scrollUpdateId}`);
+ const row = queryResult.firstRow({
+ vsync_interval_ms: NUM_NULL,
+ is_presented: NUM_NULL,
+ is_janky: NUM_NULL,
+ is_inertial: NUM_NULL,
+ is_first_scroll_update_in_scroll: NUM_NULL,
+ is_first_scroll_update_in_frame: NUM_NULL,
+ });
+ this.scrollData = {
+ vsyncInterval:
+ row.vsync_interval_ms === null
+ ? undefined
+ : Duration.fromMillis?.(row.vsync_interval_ms),
+ isPresented: fromSqlBool(row.is_presented),
+ isJanky: fromSqlBool(row.is_janky),
+ isInertial: fromSqlBool(row.is_inertial),
+ isFirstScrollUpdateInScroll: fromSqlBool(
+ row.is_first_scroll_update_in_scroll,
+ ),
+ isFirstScrollUpdateInFrame: fromSqlBool(
+ row.is_first_scroll_update_in_frame,
+ ),
+ };
+ }
+
+ render(): m.Children {
+ return m(
+ DetailsShell,
+ {
+ title: 'Slice',
+ description: this.sliceData?.name ?? 'Loading...',
+ },
+ m(
+ GridLayout,
+ m(GridLayoutColumn, this.renderSliceDetails()),
+ m(GridLayoutColumn, this.renderScrollDetails()),
+ ),
+ );
+ }
+
+ private renderSliceDetails(): m.Child {
+ let child;
+ if (this.sliceData === undefined) {
+ child = 'Loading...';
+ } else {
+ child = m(
+ Tree,
+ m(TreeNode, {
+ left: 'Name',
+ right: this.sliceData.name,
+ }),
+ m(TreeNode, {
+ left: 'Start time',
+ right: m(Timestamp, {ts: this.sliceData.ts}),
+ }),
+ m(TreeNode, {
+ left: 'Duration',
+ right: m(DurationWidget, {dur: this.sliceData.dur}),
+ }),
+ m(TreeNode, {
+ left: 'SQL ID',
+ right: m(SqlRef, {
+ table: this.tableName,
+ id: asSliceSqlId(this.id),
+ }),
+ }),
+ );
+ }
+ return m(Section, {title: 'Slice details'}, child);
+ }
+
+ private renderScrollDetails(): m.Child {
+ let child;
+ if (this.sliceData === undefined || this.scrollData === undefined) {
+ child = 'Loading...';
+ } else {
+ child = m(
+ Tree,
+ m(TreeNode, {
+ left: 'Vsync interval',
+ right:
+ this.scrollData.vsyncInterval === undefined
+ ? `${this.scrollData.vsyncInterval}`
+ : m(DurationWidget, {dur: this.scrollData.vsyncInterval}),
+ }),
+ m(TreeNode, {
+ left: 'Is presented',
+ right: `${this.scrollData.isPresented}`,
+ }),
+ m(TreeNode, {
+ left: 'Is janky',
+ right: `${this.scrollData.isJanky}`,
+ }),
+ m(TreeNode, {
+ left: 'Is inertial',
+ right: `${this.scrollData.isInertial}`,
+ }),
+ m(TreeNode, {
+ left: 'Is first scroll update in scroll',
+ right: `${this.scrollData.isFirstScrollUpdateInScroll}`,
+ }),
+ m(TreeNode, {
+ left: 'Is first scroll update in frame',
+ right: `${this.scrollData.isFirstScrollUpdateInFrame}`,
+ }),
+ m(TreeNode, {
+ left: 'SQL ID',
+ // TODO: b/383990024 - Make this a clickable reference.
+ right: `chrome_scroll_update_info[${this.sliceData.scrollUpdateId}]`,
+ }),
+ );
+ }
+ return m(Section, {title: 'Scroll details'}, child);
+ }
+}
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_timeline_track.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_timeline_track.ts
index a5fb4b6..081feda 100644
--- a/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_timeline_track.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_timeline_track.ts
@@ -12,164 +12,205 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {createPerfettoTable} from '../../trace_processor/sql_utils';
import {generateSqlWithInternalLayout} from '../../components/sql_utils/layout';
import {
NAMED_ROW,
- NamedRow,
NamedSliceTrack,
} from '../../components/tracks/named_slice_track';
import {Slice} from '../../public/track';
-import {sqlNameSafe} from '../../base/string_utils';
-import {SqlTableSliceTrackDetailsPanel} from '../../components/tracks/sql_table_slice_track_details_tab';
import {Trace} from '../../public/trace';
import {TrackEventDetailsPanel} from '../../public/details_panel';
import {TrackEventSelection} from '../../public/selection';
+import {NUM, STR, STR_NULL} from '../../trace_processor/query_result';
+import {escapeQuery} from '../../trace_processor/query_utils';
+import {Engine} from '../../trace_processor/engine';
+import {rows} from './utils';
+import {ColorScheme} from '../../base/color_scheme';
+import {JANK_COLOR} from './jank_colors';
+import {makeColorScheme} from '../../components/colorizer';
+import {HSLColor} from '../../base/color';
+import {ScrollTimelineDetailsPanel} from './scroll_timeline_details_panel';
interface StepTemplate {
- step_name: string;
- ts_column_name: string;
- dur_column_name: string;
+ // The name of a stage of a scroll.
+ // WARNING: This could be an arbitrary string so it MUST BE ESCAPED before
+ // using in an SQL query.
+ stepName: string;
+ // The name of the column in `chrome_scroll_update_info` which contains the
+ // timestamp of the step. If not null, this is guaranteed to be a valid column
+ // name, i.e. it's safe to use inline in an SQL query without any additional
+ // sanitization.
+ tsColumnName: string | null;
+ // The name of the column in `chrome_scroll_update_info` which contains the
+ // duration of the step. Null if the stage doesn't have a duration. If not
+ // null, this is guaranteed to be a valid column name, i.e. it's safe to use
+ // inline in an SQL query without any additional sanitization.
+ durColumnName: string | null;
}
-// TODO: b/383547343 - Migrate STEP_TEMPLATES to a Chrome tracing stdlib table
-// once it's stable.
-const STEP_TEMPLATES: readonly StepTemplate[] = [
- {
- step_name: 'GenerationToBrowserMain',
- ts_column_name: 'generation_ts',
- dur_column_name: 'generation_to_browser_main_dur',
- },
- {
- step_name: 'TouchMoveProcessing',
- ts_column_name: 'touch_move_received_ts',
- dur_column_name: 'touch_move_processing_dur',
- },
- {
- step_name: 'ScrollUpdateProcessing',
- ts_column_name: 'scroll_update_created_ts',
- dur_column_name: 'scroll_update_processing_dur',
- },
- {
- step_name: 'BrowserMainToRendererCompositor',
- ts_column_name: 'scroll_update_created_end_ts',
- dur_column_name: 'browser_to_compositor_delay_dur',
- },
- {
- step_name: 'RendererCompositorDispatch',
- ts_column_name: 'compositor_dispatch_ts',
- dur_column_name: 'compositor_dispatch_dur',
- },
- {
- step_name: 'RendererCompositorDispatchToOnBeginFrame',
- ts_column_name: 'compositor_dispatch_end_ts',
- dur_column_name: 'compositor_dispatch_to_on_begin_frame_delay_dur',
- },
- {
- step_name: 'RendererCompositorBeginFrame',
- ts_column_name: 'compositor_on_begin_frame_ts',
- dur_column_name: 'compositor_on_begin_frame_dur',
- },
- {
- step_name: 'RendererCompositorBeginToGenerateFrame',
- ts_column_name: 'compositor_on_begin_frame_end_ts',
- dur_column_name: 'compositor_on_begin_frame_to_generation_delay_dur',
- },
- {
- step_name: 'RendererCompositorGenerateToSubmitFrame',
- ts_column_name: 'compositor_generate_compositor_frame_ts',
- dur_column_name: 'compositor_generate_frame_to_submit_frame_dur',
- },
- {
- step_name: 'RendererCompositorSubmitFrame',
- ts_column_name: 'compositor_submit_compositor_frame_ts',
- dur_column_name: 'compositor_submit_frame_dur',
- },
- {
- step_name: 'RendererCompositorToViz',
- ts_column_name: 'compositor_submit_compositor_frame_end_ts',
- dur_column_name: 'compositor_to_viz_delay_dur',
- },
- {
- step_name: 'VizReceiveFrame',
- ts_column_name: 'viz_receive_compositor_frame_ts',
- dur_column_name: 'viz_receive_compositor_frame_dur',
- },
- {
- step_name: 'VizReceiveToDrawFrame',
- ts_column_name: 'viz_receive_compositor_frame_end_ts',
- dur_column_name: 'viz_wait_for_draw_dur',
- },
- {
- step_name: 'VizDrawToSwapFrame',
- ts_column_name: 'viz_draw_and_swap_ts',
- dur_column_name: 'viz_draw_and_swap_dur',
- },
- {
- step_name: 'VizToGpu',
- ts_column_name: 'viz_send_buffer_swap_end_ts',
- dur_column_name: 'viz_to_gpu_delay_dur',
- },
- {
- step_name: 'VizSwapBuffers',
- ts_column_name: 'viz_swap_buffers_ts',
- dur_column_name: 'viz_swap_buffers_dur',
- },
- {
- step_name: 'VizSwapBuffersToLatch',
- ts_column_name: 'viz_swap_buffers_end_ts',
- dur_column_name: 'viz_swap_buffers_to_latch_dur',
- },
- {
- step_name: 'VizLatchToSwapEnd',
- ts_column_name: 'latch_timestamp',
- dur_column_name: 'viz_latch_to_swap_end_dur',
- },
- {
- step_name: 'VizSwapEndToPresentation',
- ts_column_name: 'swap_end_timestamp',
- dur_column_name: 'swap_end_to_presentation_dur',
- },
- {
- // An artificial step to ensure that presentation_timestamp is included in
- // the calculation of scroll_update_bounds. It's filtered out in
- // unordered_slices due to NULL duration.
- step_name: '',
- ts_column_name: 'presentation_timestamp',
- dur_column_name: 'NULL',
- },
-];
+/**
+ * Classification of a scroll update for the purposes of trace visualization.
+ *
+ * If a scroll update matches multiple classifications (e.g. janky and
+ * inertial), it should be classified with the highest-priority one (e.g.
+ * janky). With the exception of `DEFAULT` and `STEP`, the values are sorted in
+ * the order of descending priority (i.e. `JANKY` has the highest priority).
+ */
+enum ScrollUpdateClassification {
+ // None of the other classifications apply.
+ DEFAULT = 0,
-export class ScrollTimelineTrack extends NamedSliceTrack<Slice, NamedRow> {
- private readonly tableName;
+ // The corresponding frame was janky.
+ // See `chrome_scroll_update_input_info.is_janky`.
+ JANKY = 1,
- constructor(trace: Trace, uri: string) {
- super(trace, uri);
- this.tableName = `scrolltimelinetrack_${sqlNameSafe(uri)}`;
+ // The input was coalesced into an earlier input's frame.
+ // See `chrome_scroll_update_input_info.is_first_scroll_update_in_frame`.
+ COALESCED = 2,
+
+ // It's the first scroll update in a scroll.
+ // Note: A first scroll update can never be janky.
+ // See `chrome_scroll_update_input_info.is_first_scroll_update_in_scroll`.
+ FIRST_SCROLL_UPDATE_IN_FRAME = 3,
+
+ // The corresponding scroll was inertial (i.e. a fling).
+ INERTIAL = 4,
+
+ // Sentinel value for slices which represent sub-steps of a scroll update.
+ STEP = -1,
+}
+
+const INDIGO = makeColorScheme(new HSLColor([231, 48, 48]));
+const GRAY = makeColorScheme(new HSLColor([0, 0, 62]));
+const DARK_GREEN = makeColorScheme(new HSLColor([120, 44, 34]));
+const TEAL = makeColorScheme(new HSLColor([187, 90, 42]));
+
+function toColorScheme(
+ classification: ScrollUpdateClassification,
+): ColorScheme | undefined {
+ switch (classification) {
+ case ScrollUpdateClassification.DEFAULT:
+ return INDIGO;
+ case ScrollUpdateClassification.JANKY:
+ return JANK_COLOR;
+ case ScrollUpdateClassification.COALESCED:
+ return GRAY;
+ case ScrollUpdateClassification.FIRST_SCROLL_UPDATE_IN_FRAME:
+ return DARK_GREEN;
+ case ScrollUpdateClassification.INERTIAL:
+ return TEAL;
+ case ScrollUpdateClassification.STEP:
+ return undefined;
}
- override async onInit(): Promise<AsyncDisposable> {
- await super.onInit();
- await this.engine.query(`INCLUDE PERFETTO MODULE chrome.chrome_scrolls;`);
+}
+
+/**
+ * If `allowedColumnNames` contains `columnName`, returns `columnName`.
+ * Otherwise, returns null.
+ */
+function checkColumnNameIsValidOrReturnNull(
+ columnName: string | null,
+ allowedColumnNames: Set<string>,
+ errorMessagePrefix: string,
+): string | null {
+ if (columnName == null || allowedColumnNames.has(columnName)) {
+ return columnName;
+ } else {
+ console.error(
+ `${errorMessagePrefix}: ${columnName}
+ (allowed column names: ${Array.from(allowedColumnNames).join(', ')})`,
+ );
+ return null;
+ }
+}
+
+const SCROLL_TIMELINE_TRACK_ROW = {
+ ...NAMED_ROW,
+ classification: NUM,
+};
+type ScrollTimelineTrackRow = typeof SCROLL_TIMELINE_TRACK_ROW;
+
+export class ScrollTimelineTrack extends NamedSliceTrack<
+ Slice,
+ ScrollTimelineTrackRow
+> {
+ /**
+ * Constructs a scroll timeline track for a given `trace`.
+ *
+ * @param trace - The trace whose data the track will display
+ * @param uri - The URI of the track
+ * @param tableName - The name of an existing SQL table which contains
+ * information about the slices of the track. IMPORTANT: You must create
+ * the table first using {@link ScrollTimelineTrack.createTableForTrack}
+ * BEFORE creating this track.
+ */
+ constructor(
+ trace: Trace,
+ uri: string,
+ private readonly tableName: string,
+ ) {
+ super(trace, uri);
+ }
+
+ override getSqlSource(): string {
+ return `SELECT * FROM ${this.tableName}`;
+ }
+
+ override getRowSpec(): ScrollTimelineTrackRow {
+ return SCROLL_TIMELINE_TRACK_ROW;
+ }
+
+ override rowToSlice(row: ScrollTimelineTrackRow): Slice {
+ const baseSlice = super.rowToSliceBase(row);
+ const colorScheme = toColorScheme(row.classification);
+ if (colorScheme === undefined) {
+ return baseSlice;
+ } else {
+ return {...baseSlice, colorScheme};
+ }
+ }
+
+ override detailsPanel(sel: TrackEventSelection): TrackEventDetailsPanel {
+ return new ScrollTimelineDetailsPanel(
+ this.trace,
+ this.tableName,
+ sel.eventId,
+ );
+ }
+
+ /**
+ * Creates a Perfetto table named `tableName` representing the slices of a
+ * {@link ScrollTimelineTrack} for a given `trace`. You can use this table to
+ * construct the track.
+ */
+ static async createTableForTrack(
+ trace: Trace,
+ tableName: string,
+ ): Promise<void> {
+ const engine = trace.engine;
+ const stepTemplates = await ScrollTimelineTrack.queryStepTemplates(engine);
// TODO: b/383549233 - Set ts+dur of each scroll update directly based on
// our knowledge of the scrolling pipeline (as opposed to aggregating over
// scroll_steps).
- return await createPerfettoTable(
- this.engine,
- this.tableName,
- `WITH
+ await engine.query(
+ `INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
+ CREATE PERFETTO TABLE ${tableName} AS
+ WITH
-- Unpivot all ts+dur columns into rows. Each row corresponds to a step
-- of a particular scroll update. Some of the rows might have null
-- ts/dur values, which will be filtered out in unordered_slices.
- -- |scroll_steps| = |chrome_scroll_update_info| * |STEP_TEMPLATES|
- scroll_steps AS (${STEP_TEMPLATES.map(
- (step) => `
- SELECT
- id AS scroll_id,
- ${step.ts_column_name} AS ts,
- ${step.dur_column_name} AS dur,
- '${step.step_name}' AS name
- FROM chrome_scroll_update_info`,
- ).join(' UNION ALL ')}),
+ -- |scroll_steps| = |chrome_scroll_update_info| * |stepTemplates|
+ scroll_steps AS (${stepTemplates
+ .map(
+ (step) => `
+ SELECT
+ id AS scroll_update_id,
+ ${step.tsColumnName ?? 'NULL'} AS ts,
+ ${step.durColumnName ?? 'NULL'} AS dur,
+ ${escapeQuery(step.stepName)} AS name
+ FROM chrome_scroll_update_info`,
+ )
+ .join(' UNION ALL ')}),
-- For each scroll update, find its ts+dur by aggregating over all steps
-- within the scroll update. We're basically trying to find MIN(COL1_ts,
-- COL2_ts, ..., COLn_ts) and MAX(COL1_ts, COL2_ts, ..., COLn_ts) from
@@ -179,24 +220,61 @@
-- values) than the scalar MIN/MAX functions (which return null if any
-- argument is null). That's why we do it in such a roundabout way by
-- joining the top-level table with the individual steps.
- scroll_update_bounds AS (
+ scroll_updates_with_bounds AS (
SELECT
- scroll_update.id AS scroll_id,
+ scroll_update.id AS scroll_update_id,
MIN(scroll_steps.ts) AS ts,
- MAX(scroll_steps.ts) - MIN(scroll_steps.ts) AS dur
+ MAX(scroll_steps.ts) - MIN(scroll_steps.ts) AS dur,
+ -- Combine all applicable scroll update classifications into the
+ -- name. For example, if a scroll update is both janky and inertial,
+ -- its name will be name 'Janky Inertial Scroll Update'.
+ CONCAT_WS(
+ ' ',
+ IIF(scroll_update.is_janky, 'Janky', NULL),
+ IIF(scroll_update.is_first_scroll_update_in_scroll, 'First', NULL),
+ IIF(
+ NOT scroll_update.is_first_scroll_update_in_frame,
+ 'Coalesced',
+ NULL
+ ),
+ IIF(scroll_update.is_inertial, 'Inertial', NULL),
+ 'Scroll Update'
+ ) AS name,
+ -- Pick the highest-priority applicable scroll update
+ -- classification. For example, if a scroll update is both janky and
+ -- inertial, classify it as janky.
+ CASE
+ WHEN scroll_update.is_janky
+ THEN ${ScrollUpdateClassification.JANKY}
+ WHEN scroll_update.is_first_scroll_update_in_scroll
+ THEN ${ScrollUpdateClassification.FIRST_SCROLL_UPDATE_IN_FRAME}
+ WHEN NOT scroll_update.is_first_scroll_update_in_frame
+ THEN ${ScrollUpdateClassification.COALESCED}
+ WHEN scroll_update.is_inertial
+ THEN ${ScrollUpdateClassification.INERTIAL}
+ ELSE ${ScrollUpdateClassification.DEFAULT}
+ END AS classification
FROM
chrome_scroll_update_info AS scroll_update
- JOIN scroll_steps ON scroll_steps.scroll_id = scroll_update.id
+ JOIN scroll_steps ON scroll_steps.scroll_update_id = scroll_update.id
GROUP BY scroll_update.id
),
-- Now that we know the ts+dur of all scroll updates, we can lay them
-- out efficiently (i.e. assign depths to them to avoid overlaps).
- scroll_update_layouts AS (
+ scroll_updates_with_layouts AS (
${generateSqlWithInternalLayout({
- columns: ['scroll_id', 'ts', 'dur'],
- sourceTable: 'scroll_update_bounds',
+ columns: [
+ 'scroll_update_id',
+ 'ts',
+ 'dur',
+ 'name',
+ 'classification',
+ ],
+ sourceTable: 'scroll_updates_with_bounds',
ts: 'ts',
dur: 'dur',
+ // Filter out scroll updates with no timestamps. See b/388756942.
+ whereClause: 'ts IS NOT NULL AND dur IS NOT NULL',
})}
),
-- We interleave the top-level scroll update slices (at even depths) and
@@ -206,16 +284,20 @@
ts,
dur,
2 * depth AS depth,
- 'Scroll Update' AS name
- FROM scroll_update_layouts
+ name,
+ classification,
+ scroll_update_id
+ FROM scroll_updates_with_layouts
UNION ALL
SELECT
scroll_steps.ts,
MAX(scroll_steps.dur, 0) AS dur,
- 2 * scroll_update_layouts.depth + 1 AS depth,
- scroll_steps.name
+ 2 * scroll_updates_with_layouts.depth + 1 AS depth,
+ scroll_steps.name,
+ ${ScrollUpdateClassification.STEP} AS classification,
+ scroll_updates_with_layouts.scroll_update_id
FROM scroll_steps
- JOIN scroll_update_layouts USING(scroll_id)
+ JOIN scroll_updates_with_layouts USING(scroll_update_id)
WHERE scroll_steps.ts IS NOT NULL AND scroll_steps.dur IS NOT NULL
)
-- Finally, we sort all slices chronologically and assign them
@@ -230,23 +312,64 @@
);
}
- override getSqlSource(): string {
- return `SELECT * FROM ${this.tableName}`;
- }
-
- override getRowSpec(): NamedRow {
- return NAMED_ROW;
- }
-
- override rowToSlice(row: NamedRow): Slice {
- return super.rowToSliceBase(row);
- }
-
- override detailsPanel(sel: TrackEventSelection): TrackEventDetailsPanel {
- return new SqlTableSliceTrackDetailsPanel(
- this.trace,
- this.tableName,
- sel.eventId,
+ /**
+ * Queries scroll step templates from
+ * `chrome_scroll_update_info_step_templates`.
+ *
+ * This function sanitizes the column names `StepTemplate.ts_column_name` and
+ * `StepTemplate.dur_column_name`. Unless null, the returned column names are
+ * guaranteed to be valid column names of `chrome_scroll_update_info`.
+ */
+ private static async queryStepTemplates(
+ engine: Engine,
+ ): Promise<StepTemplate[]> {
+ // Use a set for faster lookups.
+ const columnNames = new Set(
+ await ScrollTimelineTrack.queryChromeScrollUpdateInfoColumnNames(engine),
);
+ const stepTemplatesResult = await engine.query(`
+ INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
+ SELECT
+ step_name,
+ ts_column_name,
+ dur_column_name
+ FROM chrome_scroll_update_info_step_templates;`);
+ return rows(stepTemplatesResult, {
+ step_name: STR,
+ ts_column_name: STR_NULL,
+ dur_column_name: STR_NULL,
+ }).map(
+ // We defensively verify that the column names actually exist in the
+ // `chrome_scroll_update_info` table. We do this because we cannot update
+ // the `chrome_scroll_update_info` table and this plugin atomically
+ // (`chrome_scroll_update_info` is a part of the Chrome tracing stdlib,
+ // whose source of truth is in the Chromium repository).
+ (row) => ({
+ stepName: row.step_name,
+ tsColumnName: checkColumnNameIsValidOrReturnNull(
+ row.ts_column_name,
+ columnNames,
+ 'Invalid ts_column_name in chrome_scroll_update_info_step_templates',
+ ),
+ durColumnName: checkColumnNameIsValidOrReturnNull(
+ row.dur_column_name,
+ columnNames,
+ 'Invalid dur_column_name in chrome_scroll_update_info_step_templates',
+ ),
+ }),
+ );
+ }
+
+ /** Returns the names of columns of the `chrome_scroll_update_info` table. */
+ private static async queryChromeScrollUpdateInfoColumnNames(
+ engine: Engine,
+ ): Promise<string[]> {
+ // See https://www.sqlite.org/pragma.html#pragfunc and
+ // https://www.sqlite.org/pragma.html#pragma_table_info for more information
+ // about `pragma_table_info`.
+ const columnNamesResult = await engine.query(`
+ INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
+ SELECT name FROM pragma_table_info('chrome_scroll_update_info');`);
+ return rows(columnNamesResult, {name: STR}).map((row) => row.name);
}
}
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/selection_utils.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/selection_utils.ts
deleted file mode 100644
index 4b79e05..0000000
--- a/ui/src/plugins/org.chromium.ChromeScrollJank/selection_utils.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-import {Anchor} from '../../widgets/anchor';
-import {Icons} from '../../base/semantic_icons';
-import {Trace} from '../../public/trace';
-
-export const SCROLLS_TRACK_URI = 'perfetto.ChromeScrollJank#toplevelScrolls';
-export const EVENT_LATENCY_TRACK_URI = 'perfetto.ChromeScrollJank#eventLatency';
-export const JANKS_TRACK_URI = 'perfetto.ChromeScrollJank#scrollJankV3';
-
-export function renderSliceRef(args: {
- trace: Trace;
- id: number;
- trackUri: string;
- title: m.Children;
-}) {
- return m(
- Anchor,
- {
- icon: Icons.UpdateSelection,
- onclick: () => {
- args.trace.selection.selectTrackEvent(args.trackUri, args.id, {
- scrollToSelection: true,
- });
- },
- },
- args.title,
- );
-}
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/utils.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/utils.ts
new file mode 100644
index 0000000..1801336
--- /dev/null
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/utils.ts
@@ -0,0 +1,77 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {Anchor} from '../../widgets/anchor';
+import {Icons} from '../../base/semantic_icons';
+import {Trace} from '../../public/trace';
+import {QueryResult, Row} from '../../trace_processor/query_result';
+
+export const SCROLLS_TRACK_URI = 'perfetto.ChromeScrollJank#toplevelScrolls';
+export const EVENT_LATENCY_TRACK_URI = 'perfetto.ChromeScrollJank#eventLatency';
+export const JANKS_TRACK_URI = 'perfetto.ChromeScrollJank#scrollJankV3';
+
+export function renderSliceRef(args: {
+ trace: Trace;
+ id: number;
+ trackUri: string;
+ title: m.Children;
+}) {
+ return m(
+ Anchor,
+ {
+ icon: Icons.UpdateSelection,
+ onclick: () => {
+ args.trace.selection.selectTrackEvent(args.trackUri, args.id, {
+ scrollToSelection: true,
+ });
+ },
+ },
+ args.title,
+ );
+}
+
+/**
+ * Returns an array of the rows in `queryResult`.
+ *
+ * Warning: Only use this function in contexts where the number of rows is
+ * guaranteed to be small. Prefer doing transformations in SQL where possible.
+ */
+export function rows<R extends Row>(queryResult: QueryResult, spec: R): R[] {
+ const results: R[] = [];
+ for (const it = queryResult.iter(spec); it.valid(); it.next()) {
+ const row: Row = {};
+ for (const key of Object.keys(spec)) {
+ row[key] = it[key];
+ }
+ results.push(row as R);
+ }
+ return results;
+}
+
+/**
+ * Converts a number to a boolean according to SQLite's conversion rules.
+ *
+ * See https://www.sqlite.org/lang_expr.html#boolean_expressions.
+ */
+export function fromSqlBool(value: number): boolean;
+export function fromSqlBool(value: null): undefined;
+export function fromSqlBool(value: number | null): boolean | undefined;
+export function fromSqlBool(value: number | null): boolean | undefined {
+ if (value === null) {
+ return undefined;
+ } else {
+ return value !== 0;
+ }
+}
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/utils_unittest.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/utils_unittest.ts
new file mode 100644
index 0000000..90f46b7
--- /dev/null
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/utils_unittest.ts
@@ -0,0 +1,76 @@
+// Copyright (C) 2025 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 protos from '../../protos';
+import {
+ createQueryResult,
+ LONG_NULL,
+ NUM,
+ STR,
+} from '../../trace_processor/query_result';
+import {fromSqlBool, rows} from './utils';
+
+const T = protos.QueryResult.CellsBatch.CellType;
+
+test('rows', () => {
+ // Manually construct a QueryResult which is equivalent to running the
+ // following SQL query:
+ //
+ // SELECT
+ // column1,
+ // column2,
+ // column3
+ // FROM (
+ // VALUES
+ // ('A', 10, 100),
+ // ('B', 20, 200),
+ // ('C', 30, NULL)
+ // )
+ // ORDER BY column1 ASC
+ const batch = protos.QueryResult.CellsBatch.create({
+ cells: [
+ [T.CELL_STRING, T.CELL_FLOAT64, T.CELL_VARINT],
+ [T.CELL_STRING, T.CELL_FLOAT64, T.CELL_VARINT],
+ [T.CELL_STRING, T.CELL_FLOAT64, T.CELL_NULL],
+ ].flat(),
+ stringCells: ['A', 'B', 'C'].join('\0'),
+ float64Cells: [10, 20, 30],
+ varintCells: [100, 200],
+ isLastBatch: true,
+ });
+ const resultProto = protos.QueryResult.create({
+ columnNames: ['column1', 'column2', 'column3'],
+ batch: [batch],
+ });
+ const queryResult = createQueryResult({query: 'Some query'});
+ queryResult.appendResultBatch(
+ protos.QueryResult.encode(resultProto).finish(),
+ );
+
+ expect(
+ rows(queryResult, {column1: STR, column2: NUM, column3: LONG_NULL}),
+ ).toStrictEqual([
+ {column1: 'A', column2: 10, column3: 100n},
+ {column1: 'B', column2: 20, column3: 200n},
+ {column1: 'C', column2: 30, column3: null},
+ ]);
+});
+
+test('fromSqlBool', () => {
+ expect(fromSqlBool(null)).toBeUndefined();
+ expect(fromSqlBool(-1)).toStrictEqual(true);
+ expect(fromSqlBool(0)).toStrictEqual(false);
+ expect(fromSqlBool(0.1)).toStrictEqual(true);
+ expect(fromSqlBool(1)).toStrictEqual(true);
+});
diff --git a/ui/src/plugins/org.kernel.LinuxKernelSubsystems/index.ts b/ui/src/plugins/org.kernel.LinuxKernelSubsystems/index.ts
index b43f249..1cd149c 100644
--- a/ui/src/plugins/org.kernel.LinuxKernelSubsystems/index.ts
+++ b/ui/src/plugins/org.kernel.LinuxKernelSubsystems/index.ts
@@ -15,15 +15,15 @@
import {NUM, STR_NULL} from '../../trace_processor/query_result';
import {Trace} from '../../public/trace';
import {PerfettoPlugin} from '../../public/plugin';
-import {AsyncSliceTrack} from '../dev.perfetto.AsyncSlices/async_slice_track';
+import {TraceProcessorSliceTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_slice_track';
import {SLICE_TRACK_KIND} from '../../public/track_kinds';
import {TrackNode} from '../../public/workspace';
-import AsyncSlicesPlugin from '../dev.perfetto.AsyncSlices';
+import TraceProcessorTrackPlugin from '../dev.perfetto.TraceProcessorTrack';
// This plugin renders visualizations of subsystems of the Linux kernel.
export default class implements PerfettoPlugin {
static readonly id = 'org.kernel.LinuxKernelSubsystems';
- static readonly dependencies = [AsyncSlicesPlugin];
+ static readonly dependencies = [TraceProcessorTrackPlugin];
async onTraceLoad(ctx: Trace): Promise<void> {
const kernel = new TrackNode({
@@ -66,7 +66,7 @@
ctx.tracks.registerTrack({
uri,
title,
- track: new AsyncSliceTrack(ctx, uri, 0, [trackId]),
+ track: new TraceProcessorSliceTrack(ctx, uri, 0, [trackId]),
tags: {
kind: SLICE_TRACK_KIND,
trackIds: [trackId],
diff --git a/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts b/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts
index ca1e88c..caf75d1 100644
--- a/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts
+++ b/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import {NUM, STR_NULL} from '../../trace_processor/query_result';
-import {AsyncSliceTrack} from '../dev.perfetto.AsyncSlices/async_slice_track';
+import {TraceProcessorSliceTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_slice_track';
import {PerfettoPlugin} from '../../public/plugin';
import {Trace} from '../../public/trace';
import {TrackNode} from '../../public/workspace';
@@ -21,12 +21,12 @@
import {SuspendResumeDetailsPanel} from './suspend_resume_details';
import {ThreadMap} from '../dev.perfetto.Thread/threads';
import ThreadPlugin from '../dev.perfetto.Thread';
-import AsyncSlicesPlugin from '../dev.perfetto.AsyncSlices';
+import TraceProcessorTrackPlugin from '../dev.perfetto.TraceProcessorTrack';
// SuspendResumeSliceTrack exists so as to override the `onSliceClick` function
// in AsyncSliceTrack.
// TODO(stevegolton): Remove this?
-class SuspendResumeSliceTrack extends AsyncSliceTrack {
+class SuspendResumeSliceTrack extends TraceProcessorSliceTrack {
constructor(
trace: Trace,
uri: string,
@@ -44,7 +44,7 @@
export default class implements PerfettoPlugin {
static readonly id = 'org.kernel.SuspendResumeLatency';
- static readonly dependencies = [ThreadPlugin, AsyncSlicesPlugin];
+ static readonly dependencies = [ThreadPlugin, TraceProcessorTrackPlugin];
async onTraceLoad(ctx: Trace): Promise<void> {
const threads = ctx.plugins.getPlugin(ThreadPlugin).getThreadMap();
diff --git a/ui/src/plugins/org.kernel.Wattson/thread_aggregator.ts b/ui/src/plugins/org.kernel.Wattson/thread_aggregator.ts
index 7b5a530..867c1c4 100644
--- a/ui/src/plugins/org.kernel.Wattson/thread_aggregator.ts
+++ b/ui/src/plugins/org.kernel.Wattson/thread_aggregator.ts
@@ -55,7 +55,7 @@
-- Only get idle attribution in user defined window and filter by selected
-- CPUs and GROUP BY thread
- CREATE OR REPLACE PERFETTO TABLE _per_thread_idle_attribution AS
+ CREATE OR REPLACE PERFETTO TABLE _per_thread_idle_cost AS
SELECT
ROUND(SUM(idle_cost_mws), 2) as idle_cost_mws,
utid
@@ -141,7 +141,7 @@
tid,
pid
FROM _unioned_per_cpu_total
- LEFT JOIN _per_thread_idle_attribution USING (utid)
+ LEFT JOIN _per_thread_idle_cost USING (utid)
GROUP BY utid;
`;
diff --git a/ui/src/public/app.ts b/ui/src/public/app.ts
index 0c8321b..03ad92e 100644
--- a/ui/src/public/app.ts
+++ b/ui/src/public/app.ts
@@ -21,6 +21,7 @@
import {Trace} from './trace';
import {PageManager} from './page';
import {FeatureFlagManager} from './feature_flag';
+import {Raf} from './raf';
/**
* The API endpoint to interact programmaticaly with the UI before a trace has
@@ -52,9 +53,10 @@
*/
readonly trace?: Trace;
- // TODO(primiano): this should be needed in extremely rare cases. We should
- // probably switch to mithril auto-redraw at some point.
- scheduleFullRedraw(force?: 'force'): void;
+ /**
+ * Used to schedule things.
+ */
+ readonly raf: Raf;
/**
* Navigate to a new page.
diff --git a/ui/src/public/raf.ts b/ui/src/public/raf.ts
new file mode 100644
index 0000000..cfa1d8e
--- /dev/null
+++ b/ui/src/public/raf.ts
@@ -0,0 +1,36 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export type RedrawCallback = () => void;
+
+export interface Raf {
+ /**
+ * Schedule both a DOM and canvas redraw.
+ */
+ scheduleFullRedraw(): void;
+
+ /**
+ * Schedule a canvas redraw only.
+ */
+ scheduleCanvasRedraw(): void;
+
+ /**
+ * Add a callback for canvas redraws. `cb` will be called whenever a canvas
+ * redraw is scheduled canvas redraw using {@link scheduleCanvasRedraw()}.
+ *
+ * @param cb - The callback to called when canvas are redrawn.
+ * @returns - A disposable object that removes the callback when disposed.
+ */
+ addCanvasRedrawCallback(cb: RedrawCallback): Disposable;
+}
diff --git a/ui/src/public/selection.ts b/ui/src/public/selection.ts
index 945b398..5f1e9dc 100644
--- a/ui/src/public/selection.ts
+++ b/ui/src/public/selection.ts
@@ -13,6 +13,7 @@
// limitations under the License.
import {time, duration, TimeSpan} from '../base/time';
+import {Dataset, DatasetSchema} from '../trace_processor/dataset';
import {Engine} from '../trace_processor/engine';
import {ColumnDef, Sorting, ThreadStateExtra} from './aggregation';
import {TrackDescriptor} from './track';
@@ -74,12 +75,51 @@
registerSqlSelectionResolver(resolver: SqlSelectionResolver): void;
}
+/**
+ * Aggregator tabs are displayed in descending order of specificity, determined
+ * by the following precedence hierarchy:
+ * 1. Aggregators explicitly defining a `trackKind` string take priority over
+ * those that do not.
+ * 2. Otherwise, aggregators with schemas containing a greater number of keys
+ * (higher specificity) are prioritized over those with fewer keys.
+ * 3. In cases of identical specificity, tabs are ranked based on their
+ * registration order.
+ */
export interface AreaSelectionAggregator {
readonly id: string;
- createAggregateView(engine: Engine, area: AreaSelection): Promise<boolean>;
+
+ /**
+ * If defined, the dataset passed to `createAggregateView` will only contain
+ * tracks with a matching `kind` tag.
+ */
+ readonly trackKind?: string;
+
+ /**
+ * If defined, the dataset passed to `createAggregateView` will only contain
+ * tracks that export datasets that implement this schema.
+ */
+ readonly schema?: DatasetSchema;
+
+ /**
+ * Creates a view for the aggregated data corresponding to the selected area.
+ *
+ * The dataset provided will be filtered based on the `trackKind` and `schema`
+ * if these properties are defined.
+ *
+ * @param engine - The query engine used to execute queries.
+ * @param area - The currently selected area to aggregate.
+ * @param dataset - The dataset representing a union of the data in the
+ * selected tracks.
+ */
+ createAggregateView(
+ engine: Engine,
+ area: AreaSelection,
+ dataset?: Dataset,
+ ): Promise<boolean>;
getExtra(
engine: Engine,
area: AreaSelection,
+ dataset?: Dataset,
): Promise<ThreadStateExtra | void>;
getTabName(): string;
getDefaultSorting(): Sorting;
@@ -162,6 +202,7 @@
JAVA_HEAP_SAMPLES = 'heap_profile:com.android.art',
JAVA_HEAP_GRAPH = 'graph',
PERF_SAMPLE = 'perf',
+ INSTRUMENTS_SAMPLE = 'instruments',
}
export function profileType(s: string): ProfileType {
diff --git a/ui/src/public/standard_groups.ts b/ui/src/public/standard_groups.ts
deleted file mode 100644
index 2bc7570..0000000
--- a/ui/src/public/standard_groups.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {TrackNode, TrackNodeArgs, Workspace} from './workspace';
-
-/**
- * Gets or creates a group for user interaction
- *
- * @param workspace - The workspace on which to create the group.
- */
-export function getOrCreateUserInteractionGroup(
- workspace: Workspace,
-): TrackNode {
- return getOrCreateGroup(workspace, 'user_interaction', {
- title: 'User Interaction',
- collapsed: false, // Expand this by default
- isSummary: true,
- });
-}
-
-// Internal utility function to avoid duplicating the logic to get or create a
-// group by ID.
-function getOrCreateGroup(
- workspace: Workspace,
- id: string,
- args?: Omit<Partial<TrackNodeArgs>, 'id'>,
-): TrackNode {
- const group = workspace.getTrackById(id);
- if (group) {
- return group;
- } else {
- const group = new TrackNode({id, ...args});
- workspace.addChildInOrder(group);
- return group;
- }
-}
diff --git a/ui/src/public/track.ts b/ui/src/public/track.ts
index 6d1b1dc..b10d4e0 100644
--- a/ui/src/public/track.ts
+++ b/ui/src/public/track.ts
@@ -17,7 +17,7 @@
import {Size2D, VerticalBounds} from '../base/geom';
import {TimeScale} from '../base/time_scale';
import {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
-import {ColorScheme} from './color_scheme';
+import {ColorScheme} from '../base/color_scheme';
import {TrackEventDetailsPanel} from './details_panel';
import {TrackEventDetails, TrackEventSelection} from './selection';
import {Dataset} from '../trace_processor/dataset';
diff --git a/ui/src/public/track_kinds.ts b/ui/src/public/track_kinds.ts
index d01e43e..3c31edf 100644
--- a/ui/src/public/track_kinds.ts
+++ b/ui/src/public/track_kinds.ts
@@ -22,6 +22,8 @@
export const EXPECTED_FRAMES_SLICE_TRACK_KIND = 'ExpectedFramesSliceTrack';
export const ACTUAL_FRAMES_SLICE_TRACK_KIND = 'ActualFramesSliceTrack';
export const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack';
+export const INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND =
+ 'InstrumentsSamplesProfileTrack';
export const COUNTER_TRACK_KIND = 'CounterTrack';
export const CPUSS_ESTIMATE_TRACK_KIND = 'CpuSubsystemEstimateTrack';
export const CPU_PROFILE_TRACK_KIND = 'CpuProfileTrack';
diff --git a/ui/src/public/utils.ts b/ui/src/public/utils.ts
index 657bed6..a5f9b1b 100644
--- a/ui/src/public/utils.ts
+++ b/ui/src/public/utils.ts
@@ -73,14 +73,14 @@
return `${name} ${uid}`;
} else if (hasName) {
return `${name}`;
- } else if (hasUpid && hasPid && hasProcessName) {
- return `${processName} ${pid}`;
- } else if (hasUpid && hasPid) {
- return `Process ${pid}`;
} else if (hasThreadName && hasTid) {
return `${threadName} ${tid}`;
} else if (hasTid) {
return `Thread ${tid}`;
+ } else if (hasUpid && hasPid && hasProcessName) {
+ return `${processName} ${pid}`;
+ } else if (hasUpid && hasPid) {
+ return `Process ${pid}`;
} else if (hasUpid) {
return `upid: ${upid}${kindSuffix}`;
} else if (hasUtid) {
diff --git a/ui/src/public/workspace.ts b/ui/src/public/workspace.ts
index 013771d..3c3955b 100644
--- a/ui/src/public/workspace.ts
+++ b/ui/src/public/workspace.ts
@@ -443,7 +443,7 @@
}
/**
- * Find a track node by its id.
+ * Get a track node by its id.
*
* Node: This is an O(1) operation.
*
@@ -455,14 +455,14 @@
}
/**
- * Find a track node via its URI.
+ * Get a track node via its URI.
*
* Node: This is an O(1) operation.
*
* @param uri The uri of the track to find.
* @returns The node or undefined if no such node exists with this URI.
*/
- findTrackByUri(uri: string): TrackNode | undefined {
+ getTrackByUri(uri: string): TrackNode | undefined {
return this.tracksByUri.get(uri);
}
@@ -597,21 +597,24 @@
}
/**
- * Find a track node via its URI.
+ * Get a track node via its URI.
*
- * Note: This in an O(N) operation where N is the number of nodes in the
- * workspace.
+ * Node: This is an O(1) operation.
*
* @param uri The uri of the track to find.
- * @returns A reference to the track node if it exists in this workspace,
- * otherwise undefined.
+ * @returns The node or undefined if no such node exists with this URI.
*/
- findTrackByUri(uri: string): TrackNode | undefined {
+ getTrackByUri(uri: string): TrackNode | undefined {
return this.tracks.flatTracks.find((t) => t.uri === uri);
}
/**
- * Find a track by ID, also searching pinned tracks.
+ * Get a track node by its id.
+ *
+ * Node: This is an O(1) operation.
+ *
+ * @param id The id of the node we want to find.
+ * @returns The node or undefined if no such node exists.
*/
getTrackById(id: string): TrackNode | undefined {
return (
diff --git a/ui/src/public/workspace_unittest.ts b/ui/src/public/workspace_unittest.ts
index 333496a..215510e 100644
--- a/ui/src/public/workspace_unittest.ts
+++ b/ui/src/public/workspace_unittest.ts
@@ -64,7 +64,7 @@
expect(workspace.getTrackById('bar')).toBe(undefined);
});
- test('findTrackByUri()', () => {
+ test('getTrackByUri()', () => {
const workspace = new Workspace();
const group = new TrackNode();
@@ -75,7 +75,7 @@
// Add group to workspace
workspace.addChildLast(group);
- expect(workspace.findTrackByUri('foo')).toBe(track);
+ expect(workspace.getTrackByUri('foo')).toBe(track);
});
test('findClosestVisibleAncestor()', () => {
diff --git a/ui/src/test/aggregation.test.ts b/ui/src/test/aggregation.test.ts
index 449d23b..3913c48 100644
--- a/ui/src/test/aggregation.test.ts
+++ b/ui/src/test/aggregation.test.ts
@@ -64,7 +64,7 @@
test('frametimeline', async () => {
await page.keyboard.press('Escape');
- const sysui = pth.locateTrackGroup('com.android.systemui 25348');
+ const sysui = pth.locateTrack('com.android.systemui 25348');
await sysui.scrollIntoViewIfNeeded();
await pth.toggleTrackGroup(sysui);
const actualTimeline = pth.locateTrack(
@@ -81,7 +81,7 @@
test('slices', async () => {
await page.keyboard.press('Escape');
- const syssrv = pth.locateTrackGroup('system_server 1719');
+ const syssrv = pth.locateTrack('system_server 1719');
await syssrv.scrollIntoViewIfNeeded();
await pth.toggleTrackGroup(syssrv);
const animThread = pth
diff --git a/ui/src/test/chrome_missing_track_names.test.ts b/ui/src/test/chrome_missing_track_names.test.ts
index 3f20bc0..54d7652 100644
--- a/ui/src/test/chrome_missing_track_names.test.ts
+++ b/ui/src/test/chrome_missing_track_names.test.ts
@@ -31,6 +31,6 @@
});
test('expand all tracks', async () => {
- await page.click('.header-panel-container button[title="Expand all"]');
+ await page.click('.pf-viewer-page__header button[title="Expand all"]');
await pth.waitForIdleAndScreenshot('all_tracks_expanded.png');
});
diff --git a/ui/src/test/chrome_rendering_desktop.test.ts b/ui/src/test/chrome_rendering_desktop.test.ts
index 8cc57ee..f3212c2 100644
--- a/ui/src/test/chrome_rendering_desktop.test.ts
+++ b/ui/src/test/chrome_rendering_desktop.test.ts
@@ -31,7 +31,7 @@
});
test('expand browser', async () => {
- const grp = pth.locateTrackGroup('Browser 12685');
+ const grp = pth.locateTrack('Browser 12685');
grp.scrollIntoViewIfNeeded();
await pth.toggleTrackGroup(grp);
await pth.waitForIdleAndScreenshot('browser_expanded.png');
diff --git a/ui/src/test/ftrace_tracks_and_tab.test.ts b/ui/src/test/ftrace_tracks_and_tab.test.ts
index 7bed34b..3c4303d 100644
--- a/ui/src/test/ftrace_tracks_and_tab.test.ts
+++ b/ui/src/test/ftrace_tracks_and_tab.test.ts
@@ -27,7 +27,8 @@
});
test('ftrace tracks', async () => {
- await page.click('h1[ref="Ftrace Events"]');
+ const ftraceGroupTrack = pth.locateTrack('Ftrace Events');
+ await pth.toggleTrackGroup(ftraceGroupTrack);
await pth.waitForIdleAndScreenshot('ftrace_events.png');
});
diff --git a/ui/src/test/independent_features.test.ts b/ui/src/test/independent_features.test.ts
index c761ded..20f0c68 100644
--- a/ui/src/test/independent_features.test.ts
+++ b/ui/src/test/independent_features.test.ts
@@ -23,7 +23,7 @@
const page = await browser.newPage();
const pth = new PerfettoTestHelper(page);
await pth.openTraceFile('api32_startup_warm.perfetto-trace');
- const trackGroup = pth.locateTrackGroup(
+ const trackGroup = pth.locateTrack(
'androidx.benchmark.integration.macrobenchmark.test 7527',
);
await trackGroup.scrollIntoViewIfNeeded();
diff --git a/ui/src/test/load_and_tracks.test.ts b/ui/src/test/load_and_tracks.test.ts
index f02611c..3aaa994 100644
--- a/ui/src/test/load_and_tracks.test.ts
+++ b/ui/src/test/load_and_tracks.test.ts
@@ -71,9 +71,9 @@
});
test('track expand and collapse', async () => {
- const trackGroup = pth.locateTrackGroup('traced_probes 1054');
+ const trackGroup = pth.locateTrack('traced_probes 1054');
await trackGroup.scrollIntoViewIfNeeded();
- await trackGroup.click();
+ await pth.toggleTrackGroup(trackGroup);
await pth.waitForIdleAndScreenshot('traced_probes_expanded.png');
// Click 5 times in rapid succession.
@@ -85,7 +85,7 @@
});
test('pin tracks', async () => {
- const trackGroup = pth.locateTrackGroup('traced 1055');
+ const trackGroup = pth.locateTrack('traced 1055');
await pth.toggleTrackGroup(trackGroup);
let track = pth.locateTrack('traced 1055/mem.rss', trackGroup);
await pth.pinTrackUsingShellBtn(track);
diff --git a/ui/src/test/perfetto_ui_test_helper.ts b/ui/src/test/perfetto_ui_test_helper.ts
index 20e1f82..d2e5b16 100644
--- a/ui/src/test/perfetto_ui_test_helper.ts
+++ b/ui/src/test/perfetto_ui_test_helper.ts
@@ -82,21 +82,13 @@
await expect.soft(this.page).toHaveScreenshot(screenshotName, opts);
}
- locateTrackGroup(name: string): Locator {
- return this.page
- .locator('.pf-panel-group')
- .filter({has: this.page.locator(`h1[ref="${name}"]`)});
- }
-
async toggleTrackGroup(locator: Locator) {
- await locator.locator('.pf-track-title').first().click();
+ await locator.locator('.pf-track__shell').first().click();
await this.waitForPerfettoIdle();
}
locateTrack(name: string, trackGroup?: Locator): Locator {
- return (trackGroup ?? this.page)
- .locator('.pf-track')
- .filter({has: this.page.locator(`h1[ref="${name}"]`)});
+ return (trackGroup ?? this.page).locator(`.pf-track[ref="${name}"]`);
}
pinTrackUsingShellBtn(track: Locator) {
diff --git a/ui/src/test/track_event_ordered_tracks.test.ts b/ui/src/test/track_event_ordered_tracks.test.ts
index 8f39a3a..1097e37 100644
--- a/ui/src/test/track_event_ordered_tracks.test.ts
+++ b/ui/src/test/track_event_ordered_tracks.test.ts
@@ -31,7 +31,7 @@
});
test('chronological order', async () => {
- const chronologicalGrp = pth.locateTrackGroup('Root Chronological');
+ const chronologicalGrp = pth.locateTrack('Root Chronological');
await chronologicalGrp.scrollIntoViewIfNeeded();
await pth.toggleTrackGroup(chronologicalGrp);
@@ -39,7 +39,7 @@
});
test('explicit order', async () => {
- const explicitGrp = pth.locateTrackGroup('Root Explicit');
+ const explicitGrp = pth.locateTrack('Root Explicit');
await explicitGrp.scrollIntoViewIfNeeded();
await pth.toggleTrackGroup(explicitGrp);
@@ -47,7 +47,7 @@
});
test('lexicographic tracks', async () => {
- const lexicographicGrp = pth.locateTrackGroup('Root Lexicographic');
+ const lexicographicGrp = pth.locateTrack('Root Lexicographic');
await lexicographicGrp.scrollIntoViewIfNeeded();
await pth.toggleTrackGroup(lexicographicGrp);
diff --git a/ui/src/test/wattson.test.ts b/ui/src/test/wattson.test.ts
index 9bff14c..6fe3ee3 100644
--- a/ui/src/test/wattson.test.ts
+++ b/ui/src/test/wattson.test.ts
@@ -40,7 +40,7 @@
});
test('wattson aggregations', async () => {
- const wattsonGrp = pth.locateTrackGroup('Wattson');
+ const wattsonGrp = pth.locateTrack('Wattson');
await wattsonGrp.scrollIntoViewIfNeeded();
await pth.toggleTrackGroup(wattsonGrp);
const cpuEstimate = pth.locateTrack('Wattson/Cpu0 Estimate', wattsonGrp);
diff --git a/ui/src/trace_processor/dataset.ts b/ui/src/trace_processor/dataset.ts
index 25c64cb..42441b2 100644
--- a/ui/src/trace_processor/dataset.ts
+++ b/ui/src/trace_processor/dataset.ts
@@ -104,7 +104,7 @@
* Defines a list of columns and types that define the shape of the data
* represented by a dataset.
*/
-export type DatasetSchema = Record<string, ColumnType>;
+export type DatasetSchema = Readonly<Record<string, ColumnType>>;
/**
* A filter used to express that a column must equal a value.
diff --git a/ui/src/trace_processor/engine.ts b/ui/src/trace_processor/engine.ts
index 3d8541e..e959cbf 100644
--- a/ui/src/trace_processor/engine.ts
+++ b/ui/src/trace_processor/engine.ts
@@ -539,23 +539,25 @@
export class EngineProxy implements Engine, Disposable {
private engine: EngineBase;
private tag: string;
- private _isAlive: boolean;
+ private disposed = false;
constructor(engine: EngineBase, tag: string) {
this.engine = engine;
this.tag = tag;
- this._isAlive = true;
}
async query(query: string, tag?: string): Promise<QueryResult> {
- if (!this._isAlive) {
- throw new Error(`EngineProxy ${this.tag} was disposed.`);
+ if (this.disposed) {
+ // If we are disposed (the trace was closed), return an empty QueryResult
+ // that will never see any data or EOF. We can't do otherwise or it will
+ // cause crashes to code calling firstRow() and expecting data.
+ return createQueryResult({query});
}
return await this.engine.query(query, tag);
}
async tryQuery(query: string, tag?: string): Promise<Result<QueryResult>> {
- if (!this._isAlive) {
+ if (this.disposed) {
return errResult(`EngineProxy ${this.tag} was disposed`);
}
return await this.engine.tryQuery(query, tag);
@@ -565,8 +567,8 @@
metrics: string[],
format: 'json' | 'prototext' | 'proto',
): Promise<string | Uint8Array> {
- if (!this._isAlive) {
- return Promise.reject(new Error(`EngineProxy ${this.tag} was disposed.`));
+ if (this.disposed) {
+ return defer<string>(); // Return a promise that will hang forever.
}
return this.engine.computeMetric(metrics, format);
}
@@ -600,7 +602,7 @@
}
[Symbol.dispose]() {
- this._isAlive = false;
+ this.disposed = true;
}
}
diff --git a/ui/src/trace_processor/http_rpc_engine.ts b/ui/src/trace_processor/http_rpc_engine.ts
index 6c87f27..b54efaf 100644
--- a/ui/src/trace_processor/http_rpc_engine.ts
+++ b/ui/src/trace_processor/http_rpc_engine.ts
@@ -126,6 +126,7 @@
[Symbol.dispose]() {
this.disposed = true;
+ this.connected = false;
const websocket = this.websocket;
this.websocket = undefined;
websocket?.close();
diff --git a/ui/src/trace_processor/proto_ring_buffer.ts b/ui/src/trace_processor/proto_ring_buffer.ts
index 5dfdac5..495c348 100644
--- a/ui/src/trace_processor/proto_ring_buffer.ts
+++ b/ui/src/trace_processor/proto_ring_buffer.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {assertTrue} from '../base/logging';
+import {assertTrue, assertUnreachable} from '../base/logging';
// This class is the TypeScript equivalent of the identically-named C++ class in
// //protozero/proto_ring_buffer.h. See comments in that header for a detailed
@@ -21,12 +21,23 @@
const kGrowBytes = 128 * 1024;
const kMaxMsgSize = 1024 * 1024 * 1024;
+// There are two ways the buffer can work:
+// 1. 'PROTO_PREAMBLE' is the case where the header before each packet is a
+// length-encoded protobuf message. This is the case when paring traces or
+// the TraceProcessor RPC protocol.
+// 2. 'FIXED_SIZE' is the case where the header is a 32-bit integer representing
+// the size of the message. This is used by the traced tracing protocol in
+// https://perfetto.dev/docs/design-docs/api-and-abi#socket-protocol.
+export type ProtoTokenizationMode = 'PROTO_PREAMBLE' | 'FIXED_SIZE';
+
export class ProtoRingBuffer {
private buf = new Uint8Array(kGrowBytes);
private fastpath?: Uint8Array;
private rd = 0;
private wr = 0;
+ constructor(private mode: ProtoTokenizationMode = 'PROTO_PREAMBLE') {}
+
// The caller must call ReadMessage() after each append() call.
// The |data| might be either copied in the internal ring buffer or returned
// (% subarray()) to the next ReadMessage() call.
@@ -46,7 +57,7 @@
if (dataLen === 0) return;
assertTrue(this.fastpath === undefined);
if (this.rd === this.wr) {
- const msg = ProtoRingBuffer.tryReadMessage(data, 0, dataLen);
+ const msg = this.tryReadMessage(data, 0, dataLen);
if (
msg !== undefined &&
msg.byteOffset + msg.length === data.byteOffset + dataLen
@@ -107,7 +118,7 @@
if (this.rd >= this.wr) {
return undefined; // Completely empty.
}
- const msg = ProtoRingBuffer.tryReadMessage(this.buf, this.rd, this.wr);
+ const msg = this.tryReadMessage(this.buf, this.rd, this.wr);
if (msg === undefined) return undefined;
assertTrue(msg.buffer === this.buf.buffer);
assertTrue(this.buf.byteOffset === 0);
@@ -120,7 +131,7 @@
return msg.slice();
}
- private static tryReadMessage(
+ private tryReadMessage(
data: Uint8Array,
dataStart: number,
dataEnd: number,
@@ -128,21 +139,34 @@
assertTrue(dataEnd <= data.length);
let pos = dataStart;
if (pos >= dataEnd) return undefined;
- const tag = data[pos++]; // Assume one-byte tag.
- if (tag >= 0x80 || (tag & 0x07) !== 2 /* len delimited */) {
- throw new Error(
- `RPC framing error, unexpected tag ${tag} @ offset ${pos - 1}`,
- );
- }
-
let len = 0;
- for (let shift = 0 /* no check */; ; shift += 7) {
- if (pos >= dataEnd) {
- return undefined; // Not enough data to read varint.
+
+ if (this.mode === 'PROTO_PREAMBLE') {
+ const tag = data[pos++]; // Assume one-byte tag.
+ if (tag >= 0x80 || (tag & 0x07) !== 2 /* len delimited */) {
+ throw new Error(
+ `RPC framing error, unexpected tag ${tag} @ offset ${pos - 1}`,
+ );
}
- const val = data[pos++];
- len |= ((val & 0x7f) << shift) >>> 0;
- if (val < 0x80) break;
+
+ for (let shift = 0 /* no check */; ; shift += 7) {
+ if (pos >= dataEnd) {
+ return undefined; // Not enough data to read varint.
+ }
+ const val = data[pos++];
+ len |= ((val & 0x7f) << shift) >>> 0;
+ if (val < 0x80) break;
+ }
+ } else if (this.mode === 'FIXED_SIZE') {
+ for (let i = 0; i < 4; i++) {
+ if (pos >= dataEnd) {
+ return undefined; // Not enough data to read a uint32.
+ }
+ const val = data[pos++] & 0xff;
+ len |= (val << (i * 8)) >>> 0;
+ }
+ } else {
+ assertUnreachable(this.mode);
}
if (len >= kMaxMsgSize) {
diff --git a/ui/src/widgets/basic_table.ts b/ui/src/widgets/basic_table.ts
index 64490e5..9474895 100644
--- a/ui/src/widgets/basic_table.ts
+++ b/ui/src/widgets/basic_table.ts
@@ -13,7 +13,6 @@
// limitations under the License.
import m from 'mithril';
-import {scheduleFullRedraw} from './raf';
export interface ColumnDescriptor<T> {
readonly title: m.Children;
@@ -161,8 +160,6 @@
if (e.dataTransfer !== null) {
e.dataTransfer.setDragImage(placeholderElement, 0, 0);
}
-
- scheduleFullRedraw();
},
ondragover: (e: DragEvent) => {
let target = e.target as HTMLElement;
@@ -194,13 +191,11 @@
: dest;
if (adjustedDest !== this.drag.to) {
this.drag.to = adjustedDest;
- scheduleFullRedraw();
}
},
ondragleave: (e: DragEvent) => {
if (this.drag?.to !== index) return;
this.drag.to = undefined;
- scheduleFullRedraw();
if (e.dataTransfer !== null) {
e.dataTransfer.dropEffect = 'none';
}
@@ -215,7 +210,6 @@
}
this.drag = undefined;
- scheduleFullRedraw();
},
},
cell.content,
diff --git a/ui/src/widgets/editor.ts b/ui/src/widgets/editor.ts
index 1187be0..59b48d2 100644
--- a/ui/src/widgets/editor.ts
+++ b/ui/src/widgets/editor.ts
@@ -21,7 +21,6 @@
import {assertExists} from '../base/logging';
import {DragGestureHandler} from '../base/drag_gesture_handler';
import {DisposableStack} from '../base/disposable_stack';
-import {scheduleFullRedraw} from './raf';
export interface EditorAttrs {
// Initial state for the editor.
@@ -65,7 +64,7 @@
text = selectedText;
}
onExecute(text);
- scheduleFullRedraw('force');
+ m.redraw();
return true;
},
});
@@ -77,7 +76,7 @@
view.update([tr]);
const text = view.state.doc.toString();
onUpdate(text);
- scheduleFullRedraw('force');
+ m.redraw();
};
}
diff --git a/ui/src/widgets/flamegraph.ts b/ui/src/widgets/flamegraph.ts
index 3c831dd..7a3b022 100644
--- a/ui/src/widgets/flamegraph.ts
+++ b/ui/src/widgets/flamegraph.ts
@@ -19,7 +19,6 @@
import {Button, ButtonBar} from './button';
import {EmptyState} from './empty_state';
import {Popup, PopupPosition} from './popup';
-import {scheduleFullRedraw} from './raf';
import {Select} from './select';
import {Spinner} from './spinner';
import {TagInput} from './tag_input';
@@ -248,7 +247,8 @@
m(
'.canvas-container[ref=canvas-container]',
{
- onscroll: () => scheduleFullRedraw(),
+ // This will trigger auto redraws
+ onscroll: () => {},
},
m(
Popup,
@@ -271,7 +271,6 @@
m(`canvas[ref=canvas]`, {
style: `height:${canvasHeight}px; width:100%`,
onmousemove: ({offsetX, offsetY}: MouseEvent) => {
- scheduleFullRedraw();
this.hoveredX = offsetX;
this.hoveredY = offsetY;
if (this.tooltipPos?.state === 'CLICK') {
@@ -309,7 +308,6 @@
) {
this.tooltipPos = undefined;
}
- scheduleFullRedraw();
},
onclick: ({offsetX, offsetY}: MouseEvent) => {
const renderNode = this.renderNodes?.find((n) =>
@@ -334,7 +332,6 @@
state: 'CLICK',
};
}
- scheduleFullRedraw();
},
ondblclick: ({offsetX, offsetY}: MouseEvent) => {
const renderNode = this.renderNodes?.find((n) =>
@@ -346,7 +343,6 @@
return;
}
this.zoomRegion = renderNode?.source;
- scheduleFullRedraw();
},
}),
),
@@ -513,7 +509,6 @@
...self.attrs.state,
selectedMetricName: el.value,
});
- scheduleFullRedraw();
},
},
attrs.metrics.map((x) => {
@@ -528,12 +523,10 @@
value: this.rawFilterText,
onChange: (value: string) => {
self.rawFilterText = value;
- scheduleFullRedraw();
},
onTagAdd: (tag: string) => {
self.rawFilterText = '';
self.attrs.onStateChange(updateState(self.attrs.state, tag));
- scheduleFullRedraw();
},
onTagRemove(index: number) {
if (index === self.attrs.state.filters.length) {
@@ -549,7 +542,6 @@
filters,
});
}
- scheduleFullRedraw();
},
onfocus() {
self.filterFocus = true;
@@ -572,7 +564,6 @@
...this.attrs.state,
view: {kind: num === 0 ? 'TOP_DOWN' : 'BOTTOM_UP'},
});
- scheduleFullRedraw();
},
disabled: this.attrs.state.view.kind === 'PIVOT',
}),
@@ -621,7 +612,6 @@
const filterButtonClick = (state: FlamegraphState) => {
this.attrs.onStateChange(state);
this.tooltipPos = undefined;
- scheduleFullRedraw();
};
const percent = displayPercentage(
@@ -677,7 +667,6 @@
label: 'Zoom',
onclick: () => {
this.zoomRegion = node.source;
- scheduleFullRedraw();
},
}),
m(Button, {
@@ -729,7 +718,7 @@
onclick: () => {
filterButtonClick({
...this.attrs.state,
- view: {kind: 'PIVOT', pivot: name},
+ view: {kind: 'PIVOT', pivot: `^${name}$`},
});
},
}),
@@ -938,35 +927,36 @@
function updateState(state: FlamegraphState, filter: string): FlamegraphState {
const lwr = filter.toLowerCase();
- if (lwr.startsWith('ss: ') || lwr.startsWith('show stack: ')) {
+ const splitFilterFn = (f: string) => f.split(':', 2)[1].trim();
+ if (lwr.startsWith('ss:') || lwr.startsWith('show stack:')) {
return addFilter(state, {
kind: 'SHOW_STACK',
- filter: filter.split(': ', 2)[1],
+ filter: splitFilterFn(filter),
});
- } else if (lwr.startsWith('hs: ') || lwr.startsWith('hide stack: ')) {
+ } else if (lwr.startsWith('hs:') || lwr.startsWith('hide stack:')) {
return addFilter(state, {
kind: 'HIDE_STACK',
- filter: filter.split(': ', 2)[1],
+ filter: splitFilterFn(filter),
});
- } else if (lwr.startsWith('sff: ') || lwr.startsWith('show from frame: ')) {
+ } else if (lwr.startsWith('sff:') || lwr.startsWith('show from frame:')) {
return addFilter(state, {
kind: 'SHOW_FROM_FRAME',
- filter: filter.split(': ', 2)[1],
+ filter: splitFilterFn(filter),
});
- } else if (lwr.startsWith('hf: ') || lwr.startsWith('hide frame: ')) {
+ } else if (lwr.startsWith('hf:') || lwr.startsWith('hide frame:')) {
return addFilter(state, {
kind: 'HIDE_FRAME',
- filter: filter.split(': ', 2)[1],
+ filter: splitFilterFn(filter),
});
- } else if (lwr.startsWith('p:') || lwr.startsWith('pivot: ')) {
+ } else if (lwr.startsWith('p:') || lwr.startsWith('pivot:')) {
return {
...state,
- view: {kind: 'PIVOT', pivot: filter.split(': ', 2)[1]},
+ view: {kind: 'PIVOT', pivot: splitFilterFn(filter)},
};
}
return addFilter(state, {
kind: 'SHOW_STACK',
- filter: filter,
+ filter: filter.trim(),
});
}
diff --git a/ui/src/widgets/hotkey_context.ts b/ui/src/widgets/hotkey_context.ts
index 767683e..20b9e6e 100644
--- a/ui/src/widgets/hotkey_context.ts
+++ b/ui/src/widgets/hotkey_context.ts
@@ -14,7 +14,6 @@
import m from 'mithril';
import {checkHotkey, Hotkey} from '../base/hotkeys';
-import {scheduleFullRedraw} from './raf';
export interface HotkeyConfig {
hotkey: Hotkey;
@@ -59,7 +58,7 @@
if (checkHotkey(hotkey, e)) {
e.preventDefault();
callback();
- scheduleFullRedraw('force');
+ m.redraw();
}
});
}
diff --git a/ui/src/widgets/modal.ts b/ui/src/widgets/modal.ts
index c07e6fe..28d2077 100644
--- a/ui/src/widgets/modal.ts
+++ b/ui/src/widgets/modal.ts
@@ -15,7 +15,6 @@
import m from 'mithril';
import {defer} from '../base/deferred';
import {Icon} from './icon';
-import {scheduleFullRedraw} from './raf';
// This module deals with modal dialogs. Unlike most components, here we want to
// render the DOM elements outside of the corresponding vdom tree. For instance
@@ -80,7 +79,7 @@
onbeforeremove(vnode: m.VnodeDOM<ModalAttrs>) {
const removePromise = defer<void>();
vnode.dom.addEventListener('animationend', () => {
- scheduleFullRedraw('force');
+ m.redraw();
removePromise.resolve();
});
vnode.dom.classList.add('modal-fadeout');
@@ -234,7 +233,7 @@
// evident why a redraw is requested.
export function redrawModal() {
if (currentModal !== undefined) {
- scheduleFullRedraw('force');
+ m.redraw();
}
}
@@ -253,7 +252,7 @@
return;
}
currentModal = undefined;
- scheduleFullRedraw('force');
+ m.redraw();
}
export function getCurrentModalKey(): string | undefined {
diff --git a/ui/src/widgets/multiselect.ts b/ui/src/widgets/multiselect.ts
index a91c8ee..e6d7f43 100644
--- a/ui/src/widgets/multiselect.ts
+++ b/ui/src/widgets/multiselect.ts
@@ -18,7 +18,6 @@
import {Checkbox} from './checkbox';
import {EmptyState} from './empty_state';
import {Popup, PopupPosition} from './popup';
-import {scheduleFullRedraw} from './raf';
import {TextInput} from './text_input';
import {Intent} from './common';
@@ -110,7 +109,6 @@
.filter(({checked}) => checked)
.map(({id}) => ({id, checked: false}));
onChange(diffs);
- scheduleFullRedraw();
},
disabled: !anyChecked,
}),
@@ -138,7 +136,6 @@
.filter(({checked}) => !checked)
.map(({id}) => ({id, checked: true}));
onChange(diffs);
- scheduleFullRedraw();
},
disabled: allChecked,
}),
@@ -151,7 +148,6 @@
.filter(({checked}) => checked)
.map(({id}) => ({id, checked: false}));
onChange(diffs);
- scheduleFullRedraw();
},
disabled: !anyChecked,
}),
@@ -170,7 +166,6 @@
oninput: (event: Event) => {
const eventTarget = event.target as HTMLTextAreaElement;
this.searchText = eventTarget.value;
- scheduleFullRedraw();
},
value: this.searchText,
placeholder: 'Filter options...',
@@ -185,7 +180,6 @@
return m(Button, {
onclick: () => {
this.searchText = '';
- scheduleFullRedraw();
},
label: '',
icon: 'close',
@@ -207,7 +201,6 @@
className: 'pf-multiselect-item',
onchange: () => {
onChange([{id, checked: !checked}]);
- scheduleFullRedraw();
},
});
});
diff --git a/ui/src/widgets/popup.ts b/ui/src/widgets/popup.ts
index ac8b563..ce0b8ae 100644
--- a/ui/src/widgets/popup.ts
+++ b/ui/src/widgets/popup.ts
@@ -19,7 +19,6 @@
import {classNames} from '../base/classnames';
import {findRef, isOrContains, toHTMLElement} from '../base/dom_utils';
import {assertExists} from '../base/logging';
-import {scheduleFullRedraw} from './raf';
type CustomModifier = Modifier<'sameWidth', {}>;
type ExtendedModifiers = StrictModifiers | CustomModifier;
@@ -352,13 +351,12 @@
if (this.isOpen) {
this.isOpen = false;
this.onChange(this.isOpen);
- scheduleFullRedraw('force');
+ m.redraw();
}
}
private togglePopup() {
this.isOpen = !this.isOpen;
this.onChange(this.isOpen);
- scheduleFullRedraw('force');
}
}
diff --git a/ui/src/widgets/table.ts b/ui/src/widgets/table.ts
index 1389907..806794a 100644
--- a/ui/src/widgets/table.ts
+++ b/ui/src/widgets/table.ts
@@ -22,7 +22,6 @@
SortDirection,
withDirection,
} from '../base/comparison_utils';
-import {scheduleFullRedraw} from './raf';
import {MenuItem, PopupMenu2} from './menu';
import {Button} from './button';
@@ -147,13 +146,11 @@
if (this._sortingInfo !== undefined) {
this.reorder(this._sortingInfo);
}
- scheduleFullRedraw();
}
resetOrder() {
this.permutation = range(this.data.length);
this._sortingInfo = undefined;
- scheduleFullRedraw();
}
get sortingInfo(): SortingInfo<T> | undefined {
@@ -168,7 +165,6 @@
info.direction,
),
);
- scheduleFullRedraw();
}
}
diff --git a/ui/src/widgets/track_shell.ts b/ui/src/widgets/track_shell.ts
new file mode 100644
index 0000000..a1f082b
--- /dev/null
+++ b/ui/src/widgets/track_shell.ts
@@ -0,0 +1,434 @@
+// 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 {classNames} from '../base/classnames';
+import {DisposableStack} from '../base/disposable_stack';
+import {currentTargetOffset} from '../base/dom_utils';
+import {Bounds2D, Point2D, Vector2D} from '../base/geom';
+import {assertExists} from '../base/logging';
+import {clamp} from '../base/math_utils';
+import {hasChildren, MithrilEvent} from '../base/mithril_utils';
+import {Icons} from '../base/semantic_icons';
+import {Button, ButtonBar} from './button';
+import {Chip, ChipBar} from './chip';
+import {HTMLAttrs, Intent} from './common';
+import {MiddleEllipsis} from './middle_ellipsis';
+import {Popup} from './popup';
+
+/**
+ * This component defines the look and style of the DOM parts of a track (mainly
+ * the 'shell' part).
+ *
+ * ┌───────────────────────────────────────────────────────────────────────────┐
+ * │ pf-track │
+ * |┌─────────────────────────────────────────────────────────────────────────┐|
+ * || pf-track__header ||
+ * │|┌─────────┐┌─────────────────────────────────────────┐┌─────────────────┐│|
+ * │|│::before ||pf-track__shell ││pf-track__content││|
+ * │|│(Indent) ||┌───────────────────────────────────────┐││ ││|
+ * │|│ ||│pf-track__menubar (sticky) │││ ││|
+ * │|│ ||│┌───────────────┐┌────────────────────┐│││ ││|
+ * │|│ ||││pf-track__title││pf-track__buttons ││││ ││|
+ * │|│ ||│└───────────────┘└────────────────────┘│││ ││|
+ * │|│ ||└───────────────────────────────────────┘││ ││|
+ * │|└─────────┘└─────────────────────────────────────────┘└─────────────────┘│|
+ * |└─────────────────────────────────────────────────────────────────────────┘|
+ * |┌─────────────────────────────────────────────────────────────────────────┐|
+ * || pf-track__children (if children supplied) ||
+ * |└─────────────────────────────────────────────────────────────────────────┘|
+ * └───────────────────────────────────────────────────────────────────────────┘
+ */
+
+export interface TrackShellAttrs extends HTMLAttrs {
+ // The title of this track.
+ readonly title: string;
+
+ // Optional subtitle to display underneath the track name.
+ readonly subtitle?: string;
+
+ // Show dropdown arrow and make clickable. Defaults to false.
+ readonly collapsible?: boolean;
+
+ // Show an up or down dropdown arrow.
+ readonly collapsed: boolean;
+
+ // Height of the track in pixels. All tracks have a fixed height.
+ readonly heightPx: number;
+
+ // Optional buttons to place on the RHS of the track shell.
+ readonly buttons?: m.Children;
+
+ // Optional list of chips to display after the track title.
+ readonly chips?: ReadonlyArray<string>;
+
+ // Render this track in error colours.
+ readonly error?: Error;
+
+ // Issues a scrollTo() on this DOM element at creation time. Default: false.
+ readonly scrollToOnCreate?: boolean;
+
+ // Style the component differently.
+ readonly summary?: boolean;
+
+ // Whether to highlight the track or not.
+ readonly highlight?: boolean;
+
+ // Whether the shell should be draggable and emit drag/drop events.
+ readonly reorderable?: boolean;
+
+ // This is the depth of the track in the tree - controls the indent level and
+ // the z-index of sticky headers.
+ readonly depth?: number;
+
+ // The stick top offset - this is the offset from the top of sticky summary
+ // track headers and sticky menu bars stick from the top of the viewport. This
+ // is used to allow nested sticky track headers and menubars of nested tracks
+ // to stick below the sticky header of their parent track(s).
+ readonly stickyTop?: number;
+
+ // The ID of the plugin that created this track.
+ readonly pluginId?: string;
+
+ // Render a lighter version of the track shell, with no buttons or chips, just
+ // the track title.
+ readonly lite?: boolean;
+
+ // Called when the track is expanded or collapsed (when the node is clicked).
+ onToggleCollapsed?(): void;
+
+ // Mouse events within the track content element.
+ onTrackContentMouseMove?(pos: Point2D, contentSize: Bounds2D): void;
+ onTrackContentMouseOut?(): void;
+ onTrackContentClick?(pos: Point2D, contentSize: Bounds2D): boolean;
+
+ // If reorderable, these functions will be called when track shells are
+ // dragged and dropped.
+ onMoveBefore?(nodeId: string): void;
+ onMoveAfter?(nodeId: string): void;
+}
+
+export class TrackShell implements m.ClassComponent<TrackShellAttrs> {
+ private mouseDownPos?: Vector2D;
+ private selectionOccurred = false;
+
+ view(vnode: m.CVnode<TrackShellAttrs>) {
+ const {attrs} = vnode;
+
+ const {
+ collapsible,
+ collapsed,
+ id,
+ summary,
+ heightPx,
+ ref,
+ depth = 0,
+ stickyTop = 0,
+ lite,
+ } = attrs;
+
+ const expanded = collapsible && !collapsed;
+ const trackHeight = heightPx;
+
+ return m(
+ '.pf-track',
+ {
+ id,
+ style: {
+ '--height': trackHeight,
+ '--depth': clamp(depth, 0, 16),
+ '--sticky-top': Math.max(0, stickyTop),
+ },
+ ref,
+ },
+ m(
+ '.pf-track__header',
+ {
+ className: classNames(
+ summary && 'pf-track__header--summary',
+ expanded && 'pf-track__header--expanded',
+ summary && expanded && 'pf-track__header--expanded--summary',
+ ),
+ },
+ this.renderShell(attrs),
+ !lite && this.renderContent(attrs),
+ ),
+ hasChildren(vnode) && m('.pf-track__children', vnode.children),
+ );
+ }
+
+ oncreate({dom, attrs}: m.VnodeDOM<TrackShellAttrs>) {
+ if (attrs.scrollToOnCreate) {
+ dom.scrollIntoView({behavior: 'smooth', block: 'nearest'});
+ }
+ }
+
+ private renderShell(attrs: TrackShellAttrs): m.Children {
+ const {
+ id,
+ chips,
+ collapsible,
+ collapsed,
+ reorderable = false,
+ onMoveAfter = () => {},
+ onMoveBefore = () => {},
+ buttons,
+ highlight,
+ lite,
+ } = attrs;
+
+ const block = 'pf-track';
+ const blockElement = `${block}__shell`;
+ const dragBeforeClassName = `${blockElement}--drag-before`;
+ const dragAfterClassName = `${blockElement}--drag-after`;
+
+ return m(
+ `.pf-track__shell`,
+ {
+ className: classNames(
+ collapsible && 'pf-track__shell--clickable',
+ highlight && 'pf-track__shell--highlight',
+ ),
+ onclick: () => {
+ collapsible && attrs.onToggleCollapsed?.();
+ },
+ draggable: reorderable,
+ ondragstart: (e: DragEvent) => {
+ id && e.dataTransfer?.setData('text/plain', id);
+ },
+ ondragover: (e: DragEvent) => {
+ if (!reorderable) {
+ return;
+ }
+ const target = e.currentTarget as HTMLElement;
+ const threshold = target.offsetHeight / 2;
+ const position = currentTargetOffset(e);
+ if (position.y > threshold) {
+ target.classList.remove(dragBeforeClassName);
+ target.classList.add(dragAfterClassName);
+ } else {
+ target.classList.remove(dragAfterClassName);
+ target.classList.add(dragBeforeClassName);
+ }
+ },
+ ondragleave: (e: DragEvent) => {
+ if (!reorderable) {
+ return;
+ }
+ const target = e.currentTarget as HTMLElement;
+ const related = e.relatedTarget as HTMLElement | null;
+ if (related && !target.contains(related)) {
+ target.classList.remove(dragAfterClassName);
+ target.classList.remove(dragBeforeClassName);
+ }
+ },
+ ondrop: (e: DragEvent) => {
+ if (!reorderable) {
+ return;
+ }
+ const id = e.dataTransfer?.getData('text/plain');
+ const target = e.currentTarget as HTMLElement;
+ const threshold = target.offsetHeight / 2;
+ if (id !== undefined) {
+ const position = currentTargetOffset(e);
+ if (position.y > threshold) {
+ onMoveAfter(id);
+ } else {
+ onMoveBefore(id);
+ }
+ }
+ target.classList.remove(dragAfterClassName);
+ target.classList.remove(dragBeforeClassName);
+ },
+ },
+ lite
+ ? attrs.title
+ : m(
+ '.pf-track__menubar',
+ collapsible
+ ? m(Button, {
+ className: 'pf-track__collapse-button',
+ compact: true,
+ icon: collapsed ? Icons.ExpandDown : Icons.ExpandUp,
+ })
+ : m('.pf-track__title-spacer'),
+ m(TrackTitle, {title: attrs.title}),
+ chips &&
+ m(
+ ChipBar,
+ {className: 'pf-track__chips'},
+ chips.map((chip) =>
+ m(Chip, {label: chip, compact: true, rounded: true}),
+ ),
+ ),
+ m(
+ ButtonBar,
+ {
+ className: 'pf-track__buttons',
+ // Block button clicks from hitting the shell's on click event
+ onclick: (e: MouseEvent) => e.stopPropagation(),
+ },
+ buttons,
+ // Always render this one last
+ attrs.error && renderCrashButton(attrs.error, attrs.pluginId),
+ ),
+ attrs.subtitle &&
+ !showSubtitleInContent(attrs) &&
+ m(
+ '.pf-track__subtitle',
+ m(MiddleEllipsis, {text: attrs.subtitle}),
+ ),
+ ),
+ );
+ }
+
+ private renderContent(attrs: TrackShellAttrs): m.Children {
+ const {
+ onTrackContentMouseMove,
+ onTrackContentMouseOut,
+ onTrackContentClick,
+ error,
+ } = attrs;
+
+ return m(
+ '.pf-track__canvas',
+ {
+ className: classNames(error && 'pf-track__canvas--error'),
+ onmousemove: (e: MithrilEvent<MouseEvent>) => {
+ e.redraw = false;
+ onTrackContentMouseMove?.(
+ currentTargetOffset(e),
+ getTargetContainerSize(e),
+ );
+ },
+ onmouseout: () => {
+ onTrackContentMouseOut?.();
+ },
+ onmousedown: (e: MouseEvent) => {
+ this.mouseDownPos = currentTargetOffset(e);
+ },
+ onmouseup: (e: MouseEvent) => {
+ if (!this.mouseDownPos) return;
+ if (
+ this.mouseDownPos.sub(currentTargetOffset(e)).manhattanDistance > 1
+ ) {
+ this.selectionOccurred = true;
+ }
+ this.mouseDownPos = undefined;
+ },
+ onclick: (e: MouseEvent) => {
+ // This click event occurs after any selection mouse up/drag events
+ // so we have to look if the mouse moved during this click to know
+ // if a selection occurred.
+ if (this.selectionOccurred) {
+ this.selectionOccurred = false;
+ return;
+ }
+
+ // Returns true if something was selected, so stop propagation.
+ if (
+ onTrackContentClick?.(
+ currentTargetOffset(e),
+ getTargetContainerSize(e),
+ )
+ ) {
+ e.stopPropagation();
+ }
+ },
+ },
+ attrs.subtitle &&
+ showSubtitleInContent(attrs) &&
+ m(MiddleEllipsis, {text: attrs.subtitle}),
+ );
+ }
+}
+
+function showSubtitleInContent(attrs: TrackShellAttrs) {
+ return attrs.summary && !attrs.collapsed;
+}
+
+function getTargetContainerSize(event: MouseEvent): Bounds2D {
+ const target = event.target as HTMLElement;
+ return target.getBoundingClientRect();
+}
+
+function renderCrashButton(error: Error, pluginId: string | undefined) {
+ return m(
+ Popup,
+ {
+ trigger: m(Button, {
+ icon: Icons.Crashed,
+ compact: true,
+ }),
+ },
+ m(
+ '.pf-track__crash-popup',
+ m('span', 'This track has crashed.'),
+ pluginId && m('span', `Owning plugin: ${pluginId}`),
+ m(Button, {
+ label: 'View & Report Crash',
+ intent: Intent.Primary,
+ className: Popup.DISMISS_POPUP_GROUP_CLASS,
+ onclick: () => {
+ throw error;
+ },
+ }),
+ // TODO(stevegolton): In the future we should provide a quick way to
+ // disable the plugin, or provide a link to the plugin page, but this
+ // relies on the plugin page being fully functional.
+ ),
+ );
+}
+
+interface TrackTitleAttrs {
+ readonly title: string;
+}
+
+class TrackTitle implements m.ClassComponent<TrackTitleAttrs> {
+ private readonly trash = new DisposableStack();
+
+ view({attrs}: m.Vnode<TrackTitleAttrs>) {
+ return m(
+ MiddleEllipsis,
+ {
+ className: 'pf-track__title',
+ text: attrs.title,
+ },
+ m('.pf-track__title-popup', attrs.title),
+ );
+ }
+
+ oncreate({dom}: m.VnodeDOM<TrackTitleAttrs>) {
+ const title = dom;
+ const popup = assertExists(dom.querySelector('.pf-track__title-popup'));
+
+ const resizeObserver = new ResizeObserver(() => {
+ // Determine whether to display a title popup based on ellipsization
+ if (popup.clientWidth > title.clientWidth) {
+ popup.classList.add('pf-track__title-popup--visible');
+ } else {
+ popup.classList.remove('pf-track__title-popup--visible');
+ }
+ });
+
+ resizeObserver.observe(title);
+ resizeObserver.observe(popup);
+
+ this.trash.defer(() => resizeObserver.disconnect());
+ }
+
+ onremove() {
+ this.trash.dispose();
+ }
+}
diff --git a/ui/src/widgets/track_widget.ts b/ui/src/widgets/track_widget.ts
deleted file mode 100644
index d9819f6..0000000
--- a/ui/src/widgets/track_widget.ts
+++ /dev/null
@@ -1,372 +0,0 @@
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-import {classNames} from '../base/classnames';
-import {DisposableStack} from '../base/disposable_stack';
-import {currentTargetOffset} from '../base/dom_utils';
-import {Bounds2D, Point2D, Vector2D} from '../base/geom';
-import {clamp} from '../base/math_utils';
-import {Icons} from '../base/semantic_icons';
-import {ButtonBar} from './button';
-import {Chip, ChipBar} from './chip';
-import {Icon} from './icon';
-import {MiddleEllipsis} from './middle_ellipsis';
-import {assertExists} from '../base/logging';
-
-/**
- * The TrackWidget defines the look and style of a track.
- *
- * ┌──────────────────────────────────────────────────────────────────┐
- * │pf-track (grid) │
- * │┌─────────────────────────────────────────┐┌─────────────────────┐│
- * ││pf-track-shell ││pf-track-content ││
- * ││┌───────────────────────────────────────┐││ ││
- * │││pf-track-menubar (sticky) │││ ││
- * │││┌───────────────┐┌────────────────────┐│││ ││
- * ││││pf-track-title ││pf-track-buttons ││││ ││
- * │││└───────────────┘└────────────────────┘│││ ││
- * ││└───────────────────────────────────────┘││ ││
- * │└─────────────────────────────────────────┘└─────────────────────┘│
- * └──────────────────────────────────────────────────────────────────┘
- */
-
-export interface TrackWidgetAttrs {
- // The title of this track.
- readonly title: string;
-
- // Optional subtitle to display underneath the track name.
- readonly subtitle?: string;
-
- // The full path to this track.
- readonly path?: string;
-
- // Show dropdown arrow and make clickable. Defaults to false.
- readonly collapsible?: boolean;
-
- // Show an up or down dropdown arrow.
- readonly collapsed: boolean;
-
- // Height of the track in pixels. All tracks have a fixed height.
- readonly heightPx: number;
-
- // Optional buttons to place on the RHS of the track shell.
- readonly buttons?: m.Children;
-
- // Optional list of chips to display after the track title.
- readonly chips?: ReadonlyArray<string>;
-
- // Render this track in error colours.
- readonly error?: boolean;
-
- // The integer indentation level of this track. If omitted, defaults to 0.
- readonly indentationLevel?: number;
-
- // Track titles are sticky. This is the offset in pixels from the top of the
- // scrolling parent. Defaults to 0.
- readonly topOffsetPx?: number;
-
- // Issues a scrollTo() on this DOM element at creation time. Default: false.
- readonly revealOnCreate?: boolean;
-
- // Called when arrow clicked.
- readonly onToggleCollapsed?: () => void;
-
- // Style the component differently if it has children.
- readonly isSummary?: boolean;
-
- // HTML id applied to the root element.
- readonly id: string;
-
- // Whether to highlight the track or not.
- readonly highlight?: boolean;
-
- // Whether the shell should be draggable and emit drag/drop events.
- readonly reorderable?: boolean;
-
- // Mouse events.
- readonly onTrackContentMouseMove?: (
- pos: Point2D,
- contentSize: Bounds2D,
- ) => void;
- readonly onTrackContentMouseOut?: () => void;
- readonly onTrackContentClick?: (
- pos: Point2D,
- contentSize: Bounds2D,
- ) => boolean;
-
- // If reorderable, these functions will be called when track shells are
- // dragged and dropped.
- readonly onMoveBefore?: (nodeId: string) => void;
- readonly onMoveAfter?: (nodeId: string) => void;
-}
-
-const TRACK_HEIGHT_MIN_PX = 18;
-const INDENTATION_LEVEL_MAX = 16;
-export class TrackWidget implements m.ClassComponent<TrackWidgetAttrs> {
- private readonly trash = new DisposableStack();
-
- view({attrs}: m.CVnode<TrackWidgetAttrs>) {
- const {
- indentationLevel = 0,
- collapsible,
- collapsed,
- highlight,
- heightPx,
- id,
- isSummary,
- } = attrs;
-
- const trackHeight = Math.max(heightPx, TRACK_HEIGHT_MIN_PX);
- const expanded = collapsible && !collapsed;
-
- return m(
- '.pf-track',
- {
- id,
- className: classNames(
- expanded && 'pf-expanded',
- highlight && 'pf-highlight',
- isSummary && 'pf-is-summary',
- ),
- style: {
- // Note: Sub-pixel track heights can mess with sticky elements.
- // Round up to the nearest integer number of pixels.
- '--indent': clamp(indentationLevel, 0, INDENTATION_LEVEL_MAX),
- 'height': `${Math.ceil(trackHeight)}px`,
- },
- },
- this.renderShell(attrs),
- this.renderContent(attrs),
- );
- }
-
- oncreate({dom, attrs}: m.VnodeDOM<TrackWidgetAttrs>) {
- if (attrs.revealOnCreate) {
- dom.scrollIntoView({behavior: 'smooth', block: 'nearest'});
- }
-
- const popup = assertExists(dom.querySelector('.pf-track__title-popup'));
- const title = assertExists(dom.querySelector('.pf-track__title'));
-
- const resizeObserver = new ResizeObserver(() => {
- // Work out whether to display a title popup on hover, based on whether
- // the title is ellipsized.
- if (popup.clientWidth > title.clientWidth) {
- popup.classList.add('pf-visible');
- } else {
- popup.classList.remove('pf-visible');
- }
- });
- resizeObserver.observe(title);
- resizeObserver.observe(popup);
- this.trash.defer(() => resizeObserver.disconnect());
- }
-
- onremove() {
- this.trash.dispose();
- }
-
- private renderShell(attrs: TrackWidgetAttrs): m.Children {
- const chips =
- attrs.chips &&
- m(
- ChipBar,
- attrs.chips.map((chip) =>
- m(Chip, {label: chip, compact: true, rounded: true}),
- ),
- );
-
- const {
- id,
- topOffsetPx = 0,
- collapsible,
- collapsed,
- reorderable = false,
- onMoveAfter = () => {},
- onMoveBefore = () => {},
- } = attrs;
-
- return m(
- `.pf-track-shell[data-track-node-id=${id}]`,
- {
- className: classNames(collapsible && 'pf-clickable'),
- onclick: (e: MouseEvent) => {
- // Block all clicks on the shell from propagating through to the
- // canvas
- e.stopPropagation();
- if (collapsible) {
- attrs.onToggleCollapsed?.();
- }
- },
- draggable: reorderable,
- ondragstart: (e: DragEvent) => {
- e.dataTransfer?.setData('text/plain', id);
- },
- ondragover: (e: DragEvent) => {
- if (!reorderable) {
- return;
- }
- const target = e.currentTarget as HTMLElement;
- const threshold = target.offsetHeight / 2;
- if (e.offsetY > threshold) {
- target.classList.remove('pf-drag-before');
- target.classList.add('pf-drag-after');
- } else {
- target.classList.remove('pf-drag-after');
- target.classList.add('pf-drag-before');
- }
- },
- ondragleave: (e: DragEvent) => {
- if (!reorderable) {
- return;
- }
- const target = e.currentTarget as HTMLElement;
- const related = e.relatedTarget as HTMLElement | null;
- if (related && !target.contains(related)) {
- target.classList.remove('pf-drag-after');
- target.classList.remove('pf-drag-before');
- }
- },
- ondrop: (e: DragEvent) => {
- if (!reorderable) {
- return;
- }
- const id = e.dataTransfer?.getData('text/plain');
- const target = e.currentTarget as HTMLElement;
- const threshold = target.offsetHeight / 2;
- if (id !== undefined) {
- if (e.offsetY > threshold) {
- onMoveAfter(id);
- } else {
- onMoveBefore(id);
- }
- }
- target.classList.remove('pf-drag-after');
- target.classList.remove('pf-drag-before');
- },
- },
- m(
- '.pf-track-menubar',
- {
- style: {
- position: 'sticky',
- top: `${topOffsetPx}px`,
- },
- },
- collapsible &&
- m(Icon, {icon: collapsed ? Icons.ExpandDown : Icons.ExpandUp}),
- m(
- 'h1.pf-track-title',
- {
- ref: attrs.path, // TODO(stevegolton): Replace with aria tags?
- },
- m(
- MiddleEllipsis,
- {text: attrs.title, className: 'pf-track__title'},
- m('.pf-track__title-popup', attrs.title),
- ),
- chips,
- ),
- m(
- ButtonBar,
- {
- className: 'pf-track-buttons',
- // Block button clicks from hitting the shell's on click event
- onclick: (e: MouseEvent) => e.stopPropagation(),
- },
- attrs.buttons,
- ),
- attrs.subtitle &&
- !this.showSubtitleInContent(attrs) &&
- m('h2.pf-track-subtitle', m(MiddleEllipsis, {text: attrs.subtitle})),
- ),
- );
- }
-
- private showSubtitleInContent(attrs: TrackWidgetAttrs) {
- return attrs.isSummary && !attrs.collapsed;
- }
-
- private mouseDownPos?: Vector2D;
- private selectionOccurred = false;
-
- private renderContent(attrs: TrackWidgetAttrs): m.Children {
- const {
- heightPx,
- onTrackContentMouseMove,
- onTrackContentMouseOut,
- onTrackContentClick,
- } = attrs;
- const trackHeight = Math.max(heightPx, TRACK_HEIGHT_MIN_PX);
-
- return m(
- '.pf-track-content',
- {
- style: {
- height: `${trackHeight}px`,
- },
- className: classNames(attrs.error && 'pf-track-content-error'),
- onmousemove: (e: MouseEvent) => {
- onTrackContentMouseMove?.(
- currentTargetOffset(e),
- getTargetContainerSize(e),
- );
- },
- onmouseout: () => {
- onTrackContentMouseOut?.();
- },
- onmousedown: (e: MouseEvent) => {
- this.mouseDownPos = currentTargetOffset(e);
- },
- onmouseup: (e: MouseEvent) => {
- if (!this.mouseDownPos) return;
- if (
- this.mouseDownPos.sub(currentTargetOffset(e)).manhattanDistance > 1
- ) {
- this.selectionOccurred = true;
- }
- this.mouseDownPos = undefined;
- },
- onclick: (e: MouseEvent) => {
- // This click event occurs after any selection mouse up/drag events
- // so we have to look if the mouse moved during this click to know
- // if a selection occurred.
- if (this.selectionOccurred) {
- this.selectionOccurred = false;
- return;
- }
-
- // Returns true if something was selected, so stop propagation.
- if (
- onTrackContentClick?.(
- currentTargetOffset(e),
- getTargetContainerSize(e),
- )
- ) {
- e.stopPropagation();
- }
- },
- },
- attrs.subtitle &&
- this.showSubtitleInContent(attrs) &&
- m('h2', m(MiddleEllipsis, {text: attrs.subtitle})),
- );
- }
-}
-
-function getTargetContainerSize(event: MouseEvent): Bounds2D {
- const target = event.target as HTMLElement;
- return target.getBoundingClientRect();
-}
diff --git a/ui/src/widgets/tree.ts b/ui/src/widgets/tree.ts
index d38bc65..5a822f8 100644
--- a/ui/src/widgets/tree.ts
+++ b/ui/src/widgets/tree.ts
@@ -15,7 +15,6 @@
import m from 'mithril';
import {classNames} from '../base/classnames';
import {hasChildren} from '../base/mithril_utils';
-import {scheduleFullRedraw} from './raf';
// Heirachical tree layout with left and right values.
// Right and left values of the same indentation level are horizontally aligned.
@@ -92,7 +91,6 @@
onclick: () => {
this.collapsed = !this.isCollapsed(vnode);
onCollapseChanged(this.collapsed, attrs);
- scheduleFullRedraw();
},
}),
left,
@@ -215,19 +213,16 @@
// Expanding
if (this.renderChildren) {
this.collapsed = false;
- scheduleFullRedraw();
} else {
this.loading = true;
fetchData().then((result) => {
this.loading = false;
this.collapsed = false;
this.renderChildren = result;
- scheduleFullRedraw();
});
}
}
this.collapsed = collapsed;
- scheduleFullRedraw();
},
},
this.renderChildren && this.renderChildren(),
diff --git a/ui/src/widgets/virtual_scroll_helper.ts b/ui/src/widgets/virtual_scroll_helper.ts
index 6172c94..475c360 100644
--- a/ui/src/widgets/virtual_scroll_helper.ts
+++ b/ui/src/widgets/virtual_scroll_helper.ts
@@ -14,7 +14,6 @@
import {DisposableStack} from '../base/disposable_stack';
import {Bounds2D, Rect2D} from '../base/geom';
-import {scheduleFullRedraw} from './raf';
export interface VirtualScrollHelperOpts {
overdrawPx: number;
@@ -47,7 +46,6 @@
this._data.forEach((data) =>
recalculatePuckRect(sliderElement, containerElement, data),
);
- scheduleFullRedraw('force');
};
containerElement.addEventListener('scroll', recalculateRects, {
diff --git a/ui/src/widgets/virtual_table.ts b/ui/src/widgets/virtual_table.ts
index b5d8643..102828c 100644
--- a/ui/src/widgets/virtual_table.ts
+++ b/ui/src/widgets/virtual_table.ts
@@ -16,7 +16,6 @@
import {findRef, toHTMLElement} from '../base/dom_utils';
import {assertExists} from '../base/logging';
import {Style} from './common';
-import {scheduleFullRedraw} from './raf';
import {VirtualScrollHelper} from './virtual_scroll_helper';
import {DisposableStack} from '../base/disposable_stack';
@@ -238,7 +237,7 @@
const rowStart = Math.floor(rect.top / attrs.rowHeight / 2) * 2;
const rowCount = Math.ceil(rect.height / attrs.rowHeight / 2) * 2;
this.renderBounds = {rowStart, rowEnd: rowStart + rowCount};
- scheduleFullRedraw();
+ m.redraw();
},
},
{
@@ -248,6 +247,7 @@
const rowStart = Math.floor(rect.top / attrs.rowHeight / 2) * 2;
const rowEnd = Math.ceil(rect.bottom / attrs.rowHeight);
attrs.onReload?.(rowStart, rowEnd - rowStart);
+ m.redraw();
},
},
]);