Merge "ui: Include one or more perf tracks in area selection."
diff --git a/Android.bp b/Android.bp
index 1b10e3d..b0a5498 100644
--- a/Android.bp
+++ b/Android.bp
@@ -92,6 +92,7 @@
":perfetto_src_tracing_common",
":perfetto_src_tracing_core_core",
":perfetto_src_tracing_ipc_common",
+ ":perfetto_src_tracing_ipc_default_socket",
":perfetto_src_tracing_ipc_producer_producer",
"src/profiling/memory/main.cc",
],
@@ -347,6 +348,7 @@
":perfetto_src_tracing_common",
":perfetto_src_tracing_core_core",
":perfetto_src_tracing_ipc_common",
+ ":perfetto_src_tracing_ipc_default_socket",
":perfetto_src_tracing_ipc_producer_producer",
],
shared_libs: [
@@ -548,6 +550,7 @@
":perfetto_src_tracing_core_service",
":perfetto_src_tracing_ipc_common",
":perfetto_src_tracing_ipc_consumer_consumer",
+ ":perfetto_src_tracing_ipc_default_socket",
":perfetto_src_tracing_ipc_producer_producer",
":perfetto_src_tracing_ipc_service_service",
],
@@ -732,6 +735,7 @@
":perfetto_src_tracing_in_process_backend",
":perfetto_src_tracing_ipc_common",
":perfetto_src_tracing_ipc_consumer_consumer",
+ ":perfetto_src_tracing_ipc_default_socket",
":perfetto_src_tracing_ipc_producer_producer",
":perfetto_src_tracing_ipc_service_service",
":perfetto_src_tracing_platform_impl",
@@ -924,6 +928,7 @@
":perfetto_src_tracing_core_core",
":perfetto_src_tracing_ipc_common",
":perfetto_src_tracing_ipc_consumer_consumer",
+ ":perfetto_src_tracing_ipc_default_socket",
":perfetto_src_tracing_ipc_producer_producer",
"src/perfetto_cmd/main.cc",
],
@@ -1099,6 +1104,7 @@
":perfetto_src_tracing_core_service",
":perfetto_src_tracing_ipc_common",
":perfetto_src_tracing_ipc_consumer_consumer",
+ ":perfetto_src_tracing_ipc_default_socket",
":perfetto_src_tracing_ipc_producer_producer",
":perfetto_src_tracing_ipc_service_service",
":perfetto_test_end_to_end_integrationtests",
@@ -1355,6 +1361,7 @@
":perfetto_src_tracing_core_service",
":perfetto_src_tracing_ipc_common",
":perfetto_src_tracing_ipc_consumer_consumer",
+ ":perfetto_src_tracing_ipc_default_socket",
":perfetto_src_tracing_ipc_producer_producer",
":perfetto_src_tracing_ipc_service_service",
":perfetto_test_test_helper",
@@ -1544,6 +1551,11 @@
name: "perfetto_include_perfetto_ext_base_base",
}
+// GN: //include/perfetto/ext/base/http:http
+filegroup {
+ name: "perfetto_include_perfetto_ext_base_http_http",
+}
+
// GN: //include/perfetto/ext/ipc:ipc
filegroup {
name: "perfetto_include_perfetto_ext_ipc_ipc",
@@ -1791,6 +1803,7 @@
":perfetto_src_tracing_in_process_backend",
":perfetto_src_tracing_ipc_common",
":perfetto_src_tracing_ipc_consumer_consumer",
+ ":perfetto_src_tracing_ipc_default_socket",
":perfetto_src_tracing_ipc_producer_producer",
":perfetto_src_tracing_ipc_service_service",
":perfetto_src_tracing_platform_impl",
@@ -4136,6 +4149,7 @@
"protos/perfetto/trace/ftrace/sde.proto",
"protos/perfetto/trace/ftrace/signal.proto",
"protos/perfetto/trace/ftrace/sync.proto",
+ "protos/perfetto/trace/ftrace/synthetic.proto",
"protos/perfetto/trace/ftrace/systrace.proto",
"protos/perfetto/trace/ftrace/task.proto",
"protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -4353,6 +4367,7 @@
"protos/perfetto/trace/ftrace/sde.proto",
"protos/perfetto/trace/ftrace/signal.proto",
"protos/perfetto/trace/ftrace/sync.proto",
+ "protos/perfetto/trace/ftrace/synthetic.proto",
"protos/perfetto/trace/ftrace/systrace.proto",
"protos/perfetto/trace/ftrace/task.proto",
"protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -4404,6 +4419,7 @@
"external/perfetto/protos/perfetto/trace/ftrace/sde.gen.cc",
"external/perfetto/protos/perfetto/trace/ftrace/signal.gen.cc",
"external/perfetto/protos/perfetto/trace/ftrace/sync.gen.cc",
+ "external/perfetto/protos/perfetto/trace/ftrace/synthetic.gen.cc",
"external/perfetto/protos/perfetto/trace/ftrace/systrace.gen.cc",
"external/perfetto/protos/perfetto/trace/ftrace/task.gen.cc",
"external/perfetto/protos/perfetto/trace/ftrace/test_bundle_wrapper.gen.cc",
@@ -4455,6 +4471,7 @@
"protos/perfetto/trace/ftrace/sde.proto",
"protos/perfetto/trace/ftrace/signal.proto",
"protos/perfetto/trace/ftrace/sync.proto",
+ "protos/perfetto/trace/ftrace/synthetic.proto",
"protos/perfetto/trace/ftrace/systrace.proto",
"protos/perfetto/trace/ftrace/task.proto",
"protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -4506,6 +4523,7 @@
"external/perfetto/protos/perfetto/trace/ftrace/sde.gen.h",
"external/perfetto/protos/perfetto/trace/ftrace/signal.gen.h",
"external/perfetto/protos/perfetto/trace/ftrace/sync.gen.h",
+ "external/perfetto/protos/perfetto/trace/ftrace/synthetic.gen.h",
"external/perfetto/protos/perfetto/trace/ftrace/systrace.gen.h",
"external/perfetto/protos/perfetto/trace/ftrace/task.gen.h",
"external/perfetto/protos/perfetto/trace/ftrace/test_bundle_wrapper.gen.h",
@@ -4561,6 +4579,7 @@
"protos/perfetto/trace/ftrace/sde.proto",
"protos/perfetto/trace/ftrace/signal.proto",
"protos/perfetto/trace/ftrace/sync.proto",
+ "protos/perfetto/trace/ftrace/synthetic.proto",
"protos/perfetto/trace/ftrace/systrace.proto",
"protos/perfetto/trace/ftrace/task.proto",
"protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -4611,6 +4630,7 @@
"external/perfetto/protos/perfetto/trace/ftrace/sde.pb.cc",
"external/perfetto/protos/perfetto/trace/ftrace/signal.pb.cc",
"external/perfetto/protos/perfetto/trace/ftrace/sync.pb.cc",
+ "external/perfetto/protos/perfetto/trace/ftrace/synthetic.pb.cc",
"external/perfetto/protos/perfetto/trace/ftrace/systrace.pb.cc",
"external/perfetto/protos/perfetto/trace/ftrace/task.pb.cc",
"external/perfetto/protos/perfetto/trace/ftrace/test_bundle_wrapper.pb.cc",
@@ -4662,6 +4682,7 @@
"protos/perfetto/trace/ftrace/sde.proto",
"protos/perfetto/trace/ftrace/signal.proto",
"protos/perfetto/trace/ftrace/sync.proto",
+ "protos/perfetto/trace/ftrace/synthetic.proto",
"protos/perfetto/trace/ftrace/systrace.proto",
"protos/perfetto/trace/ftrace/task.proto",
"protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -4712,6 +4733,7 @@
"external/perfetto/protos/perfetto/trace/ftrace/sde.pb.h",
"external/perfetto/protos/perfetto/trace/ftrace/signal.pb.h",
"external/perfetto/protos/perfetto/trace/ftrace/sync.pb.h",
+ "external/perfetto/protos/perfetto/trace/ftrace/synthetic.pb.h",
"external/perfetto/protos/perfetto/trace/ftrace/systrace.pb.h",
"external/perfetto/protos/perfetto/trace/ftrace/task.pb.h",
"external/perfetto/protos/perfetto/trace/ftrace/test_bundle_wrapper.pb.h",
@@ -4767,6 +4789,7 @@
"protos/perfetto/trace/ftrace/sde.proto",
"protos/perfetto/trace/ftrace/signal.proto",
"protos/perfetto/trace/ftrace/sync.proto",
+ "protos/perfetto/trace/ftrace/synthetic.proto",
"protos/perfetto/trace/ftrace/systrace.proto",
"protos/perfetto/trace/ftrace/task.proto",
"protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -4818,6 +4841,7 @@
"external/perfetto/protos/perfetto/trace/ftrace/sde.pbzero.cc",
"external/perfetto/protos/perfetto/trace/ftrace/signal.pbzero.cc",
"external/perfetto/protos/perfetto/trace/ftrace/sync.pbzero.cc",
+ "external/perfetto/protos/perfetto/trace/ftrace/synthetic.pbzero.cc",
"external/perfetto/protos/perfetto/trace/ftrace/systrace.pbzero.cc",
"external/perfetto/protos/perfetto/trace/ftrace/task.pbzero.cc",
"external/perfetto/protos/perfetto/trace/ftrace/test_bundle_wrapper.pbzero.cc",
@@ -4869,6 +4893,7 @@
"protos/perfetto/trace/ftrace/sde.proto",
"protos/perfetto/trace/ftrace/signal.proto",
"protos/perfetto/trace/ftrace/sync.proto",
+ "protos/perfetto/trace/ftrace/synthetic.proto",
"protos/perfetto/trace/ftrace/systrace.proto",
"protos/perfetto/trace/ftrace/task.proto",
"protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -4920,6 +4945,7 @@
"external/perfetto/protos/perfetto/trace/ftrace/sde.pbzero.h",
"external/perfetto/protos/perfetto/trace/ftrace/signal.pbzero.h",
"external/perfetto/protos/perfetto/trace/ftrace/sync.pbzero.h",
+ "external/perfetto/protos/perfetto/trace/ftrace/synthetic.pbzero.h",
"external/perfetto/protos/perfetto/trace/ftrace/systrace.pbzero.h",
"external/perfetto/protos/perfetto/trace/ftrace/task.pbzero.h",
"external/perfetto/protos/perfetto/trace/ftrace/test_bundle_wrapper.pbzero.h",
@@ -6875,6 +6901,24 @@
],
}
+// GN: //src/base/http:http
+filegroup {
+ name: "perfetto_src_base_http_http",
+ srcs: [
+ "src/base/http/http_server.cc",
+ "src/base/http/sha1.cc",
+ ],
+}
+
+// GN: //src/base/http:unittests
+filegroup {
+ name: "perfetto_src_base_http_unittests",
+ srcs: [
+ "src/base/http/http_server_unittest.cc",
+ "src/base/http/sha1_unittest.cc",
+ ],
+}
+
// GN: //src/base:test_support
filegroup {
name: "perfetto_src_base_test_support",
@@ -6892,6 +6936,7 @@
srcs: [
"src/base/base64_unittest.cc",
"src/base/circular_queue_unittest.cc",
+ "src/base/flat_hash_map_unittest.cc",
"src/base/flat_set_unittest.cc",
"src/base/getopt_compat_unittest.cc",
"src/base/logging_unittest.cc",
@@ -6901,6 +6946,7 @@
"src/base/paged_memory_unittest.cc",
"src/base/periodic_task_unittest.cc",
"src/base/scoped_file_unittest.cc",
+ "src/base/small_vector_unittest.cc",
"src/base/string_splitter_unittest.cc",
"src/base/string_utils_unittest.cc",
"src/base/string_view_unittest.cc",
@@ -8183,7 +8229,6 @@
"src/trace_processor/importers/gzip/gzip_trace_parser.cc",
"src/trace_processor/importers/json/json_trace_parser.cc",
"src/trace_processor/importers/json/json_trace_tokenizer.cc",
- "src/trace_processor/importers/json/json_tracker.cc",
"src/trace_processor/importers/proto/android_probes_module.cc",
"src/trace_processor/importers/proto/android_probes_parser.cc",
"src/trace_processor/importers/proto/android_probes_tracker.cc",
@@ -8680,6 +8725,15 @@
name: "perfetto_src_traced_probes_power_power",
srcs: [
"src/traced/probes/power/android_power_data_source.cc",
+ "src/traced/probes/power/linux_power_sysfs_data_source.cc",
+ ],
+}
+
+// GN: //src/traced/probes/power:unittests
+filegroup {
+ name: "perfetto_src_traced_probes_power_unittests",
+ srcs: [
+ "src/traced/probes/power/linux_power_sysfs_data_source_unittest.cc",
],
}
@@ -8876,7 +8930,6 @@
filegroup {
name: "perfetto_src_tracing_ipc_common",
srcs: [
- "src/tracing/ipc/default_socket.cc",
"src/tracing/ipc/memfd.cc",
"src/tracing/ipc/posix_shared_memory.cc",
"src/tracing/ipc/shared_memory_windows.cc",
@@ -8891,6 +8944,14 @@
],
}
+// GN: //src/tracing/ipc:default_socket
+filegroup {
+ name: "perfetto_src_tracing_ipc_default_socket",
+ srcs: [
+ "src/tracing/ipc/default_socket.cc",
+ ],
+}
+
// GN: //src/tracing/ipc/producer:producer
filegroup {
name: "perfetto_src_tracing_ipc_producer_producer",
@@ -9178,6 +9239,7 @@
srcs: [
":perfetto_include_perfetto_base_base",
":perfetto_include_perfetto_ext_base_base",
+ ":perfetto_include_perfetto_ext_base_http_http",
":perfetto_include_perfetto_ext_ipc_ipc",
":perfetto_include_perfetto_ext_trace_processor_export_json",
":perfetto_include_perfetto_ext_trace_processor_importers_memory_tracker_memory_tracker",
@@ -9285,6 +9347,8 @@
":perfetto_src_android_stats_android_stats",
":perfetto_src_android_stats_perfetto_atoms",
":perfetto_src_base_base",
+ ":perfetto_src_base_http_http",
+ ":perfetto_src_base_http_unittests",
":perfetto_src_base_test_support",
":perfetto_src_base_unittests",
":perfetto_src_base_unix_socket",
@@ -9394,6 +9458,7 @@
":perfetto_src_traced_probes_packages_list_packages_list_parser",
":perfetto_src_traced_probes_packages_list_unittests",
":perfetto_src_traced_probes_power_power",
+ ":perfetto_src_traced_probes_power_unittests",
":perfetto_src_traced_probes_probes_src",
":perfetto_src_traced_probes_ps_ps",
":perfetto_src_traced_probes_ps_unittests",
@@ -9412,6 +9477,7 @@
":perfetto_src_tracing_core_unittests",
":perfetto_src_tracing_ipc_common",
":perfetto_src_tracing_ipc_consumer_consumer",
+ ":perfetto_src_tracing_ipc_default_socket",
":perfetto_src_tracing_ipc_producer_producer",
":perfetto_src_tracing_ipc_service_service",
":perfetto_src_tracing_ipc_unittests",
@@ -9597,6 +9663,7 @@
srcs: [
":perfetto_include_perfetto_base_base",
":perfetto_include_perfetto_ext_base_base",
+ ":perfetto_include_perfetto_ext_base_http_http",
":perfetto_include_perfetto_ext_trace_processor_export_json",
":perfetto_include_perfetto_ext_trace_processor_importers_memory_tracker_memory_tracker",
":perfetto_include_perfetto_ext_traced_sys_stats_counters",
@@ -9634,6 +9701,7 @@
":perfetto_protos_perfetto_trace_system_info_zero_gen",
":perfetto_protos_perfetto_trace_track_event_zero_gen",
":perfetto_src_base_base",
+ ":perfetto_src_base_http_http",
":perfetto_src_base_unix_socket",
":perfetto_src_profiling_deobfuscator",
":perfetto_src_profiling_symbolizer_symbolize_database",
@@ -9984,6 +10052,7 @@
":perfetto_src_tracing_core_core",
":perfetto_src_tracing_core_service",
":perfetto_src_tracing_ipc_common",
+ ":perfetto_src_tracing_ipc_default_socket",
":perfetto_src_tracing_ipc_producer_producer",
"src/profiling/perf/main.cc",
],
@@ -10148,6 +10217,7 @@
":perfetto_src_tracing_common",
":perfetto_src_tracing_core_core",
":perfetto_src_tracing_ipc_common",
+ ":perfetto_src_tracing_ipc_default_socket",
":perfetto_src_tracing_ipc_producer_producer",
"src/perfetto_cmd/trigger_perfetto_main.cc",
],
diff --git a/BUILD b/BUILD
index c818210..85ba196 100644
--- a/BUILD
+++ b/BUILD
@@ -244,6 +244,7 @@
":src_tracing_core_service",
":src_tracing_ipc_common",
":src_tracing_ipc_consumer_consumer",
+ ":src_tracing_ipc_default_socket",
":src_tracing_ipc_producer_producer",
":src_tracing_ipc_service_service",
],
@@ -330,6 +331,15 @@
],
)
+# GN target: //include/perfetto/ext/base/http:http
+perfetto_filegroup(
+ name = "include_perfetto_ext_base_http_http",
+ srcs = [
+ "include/perfetto/ext/base/http/http_server.h",
+ "include/perfetto/ext/base/http/sha1.h",
+ ],
+)
+
# GN target: //include/perfetto/ext/base:base
perfetto_filegroup(
name = "include_perfetto_ext_base_base",
@@ -343,6 +353,7 @@
"include/perfetto/ext/base/endian.h",
"include/perfetto/ext/base/event_fd.h",
"include/perfetto/ext/base/file_utils.h",
+ "include/perfetto/ext/base/flat_hash_map.h",
"include/perfetto/ext/base/getopt.h",
"include/perfetto/ext/base/getopt_compat.h",
"include/perfetto/ext/base/hash.h",
@@ -355,6 +366,7 @@
"include/perfetto/ext/base/pipe.h",
"include/perfetto/ext/base/scoped_file.h",
"include/perfetto/ext/base/small_set.h",
+ "include/perfetto/ext/base/small_vector.h",
"include/perfetto/ext/base/string_splitter.h",
"include/perfetto/ext/base/string_utils.h",
"include/perfetto/ext/base/string_view.h",
@@ -639,6 +651,25 @@
],
)
+# GN target: //src/base/http:http
+perfetto_cc_library(
+ name = "src_base_http_http",
+ srcs = [
+ "src/base/http/http_server.cc",
+ "src/base/http/sha1.cc",
+ ],
+ hdrs = [
+ ":include_perfetto_base_base",
+ ":include_perfetto_ext_base_base",
+ ":include_perfetto_ext_base_http_http",
+ ],
+ deps = [
+ ":src_base_base",
+ ":src_base_unix_socket",
+ ],
+ linkstatic = True,
+)
+
# GN target: //src/base:base
perfetto_cc_library(
name = "src_base_base",
@@ -1376,8 +1407,6 @@
"src/trace_processor/importers/json/json_trace_parser.h",
"src/trace_processor/importers/json/json_trace_tokenizer.cc",
"src/trace_processor/importers/json/json_trace_tokenizer.h",
- "src/trace_processor/importers/json/json_tracker.cc",
- "src/trace_processor/importers/json/json_tracker.h",
"src/trace_processor/importers/proto/android_probes_module.cc",
"src/trace_processor/importers/proto/android_probes_module.h",
"src/trace_processor/importers/proto/android_probes_parser.cc",
@@ -1633,6 +1662,8 @@
srcs = [
"src/traced/probes/power/android_power_data_source.cc",
"src/traced/probes/power/android_power_data_source.h",
+ "src/traced/probes/power/linux_power_sysfs_data_source.cc",
+ "src/traced/probes/power/linux_power_sysfs_data_source.h",
],
)
@@ -1778,7 +1809,6 @@
perfetto_filegroup(
name = "src_tracing_ipc_common",
srcs = [
- "src/tracing/ipc/default_socket.cc",
"src/tracing/ipc/memfd.cc",
"src/tracing/ipc/memfd.h",
"src/tracing/ipc/posix_shared_memory.cc",
@@ -1788,6 +1818,14 @@
],
)
+# GN target: //src/tracing/ipc:default_socket
+perfetto_filegroup(
+ name = "src_tracing_ipc_default_socket",
+ srcs = [
+ "src/tracing/ipc/default_socket.cc",
+ ],
+)
+
# GN target: //src/tracing:client_api_without_backends
perfetto_filegroup(
name = "src_tracing_client_api_without_backends",
@@ -2826,6 +2864,7 @@
"protos/perfetto/trace/ftrace/sde.proto",
"protos/perfetto/trace/ftrace/signal.proto",
"protos/perfetto/trace/ftrace/sync.proto",
+ "protos/perfetto/trace/ftrace/synthetic.proto",
"protos/perfetto/trace/ftrace/systrace.proto",
"protos/perfetto/trace/ftrace/task.proto",
"protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
@@ -3526,6 +3565,7 @@
":src_tracing_in_process_backend",
":src_tracing_ipc_common",
":src_tracing_ipc_consumer_consumer",
+ ":src_tracing_ipc_default_socket",
":src_tracing_ipc_producer_producer",
":src_tracing_ipc_service_service",
":src_tracing_platform_impl",
@@ -3618,6 +3658,7 @@
":src_tracing_core_core",
":src_tracing_ipc_common",
":src_tracing_ipc_consumer_consumer",
+ ":src_tracing_ipc_default_socket",
":src_tracing_ipc_producer_producer",
"src/perfetto_cmd/main.cc",
],
@@ -3840,7 +3881,7 @@
":protos_perfetto_trace_track_event_zero",
":protozero",
":src_base_base",
- ":src_base_unix_socket",
+ ":src_base_http_http",
":src_trace_processor_containers_containers",
":src_trace_processor_importers_gen_cc_chrome_track_event_descriptor",
":src_trace_processor_importers_gen_cc_config_descriptor",
@@ -4186,12 +4227,26 @@
legacy_create_init = 0,
)
+perfetto_py_library(
+ name = "batch_trace_processor",
+ srcs = glob([
+ "tools/batch_trace_processor/perfetto/batch_trace_processor/*.py"
+ ]),
+ deps = [
+ ":trace_processor_py",
+ ] + PERFETTO_CONFIG.deps.pandas_py,
+ imports = [
+ "tools/batch_trace_processor",
+ ],
+)
+
perfetto_py_binary(
name = "batch_trace_processor_shell",
srcs = ["tools/batch_trace_processor/main.py"],
main = "tools/batch_trace_processor/main.py",
deps = [
":trace_processor_py",
+ ":batch_trace_processor",
] + PERFETTO_CONFIG.deps.pandas_py,
python_version = "PY3",
legacy_create_init = 0,
diff --git a/BUILD.extras b/BUILD.extras
index 28bec08..8c39316 100644
--- a/BUILD.extras
+++ b/BUILD.extras
@@ -142,12 +142,26 @@
legacy_create_init = 0,
)
+perfetto_py_library(
+ name = "batch_trace_processor",
+ srcs = glob([
+ "tools/batch_trace_processor/perfetto/batch_trace_processor/*.py"
+ ]),
+ deps = [
+ ":trace_processor_py",
+ ] + PERFETTO_CONFIG.deps.pandas_py,
+ imports = [
+ "tools/batch_trace_processor",
+ ],
+)
+
perfetto_py_binary(
name = "batch_trace_processor_shell",
srcs = ["tools/batch_trace_processor/main.py"],
main = "tools/batch_trace_processor/main.py",
deps = [
":trace_processor_py",
+ ":batch_trace_processor",
] + PERFETTO_CONFIG.deps.pandas_py,
python_version = "PY3",
legacy_create_init = 0,
diff --git a/BUILD.gn b/BUILD.gn
index fe44ed2..b36d588 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -68,7 +68,10 @@
}
if (enable_perfetto_tools) {
- all_targets += [ "tools" ]
+ all_targets += [
+ "tools",
+ "src/websocket_bridge",
+ ]
}
if (enable_perfetto_unittests) {
diff --git a/CHANGELOG b/CHANGELOG
index e316684..0f2befe 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,10 +4,15 @@
* Changed compiler flags. Assume recent x64 CPUs (-msse4.2 -mavx -mpopcnt).
This behavior affects only standalone builds and can be changed by setting
enable_perfetto_x64_cpu_opt=false in the GN args.
+ * The java heap profiler now rescans all the processes every time for
+ continous_dump_config. The scan_pids_only_on_start can be used to restore
+ the old behavior.
+ * Added support for building on ARM Macs
Trace Processor:
* Changed LIKE comparisions to be case-senstive. This may break existing
queries but was a necessary from a performance perspective.
* Changed compiler flags, assume recent x64 CPUs (see above).
+ * Changed how displayTimeUnit is handled in JSON traces to match catapult.
UI:
*
SDK:
diff --git a/docs/analysis/metrics.md b/docs/analysis/metrics.md
index 9e14b89..2602e88 100644
--- a/docs/analysis/metrics.md
+++ b/docs/analysis/metrics.md
@@ -63,30 +63,103 @@
}
```
-### Case for upstreaming
+## Metric development guide
-NOTE: Googlers: for internal usage of metrics in Google3 (i.e. metrics which are
-confidential), please see [this internal page](https://goto.google.com/viecd).
+As metric writing requires a lot of iterations to get right, there are several
+tips which make the experience a lot smoother.
-Authors are strongly encouraged to add all metrics derived on Perfetto traces to
-the Perfetto repo unless there is a clear usecase (e.g. confidentiality) why
-these metrics should not be publicly available.
+### Hot reloading metrics
+To obtain the fastest possible iteration time when developing metrics,
+it's possible to hot reload any changes to SQL; this will skip over both
+recompilation (for builtin metrics) and trace load (for both builtin and
+custom metrics).
-In return for upstreaming metrics, authors will have first class support for
-running metrics locally and the confidence that their metrics will remain stable
-as trace processor is developed.
+To do this, trace processor is started in *interactive mode* while
+still specifying command line flags about which metrics should be run and
+the paths of any extensions. Then, in the REPL shell, the commands
+`.load-metrics-sql` (which causes any SQL on disk to be re-read) and
+`.run-metrics` (to run the metrics and print the result).
-As well as scaling upwards while developing from running on a single trace
-locally to running on a large set of traces, the reverse is also very useful.
-When an anomaly is observed in the metrics of a lab benchmark, a representative
-trace can be downloaded and the same metric can be run locally in trace
-processor.
+For example, suppose we want to iterate on the `android_startup` metric. We
+can run the following commands from a Perfetto checkout:
+```python
+> ./tools/trace_processor --interactive \
+ --run_metrics android_startup \
+ --metric-extension src/trace_processor/metric@/
+ --dev \
+ <trace>
+android_startup {
+ <contents of startup metric>
+}
-Since the same code is running locally and remotely, developers can be confident
-in reproducing the issue and use the trace processor and/or the Perfetto UI to
-identify the problem.
+# Now make any changes you want to the SQL files related to the startup
+# metric. Even adding new files in the src/trace_processor/metric works.
-## Metric Helper Functions
+# Then, we can reload the changes using `.load-metrics-sql`.
+> .load-metrics-sql
+
+# We can rerun the changed metric using `.run-metrics`
+> .run-metrics
+android_startup {
+ <contents of changed startup metric>
+}
+```
+
+NOTE: see below about why `--dev` was required for this command.
+
+This also works for custom metrics specified on the command line:
+```python
+> ./tools/trace_processor -i --run_metrics /tmp/my_custom_metric.sql <trace>
+my_custom_metric {
+ <contents of my_custom_metric>
+}
+
+# Change the SQL file as before.
+
+> .load-metrics-sql
+> .run-metrics
+my_custom_metric {
+ <contents of changed my_custom_metric>
+}
+```
+
+WARNING: it is currently not possible to reload protos in the same way. If
+protos are changed, a recompile (for built-in metrics) and reinvoking
+trace processor is necessary to pick up the changes.
+
+WARNING: Deleted files from `--metric-extension` folders are *not* removed
+and will remain available e.g. to RUN_METRIC invocations.
+
+### Modifying built-in metric SQL without recompiling
+It is possible to override the SQL of built-in metrics at runtime without
+needing to recompile trace processor. To do this, the flag `--metric-extension`
+needs to be specified with the disk path where the built-metrics live and the
+special string `/` for the virtual path.
+
+For example, from inside a Perfetto checkout:
+```python
+> ./tools/trace_processor \
+ --run_metrics android_cpu \
+ --metric-extension src/trace_processor/metrics@/
+ --dev
+ <trace>
+```
+This will run the CPU metric using the live SQL in the repo *not* the SQL
+defintion built into the binary.
+
+NOTE: protos are *not* overriden in the same way - if any proto messages are
+changed a recompile of trace processor is required for the changes to be
+available.
+
+NOTE: the `--dev` flag is required for the use of this feature. This
+flag ensures that this feature is not accidentally in production as it is only
+intended for local development.
+
+WARNING: protos are *not* overriden in the same way - if any proto messages are
+changed a recompile of trace processor is required for the changes to be
+available.
+
+## Metric helper functions
There are several useful helpers functions which are available when writing a metric.
@@ -493,7 +566,30 @@
}
```
-### Next steps
+## Next steps
* The [common tasks](/docs/contributing/common-tasks.md) page gives a list of
steps on how new metrics can be added to the trace processor.
+
+## Appendix: Case for upstreaming
+
+NOTE: Googlers: for internal usage of metrics in Google3 (i.e. metrics which are
+confidential), please see [this internal page](https://goto.google.com/viecd).
+
+Authors are strongly encouraged to add all metrics derived on Perfetto traces to
+the Perfetto repo unless there is a clear usecase (e.g. confidentiality) why
+these metrics should not be publicly available.
+
+In return for upstreaming metrics, authors will have first class support for
+running metrics locally and the confidence that their metrics will remain stable
+as trace processor is developed.
+
+As well as scaling upwards while developing from running on a single trace
+locally to running on a large set of traces, the reverse is also very useful.
+When an anomaly is observed in the metrics of a lab benchmark, a representative
+trace can be downloaded and the same metric can be run locally in trace
+processor.
+
+Since the same code is running locally and remotely, developers can be confident
+in reproducing the issue and use the trace processor and/or the Perfetto UI to
+identify the problem.
diff --git a/docs/contributing/common-tasks.md b/docs/contributing/common-tasks.md
index d0724c9..39a36c7 100644
--- a/docs/contributing/common-tasks.md
+++ b/docs/contributing/common-tasks.md
@@ -78,6 +78,7 @@
| `dur` | `int64` | Mandatory for slice, NULL for counter | The duration of the slice |
| `slice_name` | `string` | Mandatory for slice, NULL for counter | The name of the slice |
| `value` | `double` | Mandatory for counter, NULL for slice | The value of the counter |
+| `group_name` | `string` | Optional | Name of the track group under which the track appears. All tracks with the same `group_name` are placed under the same group by that name. Tracks that lack this field or have NULL value in this field are displayed without any grouping. |
#### Known issues:
diff --git a/docs/data-sources/battery-counters.md b/docs/data-sources/battery-counters.md
index 89f6668..5cc837c 100644
--- a/docs/data-sources/battery-counters.md
+++ b/docs/data-sources/battery-counters.md
@@ -85,7 +85,7 @@
Config proto:
[AndroidPowerConfig](/docs/reference/trace-config-proto.autogen#AndroidPowerConfig)
-Sample config:
+Sample config (Android):
```protobuf
data_sources: {
@@ -101,6 +101,16 @@
}
```
+Sample Config (Chrome OS or Linux):
+
+```protobuf
+data_sources: {
+ config {
+ name: "linux.sysfs_power"
+ }
+}
+```
+
## Power rails
_This data source has been introduced in Android 10 (Q) and requires the
diff --git a/docs/data-sources/java-heap-profiler.md b/docs/data-sources/java-heap-profiler.md
index f03977e..1187b7a 100644
--- a/docs/data-sources/java-heap-profiler.md
+++ b/docs/data-sources/java-heap-profiler.md
@@ -21,6 +21,10 @@
![Flamegraph of a Java heap profiler](/docs/images/java-flamegraph.png)
+The native size of certain objects is represented as an extra child node in the
+flamegraph, prefixed with "[native]". The extra node counts as an extra object.
+This is available only on Android T+.
+
## SQL
Information about the Java Heap is written to the following tables:
@@ -29,6 +33,9 @@
* [`heap_graph_object`](/docs/analysis/sql-tables.autogen#heap_graph_object)
* [`heap_graph_reference`](/docs/analysis/sql-tables.autogen#heap_graph_reference)
+`native_size` (available only on Android T+) is extracted from the related
+`libcore.util.NativeAllocationRegistry` and is not included in `self_size`.
+
For instance, to get the bytes used by class name, run the following query.
As-is this query will often return un-actionable information, as most of the
bytes in the Java heap end up being primitive arrays or strings.
diff --git a/docs/visualization/deep-linking-to-perfetto-ui.md b/docs/visualization/deep-linking-to-perfetto-ui.md
index f6a9225..5f0148f 100644
--- a/docs/visualization/deep-linking-to-perfetto-ui.md
+++ b/docs/visualization/deep-linking-to-perfetto-ui.md
@@ -64,9 +64,9 @@
### Code samples
-See https://jsfiddle.net/primiano/1hd0a4wj/68/ (also mirrored on
-[this GitHub gist](https://gist.github.com/primiano/e164868b617844ef8fa4770eb3b323b9)
-)
+See [this example caller](https://gistcdn.rawgit.org/primiano/e164868b617844ef8fa4770eb3b323b9/1d9aa2bf52cf903709ea7dd4d583fd2d07d7a255/open_with_perfetto_ui.html),
+for which the code is in
+[this GitHub gist](https://gist.github.com/primiano/e164868b617844ef8fa4770eb3b323b9).
Googlers: take a look at the
[existing examples in the internal codesearch](http://go/perfetto-ui-deeplink-cs)
diff --git a/gn/standalone/toolchain/BUILD.gn b/gn/standalone/toolchain/BUILD.gn
index 8dba119..408f1f6 100644
--- a/gn/standalone/toolchain/BUILD.gn
+++ b/gn/standalone/toolchain/BUILD.gn
@@ -119,6 +119,8 @@
_target_triplet = "x86_64-apple-darwin"
} else if (target_os == "mac" && target_cpu == "x86") {
_target_triplet = "i686-apple-darwin"
+ } else if (target_os == "mac" && target_cpu == "arm64") {
+ _target_triplet = "aarch64-apple-darwin"
} else if (target_os == "linux" && target_cpu == "arm64") {
_target_triplet = "aarch64-linux-gnu"
_default_target_sysroot =
diff --git a/include/perfetto/ext/base/BUILD.gn b/include/perfetto/ext/base/BUILD.gn
index 34e09de..91dfa72 100644
--- a/include/perfetto/ext/base/BUILD.gn
+++ b/include/perfetto/ext/base/BUILD.gn
@@ -25,6 +25,7 @@
"endian.h",
"event_fd.h",
"file_utils.h",
+ "flat_hash_map.h",
"getopt.h",
"getopt_compat.h",
"hash.h",
@@ -37,6 +38,7 @@
"pipe.h",
"scoped_file.h",
"small_set.h",
+ "small_vector.h",
"string_splitter.h",
"string_utils.h",
"string_view.h",
diff --git a/include/perfetto/ext/base/crash_keys.h b/include/perfetto/ext/base/crash_keys.h
index 1d550f6..36ef358 100644
--- a/include/perfetto/ext/base/crash_keys.h
+++ b/include/perfetto/ext/base/crash_keys.h
@@ -79,7 +79,7 @@
ScopedClear(const ScopedClear&) = delete;
ScopedClear& operator=(const ScopedClear&) = delete;
ScopedClear& operator=(ScopedClear&&) = delete;
- ScopedClear(ScopedClear&& other) : key_(other.key_) {
+ ScopedClear(ScopedClear&& other) noexcept : key_(other.key_) {
other.key_ = nullptr;
}
@@ -100,22 +100,23 @@
enum class Type : uint8_t { kUnset = 0, kInt, kStr };
void Clear() {
- int_value_ = 0;
- type_ = Type::kUnset;
+ int_value_.store(0, std::memory_order_relaxed);
+ type_.store(Type::kUnset, std::memory_order_relaxed);
}
void Set(int64_t value) {
- int_value_ = value;
- type_ = Type::kInt;
+ int_value_.store(value, std::memory_order_relaxed);
+ type_.store(Type::kInt, std::memory_order_relaxed);
if (PERFETTO_UNLIKELY(!registered_.load(std::memory_order_relaxed)))
Register();
}
void Set(StringView sv) {
size_t len = std::min(sv.size(), sizeof(str_value_) - 1);
- memcpy(str_value_, sv.data(), len);
- str_value_[len] = '\0';
- type_ = Type::kStr;
+ for (size_t i = 0; i < len; ++i)
+ str_value_[i].store(sv.data()[i], std::memory_order_relaxed);
+ str_value_[len].store('\0', std::memory_order_relaxed);
+ type_.store(Type::kStr, std::memory_order_relaxed);
if (PERFETTO_UNLIKELY(!registered_.load(std::memory_order_relaxed)))
Register();
}
@@ -130,18 +131,20 @@
return ScopedClear(this);
}
- int64_t int_value() const { return int_value_; }
+ void Register();
+
+ int64_t int_value() const {
+ return int_value_.load(std::memory_order_relaxed);
+ }
size_t ToString(char* dst, size_t len);
private:
- void Register();
-
std::atomic<bool> registered_;
- Type type_;
+ std::atomic<Type> type_;
const char* const name_;
union {
- char str_value_[kCrashKeyMaxStrSize];
- int64_t int_value_;
+ std::atomic<char> str_value_[kCrashKeyMaxStrSize];
+ std::atomic<int64_t> int_value_;
};
};
diff --git a/include/perfetto/ext/base/flat_hash_map.h b/include/perfetto/ext/base/flat_hash_map.h
new file mode 100644
index 0000000..7759dbb
--- /dev/null
+++ b/include/perfetto/ext/base/flat_hash_map.h
@@ -0,0 +1,394 @@
+/*
+ * 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.
+ */
+
+#ifndef INCLUDE_PERFETTO_EXT_BASE_FLAT_HASH_MAP_H_
+#define INCLUDE_PERFETTO_EXT_BASE_FLAT_HASH_MAP_H_
+
+#include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/utils.h"
+
+#include <algorithm>
+#include <functional>
+#include <limits>
+
+namespace perfetto {
+namespace base {
+
+// An open-addressing hashmap implementation.
+// Pointers are not stable, neither for keys nor values.
+// Has similar performances of a RobinHood hash (without the complications)
+// and 2x an unordered map.
+// Doc: http://go/perfetto-hashtables .
+//
+// When used to implement a string pool in TraceProcessor, the performance
+// characteristics obtained by replaying the set of strings seeen in a 4GB trace
+// (226M strings, 1M unique) are the following (see flat_hash_map_benchmark.cc):
+// This(Linear+AppendOnly) 879,383,676 ns 258.013M insertions/s
+// This(LinearProbe): 909,206,047 ns 249.546M insertions/s
+// This(QuadraticProbe): 1,083,844,388 ns 209.363M insertions/s
+// std::unordered_map: 6,203,351,870 ns 36.5811M insertions/s
+// tsl::robin_map: 931,403,397 ns 243.622M insertions/s
+// absl::flat_hash_map: 998,013,459 ns 227.379M insertions/s
+// FollyF14FastMap: 1,181,480,602 ns 192.074M insertions/s
+
+// The structs below define the probing algorithm used to probe slots upon a
+// collision. They are guaranteed to visit all slots as our table size is always
+// a power of two (see https://en.wikipedia.org/wiki/Quadratic_probing).
+
+// Linear probing can be faster if the hashing is well distributed and the load
+// is not high. For TraceProcessor's StringPool this is the fastest. It can
+// degenerate badly if the hashing doesn't spread (e.g., if using directly pids
+// as keys, with a no-op hashing function).
+struct LinearProbe {
+ static inline size_t Calc(size_t key_hash, size_t step, size_t capacity) {
+ return (key_hash + step) & (capacity - 1); // Linear probe
+ }
+};
+
+// Generates the sequence: 0, 3, 10, 21, 36, 55, ...
+// Can be a bit (~5%) slower than LinearProbe because it's less cache hot, but
+// avoids degenerating badly if the hash function is bad and causes clusters.
+// A good default choice unless benchmarks prove otherwise.
+struct QuadraticProbe {
+ static inline size_t Calc(size_t key_hash, size_t step, size_t capacity) {
+ return (key_hash + 2 * step * step + step) & (capacity - 1);
+ }
+};
+
+// Tends to perform in the middle between linear and quadratic.
+// It's a bit more cache-effective than the QuadraticProbe but can create more
+// clustering if the hash function doesn't spread well.
+// Generates the sequence: 0, 1, 3, 6, 10, 15, 21, ...
+struct QuadraticHalfProbe {
+ static inline size_t Calc(size_t key_hash, size_t step, size_t capacity) {
+ return (key_hash + (step * step + step) / 2) & (capacity - 1);
+ }
+};
+
+template <typename Key,
+ typename Value,
+ typename Hasher = std::hash<Key>,
+ typename Probe = QuadraticProbe,
+ bool AppendOnly = false>
+class FlatHashMap {
+ public:
+ class Iterator {
+ public:
+ explicit Iterator(const FlatHashMap* map) : map_(map) { FindNextNonFree(); }
+ ~Iterator() = default;
+ Iterator(const Iterator&) = default;
+ Iterator& operator=(const Iterator&) = default;
+ Iterator(Iterator&&) noexcept = default;
+ Iterator& operator=(Iterator&&) noexcept = default;
+
+ Key& key() { return map_->keys_[idx_]; }
+ Value& value() { return map_->values_[idx_]; }
+ const Key& key() const { return map_->keys_[idx_]; }
+ const Value& value() const { return map_->values_[idx_]; }
+
+ explicit operator bool() const { return idx_ != kEnd; }
+ Iterator& operator++() {
+ PERFETTO_DCHECK(idx_ < map_->capacity_);
+ ++idx_;
+ FindNextNonFree();
+ return *this;
+ }
+
+ private:
+ static constexpr size_t kEnd = std::numeric_limits<size_t>::max();
+
+ void FindNextNonFree() {
+ const auto& tags = map_->tags_;
+ for (; idx_ < map_->capacity_; idx_++) {
+ if (tags[idx_] != kFreeSlot && (AppendOnly || tags[idx_] != kTombstone))
+ return;
+ }
+ idx_ = kEnd;
+ }
+
+ const FlatHashMap* map_ = nullptr;
+ size_t idx_ = 0;
+ }; // Iterator
+
+ static constexpr int kDefaultLoadLimitPct = 75;
+ explicit FlatHashMap(size_t initial_capacity = 0,
+ int load_limit_pct = kDefaultLoadLimitPct)
+ : load_limit_percent_(load_limit_pct) {
+ if (initial_capacity > 0)
+ Reset(initial_capacity);
+ }
+
+ // We are calling Clear() so that the destructors for the inserted entries are
+ // called (unless they are trivial, in which case it will be a no-op).
+ ~FlatHashMap() { Clear(); }
+
+ FlatHashMap(FlatHashMap&& other) noexcept {
+ tags_ = std::move(other.tags_);
+ keys_ = std::move(other.keys_);
+ values_ = std::move(other.values_);
+ capacity_ = other.capacity_;
+ size_ = other.size_;
+ max_probe_length_ = other.max_probe_length_;
+ load_limit_ = other.load_limit_;
+ load_limit_percent_ = other.load_limit_percent_;
+
+ new (&other) FlatHashMap();
+ }
+
+ FlatHashMap& operator=(FlatHashMap&& other) noexcept {
+ this->~FlatHashMap();
+ new (this) FlatHashMap(std::move(other));
+ return *this;
+ }
+
+ FlatHashMap(const FlatHashMap&) = delete;
+ FlatHashMap& operator=(const FlatHashMap&) = delete;
+
+ std::pair<Value*, bool> Insert(Key key, Value value) {
+ const size_t key_hash = Hasher{}(key);
+ const uint8_t tag = HashToTag(key_hash);
+ static constexpr size_t kSlotNotFound = std::numeric_limits<size_t>::max();
+
+ // This for loop does in reality at most two attempts:
+ // The first iteration either:
+ // - Early-returns, because the key exists already,
+ // - Finds an insertion slot and proceeds because the load is < limit.
+ // The second iteration is only hit in the unlikely case of this insertion
+ // bringing the table beyond the target |load_limit_| (or the edge case
+ // of the HT being full, if |load_limit_pct_| = 100).
+ // We cannot simply pre-grow the table before insertion, because we must
+ // guarantee that calling Insert() with a key that already exists doesn't
+ // invalidate iterators.
+ size_t insertion_slot;
+ size_t probe_len;
+ for (;;) {
+ PERFETTO_DCHECK((capacity_ & (capacity_ - 1)) == 0); // Must be a pow2.
+ insertion_slot = kSlotNotFound;
+ // Start the iteration at the desired slot (key_hash % capacity_)
+ // searching either for a free slot or a tombstone. In the worst case we
+ // might end up scanning the whole array of slots. The Probe functions are
+ // guaranteed to visit all the slots within |capacity_| steps. If we find
+ // a free slot, we can stop the search immediately (a free slot acts as an
+ // "end of chain for entries having the same hash". If we find a
+ // tombstones (a deleted slot) we remember its position, but have to keep
+ // searching until a free slot to make sure we don't insert a duplicate
+ // key.
+ for (probe_len = 0; probe_len < capacity_;) {
+ const size_t idx = Probe::Calc(key_hash, probe_len, capacity_);
+ PERFETTO_DCHECK(idx < capacity_);
+ const uint8_t tag_idx = tags_[idx];
+ ++probe_len;
+ if (tag_idx == kFreeSlot) {
+ // Rationale for "insertion_slot == kSlotNotFound": if we encountered
+ // a tombstone while iterating we should reuse that rather than
+ // taking another slot.
+ if (AppendOnly || insertion_slot == kSlotNotFound)
+ insertion_slot = idx;
+ break;
+ }
+ // We should never encounter tombstones in AppendOnly mode.
+ PERFETTO_DCHECK(!(tag_idx == kTombstone && AppendOnly));
+ if (!AppendOnly && tag_idx == kTombstone) {
+ insertion_slot = idx;
+ continue;
+ }
+ if (tag_idx == tag && keys_[idx] == key) {
+ // The key is already in the map.
+ return std::make_pair(&values_[idx], false);
+ }
+ } // for (idx)
+
+ // If we got to this point the key does not exist (otherwise we would have
+ // hit the the return above) and we are going to insert a new entry.
+ // Before doing so, ensure we stay under the target load limit.
+ if (PERFETTO_UNLIKELY(size_ >= load_limit_)) {
+ MaybeGrowAndRehash(/*grow=*/true);
+ continue;
+ }
+ PERFETTO_DCHECK(insertion_slot != kSlotNotFound);
+ break;
+ } // for (attempt)
+
+ PERFETTO_CHECK(insertion_slot < capacity_);
+
+ // We found a free slot (or a tombstone). Proceed with the insertion.
+ Value* value_idx = &values_[insertion_slot];
+ new (&keys_[insertion_slot]) Key(std::move(key));
+ new (value_idx) Value(std::move(value));
+ tags_[insertion_slot] = tag;
+ PERFETTO_DCHECK(probe_len > 0 && probe_len <= capacity_);
+ max_probe_length_ = std::max(max_probe_length_, probe_len);
+ size_++;
+
+ return std::make_pair(value_idx, true);
+ }
+
+ Value* Find(const Key& key) const {
+ const size_t idx = FindInternal(key);
+ if (idx == kNotFound)
+ return nullptr;
+ return &values_[idx];
+ }
+
+ bool Erase(const Key& key) {
+ if (AppendOnly)
+ PERFETTO_FATAL("Erase() not supported because AppendOnly=true");
+ size_t idx = FindInternal(key);
+ if (idx == kNotFound)
+ return false;
+ EraseInternal(idx);
+ return true;
+ }
+
+ void Clear() {
+ // Avoid trivial heap operations on zero-capacity std::move()-d objects.
+ if (PERFETTO_UNLIKELY(capacity_ == 0))
+ return;
+
+ for (size_t i = 0; i < capacity_; ++i) {
+ const uint8_t tag = tags_[i];
+ if (tag != kFreeSlot && tag != kTombstone)
+ EraseInternal(i);
+ }
+ // Clear all tombstones. We really need to do this for AppendOnly.
+ MaybeGrowAndRehash(/*grow=*/false);
+ }
+
+ Value& operator[](Key key) {
+ auto it_and_inserted = Insert(std::move(key), Value{});
+ return *it_and_inserted.first;
+ }
+
+ Iterator GetIterator() { return Iterator(this); }
+ const Iterator GetIterator() const { return Iterator(this); }
+
+ size_t size() const { return size_; }
+ size_t capacity() const { return capacity_; }
+
+ // "protected" here is only for the flat_hash_map_benchmark.cc. Everything
+ // below is by all means private.
+ protected:
+ enum ReservedTags : uint8_t { kFreeSlot = 0, kTombstone = 1 };
+ static constexpr size_t kNotFound = std::numeric_limits<size_t>::max();
+
+ size_t FindInternal(const Key& key) const {
+ const size_t key_hash = Hasher{}(key);
+ const uint8_t tag = HashToTag(key_hash);
+ PERFETTO_DCHECK((capacity_ & (capacity_ - 1)) == 0); // Must be a pow2.
+ PERFETTO_DCHECK(max_probe_length_ <= capacity_);
+ for (size_t i = 0; i < max_probe_length_; ++i) {
+ const size_t idx = Probe::Calc(key_hash, i, capacity_);
+ const uint8_t tag_idx = tags_[idx];
+
+ if (tag_idx == kFreeSlot)
+ return kNotFound;
+ // HashToTag() never returns kTombstone, so the tag-check below cannot
+ // possibly match. Also we just want to skip tombstones.
+ if (tag_idx == tag && keys_[idx] == key) {
+ PERFETTO_DCHECK(tag_idx > kTombstone);
+ return idx;
+ }
+ } // for (idx)
+ return kNotFound;
+ }
+
+ void EraseInternal(size_t idx) {
+ PERFETTO_DCHECK(tags_[idx] > kTombstone);
+ PERFETTO_DCHECK(size_ > 0);
+ tags_[idx] = kTombstone;
+ keys_[idx].~Key();
+ values_[idx].~Value();
+ size_--;
+ }
+
+ PERFETTO_NO_INLINE void MaybeGrowAndRehash(bool grow) {
+ PERFETTO_DCHECK(size_ <= capacity_);
+ const size_t old_capacity = capacity_;
+
+ // Grow quickly up to 1MB, then chill.
+ const size_t old_size_bytes = old_capacity * (sizeof(Key) + sizeof(Value));
+ const size_t grow_factor = old_size_bytes < (1024u * 1024u) ? 8 : 2;
+ const size_t new_capacity =
+ grow ? std::max(old_capacity * grow_factor, size_t(1024))
+ : old_capacity;
+
+ auto old_tags(std::move(tags_));
+ auto old_keys(std::move(keys_));
+ auto old_values(std::move(values_));
+ size_t old_size = size_;
+
+ // This must be a CHECK (i.e. not just a DCHECK) to prevent UAF attacks on
+ // 32-bit archs that try to double the size of the table until wrapping.
+ PERFETTO_CHECK(new_capacity >= old_capacity);
+ Reset(new_capacity);
+
+ size_t new_size = 0; // Recompute the size.
+ for (size_t i = 0; i < old_capacity; ++i) {
+ const uint8_t old_tag = old_tags[i];
+ if (old_tag != kFreeSlot && old_tag != kTombstone) {
+ Insert(std::move(old_keys[i]), std::move(old_values[i]));
+ old_keys[i].~Key(); // Destroy the old objects.
+ old_values[i].~Value();
+ new_size++;
+ }
+ }
+ PERFETTO_DCHECK(new_size == old_size);
+ size_ = new_size;
+ }
+
+ // Doesn't call destructors. Use Clear() for that.
+ PERFETTO_NO_INLINE void Reset(size_t n) {
+ PERFETTO_DCHECK((n & (n - 1)) == 0); // Must be a pow2.
+
+ capacity_ = n;
+ max_probe_length_ = 0;
+ size_ = 0;
+ load_limit_ = n * static_cast<size_t>(load_limit_percent_) / 100;
+ load_limit_ = std::min(load_limit_, n);
+
+ tags_.reset(new uint8_t[n]);
+ memset(&tags_[0], 0, n); // Clear all tags.
+ keys_ = AlignedAllocTyped<Key[]>(n); // Deliberately not 0-initialized.
+ values_ = AlignedAllocTyped<Value[]>(n); // Deliberately not 0-initialized.
+ }
+
+ static inline uint8_t HashToTag(size_t full_hash) {
+ uint8_t tag = full_hash >> (sizeof(full_hash) * 8 - 8);
+ // Ensure the hash is always >= 2. We use 0, 1 for kFreeSlot and kTombstone.
+ tag += (tag <= kTombstone) << 1;
+ PERFETTO_DCHECK(tag > kTombstone);
+ return tag;
+ }
+
+ size_t capacity_ = 0;
+ size_t size_ = 0;
+ size_t max_probe_length_ = 0;
+ size_t load_limit_ = 0; // Updated every time |capacity_| changes.
+ int load_limit_percent_ =
+ kDefaultLoadLimitPct; // Load factor limit in % of |capacity_|.
+
+ // These arrays have always the |capacity_| elements.
+ // Note: AlignedUniquePtr just allocates memory, doesn't invoke any ctor/dtor.
+ std::unique_ptr<uint8_t[]> tags_;
+ AlignedUniquePtr<Key[]> keys_;
+ AlignedUniquePtr<Value[]> values_;
+};
+
+} // namespace base
+} // namespace perfetto
+
+#endif // INCLUDE_PERFETTO_EXT_BASE_FLAT_HASH_MAP_H_
diff --git a/include/perfetto/ext/base/http/BUILD.gn b/include/perfetto/ext/base/http/BUILD.gn
new file mode 100644
index 0000000..3e42607
--- /dev/null
+++ b/include/perfetto/ext/base/http/BUILD.gn
@@ -0,0 +1,21 @@
+# Copyright (C) 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
+#
+# 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.
+
+source_set("http") {
+ sources = [
+ "http_server.h",
+ "sha1.h",
+ ]
+ public_deps = [ "..:base" ]
+}
diff --git a/include/perfetto/ext/base/http/http_server.h b/include/perfetto/ext/base/http/http_server.h
new file mode 100644
index 0000000..c251061
--- /dev/null
+++ b/include/perfetto/ext/base/http/http_server.h
@@ -0,0 +1,189 @@
+/*
+ * 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.
+ */
+
+#ifndef INCLUDE_PERFETTO_EXT_BASE_HTTP_HTTP_SERVER_H_
+#define INCLUDE_PERFETTO_EXT_BASE_HTTP_HTTP_SERVER_H_
+
+#include <array>
+#include <initializer_list>
+#include <list>
+#include <memory>
+#include <string>
+
+#include "perfetto/base/task_runner.h"
+#include "perfetto/ext/base/optional.h"
+#include "perfetto/ext/base/paged_memory.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/ext/base/unix_socket.h"
+
+namespace perfetto {
+namespace base {
+
+class HttpServerConnection;
+
+struct HttpRequest {
+ explicit HttpRequest(HttpServerConnection* c) : conn(c) {}
+
+ Optional<StringView> GetHeader(StringView name) const;
+
+ HttpServerConnection* conn;
+
+ // These StringViews point to memory in the rxbuf owned by |conn|. They are
+ // valid only within the OnHttpRequest() call.
+ StringView method;
+ StringView uri;
+ StringView origin;
+ StringView body;
+ bool is_websocket_handshake = false;
+
+ private:
+ friend class HttpServer;
+ struct Header {
+ StringView name;
+ StringView value;
+ };
+
+ static constexpr uint32_t kMaxHeaders = 32;
+ std::array<Header, kMaxHeaders> headers{};
+ size_t num_headers = 0;
+};
+
+struct WebsocketMessage {
+ explicit WebsocketMessage(HttpServerConnection* c) : conn(c) {}
+
+ HttpServerConnection* conn;
+
+ // Note: message boundaries are not respected in case of fragmentation.
+ // This websocket implementation preserves only the byte stream, but not the
+ // atomicity of inbound messages (like SOCK_STREAM, unlike SOCK_DGRAM).
+ // Holds onto the connection's |rxbuf|. This is valid only within the scope
+ // of the OnWebsocketMessage() callback.
+ StringView data;
+
+ // If false the payload contains binary data. If true it's supposed to contain
+ // text. Note that there is no guarantee this will be the case. This merely
+ // reflect the opcode that the client sets on each message.
+ bool is_text = false;
+};
+
+class HttpServerConnection {
+ public:
+ static constexpr size_t kOmitContentLength = static_cast<size_t>(-1);
+
+ explicit HttpServerConnection(std::unique_ptr<UnixSocket>);
+ ~HttpServerConnection();
+
+ void SendResponseHeaders(const char* http_code,
+ std::initializer_list<const char*> headers = {},
+ size_t content_length = 0);
+
+ // Works also for websockets.
+ void SendResponseBody(const void* content, size_t content_length);
+ void Close();
+
+ // All the above in one shot.
+ void SendResponse(const char* http_code,
+ std::initializer_list<const char*> headers = {},
+ StringView content = {},
+ bool force_close = false);
+ void SendResponseAndClose(const char* http_code,
+ std::initializer_list<const char*> headers = {},
+ StringView content = {}) {
+ SendResponse(http_code, headers, content, true);
+ }
+
+ // The metods below are only valid for websocket connections.
+
+ // Upgrade an existing connection to a websocket. This can be called only in
+ // the context of OnHttpRequest(req) if req.is_websocket_handshake == true.
+ // If the origin is not in the |allowed_origins_|, the request will fail with
+ // a 403 error (this is because there is no browser-side CORS support for
+ // websockets).
+ void UpgradeToWebsocket(const HttpRequest&);
+ void SendWebsocketMessage(const void* data, size_t len);
+ void SendWebsocketMessage(StringView sv) {
+ SendWebsocketMessage(sv.data(), sv.size());
+ }
+ void SendWebsocketFrame(uint8_t opcode,
+ const void* payload,
+ size_t payload_len);
+
+ bool is_websocket() const { return is_websocket_; }
+
+ private:
+ friend class HttpServer;
+
+ size_t rxbuf_avail() { return rxbuf.size() - rxbuf_used; }
+
+ std::unique_ptr<UnixSocket> sock;
+ PagedMemory rxbuf;
+ size_t rxbuf_used = 0;
+ bool is_websocket_ = false;
+ bool headers_sent_ = false;
+ size_t content_len_headers_ = 0;
+ size_t content_len_actual_ = 0;
+
+ // If the origin is in the server's |allowed_origins_| this contains the
+ // origin itself. This is used to handle CORS headers.
+ std::string origin_allowed_;
+
+ // By default treat connections as keep-alive unless the client says
+ // explicitly 'Connection: close'. This improves TraceProcessor's python API.
+ // This is consistent with that nginx does.
+ bool keepalive_ = true;
+};
+
+class HttpRequestHandler {
+ public:
+ virtual ~HttpRequestHandler();
+ virtual void OnHttpRequest(const HttpRequest&) = 0;
+ virtual void OnWebsocketMessage(const WebsocketMessage&);
+ virtual void OnHttpConnectionClosed(HttpServerConnection*);
+};
+
+class HttpServer : public UnixSocket::EventListener {
+ public:
+ HttpServer(TaskRunner*, HttpRequestHandler*);
+ ~HttpServer() override;
+ void Start(int port);
+ void AddAllowedOrigin(const std::string&);
+
+ private:
+ size_t ParseOneHttpRequest(HttpServerConnection*);
+ size_t ParseOneWebsocketFrame(HttpServerConnection*);
+ void HandleCorsPreflightRequest(const HttpRequest&);
+ bool IsOriginAllowed(StringView);
+
+ // UnixSocket::EventListener implementation.
+ void OnNewIncomingConnection(UnixSocket*,
+ std::unique_ptr<UnixSocket>) override;
+ void OnConnect(UnixSocket* self, bool connected) override;
+ void OnDisconnect(UnixSocket* self) override;
+ void OnDataAvailable(UnixSocket* self) override;
+
+ TaskRunner* const task_runner_;
+ HttpRequestHandler* req_handler_;
+ std::unique_ptr<UnixSocket> sock4_;
+ std::unique_ptr<UnixSocket> sock6_;
+ std::list<HttpServerConnection> clients_;
+ std::list<std::string> allowed_origins_;
+ bool origin_error_logged_ = false;
+};
+
+} // namespace base
+} // namespace perfetto
+
+#endif // INCLUDE_PERFETTO_EXT_BASE_HTTP_HTTP_SERVER_H_
diff --git a/include/perfetto/ext/base/http/sha1.h b/include/perfetto/ext/base/http/sha1.h
new file mode 100644
index 0000000..c583d69
--- /dev/null
+++ b/include/perfetto/ext/base/http/sha1.h
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+#ifndef INCLUDE_PERFETTO_EXT_BASE_HTTP_SHA1_H_
+#define INCLUDE_PERFETTO_EXT_BASE_HTTP_SHA1_H_
+
+#include <stddef.h>
+
+#include <array>
+#include <string>
+
+namespace perfetto {
+namespace base {
+
+constexpr size_t kSHA1Length = 20;
+using SHA1Digest = std::array<uint8_t, kSHA1Length>;
+
+SHA1Digest SHA1Hash(const std::string& str);
+SHA1Digest SHA1Hash(const void* data, size_t size);
+
+} // namespace base
+} // namespace perfetto
+
+#endif // INCLUDE_PERFETTO_EXT_BASE_HTTP_SHA1_H_
diff --git a/include/perfetto/ext/base/small_vector.h b/include/perfetto/ext/base/small_vector.h
new file mode 100644
index 0000000..15b1773
--- /dev/null
+++ b/include/perfetto/ext/base/small_vector.h
@@ -0,0 +1,188 @@
+/*
+ * 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.
+ */
+
+#ifndef INCLUDE_PERFETTO_EXT_BASE_SMALL_VECTOR_H_
+#define INCLUDE_PERFETTO_EXT_BASE_SMALL_VECTOR_H_
+
+#include <algorithm>
+#include <type_traits>
+#include <utility>
+
+#include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/utils.h"
+
+namespace perfetto {
+namespace base {
+
+// Uses inline storage first, switches to dynamic storage when it overflows.
+template <typename T, size_t kSize>
+class SmallVector {
+ public:
+ static constexpr size_t kInlineSize = kSize;
+
+ explicit SmallVector() = default;
+
+ ~SmallVector() {
+ clear();
+ if (PERFETTO_UNLIKELY(is_using_heap()))
+ AlignedFree(begin_);
+ begin_ = end_ = end_of_storage_ = nullptr;
+ }
+
+ // Move operators.
+ SmallVector(SmallVector&& other) noexcept(
+ std::is_nothrow_move_constructible<T>::value) {
+ if (other.is_using_heap()) {
+ // Move the heap content, no need to move the individual objects as their
+ // location won't change.
+ begin_ = other.begin_;
+ end_ = other.end_;
+ end_of_storage_ = other.end_of_storage_;
+ } else {
+ const size_t other_size = other.size();
+ PERFETTO_DCHECK(other_size <= capacity());
+ for (size_t i = 0; i < other_size; i++) {
+ // Move the entries and destroy the ones in the moved-from object.
+ new (&begin_[i]) T(std::move(other.begin_[i]));
+ other.begin_[i].~T();
+ }
+ end_ = begin_ + other_size;
+ }
+ auto* const other_inline_storage = other.inline_storage_begin();
+ other.end_ = other.begin_ = other_inline_storage;
+ other.end_of_storage_ = other_inline_storage + kInlineSize;
+ }
+
+ SmallVector& operator=(SmallVector&& other) noexcept(
+ std::is_nothrow_move_constructible<T>::value) {
+ this->~SmallVector();
+ new (this) SmallVector<T, kSize>(std::move(other));
+ return *this;
+ }
+
+ // Copy operators.
+ SmallVector(const SmallVector& other) {
+ const size_t other_size = other.size();
+ if (other_size > capacity())
+ Grow(other_size);
+ // Copy-construct the elements.
+ for (size_t i = 0; i < other_size; ++i)
+ new (&begin_[i]) T(other.begin_[i]);
+ end_ = begin_ + other_size;
+ }
+
+ SmallVector& operator=(const SmallVector& other) {
+ if (PERFETTO_UNLIKELY(this == &other))
+ return *this;
+ this->~SmallVector();
+ new (this) SmallVector<T, kSize>(other);
+ return *this;
+ }
+
+ T* data() { return begin_; }
+ const T* data() const { return begin_; }
+
+ T* begin() { return begin_; }
+ const T* begin() const { return begin_; }
+
+ T* end() { return end_; }
+ const T* end() const { return end_; }
+
+ size_t size() const { return static_cast<size_t>(end_ - begin_); }
+
+ bool empty() const { return end_ == begin_; }
+
+ size_t capacity() const {
+ return static_cast<size_t>(end_of_storage_ - begin_);
+ }
+
+ T& back() {
+ PERFETTO_DCHECK(!empty());
+ return end_[-1];
+ }
+ const T& back() const {
+ PERFETTO_DCHECK(!empty());
+ return end_[-1];
+ }
+
+ T& operator[](size_t index) {
+ PERFETTO_DCHECK(index < size());
+ return begin_[index];
+ }
+
+ const T& operator[](size_t index) const {
+ PERFETTO_DCHECK(index < size());
+ return begin_[index];
+ }
+
+ template <typename... Args>
+ void emplace_back(Args&&... args) {
+ T* end = end_;
+ if (PERFETTO_UNLIKELY(end == end_of_storage_))
+ end = Grow();
+ new (end) T(std::forward<Args>(args)...);
+ end_ = end + 1;
+ }
+
+ void pop_back() {
+ PERFETTO_DCHECK(!empty());
+ back().~T();
+ --end_;
+ }
+
+ // Clear without reverting back to inline storage.
+ void clear() {
+ while (!empty())
+ pop_back();
+ }
+
+ private:
+ PERFETTO_NO_INLINE T* Grow(size_t desired_capacity = 0) {
+ size_t cur_size = size();
+ size_t new_capacity = desired_capacity;
+ if (desired_capacity <= cur_size)
+ new_capacity = std::max(capacity() * 2, size_t(128));
+ T* new_storage =
+ static_cast<T*>(AlignedAlloc(alignof(T), new_capacity * sizeof(T)));
+ for (size_t i = 0; i < cur_size; ++i) {
+ // Move the elements into the new heap buffer and destroy the old ones.
+ new (&new_storage[i]) T(std::move(begin_[i]));
+ begin_[i].~T();
+ }
+ if (is_using_heap())
+ AlignedFree(begin_);
+ begin_ = new_storage;
+ end_ = new_storage + cur_size;
+ end_of_storage_ = new_storage + new_capacity;
+ return end_;
+ }
+
+ T* inline_storage_begin() { return reinterpret_cast<T*>(&inline_storage_); }
+ bool is_using_heap() { return begin_ != inline_storage_begin(); }
+
+ T* begin_ = inline_storage_begin();
+ T* end_ = begin_;
+ T* end_of_storage_ = begin_ + kInlineSize;
+
+ typename std::aligned_storage<sizeof(T) * kInlineSize, alignof(T)>::type
+ inline_storage_;
+};
+
+} // namespace base
+} // namespace perfetto
+
+#endif // INCLUDE_PERFETTO_EXT_BASE_SMALL_VECTOR_H_
diff --git a/include/perfetto/ext/base/string_view.h b/include/perfetto/ext/base/string_view.h
index 83c7ea6..81c1a75 100644
--- a/include/perfetto/ext/base/string_view.h
+++ b/include/perfetto/ext/base/string_view.h
@@ -106,7 +106,7 @@
return StringView(data_ + pos, rcount);
}
- bool CaseInsensitiveEq(const StringView& other) {
+ bool CaseInsensitiveEq(const StringView& other) const {
if (size() != other.size())
return false;
if (size() == 0)
diff --git a/include/perfetto/ext/base/unix_socket.h b/include/perfetto/ext/base/unix_socket.h
index 15f2eaa..a769423 100644
--- a/include/perfetto/ext/base/unix_socket.h
+++ b/include/perfetto/ext/base/unix_socket.h
@@ -140,6 +140,10 @@
const int* send_fds = nullptr,
size_t num_fds = 0);
+ ssize_t SendStr(const std::string& str) {
+ return Send(str.data(), str.size());
+ }
+
// |fd_vec| and |max_files| are ignored on Windows.
ssize_t Receive(void* msg,
size_t len,
@@ -171,6 +175,7 @@
#endif
SockFamily family_ = SockFamily::kUnix;
SockType type_ = SockType::kStream;
+ uint32_t tx_timeout_ms_ = 0;
};
// A non-blocking UNIX domain socket. Allows also to transfer file descriptors.
diff --git a/include/perfetto/ext/base/utils.h b/include/perfetto/ext/base/utils.h
index b88655a..ae1fc33 100644
--- a/include/perfetto/ext/base/utils.h
+++ b/include/perfetto/ext/base/utils.h
@@ -161,6 +161,37 @@
static_cast<TU*>(AlignedAlloc(alignof(TU), sizeof(TU) * n_membs)));
}
+// A RAII wrapper to invoke a function when leaving a function/scope.
+template <typename Func>
+class OnScopeExitWrapper {
+ public:
+ explicit OnScopeExitWrapper(Func f) : f_(std::move(f)), active_(true) {}
+ OnScopeExitWrapper(OnScopeExitWrapper&& other) noexcept
+ : f_(std::move(other.f_)), active_(other.active_) {
+ other.active_ = false;
+ }
+ ~OnScopeExitWrapper() {
+ if (active_)
+ f_();
+ }
+
+ private:
+ Func f_;
+ bool active_;
+};
+
+template <typename Func>
+PERFETTO_WARN_UNUSED_RESULT OnScopeExitWrapper<Func> OnScopeExit(Func f) {
+ return OnScopeExitWrapper<Func>(std::move(f));
+}
+
+// Returns a xxd-style hex dump (hex + ascii chars) of the input data.
+std::string HexDump(const void* data, size_t len, size_t bytes_per_line = 16);
+inline std::string HexDump(const std::string& data,
+ size_t bytes_per_line = 16) {
+ return HexDump(data.data(), data.size(), bytes_per_line);
+}
+
} // namespace base
} // namespace perfetto
diff --git a/infra/perfetto.dev/src/assets/style.scss b/infra/perfetto.dev/src/assets/style.scss
index 0d0bbbe..7f23862 100644
--- a/infra/perfetto.dev/src/assets/style.scss
+++ b/infra/perfetto.dev/src/assets/style.scss
@@ -86,7 +86,7 @@
--sh-padding-y: 5px;
max-height: var(--site-header-height);
padding: var(--sh-padding-y) 30px;
- box-shadow: rgba(0, 0, 0, 0.3) 0px 3px 3px 0px;
+ box-shadow: rgba(0, 0, 0, 0.3) 0 3px 3px 0;
overflow: hidden;
display: flex;
z-index: 10;
diff --git a/protos/perfetto/common/perf_events.proto b/protos/perfetto/common/perf_events.proto
index 0d594c9..8c48396 100644
--- a/protos/perfetto/common/perf_events.proto
+++ b/protos/perfetto/common/perf_events.proto
@@ -18,6 +18,7 @@
package perfetto.protos;
+// Next id: 12
message PerfEvents {
// What event to sample on, and how often. Commented from the perspective of
// its use in |PerfEventConfig|.
@@ -50,6 +51,13 @@
RawEvent raw_event = 5;
}
+ // If set, samples will be timestamped with the given clock.
+ // If unset, the clock is chosen by the implementation.
+ // For software events, prefer PERF_CLOCK_BOOTTIME. However it cannot be
+ // used for hardware events (due to interrupt safety), for which the
+ // recommendation is to use one of the monotonic clocks.
+ optional PerfClock timestamp_clock = 11;
+
// Optional arbitrary name for the event, to identify it in the parsed
// trace. Does *not* affect the profiling itself. If unset, the trace
// parser will choose a suitable name.
@@ -89,4 +97,15 @@
optional uint64 config1 = 3;
optional uint64 config2 = 4;
}
+
+ // Subset of clocks that is supported by perf timestamping.
+ // CLOCK_TAI is excluded since it's not expected to be used in practice, but
+ // would require additions to the trace clock synchronisation logic.
+ enum PerfClock {
+ UNKNOWN_PERF_CLOCK = 0;
+ PERF_CLOCK_REALTIME = 1;
+ PERF_CLOCK_MONOTONIC = 2;
+ PERF_CLOCK_MONOTONIC_RAW = 3;
+ PERF_CLOCK_BOOTTIME = 4;
+ }
}
diff --git a/protos/perfetto/config/ftrace/ftrace_config.proto b/protos/perfetto/config/ftrace/ftrace_config.proto
index 070b481..7a38d74 100644
--- a/protos/perfetto/config/ftrace/ftrace_config.proto
+++ b/protos/perfetto/config/ftrace/ftrace_config.proto
@@ -48,4 +48,20 @@
// initialized synchronously on the data source start and hence avoiding
// timing races in tests.
optional bool initialize_ksyms_synchronously_for_testing = 14;
+
+ // When this boolean is true AND the ftrace_events contains "kmem/rss_stat",
+ // this option causes traced_probes to enable the "kmem/rss_stat_throttled"
+ // event instad if present, and fall back to "kmem/rss_stat" if not present.
+ // The historical context for this is the following:
+ // - Up to Android S (12), the rss_stat was internally throttled in its
+ // kernel implementation.
+ // - A change introduced in the kernels after S has introduced a new
+ // "rss_stat_throttled" making the original "rss_stat" event unthrottled
+ // (hence very spammy).
+ // - Not all Android T/13 devices will receive a new kernel though, hence we
+ // need to deal with both cases.
+ // For more context: go/rss-stat-throttled.
+ // TODO(kaleshsingh): implement the logic behind this. Right now this flag
+ // does nothing.
+ optional bool throttle_rss_stat = 15;
}
diff --git a/protos/perfetto/config/perfetto_config.proto b/protos/perfetto/config/perfetto_config.proto
index 5c83bee..52f256a 100644
--- a/protos/perfetto/config/perfetto_config.proto
+++ b/protos/perfetto/config/perfetto_config.proto
@@ -401,6 +401,22 @@
// initialized synchronously on the data source start and hence avoiding
// timing races in tests.
optional bool initialize_ksyms_synchronously_for_testing = 14;
+
+ // When this boolean is true AND the ftrace_events contains "kmem/rss_stat",
+ // this option causes traced_probes to enable the "kmem/rss_stat_throttled"
+ // event instad if present, and fall back to "kmem/rss_stat" if not present.
+ // The historical context for this is the following:
+ // - Up to Android S (12), the rss_stat was internally throttled in its
+ // kernel implementation.
+ // - A change introduced in the kernels after S has introduced a new
+ // "rss_stat_throttled" making the original "rss_stat" event unthrottled
+ // (hence very spammy).
+ // - Not all Android T/13 devices will receive a new kernel though, hence we
+ // need to deal with both cases.
+ // For more context: go/rss-stat-throttled.
+ // TODO(kaleshsingh): implement the logic behind this. Right now this flag
+ // does nothing.
+ optional bool throttle_rss_stat = 15;
}
// End of protos/perfetto/config/ftrace/ftrace_config.proto
@@ -769,6 +785,13 @@
optional uint32 dump_phase_ms = 1;
// ms to wait between following dumps.
optional uint32 dump_interval_ms = 2;
+ // If true, scans all the processes to find `process_cmdline` and filter by
+ // `min_anonymous_memory_kb` only at data source start. Default on Android
+ // S-.
+ //
+ // If false, rescans all the processes to find on every dump. Default on
+ // Android T+.
+ optional bool scan_pids_only_on_start = 3;
}
// This input is normalized in the following way: if it contains slashes,
@@ -813,6 +836,7 @@
// Begin of protos/perfetto/common/perf_events.proto
+// Next id: 12
message PerfEvents {
// What event to sample on, and how often. Commented from the perspective of
// its use in |PerfEventConfig|.
@@ -845,6 +869,13 @@
RawEvent raw_event = 5;
}
+ // If set, samples will be timestamped with the given clock.
+ // If unset, the clock is chosen by the implementation.
+ // For software events, prefer PERF_CLOCK_BOOTTIME. However it cannot be
+ // used for hardware events (due to interrupt safety), for which the
+ // recommendation is to use one of the monotonic clocks.
+ optional PerfClock timestamp_clock = 11;
+
// Optional arbitrary name for the event, to identify it in the parsed
// trace. Does *not* affect the profiling itself. If unset, the trace
// parser will choose a suitable name.
@@ -884,6 +915,17 @@
optional uint64 config1 = 3;
optional uint64 config2 = 4;
}
+
+ // Subset of clocks that is supported by perf timestamping.
+ // CLOCK_TAI is excluded since it's not expected to be used in practice, but
+ // would require additions to the trace clock synchronisation logic.
+ enum PerfClock {
+ UNKNOWN_PERF_CLOCK = 0;
+ PERF_CLOCK_REALTIME = 1;
+ PERF_CLOCK_MONOTONIC = 2;
+ PERF_CLOCK_MONOTONIC_RAW = 3;
+ PERF_CLOCK_BOOTTIME = 4;
+ }
}
// End of protos/perfetto/common/perf_events.proto
diff --git a/protos/perfetto/config/profiling/java_hprof_config.proto b/protos/perfetto/config/profiling/java_hprof_config.proto
index d504678..9245ee6 100644
--- a/protos/perfetto/config/profiling/java_hprof_config.proto
+++ b/protos/perfetto/config/profiling/java_hprof_config.proto
@@ -27,6 +27,13 @@
optional uint32 dump_phase_ms = 1;
// ms to wait between following dumps.
optional uint32 dump_interval_ms = 2;
+ // If true, scans all the processes to find `process_cmdline` and filter by
+ // `min_anonymous_memory_kb` only at data source start. Default on Android
+ // S-.
+ //
+ // If false, rescans all the processes to find on every dump. Default on
+ // Android T+.
+ optional bool scan_pids_only_on_start = 3;
}
// This input is normalized in the following way: if it contains slashes,
diff --git a/protos/perfetto/metrics/android/java_heap_stats.proto b/protos/perfetto/metrics/android/java_heap_stats.proto
index 3f269c0..2579888 100644
--- a/protos/perfetto/metrics/android/java_heap_stats.proto
+++ b/protos/perfetto/metrics/android/java_heap_stats.proto
@@ -26,14 +26,19 @@
optional int64 obj_count = 3;
}
- // Next id: 7
+ // Next id: 10
message Sample {
optional int64 ts = 1;
// Size of the Java heap in bytes
optional int64 heap_size = 2;
+ // Native size of all the objects (not included in heap_size)
+ optional int64 heap_native_size = 8;
optional int64 obj_count = 4;
// Size of the reachable objects in bytes.
optional int64 reachable_heap_size = 3;
+ // Native size of all the reachable objects (not included in
+ // reachable_heap_size)
+ optional int64 reachable_heap_native_size = 9;
optional int64 reachable_obj_count = 5;
// Sum of anonymous RSS + swap pages in bytes.
optional int64 anon_rss_and_swap_size = 6;
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index 6cf96ca..15a8d87 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -607,14 +607,19 @@
optional int64 obj_count = 3;
}
- // Next id: 7
+ // Next id: 10
message Sample {
optional int64 ts = 1;
// Size of the Java heap in bytes
optional int64 heap_size = 2;
+ // Native size of all the objects (not included in heap_size)
+ optional int64 heap_native_size = 8;
optional int64 obj_count = 4;
// Size of the reachable objects in bytes.
optional int64 reachable_heap_size = 3;
+ // Native size of all the reachable objects (not included in
+ // reachable_heap_size)
+ optional int64 reachable_heap_native_size = 9;
optional int64 reachable_obj_count = 5;
// Sum of anonymous RSS + swap pages in bytes.
optional int64 anon_rss_and_swap_size = 6;
diff --git a/protos/perfetto/trace/ftrace/all_protos.gni b/protos/perfetto/trace/ftrace/all_protos.gni
index 8910853..ec0b55d 100644
--- a/protos/perfetto/trace/ftrace/all_protos.gni
+++ b/protos/perfetto/trace/ftrace/all_protos.gni
@@ -54,6 +54,7 @@
"sde.proto",
"signal.proto",
"sync.proto",
+ "synthetic.proto",
"systrace.proto",
"task.proto",
"thermal.proto",
diff --git a/protos/perfetto/trace/ftrace/ftrace_event.proto b/protos/perfetto/trace/ftrace/ftrace_event.proto
index 31e5b92..e236b0d 100644
--- a/protos/perfetto/trace/ftrace/ftrace_event.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_event.proto
@@ -54,6 +54,7 @@
import "protos/perfetto/trace/ftrace/sde.proto";
import "protos/perfetto/trace/ftrace/signal.proto";
import "protos/perfetto/trace/ftrace/sync.proto";
+import "protos/perfetto/trace/ftrace/synthetic.proto";
import "protos/perfetto/trace/ftrace/systrace.proto";
import "protos/perfetto/trace/ftrace/task.proto";
import "protos/perfetto/trace/ftrace/thermal.proto";
@@ -447,5 +448,6 @@
SdeSdePerfCrtcUpdateFtraceEvent sde_sde_perf_crtc_update = 356;
SdeSdePerfSetQosLutsFtraceEvent sde_sde_perf_set_qos_luts = 357;
SdeSdePerfUpdateBusFtraceEvent sde_sde_perf_update_bus = 358;
+ RssStatThrottledFtraceEvent rss_stat_throttled = 359;
}
}
diff --git a/protos/perfetto/trace/ftrace/synthetic.proto b/protos/perfetto/trace/ftrace/synthetic.proto
new file mode 100644
index 0000000..32ea403
--- /dev/null
+++ b/protos/perfetto/trace/ftrace/synthetic.proto
@@ -0,0 +1,13 @@
+// Autogenerated by:
+// ../../tools/ftrace_proto_gen/ftrace_proto_gen.cc
+// Do not edit.
+
+syntax = "proto2";
+package perfetto.protos;
+
+message RssStatThrottledFtraceEvent {
+ optional uint32 curr = 1;
+ optional int32 member = 2;
+ optional uint32 mm_id = 3;
+ optional int64 size = 4;
+}
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 1c58fbc..c4eb417 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -401,6 +401,22 @@
// initialized synchronously on the data source start and hence avoiding
// timing races in tests.
optional bool initialize_ksyms_synchronously_for_testing = 14;
+
+ // When this boolean is true AND the ftrace_events contains "kmem/rss_stat",
+ // this option causes traced_probes to enable the "kmem/rss_stat_throttled"
+ // event instad if present, and fall back to "kmem/rss_stat" if not present.
+ // The historical context for this is the following:
+ // - Up to Android S (12), the rss_stat was internally throttled in its
+ // kernel implementation.
+ // - A change introduced in the kernels after S has introduced a new
+ // "rss_stat_throttled" making the original "rss_stat" event unthrottled
+ // (hence very spammy).
+ // - Not all Android T/13 devices will receive a new kernel though, hence we
+ // need to deal with both cases.
+ // For more context: go/rss-stat-throttled.
+ // TODO(kaleshsingh): implement the logic behind this. Right now this flag
+ // does nothing.
+ optional bool throttle_rss_stat = 15;
}
// End of protos/perfetto/config/ftrace/ftrace_config.proto
@@ -769,6 +785,13 @@
optional uint32 dump_phase_ms = 1;
// ms to wait between following dumps.
optional uint32 dump_interval_ms = 2;
+ // If true, scans all the processes to find `process_cmdline` and filter by
+ // `min_anonymous_memory_kb` only at data source start. Default on Android
+ // S-.
+ //
+ // If false, rescans all the processes to find on every dump. Default on
+ // Android T+.
+ optional bool scan_pids_only_on_start = 3;
}
// This input is normalized in the following way: if it contains slashes,
@@ -813,6 +836,7 @@
// Begin of protos/perfetto/common/perf_events.proto
+// Next id: 12
message PerfEvents {
// What event to sample on, and how often. Commented from the perspective of
// its use in |PerfEventConfig|.
@@ -845,6 +869,13 @@
RawEvent raw_event = 5;
}
+ // If set, samples will be timestamped with the given clock.
+ // If unset, the clock is chosen by the implementation.
+ // For software events, prefer PERF_CLOCK_BOOTTIME. However it cannot be
+ // used for hardware events (due to interrupt safety), for which the
+ // recommendation is to use one of the monotonic clocks.
+ optional PerfClock timestamp_clock = 11;
+
// Optional arbitrary name for the event, to identify it in the parsed
// trace. Does *not* affect the profiling itself. If unset, the trace
// parser will choose a suitable name.
@@ -884,6 +915,17 @@
optional uint64 config1 = 3;
optional uint64 config2 = 4;
}
+
+ // Subset of clocks that is supported by perf timestamping.
+ // CLOCK_TAI is excluded since it's not expected to be used in practice, but
+ // would require additions to the trace clock synchronisation logic.
+ enum PerfClock {
+ UNKNOWN_PERF_CLOCK = 0;
+ PERF_CLOCK_REALTIME = 1;
+ PERF_CLOCK_MONOTONIC = 2;
+ PERF_CLOCK_MONOTONIC_RAW = 3;
+ PERF_CLOCK_BOOTTIME = 4;
+ }
}
// End of protos/perfetto/common/perf_events.proto
@@ -5113,6 +5155,17 @@
// End of protos/perfetto/trace/ftrace/sync.proto
+// Begin of protos/perfetto/trace/ftrace/synthetic.proto
+
+message RssStatThrottledFtraceEvent {
+ optional uint32 curr = 1;
+ optional int32 member = 2;
+ optional uint32 mm_id = 3;
+ optional int64 size = 4;
+}
+
+// End of protos/perfetto/trace/ftrace/synthetic.proto
+
// Begin of protos/perfetto/trace/ftrace/systrace.proto
message ZeroFtraceEvent {
@@ -5584,6 +5637,7 @@
SdeSdePerfCrtcUpdateFtraceEvent sde_sde_perf_crtc_update = 356;
SdeSdePerfSetQosLutsFtraceEvent sde_sde_perf_set_qos_luts = 357;
SdeSdePerfUpdateBusFtraceEvent sde_sde_perf_update_bus = 358;
+ RssStatThrottledFtraceEvent rss_stat_throttled = 359;
}
}
@@ -6903,6 +6957,9 @@
// Next id: 2
message ChromeRendererSchedulerState {
optional ChromeRAILMode rail_mode = 1;
+
+ optional bool is_backgrounded = 2;
+ optional bool is_hidden = 3;
}
// End of protos/perfetto/trace/track_event/chrome_renderer_scheduler_state.proto
@@ -8144,9 +8201,9 @@
// individual sample with a counter value, and optionally a
// callstack.
//
-// Timestamps are within the root packet. This used to use the CLOCK_BOOTTIME
-// domain, but now the default is CLOCK_MONOTONIC_RAW which is compatible with
-// more event types.
+// Timestamps are within the root packet. The config can specify the clock, or
+// the implementation will default to CLOCK_MONOTONIC_RAW. Within the Android R
+// timeframe, the default was CLOCK_BOOTTIME.
//
// There are several distinct views of this message:
// * indication of kernel buffer data loss (kernel_records_lost set)
@@ -8819,6 +8876,7 @@
THREAD_DEVTOOLSADB = 38;
THREAD_NETWORKCONFIGWATCHER = 39;
THREAD_WASAPI_RENDER = 40;
+ THREAD_LOADER_LOCK_SAMPLER = 41;
THREAD_MEMORY_INFRA = 50;
THREAD_SAMPLING_PROFILER = 51;
diff --git a/protos/perfetto/trace/profiling/profile_packet.proto b/protos/perfetto/trace/profiling/profile_packet.proto
index 8b147fa..f599f17 100644
--- a/protos/perfetto/trace/profiling/profile_packet.proto
+++ b/protos/perfetto/trace/profiling/profile_packet.proto
@@ -279,9 +279,9 @@
// individual sample with a counter value, and optionally a
// callstack.
//
-// Timestamps are within the root packet. This used to use the CLOCK_BOOTTIME
-// domain, but now the default is CLOCK_MONOTONIC_RAW which is compatible with
-// more event types.
+// Timestamps are within the root packet. The config can specify the clock, or
+// the implementation will default to CLOCK_MONOTONIC_RAW. Within the Android R
+// timeframe, the default was CLOCK_BOOTTIME.
//
// There are several distinct views of this message:
// * indication of kernel buffer data loss (kernel_records_lost set)
diff --git a/protos/perfetto/trace/track_event/chrome_renderer_scheduler_state.proto b/protos/perfetto/trace/track_event/chrome_renderer_scheduler_state.proto
index ade7897..436cc79 100644
--- a/protos/perfetto/trace/track_event/chrome_renderer_scheduler_state.proto
+++ b/protos/perfetto/trace/track_event/chrome_renderer_scheduler_state.proto
@@ -34,4 +34,7 @@
// Next id: 2
message ChromeRendererSchedulerState {
optional ChromeRAILMode rail_mode = 1;
+
+ optional bool is_backgrounded = 2;
+ optional bool is_hidden = 3;
}
diff --git a/protos/perfetto/trace/track_event/chrome_thread_descriptor.proto b/protos/perfetto/trace/track_event/chrome_thread_descriptor.proto
index f142e49..bd82b5a 100644
--- a/protos/perfetto/trace/track_event/chrome_thread_descriptor.proto
+++ b/protos/perfetto/trace/track_event/chrome_thread_descriptor.proto
@@ -73,6 +73,7 @@
THREAD_DEVTOOLSADB = 38;
THREAD_NETWORKCONFIGWATCHER = 39;
THREAD_WASAPI_RENDER = 40;
+ THREAD_LOADER_LOCK_SAMPLER = 41;
THREAD_MEMORY_INFRA = 50;
THREAD_SAMPLING_PROFILER = 51;
diff --git a/protos/perfetto/trace_processor/trace_processor.proto b/protos/perfetto/trace_processor/trace_processor.proto
index 7fb2c33..02a230f 100644
--- a/protos/perfetto/trace_processor/trace_processor.proto
+++ b/protos/perfetto/trace_processor/trace_processor.proto
@@ -41,7 +41,7 @@
// every time a new feature that the UI depends on is being introduced (e.g.
// new tables, new SQL operators, metrics that are required by the UI).
// See also TraceProcessorVersion (below).
- TRACE_PROCESSOR_CURRENT_API_VERSION = 2;
+ TRACE_PROCESSOR_CURRENT_API_VERSION = 3;
}
// At lowest level, the wire-format of the RPC procol is a linear sequence of
diff --git a/src/base/BUILD.gn b/src/base/BUILD.gn
index f6d86b0..4774846 100644
--- a/src/base/BUILD.gn
+++ b/src/base/BUILD.gn
@@ -159,9 +159,14 @@
"../../gn:gtest_and_gmock",
]
+ if (enable_perfetto_ipc) {
+ deps += [ "http:unittests" ]
+ }
+
sources = [
"base64_unittest.cc",
"circular_queue_unittest.cc",
+ "flat_hash_map_unittest.cc",
"flat_set_unittest.cc",
"getopt_compat_unittest.cc",
"logging_unittest.cc",
@@ -170,6 +175,7 @@
"paged_memory_unittest.cc",
"periodic_task_unittest.cc",
"scoped_file_unittest.cc",
+ "small_vector_unittest.cc",
"string_splitter_unittest.cc",
"string_utils_unittest.cc",
"string_view_unittest.cc",
@@ -210,13 +216,33 @@
}
if (enable_perfetto_benchmarks) {
+ declare_args() {
+ perfetto_benchmark_3p_libs_prefix = ""
+ }
source_set("benchmarks") {
+ # If you intend to reproduce the comparison with {Absl, Folly, Tessil}
+ # you need to manually install those libraries and then set the GN arg
+ # perfetto_benchmark_3p_libs_prefix = "/usr/local"
testonly = true
deps = [
":base",
"../../gn:benchmark",
"../../gn:default_deps",
]
- sources = [ "flat_set_benchmark.cc" ]
+ if (perfetto_benchmark_3p_libs_prefix != "") {
+ configs -= [ "//gn/standalone:c++11" ]
+ configs += [ "//gn/standalone:c++17" ]
+ defines = [ "PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS" ]
+ cflags = [ "-isystem${perfetto_benchmark_3p_libs_prefix}/include" ]
+ libs = [
+ "${perfetto_benchmark_3p_libs_prefix}/lib/libfolly.a",
+ "${perfetto_benchmark_3p_libs_prefix}/lib/libabsl_raw_hash_set.a",
+ "${perfetto_benchmark_3p_libs_prefix}/lib/libabsl_hash.a",
+ ]
+ }
+ sources = [
+ "flat_hash_map_benchmark.cc",
+ "flat_set_benchmark.cc",
+ ]
}
}
diff --git a/src/base/base64.cc b/src/base/base64.cc
index 437ad67..bb2e3c4 100644
--- a/src/base/base64.cc
+++ b/src/base/base64.cc
@@ -146,7 +146,7 @@
PERFETTO_CHECK(res <= static_cast<ssize_t>(dst.size()));
dst.resize(static_cast<size_t>(res));
- return make_optional(dst);
+ return base::make_optional(dst);
}
} // namespace base
diff --git a/src/base/crash_keys.cc b/src/base/crash_keys.cc
index 5c975df..13e1c51 100644
--- a/src/base/crash_keys.cc
+++ b/src/base/crash_keys.cc
@@ -54,15 +54,19 @@
size_t CrashKey::ToString(char* dst, size_t len) {
if (len > 0)
*dst = '\0';
- switch (type_) {
+ switch (type_.load(std::memory_order_relaxed)) {
case Type::kUnset:
break;
case Type::kInt:
- return SprintfTrunc(dst, len, "%s: %" PRId64 "\n", name_, int_value_);
+ return SprintfTrunc(dst, len, "%s: %" PRId64 "\n", name_,
+ int_value_.load(std::memory_order_relaxed));
case Type::kStr:
+ char buf[sizeof(str_value_)];
+ for (size_t i = 0; i < sizeof(str_value_); i++)
+ buf[i] = str_value_[i].load(std::memory_order_relaxed);
+
// Don't assume |str_value_| is properly null-terminated.
- return SprintfTrunc(dst, len, "%s: %.*s\n", name_,
- int(sizeof(str_value_)), str_value_);
+ return SprintfTrunc(dst, len, "%s: %.*s\n", name_, int(sizeof(buf)), buf);
}
return 0;
}
diff --git a/src/base/flat_hash_map_benchmark.cc b/src/base/flat_hash_map_benchmark.cc
new file mode 100644
index 0000000..33da669
--- /dev/null
+++ b/src/base/flat_hash_map_benchmark.cc
@@ -0,0 +1,378 @@
+// Copyright (C) 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
+//
+// 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 <functional>
+#include <random>
+#include <unordered_map>
+
+#include <benchmark/benchmark.h>
+#include <stdio.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/hash.h"
+#include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/base/string_view.h"
+
+// This benchmark allows to compare our FlatHashMap implementation against
+// reference implementations from Absl (Google), Folly F14 (FB), and Tssil's
+// reference RobinHood hashmap.
+// Those libraries are not checked in into the repo. If you want to reproduce
+// the benchmark you need to:
+// - Manually install the three libraries using following the instructions in
+// their readme (they all use cmake).
+// - When running cmake, remember to pass
+// -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS='-DNDEBUG -O3 -msse4.2 -mavx'.
+// That sets cflags for a more fair comparison.
+// - Set is_debug=false in the GN args.
+// - Set the GN var perfetto_benchmark_3p_libs_prefix="/usr/local" (or whatever
+// other directory you set as DESTDIR when running make install).
+// The presence of the perfetto_benchmark_3p_libs_prefix GN variable will
+// automatically define PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS.
+
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+
+// Last tested: https://github.com/abseil/abseil-cpp @ f2dbd918d.
+#include <absl/container/flat_hash_map.h>
+
+// Last tested: https://github.com/facebook/folly @ 028a9abae3.
+#include <folly/container/F14Map.h>
+
+// Last tested: https://github.com/Tessil/robin-map @ a603419b9.
+#include <tsl/robin_map.h>
+#endif
+
+namespace {
+
+using namespace perfetto;
+using benchmark::Counter;
+using perfetto::base::AlreadyHashed;
+using perfetto::base::LinearProbe;
+using perfetto::base::QuadraticHalfProbe;
+using perfetto::base::QuadraticProbe;
+
+// Our FlatHashMap doesn't have a STL-like interface, mainly because we use
+// columnar-oriented storage, not array-of-tuples, so we can't easily map into
+// that interface. This wrapper makes our FlatHashMap compatible with STL (just
+// for what it takes to build this translation unit), at the cost of some small
+// performance penalty (around 1-2%).
+template <typename Key, typename Value, typename Hasher, typename Probe>
+class Ours : public base::FlatHashMap<Key, Value, Hasher, Probe> {
+ public:
+ struct Iterator {
+ using value_type = std::pair<const Key&, Value&>;
+ Iterator(const Key& k, Value& v) : pair_{k, v} {}
+ value_type* operator->() { return &pair_; }
+ value_type& operator*() { return pair_; }
+ bool operator==(const Iterator& other) const {
+ return &pair_.first == &other.pair_.first;
+ }
+ bool operator!=(const Iterator& other) const { return !operator==(other); }
+ value_type pair_;
+ };
+
+ void insert(std::pair<Key, Value>&& pair) {
+ this->Insert(std::move(pair.first), std::move(pair.second));
+ }
+
+ Iterator find(const Key& key) {
+ const size_t idx = this->FindInternal(key);
+ return Iterator(this->keys_[idx], this->values_[idx]);
+ }
+
+ Iterator end() {
+ return Iterator(this->keys_[this->kNotFound],
+ this->values_[this->kNotFound]);
+ }
+};
+
+std::vector<uint64_t> LoadTraceStrings(benchmark::State& state) {
+ std::vector<uint64_t> str_hashes;
+ // This requires that the user has downloaded the file
+ // go/perfetto-benchmark-trace-strings into /tmp/trace_strings. The file is
+ // too big (2.3 GB after uncompression) and it's not worth adding it to the
+ // //test/data. Also it contains data from a team member's phone and cannot
+ // be public.
+ base::ScopedFstream f(fopen("/tmp/trace_strings", "re"));
+ if (!f) {
+ state.SkipWithError(
+ "Test strings missing. Googlers: download "
+ "go/perfetto-benchmark-trace-strings and save into /tmp/trace_strings");
+ return str_hashes;
+ }
+ char line[4096];
+ while (fgets(line, sizeof(line), *f)) {
+ base::Hash hasher;
+ hasher.Update(line, strlen(line));
+ str_hashes.emplace_back(hasher.digest());
+ }
+ return str_hashes;
+}
+
+bool IsBenchmarkFunctionalOnly() {
+ return getenv("BENCHMARK_FUNCTIONAL_TEST_ONLY") != nullptr;
+}
+
+size_t num_samples() {
+ return IsBenchmarkFunctionalOnly() ? size_t(100) : size_t(10 * 1000 * 1000);
+}
+
+// Uses directly the base::FlatHashMap with no STL wrapper. Configures the map
+// in append-only mode.
+void BM_HashMap_InsertTraceStrings_AppendOnly(benchmark::State& state) {
+ std::vector<uint64_t> hashes = LoadTraceStrings(state);
+ for (auto _ : state) {
+ base::FlatHashMap<uint64_t, uint64_t, AlreadyHashed<uint64_t>, LinearProbe,
+ /*AppendOnly=*/true>
+ mapz;
+ for (uint64_t hash : hashes)
+ mapz.Insert(hash, 42);
+
+ benchmark::ClobberMemory();
+ state.counters["uniq"] = Counter(static_cast<double>(mapz.size()));
+ }
+ state.counters["tot"] = Counter(static_cast<double>(hashes.size()),
+ Counter::kIsIterationInvariant);
+ state.counters["rate"] = Counter(static_cast<double>(hashes.size()),
+ Counter::kIsIterationInvariantRate);
+}
+
+template <typename MapType>
+void BM_HashMap_InsertTraceStrings(benchmark::State& state) {
+ std::vector<uint64_t> hashes = LoadTraceStrings(state);
+ for (auto _ : state) {
+ MapType mapz;
+ for (uint64_t hash : hashes)
+ mapz.insert({hash, 42});
+
+ benchmark::ClobberMemory();
+ state.counters["uniq"] = Counter(static_cast<double>(mapz.size()));
+ }
+ state.counters["tot"] = Counter(static_cast<double>(hashes.size()),
+ Counter::kIsIterationInvariant);
+ state.counters["rate"] = Counter(static_cast<double>(hashes.size()),
+ Counter::kIsIterationInvariantRate);
+}
+
+template <typename MapType>
+void BM_HashMap_TraceTids(benchmark::State& state) {
+ std::vector<std::pair<char, int>> ops_and_tids;
+ {
+ base::ScopedFstream f(fopen("/tmp/tids", "re"));
+ if (!f) {
+ // This test requires a large (800MB) test file. It's not checked into the
+ // repository's //test/data because it would slow down all developers for
+ // a marginal benefit.
+ state.SkipWithError(
+ "Please run `curl -Lo /tmp/tids "
+ "https://storage.googleapis.com/perfetto/test_datalong_trace_tids.txt"
+ "` and try again.");
+ return;
+ }
+ char op;
+ int tid;
+ while (fscanf(*f, "%c %d\n", &op, &tid) == 2)
+ ops_and_tids.emplace_back(op, tid);
+ }
+
+ for (auto _ : state) {
+ MapType mapz;
+ for (const auto& ops_and_tid : ops_and_tids) {
+ if (ops_and_tid.first == '[') {
+ mapz[ops_and_tid.second]++;
+ } else {
+ mapz.insert({ops_and_tid.second, 0});
+ }
+ }
+
+ benchmark::ClobberMemory();
+ state.counters["uniq"] = Counter(static_cast<double>(mapz.size()));
+ }
+ state.counters["rate"] = Counter(static_cast<double>(ops_and_tids.size()),
+ Counter::kIsIterationInvariantRate);
+}
+
+template <typename MapType>
+void BM_HashMap_InsertRandInts(benchmark::State& state) {
+ std::minstd_rand0 rng(0);
+ std::vector<size_t> keys(static_cast<size_t>(num_samples()));
+ std::shuffle(keys.begin(), keys.end(), rng);
+ for (auto _ : state) {
+ MapType mapz;
+ for (const auto key : keys)
+ mapz.insert({key, key});
+ benchmark::DoNotOptimize(mapz);
+ benchmark::ClobberMemory();
+ }
+ state.counters["insertions"] = Counter(static_cast<double>(keys.size()),
+ Counter::kIsIterationInvariantRate);
+}
+
+// This test is performs insertions on integers that are designed to create
+// lot of clustering on the same small set of buckets.
+// This covers the unlucky case of using a map with a poor hashing function.
+template <typename MapType>
+void BM_HashMap_InsertCollidingInts(benchmark::State& state) {
+ std::vector<size_t> keys;
+ const size_t kNumSamples = num_samples();
+
+ // Generates numbers that are all distinct from each other, but that are
+ // designed to collide on the same buckets.
+ constexpr size_t kShift = 8; // Collide on the same 2^8 = 256 buckets.
+ for (size_t i = 0; i < kNumSamples; i++) {
+ size_t bucket = i & ((1 << kShift) - 1); // [0, 256].
+ size_t multiplier = i >> kShift; // 0,0,0... 1,1,1..., 2,2,2...
+ size_t key = 8192 * multiplier + bucket;
+ keys.push_back(key);
+ }
+ for (auto _ : state) {
+ MapType mapz;
+ for (const size_t key : keys)
+ mapz.insert({key, key});
+ benchmark::DoNotOptimize(mapz);
+ benchmark::ClobberMemory();
+ }
+ state.counters["insertions"] = Counter(static_cast<double>(keys.size()),
+ Counter::kIsIterationInvariantRate);
+}
+
+// Unlike the previous benchmark, here integers don't just collide on the same
+// buckets, they have a large number of duplicates with the same values.
+// Most of those insertions are no-ops. This tests the ability of the hashmap
+// to deal with cases where the hash function is good but the insertions contain
+// lot of dupes (e.g. dealing with pids).
+template <typename MapType>
+void BM_HashMap_InsertDupeInts(benchmark::State& state) {
+ std::vector<size_t> keys;
+ const size_t kNumSamples = num_samples();
+
+ for (size_t i = 0; i < kNumSamples; i++)
+ keys.push_back(i % 16384);
+
+ for (auto _ : state) {
+ MapType mapz;
+ for (const size_t key : keys)
+ mapz.insert({key, key});
+ benchmark::DoNotOptimize(mapz);
+ benchmark::ClobberMemory();
+ }
+ state.counters["insertions"] = Counter(static_cast<double>(keys.size()),
+ Counter::kIsIterationInvariantRate);
+}
+
+template <typename MapType>
+void BM_HashMap_LookupRandInts(benchmark::State& state) {
+ std::minstd_rand0 rng(0);
+ std::vector<size_t> keys(static_cast<size_t>(num_samples()));
+ std::shuffle(keys.begin(), keys.end(), rng);
+
+ MapType mapz;
+ for (const size_t key : keys)
+ mapz.insert({key, key});
+
+ for (auto _ : state) {
+ int64_t total = 0;
+ for (const size_t key : keys) {
+ auto it = mapz.find(static_cast<uint64_t>(key));
+ PERFETTO_CHECK(it != mapz.end());
+ total += it->second;
+ }
+ benchmark::DoNotOptimize(total);
+ benchmark::ClobberMemory();
+ state.counters["sum"] = Counter(static_cast<double>(total));
+ }
+ state.counters["lookups"] = Counter(static_cast<double>(keys.size()),
+ Counter::kIsIterationInvariantRate);
+}
+
+} // namespace
+
+using Ours_LinearProbing =
+ Ours<uint64_t, uint64_t, AlreadyHashed<uint64_t>, LinearProbe>;
+using Ours_QuadProbing =
+ Ours<uint64_t, uint64_t, AlreadyHashed<uint64_t>, QuadraticProbe>;
+using Ours_QuadCompProbing =
+ Ours<uint64_t, uint64_t, AlreadyHashed<uint64_t>, QuadraticHalfProbe>;
+using StdUnorderedMap =
+ std::unordered_map<uint64_t, uint64_t, AlreadyHashed<uint64_t>>;
+
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+using RobinMap = tsl::robin_map<uint64_t, uint64_t, AlreadyHashed<uint64_t>>;
+using AbslFlatHashMap =
+ absl::flat_hash_map<uint64_t, uint64_t, AlreadyHashed<uint64_t>>;
+using FollyF14FastMap =
+ folly::F14FastMap<uint64_t, uint64_t, AlreadyHashed<uint64_t>>;
+#endif
+
+BENCHMARK(BM_HashMap_InsertTraceStrings_AppendOnly);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertTraceStrings, Ours_LinearProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertTraceStrings, Ours_QuadProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertTraceStrings, StdUnorderedMap);
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+BENCHMARK_TEMPLATE(BM_HashMap_InsertTraceStrings, RobinMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertTraceStrings, AbslFlatHashMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertTraceStrings, FollyF14FastMap);
+#endif
+
+#define TID_ARGS int, uint64_t, std::hash<int>
+BENCHMARK_TEMPLATE(BM_HashMap_TraceTids, Ours<TID_ARGS, LinearProbe>);
+BENCHMARK_TEMPLATE(BM_HashMap_TraceTids, Ours<TID_ARGS, QuadraticProbe>);
+BENCHMARK_TEMPLATE(BM_HashMap_TraceTids, Ours<TID_ARGS, QuadraticHalfProbe>);
+BENCHMARK_TEMPLATE(BM_HashMap_TraceTids, std::unordered_map<TID_ARGS>);
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+BENCHMARK_TEMPLATE(BM_HashMap_TraceTids, tsl::robin_map<TID_ARGS>);
+BENCHMARK_TEMPLATE(BM_HashMap_TraceTids, absl::flat_hash_map<TID_ARGS>);
+BENCHMARK_TEMPLATE(BM_HashMap_TraceTids, folly::F14FastMap<TID_ARGS>);
+#endif
+
+BENCHMARK_TEMPLATE(BM_HashMap_InsertRandInts, Ours_LinearProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertRandInts, Ours_QuadProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertRandInts, StdUnorderedMap);
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+BENCHMARK_TEMPLATE(BM_HashMap_InsertRandInts, RobinMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertRandInts, AbslFlatHashMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertRandInts, FollyF14FastMap);
+#endif
+
+BENCHMARK_TEMPLATE(BM_HashMap_InsertCollidingInts, Ours_LinearProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertCollidingInts, Ours_QuadProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertCollidingInts, Ours_QuadCompProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertCollidingInts, StdUnorderedMap);
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+BENCHMARK_TEMPLATE(BM_HashMap_InsertCollidingInts, RobinMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertCollidingInts, AbslFlatHashMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertCollidingInts, FollyF14FastMap);
+#endif
+
+BENCHMARK_TEMPLATE(BM_HashMap_InsertDupeInts, Ours_LinearProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertDupeInts, Ours_QuadProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertDupeInts, Ours_QuadCompProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertDupeInts, StdUnorderedMap);
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+BENCHMARK_TEMPLATE(BM_HashMap_InsertDupeInts, RobinMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertDupeInts, AbslFlatHashMap);
+BENCHMARK_TEMPLATE(BM_HashMap_InsertDupeInts, FollyF14FastMap);
+#endif
+
+BENCHMARK_TEMPLATE(BM_HashMap_LookupRandInts, Ours_LinearProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_LookupRandInts, Ours_QuadProbing);
+BENCHMARK_TEMPLATE(BM_HashMap_LookupRandInts, StdUnorderedMap);
+#if defined(PERFETTO_HASH_MAP_COMPARE_THIRD_PARTY_LIBS)
+BENCHMARK_TEMPLATE(BM_HashMap_LookupRandInts, RobinMap);
+BENCHMARK_TEMPLATE(BM_HashMap_LookupRandInts, AbslFlatHashMap);
+BENCHMARK_TEMPLATE(BM_HashMap_LookupRandInts, FollyF14FastMap);
+#endif
diff --git a/src/base/flat_hash_map_unittest.cc b/src/base/flat_hash_map_unittest.cc
new file mode 100644
index 0000000..3dd02a4
--- /dev/null
+++ b/src/base/flat_hash_map_unittest.cc
@@ -0,0 +1,388 @@
+/*
+ * 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 "perfetto/ext/base/flat_hash_map.h"
+
+#include <array>
+#include <functional>
+#include <random>
+#include <set>
+#include <unordered_map>
+
+#include "perfetto/ext/base/hash.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace base {
+namespace {
+
+using ::testing::Types;
+
+struct CollidingHasher {
+ size_t operator()(int n) const { return static_cast<size_t>(n % 1000); }
+};
+
+template <typename T>
+class FlatHashMapTest : public testing::Test {
+ public:
+ using Probe = T;
+};
+
+using ProbeTypes = Types<LinearProbe, QuadraticHalfProbe, QuadraticProbe>;
+TYPED_TEST_SUITE(FlatHashMapTest, ProbeTypes, /* trailing ',' for GCC*/);
+
+struct Key {
+ static int instances;
+
+ explicit Key(int v) : val(v) {}
+ ~Key() { instances--; }
+ Key(Key&& other) noexcept {
+ val = other.val;
+ other.val = -1;
+ }
+ bool operator==(const Key& other) { return val == other.val; }
+ int val = 0;
+ int id = instances++;
+};
+
+struct Value {
+ static int instances;
+
+ explicit Value(int v = 0) : val(v) {}
+ ~Value() { instances--; }
+ Value(Value&& other) noexcept {
+ val = other.val;
+ other.val = -1;
+ }
+ Value(const Value&) = delete;
+ int val = 0;
+ int id = instances++;
+};
+
+struct Hasher {
+ size_t operator()(const Key& k) const { return static_cast<size_t>(k.val); }
+};
+
+int Key::instances = 0;
+int Value::instances = 0;
+
+TYPED_TEST(FlatHashMapTest, NonTrivialKeyValues) {
+ FlatHashMap<Key, Value, Hasher, typename TestFixture::Probe> fmap;
+
+ for (int iteration = 0; iteration < 3; iteration++) {
+ const int kNum = 10;
+ for (int i = 0; i < kNum; i++) {
+ ASSERT_TRUE(fmap.Insert(Key(i), Value(i * 2)).second);
+ Value* value = fmap.Find(Key(i));
+ ASSERT_NE(value, nullptr);
+ ASSERT_EQ(value->val, i * 2);
+ ASSERT_EQ(Key::instances, i + 1);
+ ASSERT_EQ(Value::instances, i + 1);
+ }
+
+ ASSERT_TRUE(fmap.Erase(Key(1)));
+ ASSERT_TRUE(fmap.Erase(Key(5)));
+ ASSERT_TRUE(fmap.Erase(Key(9)));
+
+ ASSERT_EQ(Key::instances, kNum - 3);
+ ASSERT_EQ(Value::instances, kNum - 3);
+
+ FlatHashMap<Key, Value, Hasher, typename TestFixture::Probe> fmap2(
+ std::move(fmap));
+ ASSERT_EQ(fmap.size(), 0u);
+ ASSERT_EQ(fmap2.size(), static_cast<size_t>(kNum - 3));
+
+ ASSERT_EQ(Key::instances, kNum - 3);
+ ASSERT_EQ(Value::instances, kNum - 3);
+
+ // Ensure the moved-from map is usable.
+ fmap.Insert(Key(1), Value(-1));
+ fmap.Insert(Key(5), Value(-5));
+ fmap.Insert(Key(9), Value(-9));
+ ASSERT_EQ(Key::instances, (kNum - 3) + 3);
+ ASSERT_EQ(Value::instances, (kNum - 3) + 3);
+
+ fmap2.Clear();
+ ASSERT_EQ(fmap2.size(), 0u);
+ ASSERT_EQ(fmap.size(), 3u);
+ ASSERT_EQ(Key::instances, 3);
+ ASSERT_EQ(Value::instances, 3);
+ ASSERT_EQ(fmap.Find(Key(1))->val, -1);
+ ASSERT_EQ(fmap.Find(Key(5))->val, -5);
+ ASSERT_EQ(fmap.Find(Key(9))->val, -9);
+
+ fmap = std::move(fmap2);
+ ASSERT_EQ(Key::instances, 0);
+ ASSERT_EQ(Value::instances, 0);
+ ASSERT_EQ(fmap.size(), 0u);
+ }
+
+ // Test that operator[] behaves rationally.
+ fmap = decltype(fmap)(); // Re-assign with a copy constructor.
+ fmap[Key{2}].val = 102;
+ fmap[Key{1}].val = 101;
+ ASSERT_EQ(fmap.Find(Key{2})->val, 102);
+ ASSERT_EQ(fmap.Find(Key{1})->val, 101);
+ fmap[Key{2}].val = 122;
+ ASSERT_EQ(fmap.Find(Key{2})->val, 122);
+ ASSERT_EQ(fmap[Key{1}].val, 101);
+ auto fmap2(std::move(fmap));
+ ASSERT_EQ(fmap[Key{1}].val, 0);
+ ASSERT_EQ(fmap.size(), 1u);
+}
+
+TYPED_TEST(FlatHashMapTest, AllTagsAreValid) {
+ FlatHashMap<size_t, size_t, base::AlreadyHashed<size_t>,
+ typename TestFixture::Probe>
+ fmap;
+ auto make_key = [](size_t tag) {
+ return tag << ((sizeof(size_t) - 1) * size_t(8));
+ };
+ for (size_t i = 0; i < 256; i++) {
+ size_t key = make_key(i);
+ fmap.Insert(key, i);
+ ASSERT_EQ(fmap.size(), i + 1);
+ }
+ for (size_t i = 0; i < 256; i++) {
+ size_t key = make_key(i);
+ ASSERT_NE(fmap.Find(key), nullptr);
+ ASSERT_EQ(*fmap.Find(key), i);
+ }
+ for (size_t i = 0; i < 256; i++) {
+ size_t key = make_key(i);
+ fmap.Erase(key);
+ ASSERT_EQ(fmap.size(), 255 - i);
+ ASSERT_EQ(fmap.Find(key), nullptr);
+ }
+}
+
+TYPED_TEST(FlatHashMapTest, FillWithTombstones) {
+ FlatHashMap<Key, Value, Hasher, typename TestFixture::Probe> fmap(
+ /*initial_capacity=*/0, /*load_limit_pct=*/100);
+
+ for (int rep = 0; rep < 3; rep++) {
+ for (int i = 0; i < 1024; i++)
+ ASSERT_TRUE(fmap.Insert(Key(i), Value(i)).second);
+
+ ASSERT_EQ(fmap.size(), 1024u);
+ ASSERT_EQ(Key::instances, 1024);
+ ASSERT_EQ(Value::instances, 1024);
+
+ // Erase all entries.
+ for (int i = 0; i < 1024; i++)
+ ASSERT_TRUE(fmap.Erase(Key(i)));
+
+ ASSERT_EQ(fmap.size(), 0u);
+ ASSERT_EQ(Key::instances, 0);
+ ASSERT_EQ(Value::instances, 0);
+ }
+}
+
+TYPED_TEST(FlatHashMapTest, Collisions) {
+ FlatHashMap<int, int, CollidingHasher, typename TestFixture::Probe> fmap(
+ /*initial_capacity=*/0, /*load_limit_pct=*/100);
+
+ for (int rep = 0; rep < 3; rep++) {
+ // Insert four values which collide on the same bucket.
+ ASSERT_TRUE(fmap.Insert(1001, 1001).second);
+ ASSERT_TRUE(fmap.Insert(2001, 2001).second);
+ ASSERT_TRUE(fmap.Insert(3001, 3001).second);
+ ASSERT_TRUE(fmap.Insert(4001, 4001).second);
+
+ // Erase the 2nd one, it will create a tombstone.
+ ASSERT_TRUE(fmap.Erase(2001));
+ ASSERT_EQ(fmap.size(), 3u);
+
+ // Insert an entry that exists already, but happens to be located after the
+ // tombstone. Should still fail.
+ ASSERT_FALSE(fmap.Insert(3001, 3001).second);
+ ASSERT_EQ(fmap.size(), 3u);
+
+ ASSERT_TRUE(fmap.Erase(3001));
+ ASSERT_FALSE(fmap.Erase(2001));
+ ASSERT_TRUE(fmap.Erase(4001));
+
+ // The only element left is 101.
+ ASSERT_EQ(fmap.size(), 1u);
+
+ ASSERT_TRUE(fmap.Erase(1001));
+ ASSERT_EQ(fmap.size(), 0u);
+ }
+}
+
+TYPED_TEST(FlatHashMapTest, ProbeVisitsAllSlots) {
+ const int kIterations = 1024;
+ FlatHashMap<int, int, CollidingHasher, typename TestFixture::Probe> fmap(
+ /*initial_capacity=*/kIterations, /*load_limit_pct=*/100);
+ for (int i = 0; i < kIterations; i++) {
+ ASSERT_TRUE(fmap.Insert(i, i).second);
+ }
+ // If the hashmap hits an expansion the tests doesn't make sense. This test
+ // makes sense only if we actually saturate all buckets.
+ EXPECT_EQ(fmap.capacity(), static_cast<size_t>(kIterations));
+}
+
+TYPED_TEST(FlatHashMapTest, Iterator) {
+ FlatHashMap<int, int, base::AlreadyHashed<int>, typename TestFixture::Probe>
+ fmap;
+
+ auto it = fmap.GetIterator();
+ ASSERT_FALSE(it);
+
+ // Insert 3 values and iterate.
+ ASSERT_TRUE(fmap.Insert(1, 1001).second);
+ ASSERT_TRUE(fmap.Insert(2, 2001).second);
+ ASSERT_TRUE(fmap.Insert(3, 3001).second);
+ it = fmap.GetIterator();
+ for (int i = 1; i <= 3; i++) {
+ ASSERT_TRUE(it);
+ ASSERT_EQ(it.key(), i);
+ ASSERT_EQ(it.value(), i * 1000 + 1);
+ ++it;
+ }
+ ASSERT_FALSE(it);
+
+ // Erase the middle one and iterate.
+ fmap.Erase(2);
+ it = fmap.GetIterator();
+ ASSERT_TRUE(it);
+ ASSERT_EQ(it.key(), 1);
+ ++it;
+ ASSERT_TRUE(it);
+ ASSERT_EQ(it.key(), 3);
+ ++it;
+ ASSERT_FALSE(it);
+
+ // Erase everything and iterate.
+ fmap.Clear();
+ it = fmap.GetIterator();
+ ASSERT_FALSE(it);
+}
+
+// Test that Insert() and operator[] don't invalidate pointers if the key exists
+// already, regardless of the load factor.
+TYPED_TEST(FlatHashMapTest, DontRehashIfKeyAlreadyExists) {
+ static constexpr size_t kInitialCapacity = 128;
+ static std::array<size_t, 3> kLimitPct{25, 50, 100};
+
+ for (size_t limit_pct : kLimitPct) {
+ FlatHashMap<size_t, size_t, AlreadyHashed<size_t>,
+ typename TestFixture::Probe>
+ fmap(kInitialCapacity, static_cast<int>(limit_pct));
+
+ const size_t limit = kInitialCapacity * limit_pct / 100u;
+ ASSERT_EQ(fmap.capacity(), kInitialCapacity);
+ std::vector<size_t*> key_ptrs;
+ for (size_t i = 0; i < limit; i++) {
+ auto it_and_ins = fmap.Insert(i, i);
+ ASSERT_TRUE(it_and_ins.second);
+ ASSERT_EQ(fmap.capacity(), kInitialCapacity);
+ key_ptrs.push_back(it_and_ins.first);
+ }
+
+ // Re-insert existing items. It should not cause rehashing.
+ for (size_t i = 0; i < limit; i++) {
+ auto it_and_ins = fmap.Insert(i, i);
+ ASSERT_FALSE(it_and_ins.second);
+ ASSERT_EQ(it_and_ins.first, key_ptrs[i]);
+
+ size_t* key_ptr = &fmap[i];
+ ASSERT_EQ(key_ptr, key_ptrs[i]);
+ ASSERT_EQ(fmap.capacity(), kInitialCapacity);
+ }
+ }
+}
+
+TYPED_TEST(FlatHashMapTest, VsUnorderedMap) {
+ std::unordered_map<int, int, CollidingHasher> umap;
+ FlatHashMap<int, int, CollidingHasher, typename TestFixture::Probe> fmap;
+ std::minstd_rand0 rng(0);
+
+ for (int rep = 0; rep < 2; rep++) {
+ std::set<int> keys_copy;
+ const int kRange = 1024;
+
+ // Insert some random elements.
+ for (int i = 0; i < kRange; i++) {
+ int key = static_cast<int>(rng());
+ keys_copy.insert(key);
+ int value = key * 2;
+ auto it_and_inserted_u = umap.insert({key, value});
+ auto it_and_inserted_f = fmap.Insert(key, value);
+ ASSERT_EQ(it_and_inserted_u.second, it_and_inserted_f.second);
+ ASSERT_EQ(*it_and_inserted_f.first, value);
+ ASSERT_EQ(umap.size(), fmap.size());
+ int* res = fmap.Find(key);
+ ASSERT_NE(res, nullptr);
+ ASSERT_EQ(*res, value);
+ ASSERT_EQ(fmap[key], value); // Test that operator[] behaves like Find().
+ }
+ // Look them up.
+ for (int key : keys_copy) {
+ int* res = fmap.Find(key);
+ ASSERT_NE(res, nullptr);
+ ASSERT_EQ(*res, key * 2);
+ ASSERT_EQ(umap.size(), fmap.size());
+ }
+
+ // Some further deletions / insertions / reinsertions.
+ for (int key : keys_copy) {
+ auto op = rng() % 4;
+
+ if (op < 2) {
+ // With a 50% chance, erase the key.
+ bool erased_u = umap.erase(key) > 0;
+ bool erased_f = fmap.Erase(key);
+ ASSERT_EQ(erased_u, erased_f);
+ } else if (op == 3) {
+ // With a 25% chance, re-insert the same key (should fail).
+ umap.insert({key, 0});
+ ASSERT_FALSE(fmap.Insert(key, 0).second);
+ } else {
+ // With a 25% chance, insert a new key.
+ umap.insert({key + kRange, (key + kRange) * 2});
+ ASSERT_TRUE(fmap.Insert(key + kRange, (key + kRange) * 2).second);
+ }
+
+ ASSERT_EQ(umap.size(), fmap.size());
+ }
+
+ // Re-look up keys. Note some of them might be deleted by the loop above.
+ for (int k : keys_copy) {
+ for (int i = 0; i < 2; i++) {
+ const int key = k + kRange * i;
+ int* res = fmap.Find(key);
+ if (umap.count(key)) {
+ ASSERT_NE(res, nullptr);
+ ASSERT_EQ(*res, key * 2);
+ } else {
+ ASSERT_EQ(res, nullptr);
+ }
+ }
+ }
+
+ fmap.Clear();
+ umap.clear();
+ ASSERT_EQ(fmap.size(), 0u);
+
+ for (int key : keys_copy)
+ ASSERT_EQ(fmap.Find(key), nullptr);
+ }
+}
+
+} // namespace
+} // namespace base
+} // namespace perfetto
diff --git a/src/base/http/BUILD.gn b/src/base/http/BUILD.gn
new file mode 100644
index 0000000..e086134
--- /dev/null
+++ b/src/base/http/BUILD.gn
@@ -0,0 +1,47 @@
+# 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("../../../gn/perfetto.gni")
+import("../../../gn/perfetto_component.gni")
+import("../../../gn/test.gni")
+
+perfetto_component("http") {
+ deps = [
+ "..:base",
+ "..:unix_socket",
+ "../../../gn:default_deps",
+ ]
+ public_deps = [
+ "../../../include/perfetto/base",
+ "../../../include/perfetto/ext/base/http",
+ ]
+ sources = [
+ "http_server.cc",
+ "sha1.cc",
+ ]
+}
+
+perfetto_unittest_source_set("unittests") {
+ testonly = true
+ deps = [
+ ":http",
+ "..:test_support",
+ "../../../gn:default_deps",
+ "../../../gn:gtest_and_gmock",
+ ]
+ sources = [
+ "http_server_unittest.cc",
+ "sha1_unittest.cc",
+ ]
+}
diff --git a/src/base/http/http_server.cc b/src/base/http/http_server.cc
new file mode 100644
index 0000000..17a1652
--- /dev/null
+++ b/src/base/http/http_server.cc
@@ -0,0 +1,579 @@
+/*
+ * 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 "perfetto/ext/base/http/http_server.h"
+
+#include <cinttypes>
+
+#include <vector>
+
+#include "perfetto/ext/base/base64.h"
+#include "perfetto/ext/base/endian.h"
+#include "perfetto/ext/base/http/sha1.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/string_view.h"
+
+namespace perfetto {
+namespace base {
+
+namespace {
+constexpr size_t kMaxPayloadSize = 32 * 1024 * 1024;
+constexpr size_t kMaxRequestSize = kMaxPayloadSize + 4096;
+
+enum WebsocketOpcode : uint8_t {
+ kOpcodeContinuation = 0x0,
+ kOpcodeText = 0x1,
+ kOpcodeBinary = 0x2,
+ kOpcodeDataUnused = 0x3,
+ kOpcodeClose = 0x8,
+ kOpcodePing = 0x9,
+ kOpcodePong = 0xA,
+ kOpcodeControlUnused = 0xB,
+};
+
+// From https://datatracker.ietf.org/doc/html/rfc6455#section-1.3.
+constexpr char kWebsocketGuid[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+} // namespace
+
+HttpServer::HttpServer(TaskRunner* task_runner, HttpRequestHandler* req_handler)
+ : task_runner_(task_runner), req_handler_(req_handler) {}
+HttpServer::~HttpServer() = default;
+
+void HttpServer::Start(int port) {
+ std::string ipv4_addr = "127.0.0.1:" + std::to_string(port);
+ std::string ipv6_addr = "[::1]:" + std::to_string(port);
+
+ sock4_ = UnixSocket::Listen(ipv4_addr, this, task_runner_, SockFamily::kInet,
+ SockType::kStream);
+ bool ipv4_listening = sock4_ && sock4_->is_listening();
+ if (!ipv4_listening) {
+ PERFETTO_PLOG("Failed to listen on IPv4 socket");
+ sock4_.reset();
+ }
+
+ sock6_ = UnixSocket::Listen(ipv6_addr, this, task_runner_, SockFamily::kInet6,
+ SockType::kStream);
+ bool ipv6_listening = sock6_ && sock6_->is_listening();
+ if (!ipv6_listening) {
+ PERFETTO_PLOG("Failed to listen on IPv6 socket");
+ sock6_.reset();
+ }
+}
+
+void HttpServer::AddAllowedOrigin(const std::string& origin) {
+ allowed_origins_.emplace_back(origin);
+}
+
+void HttpServer::OnNewIncomingConnection(
+ UnixSocket*, // The listening socket, irrelevant here.
+ std::unique_ptr<UnixSocket> sock) {
+ PERFETTO_LOG("[HTTP] New connection");
+ clients_.emplace_back(std::move(sock));
+}
+
+void HttpServer::OnConnect(UnixSocket*, bool) {}
+
+void HttpServer::OnDisconnect(UnixSocket* sock) {
+ PERFETTO_LOG("[HTTP] Client disconnected");
+ for (auto it = clients_.begin(); it != clients_.end(); ++it) {
+ if (it->sock.get() == sock) {
+ req_handler_->OnHttpConnectionClosed(&*it);
+ clients_.erase(it);
+ return;
+ }
+ }
+ PERFETTO_DFATAL("[HTTP] Untracked client in OnDisconnect()");
+}
+
+void HttpServer::OnDataAvailable(UnixSocket* sock) {
+ HttpServerConnection* conn = nullptr;
+ for (auto it = clients_.begin(); it != clients_.end() && !conn; ++it)
+ conn = (it->sock.get() == sock) ? &*it : nullptr;
+ PERFETTO_CHECK(conn);
+
+ char* rxbuf = reinterpret_cast<char*>(conn->rxbuf.Get());
+ for (;;) {
+ size_t avail = conn->rxbuf_avail();
+ PERFETTO_CHECK(avail <= kMaxRequestSize);
+ if (avail == 0) {
+ conn->SendResponseAndClose("413 Payload Too Large");
+ return;
+ }
+ size_t rsize = sock->Receive(&rxbuf[conn->rxbuf_used], avail);
+ conn->rxbuf_used += rsize;
+ if (rsize == 0 || conn->rxbuf_avail() == 0)
+ break;
+ }
+
+ // At this point |rxbuf| can contain a partial HTTP request, a full one or
+ // more (in case of HTTP Keepalive pipelining).
+ for (;;) {
+ size_t bytes_consumed;
+
+ if (conn->is_websocket()) {
+ bytes_consumed = ParseOneWebsocketFrame(conn);
+ } else {
+ bytes_consumed = ParseOneHttpRequest(conn);
+ }
+
+ if (bytes_consumed == 0)
+ break;
+ memmove(rxbuf, &rxbuf[bytes_consumed], conn->rxbuf_used - bytes_consumed);
+ conn->rxbuf_used -= bytes_consumed;
+ }
+}
+
+// Parses the HTTP request and invokes HandleRequest(). It returns the size of
+// the HTTP header + body that has been processed or 0 if there isn't enough
+// data for a full HTTP request in the buffer.
+size_t HttpServer::ParseOneHttpRequest(HttpServerConnection* conn) {
+ auto* rxbuf = reinterpret_cast<char*>(conn->rxbuf.Get());
+ StringView buf_view(rxbuf, conn->rxbuf_used);
+ bool has_parsed_first_line = false;
+ bool all_headers_received = false;
+ HttpRequest http_req(conn);
+ size_t body_size = 0;
+
+ // This loop parses the HTTP request headers and sets the |body_offset|.
+ while (!buf_view.empty()) {
+ size_t next = buf_view.find('\n');
+ if (next == StringView::npos)
+ break;
+ StringView line = buf_view.substr(0, next);
+ buf_view = buf_view.substr(next + 1); // Eat the current line.
+ while (!line.empty() && (line.at(line.size() - 1) == '\r' ||
+ line.at(line.size() - 1) == '\n')) {
+ line = line.substr(0, line.size() - 1);
+ }
+
+ if (!has_parsed_first_line) {
+ // Parse the "GET /xxx HTTP/1.1" line.
+ has_parsed_first_line = true;
+ size_t space = line.find(' ');
+ if (space == std::string::npos || space + 2 >= line.size()) {
+ conn->SendResponseAndClose("400 Bad Request");
+ return 0;
+ }
+ http_req.method = line.substr(0, space);
+ size_t uri_size = line.find(' ', space + 1) - (space + 1);
+ http_req.uri = line.substr(space + 1, uri_size);
+ } else if (line.empty()) {
+ all_headers_received = true;
+ // The CR-LF marker that separates headers from body.
+ break;
+ } else {
+ // Parse HTTP headers, e.g. "Content-Length: 1234".
+ size_t col = line.find(':');
+ if (col == StringView::npos) {
+ PERFETTO_DLOG("[HTTP] Malformed HTTP header: \"%s\"",
+ line.ToStdString().c_str());
+ conn->SendResponseAndClose("400 Bad Request", {}, "Bad HTTP header");
+ return 0;
+ }
+ auto hdr_name = line.substr(0, col);
+ auto hdr_value = line.substr(col + 2);
+ if (http_req.num_headers < http_req.headers.size()) {
+ http_req.headers[http_req.num_headers++] = {hdr_name, hdr_value};
+ } else {
+ conn->SendResponseAndClose("400 Bad Request", {},
+ "Too many HTTP headers");
+ }
+
+ if (hdr_name.CaseInsensitiveEq("content-length")) {
+ body_size = static_cast<size_t>(atoi(hdr_value.ToStdString().c_str()));
+ } else if (hdr_name.CaseInsensitiveEq("origin")) {
+ http_req.origin = hdr_value;
+ if (IsOriginAllowed(hdr_value))
+ conn->origin_allowed_ = hdr_value.ToStdString();
+ } else if (hdr_name.CaseInsensitiveEq("connection")) {
+ conn->keepalive_ = hdr_value.CaseInsensitiveEq("keep-alive");
+ http_req.is_websocket_handshake =
+ hdr_value.CaseInsensitiveEq("upgrade");
+ }
+ }
+ }
+
+ // At this point |buf_view| has been stripped of the header and contains the
+ // request body. We don't know yet if we have all the bytes for it or not.
+ PERFETTO_CHECK(buf_view.size() <= conn->rxbuf_used);
+ const size_t headers_size = conn->rxbuf_used - buf_view.size();
+
+ if (body_size + headers_size >= kMaxRequestSize ||
+ body_size > kMaxPayloadSize) {
+ conn->SendResponseAndClose("413 Payload Too Large");
+ return 0;
+ }
+
+ // If we can't read the full request return and try again next time with more
+ // data.
+ if (!all_headers_received || buf_view.size() < body_size)
+ return 0;
+
+ http_req.body = buf_view.substr(0, body_size);
+
+ PERFETTO_LOG("[HTTP] %.*s %.*s [body=%zuB, origin=\"%.*s\"]",
+ static_cast<int>(http_req.method.size()), http_req.method.data(),
+ static_cast<int>(http_req.uri.size()), http_req.uri.data(),
+ http_req.body.size(), static_cast<int>(http_req.origin.size()),
+ http_req.origin.data());
+
+ if (http_req.method == "OPTIONS") {
+ HandleCorsPreflightRequest(http_req);
+ } else {
+ // Let the HttpHandler handle the request.
+ req_handler_->OnHttpRequest(http_req);
+ }
+
+ // The handler is expected to send a response. If not, bail with a HTTP 500.
+ if (!conn->headers_sent_)
+ conn->SendResponseAndClose("500 Internal Server Error");
+
+ // Allow chaining multiple responses in the same HTTP-Keepalive connection.
+ conn->headers_sent_ = false;
+
+ return headers_size + body_size;
+}
+
+void HttpServer::HandleCorsPreflightRequest(const HttpRequest& req) {
+ req.conn->SendResponseAndClose(
+ "204 No Content",
+ {
+ "Access-Control-Allow-Methods: POST, GET, OPTIONS", //
+ "Access-Control-Allow-Headers: *", //
+ "Access-Control-Max-Age: 86400", //
+ });
+}
+
+bool HttpServer::IsOriginAllowed(StringView origin) {
+ for (const std::string& allowed_origin : allowed_origins_) {
+ if (origin.CaseInsensitiveEq(StringView(allowed_origin))) {
+ return true;
+ }
+ }
+ if (!origin_error_logged_ && !origin.empty()) {
+ origin_error_logged_ = true;
+ PERFETTO_ELOG(
+ "[HTTP] The origin \"%.*s\" is not allowed, Access-Control-Allow-Origin"
+ " won't be emitted. If this request comes from a browser it will fail.",
+ static_cast<int>(origin.size()), origin.data());
+ }
+ return false;
+}
+
+void HttpServerConnection::UpgradeToWebsocket(const HttpRequest& req) {
+ PERFETTO_CHECK(req.is_websocket_handshake);
+
+ // |origin_allowed_| is set to the req.origin only if it's in the allowlist.
+ if (origin_allowed_.empty())
+ return SendResponseAndClose("403 Forbidden", {}, "Origin not allowed");
+
+ auto ws_ver = req.GetHeader("sec-webSocket-version").value_or(StringView());
+ auto ws_key = req.GetHeader("sec-webSocket-key").value_or(StringView());
+
+ if (!ws_ver.CaseInsensitiveEq("13"))
+ return SendResponseAndClose("505 HTTP Version Not Supported", {});
+
+ if (ws_key.size() != 24) {
+ // The nonce must be a base64 encoded 16 bytes value (24 after base64).
+ return SendResponseAndClose("400 Bad Request", {});
+ }
+
+ // From https://datatracker.ietf.org/doc/html/rfc6455#section-1.3 :
+ // For this header field, the server has to take the value (as present
+ // in the header field, e.g., the base64-encoded [RFC4648] version minus
+ // any leading and trailing whitespace) and concatenate this with the
+ // Globally Unique Identifier (GUID, [RFC4122]) "258EAFA5-E914-47DA-
+ // 95CA-C5AB0DC85B11" in string form, which is unlikely to be used by
+ // network endpoints that do not understand the WebSocket Protocol. A
+ // SHA-1 hash (160 bits) [FIPS.180-3], base64-encoded (see Section 4 of
+ // [RFC4648]), of this concatenation is then returned in the server's
+ // handshake.
+ StackString<128> signed_nonce("%.*s%s", static_cast<int>(ws_key.size()),
+ ws_key.data(), kWebsocketGuid);
+ auto digest = SHA1Hash(signed_nonce.c_str(), signed_nonce.len());
+ std::string digest_b64 = Base64Encode(digest.data(), digest.size());
+
+ StackString<128> accept_hdr("Sec-WebSocket-Accept: %s", digest_b64.c_str());
+
+ std::initializer_list<const char*> headers = {
+ "Upgrade: websocket", //
+ "Connection: Upgrade", //
+ accept_hdr.c_str(), //
+ };
+ PERFETTO_DLOG("[HTTP] Handshaking WebSocket for %.*s",
+ static_cast<int>(req.uri.size()), req.uri.data());
+ for (const char* hdr : headers)
+ PERFETTO_DLOG("> %s", hdr);
+
+ SendResponseHeaders("101 Switching Protocols", headers,
+ HttpServerConnection::kOmitContentLength);
+
+ is_websocket_ = true;
+}
+
+size_t HttpServer::ParseOneWebsocketFrame(HttpServerConnection* conn) {
+ auto* rxbuf = reinterpret_cast<uint8_t*>(conn->rxbuf.Get());
+ const size_t frame_size = conn->rxbuf_used;
+ uint8_t* rd = rxbuf;
+ uint8_t* const end = rxbuf + frame_size;
+
+ auto avail = [&] {
+ PERFETTO_CHECK(rd <= end);
+ return static_cast<size_t>(end - rd);
+ };
+
+ // From https://datatracker.ietf.org/doc/html/rfc6455#section-5.2 :
+ // 0 1 2 3
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ // +-+-+-+-+-------+-+-------------+-------------------------------+
+ // |F|R|R|R| opcode|M| Payload len | Extended payload length |
+ // |I|S|S|S| (4) |A| (7) | (16/64) |
+ // |N|V|V|V| |S| | (if payload len==126/127) |
+ // | |1|2|3| |K| | |
+ // +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
+ // | Extended payload length continued, if payload len == 127 |
+ // + - - - - - - - - - - - - - - - +-------------------------------+
+ // | |Masking-key, if MASK set to 1 |
+ // +-------------------------------+-------------------------------+
+ // | Masking-key (continued) | Payload Data |
+ // +-------------------------------- - - - - - - - - - - - - - - - +
+ // : Payload Data continued ... :
+ // + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ // | Payload Data continued ... |
+ // +---------------------------------------------------------------+
+
+ if (avail() < 2)
+ return 0; // Can't even decode the frame header. Wait for more data.
+
+ uint8_t h0 = *(rd++);
+ uint8_t h1 = *(rd++);
+ const bool fin = !!(h0 & 0x80); // This bit is set if this frame is the last
+ // data to complete this message.
+ const uint8_t opcode = h0 & 0x0F;
+
+ const bool has_mask = !!(h1 & 0x80);
+ uint64_t payload_len_u64 = (h1 & 0x7F);
+ uint8_t extended_payload_size = 0;
+ if (payload_len_u64 == 126) {
+ extended_payload_size = 2;
+ } else if (payload_len_u64 == 127) {
+ extended_payload_size = 8;
+ }
+
+ if (extended_payload_size > 0) {
+ if (avail() < extended_payload_size)
+ return 0; // Not enough data to read the extended header.
+ payload_len_u64 = 0;
+ for (uint8_t i = 0; i < extended_payload_size; ++i) {
+ payload_len_u64 <<= 8;
+ payload_len_u64 |= *(rd++);
+ }
+ }
+
+ if (payload_len_u64 >= kMaxPayloadSize) {
+ PERFETTO_ELOG("[HTTP] Websocket payload too big (%" PRIu64 " > %zu)",
+ payload_len_u64, kMaxPayloadSize);
+ conn->Close();
+ return 0;
+ }
+ const size_t payload_len = static_cast<size_t>(payload_len_u64);
+
+ if (!has_mask) {
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-5.1
+ // The server MUST close the connection upon receiving a frame that is
+ // not masked.
+ PERFETTO_ELOG("[HTTP] Websocket inbound frames must be masked");
+ conn->Close();
+ return 0;
+ }
+
+ uint8_t mask[4];
+ if (avail() < sizeof(mask))
+ return 0; // Not enough data to read the masking key.
+ memcpy(mask, rd, sizeof(mask));
+ rd += sizeof(mask);
+
+ PERFETTO_DLOG(
+ "[HTTP] Websocket fin=%d opcode=%u, payload_len=%zu (avail=%zu), "
+ "mask=%02x%02x%02x%02x",
+ fin, opcode, payload_len, avail(), mask[0], mask[1], mask[2], mask[3]);
+
+ if (avail() < payload_len)
+ return 0; // Not enouh data to read the payload.
+ uint8_t* const payload_start = rd;
+
+ // Unmask the payload.
+ for (uint32_t i = 0; i < payload_len; ++i)
+ payload_start[i] ^= mask[i % sizeof(mask)];
+
+ if (opcode == kOpcodePing) {
+ PERFETTO_DLOG("[HTTP] Websocket PING");
+ conn->SendWebsocketFrame(kOpcodePong, payload_start, payload_len);
+ } else if (opcode == kOpcodeBinary || opcode == kOpcodeText ||
+ opcode == kOpcodeContinuation) {
+ // We do NOT handle fragmentation. We propagate all fragments as individual
+ // messages, breaking the message-oriented nature of websockets. We do this
+ // because in all our use cases we need only a byte stream without caring
+ // about message boundaries.
+ // If we wanted to support fragmentation, we'd have to stash
+ // kOpcodeContinuation messages in a buffer, until we FIN bit is set.
+ // When loading traces with trace processor, the messages can be up to
+ // 32MB big (SLICE_SIZE in trace_stream.ts). The double-buffering would
+ // slow down significantly trace loading with no benefits.
+ WebsocketMessage msg(conn);
+ msg.data =
+ StringView(reinterpret_cast<const char*>(payload_start), payload_len);
+ msg.is_text = opcode == kOpcodeText;
+ req_handler_->OnWebsocketMessage(msg);
+ } else if (opcode == kOpcodeClose) {
+ conn->Close();
+ } else {
+ PERFETTO_LOG("Unsupported WebSocket opcode: %d", opcode);
+ }
+ return static_cast<size_t>(rd - rxbuf) + payload_len;
+}
+
+void HttpServerConnection::SendResponseHeaders(
+ const char* http_code,
+ std::initializer_list<const char*> headers,
+ size_t content_length) {
+ PERFETTO_CHECK(!headers_sent_);
+ PERFETTO_CHECK(!is_websocket_);
+ headers_sent_ = true;
+ std::vector<char> resp_hdr;
+ resp_hdr.reserve(512);
+ bool has_connection_header = false;
+
+ auto append = [&resp_hdr](const char* str) {
+ resp_hdr.insert(resp_hdr.end(), str, str + strlen(str));
+ };
+
+ append("HTTP/1.1 ");
+ append(http_code);
+ append("\r\n");
+ for (const char* hdr : headers) {
+ if (strlen(hdr) == 0)
+ continue;
+ has_connection_header |= strncasecmp(hdr, "connection:", 11) == 0;
+ append(hdr);
+ append("\r\n");
+ }
+ content_len_actual_ = 0;
+ content_len_headers_ = content_length;
+ if (content_length != kOmitContentLength) {
+ append("Content-Length: ");
+ append(std::to_string(content_length).c_str());
+ append("\r\n");
+ }
+ if (!has_connection_header) {
+ // Various clients (e.g., python's http.client) assume that a HTTP
+ // connection is keep-alive if the server says nothing, even when they do
+ // NOT ask for it. Hence we must be explicit. If we are about to close the
+ // connection, we must say so.
+ append(keepalive_ ? "Connection: keep-alive\r\n" : "Connection: close\r\n");
+ }
+ if (!origin_allowed_.empty()) {
+ append("Access-Control-Allow-Origin: ");
+ append(origin_allowed_.c_str());
+ append("\r\n");
+ append("Vary: Origin\r\n");
+ }
+ append("\r\n"); // End-of-headers marker.
+ sock->Send(resp_hdr.data(),
+ resp_hdr.size()); // Send response headers.
+}
+
+void HttpServerConnection::SendResponseBody(const void* data, size_t len) {
+ PERFETTO_CHECK(!is_websocket_);
+ if (data == nullptr) {
+ PERFETTO_DCHECK(len == 0);
+ return;
+ }
+ content_len_actual_ += len;
+ PERFETTO_CHECK(content_len_actual_ <= content_len_headers_ ||
+ content_len_headers_ == kOmitContentLength);
+ sock->Send(data, len);
+}
+
+void HttpServerConnection::Close() {
+ sock->Shutdown(/*notify=*/true);
+}
+
+void HttpServerConnection::SendResponse(
+ const char* http_code,
+ std::initializer_list<const char*> headers,
+ StringView content,
+ bool force_close) {
+ if (force_close)
+ keepalive_ = false;
+ SendResponseHeaders(http_code, headers, content.size());
+ SendResponseBody(content.data(), content.size());
+ if (!keepalive_)
+ Close();
+}
+
+void HttpServerConnection::SendWebsocketMessage(const void* data, size_t len) {
+ SendWebsocketFrame(kOpcodeBinary, data, len);
+}
+
+void HttpServerConnection::SendWebsocketFrame(uint8_t opcode,
+ const void* payload,
+ size_t payload_len) {
+ PERFETTO_CHECK(is_websocket_);
+
+ uint8_t hdr[10]{};
+ uint32_t hdr_len = 0;
+
+ hdr[0] = opcode | 0x80 /* FIN=1, no fragmentation */;
+ if (payload_len < 126) {
+ hdr_len = 2;
+ hdr[1] = static_cast<uint8_t>(payload_len);
+ } else if (payload_len < 0xffff) {
+ hdr_len = 4;
+ hdr[1] = 126; // Special value: Header extends for 2 bytes.
+ uint16_t len_be = HostToBE16(static_cast<uint16_t>(payload_len));
+ memcpy(&hdr[2], &len_be, sizeof(len_be));
+ } else {
+ hdr_len = 10;
+ hdr[1] = 127; // Special value: Header extends for 4 bytes.
+ uint64_t len_be = HostToBE64(payload_len);
+ memcpy(&hdr[2], &len_be, sizeof(len_be));
+ }
+
+ sock->Send(hdr, hdr_len);
+ if (payload && payload_len > 0)
+ sock->Send(payload, payload_len);
+}
+
+HttpServerConnection::HttpServerConnection(std::unique_ptr<UnixSocket> s)
+ : sock(std::move(s)), rxbuf(PagedMemory::Allocate(kMaxRequestSize)) {}
+
+HttpServerConnection::~HttpServerConnection() = default;
+
+Optional<StringView> HttpRequest::GetHeader(StringView name) const {
+ for (size_t i = 0; i < num_headers; i++) {
+ if (headers[i].name.CaseInsensitiveEq(name))
+ return headers[i].value;
+ }
+ return nullopt;
+}
+
+HttpRequestHandler::~HttpRequestHandler() = default;
+void HttpRequestHandler::OnWebsocketMessage(const WebsocketMessage&) {}
+void HttpRequestHandler::OnHttpConnectionClosed(HttpServerConnection*) {}
+
+} // namespace base
+} // namespace perfetto
diff --git a/src/base/http/http_server_unittest.cc b/src/base/http/http_server_unittest.cc
new file mode 100644
index 0000000..9726ab6
--- /dev/null
+++ b/src/base/http/http_server_unittest.cc
@@ -0,0 +1,321 @@
+/*
+ * 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 "perfetto/ext/base/http/http_server.h"
+
+#include <initializer_list>
+#include <string>
+
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/unix_socket.h"
+#include "src/base/test/test_task_runner.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace base {
+namespace {
+
+using testing::_;
+using testing::Invoke;
+using testing::InvokeWithoutArgs;
+using testing::NiceMock;
+
+constexpr int kTestPort = 5127; // Chosen with a fair dice roll.
+
+class MockHttpHandler : public HttpRequestHandler {
+ public:
+ MOCK_METHOD1(OnHttpRequest, void(const HttpRequest&));
+ MOCK_METHOD1(OnHttpConnectionClosed, void(HttpServerConnection*));
+ MOCK_METHOD1(OnWebsocketMessage, void(const WebsocketMessage&));
+};
+
+class HttpCli {
+ public:
+ explicit HttpCli(TestTaskRunner* ttr) : task_runner_(ttr) {
+ sock = UnixSocketRaw::CreateMayFail(SockFamily::kInet, SockType::kStream);
+ sock.SetBlocking(true);
+ sock.Connect("127.0.0.1:" + std::to_string(kTestPort));
+ }
+
+ void SendHttpReq(std::initializer_list<std::string> headers,
+ const std::string& body = "") {
+ for (auto& header : headers)
+ sock.SendStr(header + "\r\n");
+ if (!body.empty())
+ sock.SendStr("Content-Length: " + std::to_string(body.size()) + "\r\n");
+ sock.SendStr("\r\n");
+ sock.SendStr(body);
+ }
+
+ std::string Recv(size_t min_bytes) {
+ static int n = 0;
+ auto checkpoint_name = "rx_" + std::to_string(n++);
+ auto checkpoint = task_runner_->CreateCheckpoint(checkpoint_name);
+ std::string rxbuf;
+ sock.SetBlocking(false);
+ task_runner_->AddFileDescriptorWatch(sock.fd(), [&] {
+ char buf[1024]{};
+ auto rsize = PERFETTO_EINTR(sock.Receive(buf, sizeof(buf)));
+ ASSERT_GE(rsize, 0);
+ rxbuf.append(buf, static_cast<size_t>(rsize));
+ if (rsize == 0 || (min_bytes && rxbuf.length() >= min_bytes))
+ checkpoint();
+ });
+ task_runner_->RunUntilCheckpoint(checkpoint_name);
+ task_runner_->RemoveFileDescriptorWatch(sock.fd());
+ return rxbuf;
+ }
+
+ std::string RecvAndWaitConnClose() { return Recv(0); }
+
+ TestTaskRunner* task_runner_;
+ UnixSocketRaw sock;
+};
+
+class HttpServerTest : public ::testing::Test {
+ public:
+ HttpServerTest() : srv_(&task_runner_, &handler_) { srv_.Start(kTestPort); }
+
+ TestTaskRunner task_runner_;
+ MockHttpHandler handler_;
+ HttpServer srv_;
+};
+
+TEST_F(HttpServerTest, GET) {
+ const int kIterations = 3;
+ EXPECT_CALL(handler_, OnHttpRequest(_))
+ .Times(kIterations)
+ .WillRepeatedly(Invoke([](const HttpRequest& req) {
+ EXPECT_EQ(req.uri.ToStdString(), "/foo/bar");
+ EXPECT_EQ(req.method.ToStdString(), "GET");
+ EXPECT_EQ(req.origin.ToStdString(), "https://example.com");
+ EXPECT_EQ("42",
+ req.GetHeader("X-header").value_or("N/A").ToStdString());
+ EXPECT_EQ("foo",
+ req.GetHeader("X-header2").value_or("N/A").ToStdString());
+ EXPECT_FALSE(req.is_websocket_handshake);
+ req.conn->SendResponseAndClose("200 OK", {}, "<html>");
+ }));
+ EXPECT_CALL(handler_, OnHttpConnectionClosed(_)).Times(kIterations);
+
+ for (int i = 0; i < 3; i++) {
+ HttpCli cli(&task_runner_);
+ cli.SendHttpReq(
+ {
+ "GET /foo/bar HTTP/1.1", //
+ "Origin: https://example.com", //
+ "X-header: 42", //
+ "X-header2: foo", //
+ },
+ "");
+ EXPECT_EQ(cli.RecvAndWaitConnClose(),
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 6\r\n"
+ "Connection: close\r\n"
+ "\r\n<html>");
+ }
+}
+
+TEST_F(HttpServerTest, GET_404) {
+ HttpCli cli(&task_runner_);
+ EXPECT_CALL(handler_, OnHttpRequest(_))
+ .WillOnce(Invoke([&](const HttpRequest& req) {
+ EXPECT_EQ(req.uri.ToStdString(), "/404");
+ EXPECT_EQ(req.method.ToStdString(), "GET");
+ req.conn->SendResponseAndClose("404 Not Found");
+ }));
+ cli.SendHttpReq({"GET /404 HTTP/1.1"}, "");
+ EXPECT_CALL(handler_, OnHttpConnectionClosed(_));
+ EXPECT_EQ(cli.RecvAndWaitConnClose(),
+ "HTTP/1.1 404 Not Found\r\n"
+ "Content-Length: 0\r\n"
+ "Connection: close\r\n"
+ "\r\n");
+}
+
+TEST_F(HttpServerTest, POST) {
+ HttpCli cli(&task_runner_);
+
+ EXPECT_CALL(handler_, OnHttpRequest(_))
+ .WillOnce(Invoke([&](const HttpRequest& req) {
+ EXPECT_EQ(req.uri.ToStdString(), "/rpc");
+ EXPECT_EQ(req.method.ToStdString(), "POST");
+ EXPECT_EQ(req.origin.ToStdString(), "https://example.com");
+ EXPECT_EQ("foo", req.GetHeader("X-1").value_or("N/A").ToStdString());
+ EXPECT_EQ(req.body.ToStdString(), "the\r\npost\nbody\r\n\r\n");
+ req.conn->SendResponseAndClose("200 OK");
+ }));
+
+ cli.SendHttpReq(
+ {"POST /rpc HTTP/1.1", "Origin: https://example.com", "X-1: foo"},
+ "the\r\npost\nbody\r\n\r\n");
+ EXPECT_CALL(handler_, OnHttpConnectionClosed(_));
+ EXPECT_EQ(cli.RecvAndWaitConnClose(),
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 0\r\n"
+ "Connection: close\r\n"
+ "\r\n");
+}
+
+// An unhandled request should cause a HTTP 500.
+TEST_F(HttpServerTest, Unhadled_500) {
+ HttpCli cli(&task_runner_);
+ EXPECT_CALL(handler_, OnHttpRequest(_));
+ cli.SendHttpReq({"GET /unhandled HTTP/1.1"});
+ EXPECT_CALL(handler_, OnHttpConnectionClosed(_));
+ EXPECT_EQ(cli.RecvAndWaitConnClose(),
+ "HTTP/1.1 500 Internal Server Error\r\n"
+ "Content-Length: 0\r\n"
+ "Connection: close\r\n"
+ "\r\n");
+}
+
+// Send three requests within the same keepalive connection.
+TEST_F(HttpServerTest, POST_Keepalive) {
+ HttpCli cli(&task_runner_);
+ static const int kNumRequests = 3;
+ int req_num = 0;
+ EXPECT_CALL(handler_, OnHttpConnectionClosed(_)).Times(1);
+ EXPECT_CALL(handler_, OnHttpRequest(_))
+ .Times(3)
+ .WillRepeatedly(Invoke([&](const HttpRequest& req) {
+ EXPECT_EQ(req.uri.ToStdString(), "/" + std::to_string(req_num));
+ EXPECT_EQ(req.method.ToStdString(), "POST");
+ EXPECT_EQ(req.body.ToStdString(), "body" + std::to_string(req_num));
+ req.conn->SendResponseHeaders("200 OK");
+ if (++req_num == kNumRequests)
+ req.conn->Close();
+ }));
+
+ for (int i = 0; i < kNumRequests; i++) {
+ auto i_str = std::to_string(i);
+ cli.SendHttpReq({"POST /" + i_str + " HTTP/1.1", "Connection: keep-alive"},
+ "body" + i_str);
+ }
+
+ std::string expected_response;
+ for (int i = 0; i < kNumRequests; i++) {
+ expected_response +=
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 0\r\n"
+ "Connection: keep-alive\r\n"
+ "\r\n";
+ }
+ EXPECT_EQ(cli.RecvAndWaitConnClose(), expected_response);
+}
+
+TEST_F(HttpServerTest, Websocket) {
+ srv_.AddAllowedOrigin("http://foo.com");
+ srv_.AddAllowedOrigin("http://websocket.com");
+ for (int rep = 0; rep < 3; rep++) {
+ HttpCli cli(&task_runner_);
+ EXPECT_CALL(handler_, OnHttpRequest(_))
+ .WillOnce(Invoke([&](const HttpRequest& req) {
+ EXPECT_EQ(req.uri.ToStdString(), "/websocket");
+ EXPECT_EQ(req.method.ToStdString(), "GET");
+ EXPECT_EQ(req.origin.ToStdString(), "http://websocket.com");
+ EXPECT_TRUE(req.is_websocket_handshake);
+ req.conn->UpgradeToWebsocket(req);
+ }));
+
+ cli.SendHttpReq({
+ "GET /websocket HTTP/1.1", //
+ "Origin: http://websocket.com", //
+ "Connection: upgrade", //
+ "Upgrade: websocket", //
+ "Sec-WebSocket-Version: 13", //
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==", //
+ });
+ std::string expected_resp =
+ "HTTP/1.1 101 Switching Protocols\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"
+ "Access-Control-Allow-Origin: http://websocket.com\r\n"
+ "Vary: Origin\r\n"
+ "\r\n";
+ EXPECT_EQ(cli.Recv(expected_resp.size()), expected_resp);
+
+ for (int i = 0; i < 3; i++) {
+ EXPECT_CALL(handler_, OnWebsocketMessage(_))
+ .WillOnce(Invoke([i](const WebsocketMessage& msg) {
+ EXPECT_EQ(msg.data.ToStdString(), "test message");
+ StackString<6> resp("PONG%d", i);
+ msg.conn->SendWebsocketMessage(resp.c_str(), resp.len());
+ }));
+
+ // A frame from a real tcpdump capture:
+ // 1... .... = Fin: True
+ // .000 .... = Reserved: 0x0
+ // .... 0001 = Opcode: Text (1)
+ // 1... .... = Mask: True
+ // .000 1100 = Payload length: 12
+ // Masking-Key: e17e8eb9
+ // Masked payload: "test message"
+ cli.sock.SendStr(
+ "\x81\x8c\xe1\x7e\x8e\xb9\x95\x1b\xfd\xcd\xc1\x13\xeb\xca\x92\x1f\xe9"
+ "\xdc");
+ EXPECT_EQ(cli.Recv(2 + 5), "\x82\x05PONG" + std::to_string(i));
+ }
+
+ cli.sock.Shutdown();
+ auto checkpoint_name = "ws_close_" + std::to_string(rep);
+ auto ws_close = task_runner_.CreateCheckpoint(checkpoint_name);
+ EXPECT_CALL(handler_, OnHttpConnectionClosed(_))
+ .WillOnce(InvokeWithoutArgs(ws_close));
+ task_runner_.RunUntilCheckpoint(checkpoint_name);
+ }
+}
+
+TEST_F(HttpServerTest, Websocket_OriginNotAllowed) {
+ srv_.AddAllowedOrigin("http://websocket.com");
+ srv_.AddAllowedOrigin("http://notallowed.commando");
+ srv_.AddAllowedOrigin("http://iamnotallowed.com");
+ srv_.AddAllowedOrigin("iamnotallowed.com");
+ // The origin must match in full, including scheme. This won't match.
+ srv_.AddAllowedOrigin("notallowed.com");
+
+ HttpCli cli(&task_runner_);
+ EXPECT_CALL(handler_, OnHttpConnectionClosed(_));
+ EXPECT_CALL(handler_, OnHttpRequest(_))
+ .WillOnce(Invoke([&](const HttpRequest& req) {
+ EXPECT_EQ(req.origin.ToStdString(), "http://notallowed.com");
+ EXPECT_TRUE(req.is_websocket_handshake);
+ req.conn->UpgradeToWebsocket(req);
+ }));
+
+ cli.SendHttpReq({
+ "GET /websocket HTTP/1.1", //
+ "Origin: http://notallowed.com", //
+ "Connection: upgrade", //
+ "Upgrade: websocket", //
+ "Sec-WebSocket-Version: 13", //
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==", //
+ });
+ std::string expected_resp =
+ "HTTP/1.1 403 Forbidden\r\n"
+ "Content-Length: 18\r\n"
+ "Connection: close\r\n"
+ "\r\n"
+ "Origin not allowed";
+
+ EXPECT_EQ(cli.Recv(expected_resp.size()), expected_resp);
+ cli.sock.Shutdown();
+}
+
+} // namespace
+} // namespace base
+} // namespace perfetto
diff --git a/src/base/http/sha1.cc b/src/base/http/sha1.cc
new file mode 100644
index 0000000..da3f753
--- /dev/null
+++ b/src/base/http/sha1.cc
@@ -0,0 +1,242 @@
+/*
+ * 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 "perfetto/ext/base/http/sha1.h"
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+// From chrome_elf/sha1/sha1.cc.
+
+namespace perfetto {
+namespace base {
+
+namespace {
+
+inline uint32_t BSwap32(uint32_t x) {
+#if defined(__GNUC__)
+ return __builtin_bswap32(x);
+#elif defined(_MSC_VER)
+ return _byteswap_ulong(val);
+#else
+ return (((x & 0xff000000u) >> 24) | ((x & 0x00ff0000u) >> 8) |
+ ((x & 0x0000ff00u) << 8) | ((x & 0x000000ffu) << 24));
+#endif
+}
+
+// Usage example:
+//
+// SecureHashAlgorithm sha;
+// while(there is data to hash)
+// sha.Update(moredata, size of data);
+// sha.Final();
+// memcpy(somewhere, sha.Digest(), 20);
+//
+// to reuse the instance of sha, call sha.Init();
+class SecureHashAlgorithm {
+ public:
+ SecureHashAlgorithm() { Init(); }
+
+ void Init();
+ void Update(const void* data, size_t nbytes);
+ void Final();
+
+ // 20 bytes of message digest.
+ const unsigned char* Digest() const {
+ return reinterpret_cast<const unsigned char*>(H);
+ }
+
+ private:
+ void Pad();
+ void Process();
+
+ uint32_t A, B, C, D, E;
+
+ uint32_t H[5];
+
+ union {
+ uint32_t W[80];
+ uint8_t M[64];
+ };
+
+ uint32_t cursor;
+ uint64_t l;
+};
+
+//------------------------------------------------------------------------------
+// Private functions
+//------------------------------------------------------------------------------
+
+// Identifier names follow notation in FIPS PUB 180-3, where you'll
+// also find a description of the algorithm:
+// http://csrc.nist.gov/publications/fips/fips180-3/fips180-3_final.pdf
+
+inline uint32_t f(uint32_t t, uint32_t B, uint32_t C, uint32_t D) {
+ if (t < 20) {
+ return (B & C) | ((~B) & D);
+ } else if (t < 40) {
+ return B ^ C ^ D;
+ } else if (t < 60) {
+ return (B & C) | (B & D) | (C & D);
+ } else {
+ return B ^ C ^ D;
+ }
+}
+
+inline uint32_t S(uint32_t n, uint32_t X) {
+ return (X << n) | (X >> (32 - n));
+}
+
+inline uint32_t K(uint32_t t) {
+ if (t < 20) {
+ return 0x5a827999;
+ } else if (t < 40) {
+ return 0x6ed9eba1;
+ } else if (t < 60) {
+ return 0x8f1bbcdc;
+ } else {
+ return 0xca62c1d6;
+ }
+}
+
+void SecureHashAlgorithm::Init() {
+ A = 0;
+ B = 0;
+ C = 0;
+ D = 0;
+ E = 0;
+ cursor = 0;
+ l = 0;
+ H[0] = 0x67452301;
+ H[1] = 0xefcdab89;
+ H[2] = 0x98badcfe;
+ H[3] = 0x10325476;
+ H[4] = 0xc3d2e1f0;
+}
+
+void SecureHashAlgorithm::Update(const void* data, size_t nbytes) {
+ const uint8_t* d = reinterpret_cast<const uint8_t*>(data);
+ while (nbytes--) {
+ M[cursor++] = *d++;
+ if (cursor >= 64)
+ Process();
+ l += 8;
+ }
+}
+
+void SecureHashAlgorithm::Final() {
+ Pad();
+ Process();
+
+ for (size_t t = 0; t < 5; ++t)
+ H[t] = BSwap32(H[t]);
+}
+
+void SecureHashAlgorithm::Process() {
+ uint32_t t;
+
+ // Each a...e corresponds to a section in the FIPS 180-3 algorithm.
+
+ // a.
+ //
+ // W and M are in a union, so no need to memcpy.
+ // memcpy(W, M, sizeof(M));
+ for (t = 0; t < 16; ++t)
+ W[t] = BSwap32(W[t]);
+
+ // b.
+ for (t = 16; t < 80; ++t)
+ W[t] = S(1, W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16]);
+
+ // c.
+ A = H[0];
+ B = H[1];
+ C = H[2];
+ D = H[3];
+ E = H[4];
+
+ // d.
+ for (t = 0; t < 80; ++t) {
+ uint32_t TEMP = S(5, A) + f(t, B, C, D) + E + W[t] + K(t);
+ E = D;
+ D = C;
+ C = S(30, B);
+ B = A;
+ A = TEMP;
+ }
+
+ // e.
+ H[0] += A;
+ H[1] += B;
+ H[2] += C;
+ H[3] += D;
+ H[4] += E;
+
+ cursor = 0;
+}
+
+void SecureHashAlgorithm::Pad() {
+ M[cursor++] = 0x80;
+
+ if (cursor > 64 - 8) {
+ // pad out to next block
+ while (cursor < 64)
+ M[cursor++] = 0;
+
+ Process();
+ }
+
+ while (cursor < 64 - 8)
+ M[cursor++] = 0;
+
+ M[cursor++] = (l >> 56) & 0xff;
+ M[cursor++] = (l >> 48) & 0xff;
+ M[cursor++] = (l >> 40) & 0xff;
+ M[cursor++] = (l >> 32) & 0xff;
+ M[cursor++] = (l >> 24) & 0xff;
+ M[cursor++] = (l >> 16) & 0xff;
+ M[cursor++] = (l >> 8) & 0xff;
+ M[cursor++] = l & 0xff;
+}
+
+// Computes the SHA-1 hash of the |len| bytes in |data| and puts the hash
+// in |hash|. |hash| must be kSHA1Length bytes long.
+void SHA1HashBytes(const unsigned char* data, size_t len, unsigned char* hash) {
+ SecureHashAlgorithm sha;
+ sha.Update(data, len);
+ sha.Final();
+
+ ::memcpy(hash, sha.Digest(), kSHA1Length);
+}
+
+} // namespace
+
+//------------------------------------------------------------------------------
+// Public functions
+//------------------------------------------------------------------------------
+SHA1Digest SHA1Hash(const void* data, size_t size) {
+ SHA1Digest digest;
+ SHA1HashBytes(static_cast<const unsigned char*>(data), size,
+ reinterpret_cast<unsigned char*>(&digest[0]));
+ return digest;
+}
+
+SHA1Digest SHA1Hash(const std::string& str) {
+ return SHA1Hash(str.data(), str.size());
+}
+
+} // namespace base
+} // namespace perfetto
diff --git a/src/base/http/sha1_unittest.cc b/src/base/http/sha1_unittest.cc
new file mode 100644
index 0000000..269afc1
--- /dev/null
+++ b/src/base/http/sha1_unittest.cc
@@ -0,0 +1,56 @@
+/*
+ * 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 "perfetto/ext/base/http/sha1.h"
+
+#include <string>
+
+#include "perfetto/ext/base/string_view.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace base {
+namespace {
+
+using testing::ElementsAreArray;
+
+TEST(SHA1Test, Hash) {
+ EXPECT_THAT(SHA1Hash(""), ElementsAreArray<uint8_t>(
+ {0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b,
+ 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60,
+ 0x18, 0x90, 0xaf, 0xd8, 0x07, 0x09}));
+
+ EXPECT_THAT(SHA1Hash("abc"), ElementsAreArray<uint8_t>(
+ {0xa9, 0x99, 0x3e, 0x36, 0x47, 0x06, 0x81,
+ 0x6a, 0xba, 0x3e, 0x25, 0x71, 0x78, 0x50,
+ 0xc2, 0x6c, 0x9c, 0xd0, 0xd8, 0x9d}));
+
+ EXPECT_THAT(
+ SHA1Hash("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"),
+ ElementsAreArray<uint8_t>({0x84, 0x98, 0x3e, 0x44, 0x1c, 0x3b, 0xd2,
+ 0x6e, 0xba, 0xae, 0x4a, 0xa1, 0xf9, 0x51,
+ 0x29, 0xe5, 0xe5, 0x46, 0x70, 0xf1}));
+
+ EXPECT_THAT(
+ SHA1Hash(std::string(1000000, 'a')),
+ ElementsAreArray<uint8_t>({0x34, 0xaa, 0x97, 0x3c, 0xd4, 0xc4, 0xda,
+ 0xa4, 0xf6, 0x1e, 0xeb, 0x2b, 0xdb, 0xad,
+ 0x27, 0x31, 0x65, 0x34, 0x01, 0x6f}));
+}
+
+} // namespace
+} // namespace base
+} // namespace perfetto
diff --git a/src/base/small_vector_unittest.cc b/src/base/small_vector_unittest.cc
new file mode 100644
index 0000000..e99d84a
--- /dev/null
+++ b/src/base/small_vector_unittest.cc
@@ -0,0 +1,232 @@
+/*
+ * 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 "perfetto/ext/base/small_vector.h"
+
+#include <tuple>
+#include <utility>
+
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace base {
+namespace {
+
+int g_instances = 0;
+
+struct Obj {
+ explicit Obj(size_t v = 0) : value(v) {
+ EXPECT_FALSE(constructed);
+ constructed = true;
+ g_instances++;
+ }
+
+ ~Obj() {
+ EXPECT_TRUE(constructed);
+ g_instances--;
+ }
+
+ // Move operators.
+ Obj(Obj&& other) noexcept {
+ g_instances++;
+ constructed = true;
+ moved_into = true;
+ value = other.value;
+ other.moved_from = true;
+ other.value = 0xffffffff - value;
+ }
+
+ Obj& operator=(Obj&& other) noexcept {
+ this->~Obj();
+ new (this) Obj(std::move(other));
+ return *this;
+ }
+
+ // Copy operators.
+ Obj(const Obj& other) {
+ other.copied_from = true;
+ g_instances++;
+ constructed = true;
+ copied_into = true;
+ value = other.value;
+ }
+
+ Obj& operator=(const Obj& other) {
+ this->~Obj();
+ new (this) Obj(other);
+ return *this;
+ }
+
+ uintptr_t addr = reinterpret_cast<uintptr_t>(this);
+ bool constructed = false;
+ size_t value = 0;
+ bool moved_from = false;
+ mutable bool copied_from = false;
+ bool moved_into = false;
+ bool copied_into = false;
+};
+
+TEST(SmallVectorTest, StaySmall) {
+ SmallVector<Obj, 8> v;
+ EXPECT_EQ(g_instances, 0);
+ EXPECT_EQ(v.size(), 0u);
+ EXPECT_TRUE(v.empty());
+ EXPECT_EQ(v.begin(), v.end());
+
+ for (size_t i = 1; i <= 8; i++) {
+ v.emplace_back(i);
+ EXPECT_EQ(g_instances, static_cast<int>(i));
+ EXPECT_FALSE(v.empty());
+ EXPECT_EQ(v.end(), v.begin() + i);
+ EXPECT_EQ(v.back().value, i);
+ EXPECT_EQ(v[static_cast<size_t>(i - 1)].value, i);
+ EXPECT_EQ(v[static_cast<size_t>(i - 1)].value, i);
+ }
+
+ for (size_t i = 1; i <= 3; i++) {
+ v.pop_back();
+ EXPECT_EQ(g_instances, 8 - static_cast<int>(i));
+ }
+
+ v.clear();
+ EXPECT_EQ(g_instances, 0);
+}
+
+TEST(SmallVectorTest, GrowOnHeap) {
+ SmallVector<Obj, 4> v;
+ for (size_t i = 0; i < 10; i++) {
+ v.emplace_back(i);
+ EXPECT_EQ(g_instances, static_cast<int>(i + 1));
+ EXPECT_FALSE(v.empty());
+ EXPECT_EQ(v.end(), v.begin() + i + 1);
+ EXPECT_EQ(v[i].value, i);
+ }
+
+ // Do a second pass and check that the initial elements aren't corrupt.
+ for (size_t i = 0; i < 10; i++) {
+ EXPECT_EQ(v[i].value, i);
+ EXPECT_TRUE(v[i].constructed);
+ }
+
+ // The first 4 elements must have been moved into because of the heap growth.
+ for (size_t i = 0; i < 4; i++)
+ EXPECT_TRUE(v[i].moved_into);
+ EXPECT_FALSE(v.back().moved_into);
+}
+
+class SmallVectorTestP : public testing::TestWithParam<size_t> {};
+
+TEST_P(SmallVectorTestP, MoveOperators) {
+ size_t num_elements = GetParam();
+ static constexpr size_t kInlineCapacity = 4;
+ SmallVector<Obj, kInlineCapacity> v1;
+ for (size_t i = 0; i < num_elements; i++)
+ v1.emplace_back(i);
+
+ SmallVector<Obj, kInlineCapacity> v2(std::move(v1));
+ EXPECT_TRUE(v1.empty());
+ EXPECT_EQ(v2.size(), num_elements);
+
+ // Check that v2 (the moved into vector) is consistent.
+ for (size_t i = 0; i < num_elements; i++) {
+ EXPECT_EQ(v2[i].value, i);
+ EXPECT_TRUE(v2[i].constructed);
+ if (num_elements <= kInlineCapacity) {
+ EXPECT_TRUE(v2[i].moved_into);
+ }
+ }
+
+ // Check that v1 (the moved-from object) is still usable.
+ EXPECT_EQ(v1.size(), 0u);
+
+ for (size_t i = 0; i < num_elements; i++) {
+ v1.emplace_back(1000 + i);
+ EXPECT_EQ(v1.size(), i + 1);
+ }
+
+ EXPECT_NE(v1.data(), v2.data());
+
+ for (size_t i = 0; i < num_elements; i++) {
+ EXPECT_EQ(v1[i].value, 1000 + i);
+ EXPECT_EQ(v2[i].value, i);
+ EXPECT_TRUE(v1[i].constructed);
+ EXPECT_FALSE(v1[i].moved_from);
+ }
+
+ // Now swap again using the move-assignment.
+
+ v1 = std::move(v2);
+ EXPECT_EQ(v1.size(), num_elements);
+ EXPECT_TRUE(v2.empty());
+ for (size_t i = 0; i < num_elements; i++) {
+ EXPECT_EQ(v1[i].value, i);
+ EXPECT_TRUE(v1[i].constructed);
+ }
+
+ { auto destroy = std::move(v1); }
+
+ EXPECT_EQ(g_instances, 0);
+}
+
+TEST_P(SmallVectorTestP, CopyOperators) {
+ size_t num_elements = GetParam();
+ static constexpr size_t kInlineCapacity = 4;
+ SmallVector<Obj, kInlineCapacity> v1;
+ for (size_t i = 0; i < num_elements; i++)
+ v1.emplace_back(i);
+
+ SmallVector<Obj, kInlineCapacity> v2(v1);
+ EXPECT_EQ(v1.size(), num_elements);
+ EXPECT_EQ(v2.size(), num_elements);
+ EXPECT_EQ(g_instances, static_cast<int>(num_elements * 2));
+
+ for (size_t i = 0; i < num_elements; i++) {
+ EXPECT_EQ(v1[i].value, i);
+ EXPECT_TRUE(v1[i].copied_from);
+ EXPECT_EQ(v2[i].value, i);
+ EXPECT_TRUE(v2[i].copied_into);
+ }
+
+ // Now edit v2.
+ for (size_t i = 0; i < num_elements; i++)
+ v2[i].value = i + 100;
+ EXPECT_EQ(g_instances, static_cast<int>(num_elements * 2));
+
+ // Append some extra elements.
+ for (size_t i = 0; i < num_elements; i++)
+ v2.emplace_back(i + 200);
+ EXPECT_EQ(g_instances, static_cast<int>(num_elements * 3));
+
+ for (size_t i = 0; i < num_elements * 2; i++) {
+ if (i < num_elements) {
+ EXPECT_EQ(v1[i].value, i);
+ EXPECT_EQ(v2[i].value, 100 + i);
+ } else {
+ EXPECT_EQ(v2[i].value, 200 + i - num_elements);
+ }
+ }
+
+ v2.clear();
+ EXPECT_EQ(g_instances, static_cast<int>(num_elements));
+}
+
+INSTANTIATE_TEST_SUITE_P(SmallVectorTest,
+ SmallVectorTestP,
+ testing::Values(2, 4, 7, 512));
+
+} // namespace
+} // namespace base
+} // namespace perfetto
diff --git a/src/base/test/utils.cc b/src/base/test/utils.cc
index ff00f64..56e51a8 100644
--- a/src/base/test/utils.cc
+++ b/src/base/test/utils.cc
@@ -40,28 +40,5 @@
return path;
}
-std::string HexDump(const void* data_void, size_t len, size_t bytes_per_line) {
- const char* data = reinterpret_cast<const char*>(data_void);
- std::string res;
- static const size_t kPadding = bytes_per_line * 3 + 12;
- std::unique_ptr<char[]> line(new char[bytes_per_line * 4 + 128]);
- for (size_t i = 0; i < len; i += bytes_per_line) {
- char* wptr = line.get();
- wptr += sprintf(wptr, "%08zX: ", i);
- for (size_t j = i; j < i + bytes_per_line && j < len; j++)
- wptr += sprintf(wptr, "%02X ", static_cast<unsigned>(data[j]) & 0xFF);
- for (size_t j = static_cast<size_t>(wptr - line.get()); j < kPadding; ++j)
- *(wptr++) = ' ';
- for (size_t j = i; j < i + bytes_per_line && j < len; j++) {
- char c = data[j];
- *(wptr++) = (c >= 32 && c < 127) ? c : '.';
- }
- *(wptr++) = '\n';
- *(wptr++) = '\0';
- res.append(line.get());
- }
- return res;
-}
-
} // namespace base
} // namespace perfetto
diff --git a/src/base/test/utils.h b/src/base/test/utils.h
index 032194b..4dfc281 100644
--- a/src/base/test/utils.h
+++ b/src/base/test/utils.h
@@ -52,13 +52,6 @@
std::string GetTestDataPath(const std::string& path);
-// Returns a xxd-style hex dump (hex + ascii chars) of the input data.
-std::string HexDump(const void* data, size_t len, size_t bytes_per_line = 16);
-inline std::string HexDump(const std::string& data,
- size_t bytes_per_line = 16) {
- return HexDump(data.data(), data.size(), bytes_per_line);
-}
-
} // namespace base
} // namespace perfetto
diff --git a/src/base/unix_socket.cc b/src/base/unix_socket.cc
index 5986917..3088399 100644
--- a/src/base/unix_socket.cc
+++ b/src/base/unix_socket.cc
@@ -36,6 +36,7 @@
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
+#include <poll.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
@@ -51,6 +52,7 @@
#include "perfetto/base/build_config.h"
#include "perfetto/base/logging.h"
#include "perfetto/base/task_runner.h"
+#include "perfetto/base/time.h"
#include "perfetto/ext/base/string_utils.h"
#include "perfetto/ext/base/utils.h"
@@ -66,16 +68,6 @@
namespace {
-// MSG_NOSIGNAL is not supported on Mac OS X, but in that case the socket is
-// created with SO_NOSIGPIPE (See InitializeSocket()).
-// On Windows this does't apply as signals don't exist.
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
-#elif PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
-constexpr int kNoSigPipe = 0;
-#else
-constexpr int kNoSigPipe = MSG_NOSIGNAL;
-#endif
-
// Android takes an int instead of socklen_t for the control buffer size.
#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
using CBufLenType = size_t;
@@ -431,19 +423,60 @@
// This does not make sense on non-blocking sockets.
PERFETTO_DCHECK(fd_);
+ const bool is_blocking_with_timeout =
+ tx_timeout_ms_ > 0 && ((fcntl(*fd_, F_GETFL, 0) & O_NONBLOCK) == 0);
+ const int64_t start_ms = GetWallTimeMs().count();
+
+ // Waits until some space is available in the tx buffer.
+ // Returns true if some buffer space is available, false if times out.
+ auto poll_or_timeout = [&] {
+ PERFETTO_DCHECK(is_blocking_with_timeout);
+ const int64_t deadline = start_ms + tx_timeout_ms_;
+ const int64_t now_ms = GetWallTimeMs().count();
+ if (now_ms >= deadline)
+ return false; // Timed out
+ const int timeout_ms = static_cast<int>(deadline - now_ms);
+ pollfd pfd{*fd_, POLLOUT, 0};
+ return PERFETTO_EINTR(poll(&pfd, 1, timeout_ms)) > 0;
+ };
+
+// We implement blocking sends that require a timeout as non-blocking + poll.
+// This is because SO_SNDTIMEO doesn't work as expected (b/193234818). On linux
+// we can just pass MSG_DONTWAIT to force the send to be non-blocking. On Mac,
+// instead we need to flip the O_NONBLOCK flag back and forth.
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
+ // MSG_NOSIGNAL is not supported on Mac OS X, but in that case the socket is
+ // created with SO_NOSIGPIPE (See InitializeSocket()).
+ int send_flags = 0;
+
+ if (is_blocking_with_timeout)
+ SetBlocking(false);
+
+ auto reset_nonblock_on_exit = OnScopeExit([&] {
+ if (is_blocking_with_timeout)
+ SetBlocking(true);
+ });
+#else
+ int send_flags = MSG_NOSIGNAL | (is_blocking_with_timeout ? MSG_DONTWAIT : 0);
+#endif
+
ssize_t total_sent = 0;
while (msg->msg_iov) {
- ssize_t sent = PERFETTO_EINTR(sendmsg(*fd_, msg, kNoSigPipe));
- if (sent <= 0) {
- if (sent == -1 && IsAgain(errno))
- return total_sent;
- return sent;
+ ssize_t send_res = PERFETTO_EINTR(sendmsg(*fd_, msg, send_flags));
+ if (send_res == -1 && IsAgain(errno)) {
+ if (is_blocking_with_timeout && poll_or_timeout()) {
+ continue; // Tx buffer unblocked, repeat the loop.
+ }
+ return total_sent;
+ } else if (send_res <= 0) {
+ return send_res; // An error occurred.
+ } else {
+ total_sent += send_res;
+ ShiftMsgHdrPosix(static_cast<size_t>(send_res), msg);
+ // Only send the ancillary data with the first sendmsg call.
+ msg->msg_control = nullptr;
+ msg->msg_controllen = 0;
}
- total_sent += sent;
- ShiftMsgHdrPosix(static_cast<size_t>(sent), msg);
- // Only send the ancillary data with the first sendmsg call.
- msg->msg_control = nullptr;
- msg->msg_controllen = 0;
}
return total_sent;
}
@@ -541,8 +574,14 @@
bool UnixSocketRaw::SetTxTimeout(uint32_t timeout_ms) {
PERFETTO_DCHECK(fd_);
+ // On Unix-based systems, SO_SNDTIMEO isn't used for Send() because it's
+ // unreliable (b/193234818). Instead we use non-blocking sendmsg() + poll().
+ // See SendMsgAllPosix(). We still make the setsockopt call because
+ // SO_SNDTIMEO also affects connect().
+ tx_timeout_ms_ = timeout_ms;
#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
DWORD timeout = timeout_ms;
+ ignore_result(tx_timeout_ms_);
#else
struct timeval timeout {};
uint32_t timeout_sec = timeout_ms / 1000;
diff --git a/src/base/unix_socket_unittest.cc b/src/base/unix_socket_unittest.cc
index 86a6bb5..c838663 100644
--- a/src/base/unix_socket_unittest.cc
+++ b/src/base/unix_socket_unittest.cc
@@ -30,6 +30,7 @@
#include "perfetto/base/build_config.h"
#include "perfetto/base/logging.h"
#include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/periodic_task.h"
#include "perfetto/ext/base/pipe.h"
#include "perfetto/ext/base/temp_file.h"
#include "perfetto/ext/base/utils.h"
@@ -331,6 +332,53 @@
tx_thread.join();
}
+// Regression test for b/193234818. SO_SNDTIMEO is unreliable on most systems.
+// It doesn't guarantee that the whole send() call blocks for at most X, as the
+// kernel rearms the timeout if the send buffers frees up and allows a partial
+// send. This test reproduces the issue 100% on Mac. Unfortunately on Linux the
+// repro seem to happen only when a suspend happens in the middle.
+TEST_F(UnixSocketTest, BlockingSendTimeout) {
+ TestTaskRunner ttr;
+ UnixSocketRaw send_sock;
+ UnixSocketRaw recv_sock;
+ std::tie(send_sock, recv_sock) =
+ UnixSocketRaw::CreatePairPosix(kTestSocket.family(), SockType::kStream);
+
+ auto blocking_send_done = ttr.CreateCheckpoint("blocking_send_done");
+
+ std::thread tx_thread([&] {
+ // Fill the tx buffer in non-blocking mode.
+ send_sock.SetBlocking(false);
+ char buf[1024 * 16]{};
+ while (send_sock.Send(buf, sizeof(buf)) > 0) {
+ }
+
+ // Then do a blocking send. It should return a partial value within the tx
+ // timeout.
+ send_sock.SetBlocking(true);
+ send_sock.SetTxTimeout(10);
+ ASSERT_LT(send_sock.Send(buf, sizeof(buf)),
+ static_cast<ssize_t>(sizeof(buf)));
+ ttr.PostTask(blocking_send_done);
+ });
+
+ // This task needs to be slow enough so that doesn't unblock the send, but
+ // fast enough so that within a blocking cycle, the send re-attempts and
+ // re-arms the timeout.
+ PeriodicTask read_slowly_task(&ttr);
+ PeriodicTask::Args args;
+ args.period_ms = 1; // Read 1 byte every ms (1 KiB/s).
+ args.task = [&] {
+ char rxbuf[1]{};
+ recv_sock.Receive(rxbuf, sizeof(rxbuf));
+ };
+ read_slowly_task.Start(args);
+
+ ttr.RunUntilCheckpoint("blocking_send_done");
+ read_slowly_task.Reset();
+ tx_thread.join();
+}
+
// Regression test for b/76155349 . If the receiver end disconnects while the
// sender is in the middle of a large send(), the socket should gracefully give
// up (i.e. Shutdown()) but not crash.
diff --git a/src/base/utils.cc b/src/base/utils.cc
index b5d3d3c..363a0d6 100644
--- a/src/base/utils.cc
+++ b/src/base/utils.cc
@@ -266,5 +266,28 @@
#endif
}
+std::string HexDump(const void* data_void, size_t len, size_t bytes_per_line) {
+ const char* data = reinterpret_cast<const char*>(data_void);
+ std::string res;
+ static const size_t kPadding = bytes_per_line * 3 + 12;
+ std::unique_ptr<char[]> line(new char[bytes_per_line * 4 + 128]);
+ for (size_t i = 0; i < len; i += bytes_per_line) {
+ char* wptr = line.get();
+ wptr += sprintf(wptr, "%08zX: ", i);
+ for (size_t j = i; j < i + bytes_per_line && j < len; j++)
+ wptr += sprintf(wptr, "%02X ", static_cast<unsigned>(data[j]) & 0xFF);
+ for (size_t j = static_cast<size_t>(wptr - line.get()); j < kPadding; ++j)
+ *(wptr++) = ' ';
+ for (size_t j = i; j < i + bytes_per_line && j < len; j++) {
+ char c = data[j];
+ *(wptr++) = (c >= 32 && c < 127) ? c : '.';
+ }
+ *(wptr++) = '\n';
+ *(wptr++) = '\0';
+ res.append(line.get());
+ }
+ return res;
+}
+
} // namespace base
} // namespace perfetto
diff --git a/src/base/utils_unittest.cc b/src/base/utils_unittest.cc
index ef6e657..b75ec05 100644
--- a/src/base/utils_unittest.cc
+++ b/src/base/utils_unittest.cc
@@ -202,6 +202,19 @@
EXPECT_EQ(0xffffff00u, AlignUp<16>(0xffffff00 - 1));
}
+TEST(UtilsTest, HexDump) {
+ char input[] = {0x00, 0x00, 'a', 'b', 'c', 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'};
+
+ std::string output = HexDump(input, sizeof(input));
+
+ EXPECT_EQ(
+ output,
+ R"(00000000: 00 00 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E ..abcdefghijklmn
+00000010: 6F 70 op
+)");
+}
+
} // namespace
} // namespace base
} // namespace perfetto
diff --git a/src/base/watchdog_posix.cc b/src/base/watchdog_posix.cc
index 6133b61..f352c57 100644
--- a/src/base/watchdog_posix.cc
+++ b/src/base/watchdog_posix.cc
@@ -50,6 +50,10 @@
base::CrashKey g_crash_key_reason("wdog_reason");
+// TODO(primiano): for debugging b/191600928. Remove in Jan 2022.
+base::CrashKey g_crash_key_mono("wdog_mono");
+base::CrashKey g_crash_key_boot("wdog_boot");
+
bool IsMultipleOf(uint32_t number, uint32_t divisor) {
return number >= divisor && number % divisor == 0;
}
@@ -211,6 +215,11 @@
}
void Watchdog::ThreadMain() {
+ // Register crash keys explicitly to avoid running out of slots at crash time.
+ g_crash_key_reason.Register();
+ g_crash_key_boot.Register();
+ g_crash_key_mono.Register();
+
base::ScopedFile stat_fd(base::OpenFile("/proc/self/stat", O_RDONLY));
if (!stat_fd) {
PERFETTO_ELOG("Failed to open stat file to enforce resource limits.");
@@ -294,6 +303,8 @@
void Watchdog::SerializeLogsAndKillThread(int tid,
WatchdogCrashReason crash_reason) {
g_crash_key_reason.Set(static_cast<int>(crash_reason));
+ g_crash_key_boot.Set(base::GetBootTimeS().count());
+ g_crash_key_mono.Set(base::GetWallTimeS().count());
// We are about to die. Serialize the logs into the crash buffer so the
// debuggerd crash handler picks them up and attaches to the bugreport.
diff --git a/src/ipc/host_impl.cc b/src/ipc/host_impl.cc
index da1ce4c..fa85c2b 100644
--- a/src/ipc/host_impl.cc
+++ b/src/ipc/host_impl.cc
@@ -20,7 +20,9 @@
#include <cinttypes>
#include <utility>
+#include "perfetto/base/build_config.h"
#include "perfetto/base/task_runner.h"
+#include "perfetto/base/time.h"
#include "perfetto/ext/base/crash_keys.h"
#include "perfetto/ext/base/utils.h"
#include "perfetto/ext/ipc/service.h"
@@ -28,6 +30,15 @@
#include "protos/perfetto/ipc/wire_protocol.gen.h"
+#if (PERFETTO_BUILDFLAG(PERFETTO_STANDALONE_BUILD) || \
+ PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)) && \
+ (PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
+ PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID))
+#define PERFETTO_LOG_TXBUF_FOR_B_191600928
+// TODO(primiano): temporary for investigating b/191600928. Remove in Jan 2022
+#include <sys/ioctl.h>
+#endif
+
// TODO(primiano): put limits on #connections/uid and req. queue (b/69093705).
namespace perfetto {
@@ -38,7 +49,11 @@
constexpr base::SockFamily kHostSockFamily =
kUseTCPSocket ? base::SockFamily::kInet : base::SockFamily::kUnix;
+// TODO(primiano): temporary for investigating b/191600928. Remove in Jan 2022.
base::CrashKey g_crash_key_uid("ipc_uid");
+base::CrashKey g_crash_key_tx_b("ipc_tx_boot");
+base::CrashKey g_crash_key_tx_m("ipc_tx_mono");
+base::CrashKey g_crash_key_tx_qlen("ipc_tx_qlen");
uid_t GetPosixPeerUid(base::UnixSocket* sock) {
#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
@@ -263,6 +278,17 @@
std::string buf = BufferedFrameDeserializer::Serialize(frame);
+ auto crash_key_b = g_crash_key_tx_b.SetScoped(base::GetBootTimeS().count());
+ auto crash_key_w = g_crash_key_tx_m.SetScoped(base::GetWallTimeS().count());
+
+#if defined(PERFETTO_LOG_TXBUF_FOR_B_191600928)
+ int32_t tx_queue_len = 0;
+ ioctl(client->sock->fd(), TIOCOUTQ, &tx_queue_len);
+ auto crash_key_qlen = g_crash_key_tx_qlen.SetScoped(tx_queue_len);
+#else
+ base::ignore_result(g_crash_key_tx_qlen);
+#endif
+
// When a new Client connects in OnNewClientConnection we set a timeout on
// Send (see call to SetTxTimeout).
//
diff --git a/src/perfetto_cmd/perfetto_cmd.cc b/src/perfetto_cmd/perfetto_cmd.cc
index ef1492d..92c8bfa 100644
--- a/src/perfetto_cmd/perfetto_cmd.cc
+++ b/src/perfetto_cmd/perfetto_cmd.cc
@@ -514,8 +514,9 @@
trace_config_.reset(new TraceConfig());
bool parsed = false;
- const bool will_trace = !is_attach() && !query_service_ && !bugreport_;
- if (!will_trace) {
+ const bool will_trace_or_trigger =
+ !is_attach() && !query_service_ && !bugreport_;
+ if (!will_trace_or_trigger) {
if ((!trace_config_raw.empty() || has_config_options)) {
PERFETTO_ELOG("Cannot specify a trace config with this option");
return 1;
@@ -545,7 +546,7 @@
if (parsed) {
*trace_config_->mutable_statsd_metadata() = std::move(statsd_metadata);
trace_config_raw.clear();
- } else if (will_trace) {
+ } else if (will_trace_or_trigger) {
PERFETTO_ELOG("The trace config is invalid, bailing out.");
return 1;
}
@@ -573,21 +574,23 @@
return 1;
}
- if (trace_config_->activate_triggers().empty() &&
- trace_config_->incident_report_config().destination_package().empty() &&
+ // Only save to incidentd if:
+ // 1) --upload is set
+ // 2) |skip_incidentd| is absent or false.
+ // 3) we are not simply activating triggers.
+ save_to_incidentd_ =
+ upload_flag_ &&
!trace_config_->incident_report_config().skip_incidentd() &&
- upload_flag_) {
+ trace_config_->activate_triggers().empty();
+
+ if (save_to_incidentd_ &&
+ trace_config_->incident_report_config().destination_package().empty()) {
PERFETTO_ELOG(
"Missing IncidentReportConfig.destination_package with --dropbox / "
"--upload.");
return 1;
}
- // Only save to incidentd if both --upload is set and |skip_incidentd| is
- // absent or false.
- save_to_incidentd_ =
- upload_flag_ && !trace_config_->incident_report_config().skip_incidentd();
-
// Respect the wishes of the config with respect to statsd logging or fall
// back on the presence of the --upload flag if not set.
switch (trace_config_->statsd_logging()) {
@@ -644,7 +647,7 @@
}
bool open_out_file = true;
- if (!will_trace) {
+ if (!will_trace_or_trigger) {
open_out_file = false;
if (!trace_out_path_.empty() || upload_flag_) {
PERFETTO_ELOG("Can't pass an --out file (or --upload) with this option");
diff --git a/src/profiling/memory/java_hprof_producer.cc b/src/profiling/memory/java_hprof_producer.cc
index aef9b5b..6c68995 100644
--- a/src/profiling/memory/java_hprof_producer.cc
+++ b/src/profiling/memory/java_hprof_producer.cc
@@ -40,8 +40,11 @@
auto it = data_sources_.find(id);
if (it == data_sources_.end())
return;
- const DataSource& ds = it->second;
- SignalDataSource(ds);
+ DataSource& ds = it->second;
+ if (!ds.config().continuous_dump_config().scan_pids_only_on_start()) {
+ ds.CollectPids();
+ }
+ ds.SendSignal();
auto weak_producer = weak_factory_.GetWeakPtr();
task_runner_->PostDelayedTask(
[weak_producer, id, dump_interval] {
@@ -52,10 +55,16 @@
dump_interval);
}
-// static
-void JavaHprofProducer::SignalDataSource(const DataSource& ds) {
- const std::set<pid_t>& pids = ds.pids;
- for (pid_t pid : pids) {
+JavaHprofProducer::DataSource::DataSource(
+ DataSourceConfig ds_config,
+ JavaHprofConfig config,
+ std::vector<std::string> normalized_cmdlines)
+ : ds_config_(ds_config),
+ config_(config),
+ normalized_cmdlines_(normalized_cmdlines) {}
+
+void JavaHprofProducer::DataSource::SendSignal() const {
+ for (pid_t pid : pids_) {
auto opt_status = ReadStatus(pid);
if (!opt_status) {
PERFETTO_PLOG("Failed to read /proc/%d/status. Not signalling.", pid);
@@ -69,23 +78,32 @@
pid);
continue;
}
- if (!CanProfile(ds.ds_config, uids->effective,
- ds.config.target_installed_by())) {
+ if (!CanProfile(ds_config_, uids->effective,
+ config_.target_installed_by())) {
PERFETTO_ELOG("%d (UID %" PRIu64 ") not profileable.", pid,
uids->effective);
continue;
}
PERFETTO_DLOG("Sending %d to %d", kJavaHeapprofdSignal, pid);
union sigval signal_value;
- signal_value.sival_int =
- static_cast<int32_t>(ds.ds_config.tracing_session_id() %
- std::numeric_limits<int32_t>::max());
+ signal_value.sival_int = static_cast<int32_t>(
+ ds_config_.tracing_session_id() % std::numeric_limits<int32_t>::max());
if (sigqueue(pid, kJavaHeapprofdSignal, signal_value) != 0) {
PERFETTO_DPLOG("sigqueue");
}
}
}
+void JavaHprofProducer::DataSource::CollectPids() {
+ pids_.clear();
+ for (uint64_t pid : config_.pid()) {
+ pids_.emplace(static_cast<pid_t>(pid));
+ }
+ FindPidsForCmdlines(normalized_cmdlines_, &pids_);
+ if (config_.min_anonymous_memory_kb() > 0)
+ RemoveUnderAnonThreshold(config_.min_anonymous_memory_kb(), &pids_);
+}
+
void JavaHprofProducer::IncreaseConnectionBackoff() {
connection_backoff_ms_ *= 2;
if (connection_backoff_ms_ > kMaxConnectionBackoffMs)
@@ -104,23 +122,15 @@
}
JavaHprofConfig config;
config.ParseFromString(ds_config.java_hprof_config_raw());
- DataSource ds;
- ds.id = id;
- for (uint64_t pid : config.pid())
- ds.pids.emplace(static_cast<pid_t>(pid));
base::Optional<std::vector<std::string>> normalized_cmdlines =
NormalizeCmdlines(config.process_cmdline());
if (!normalized_cmdlines.has_value()) {
PERFETTO_ELOG("Rejecting data source due to invalid cmdline in config.");
return;
}
- FindPidsForCmdlines(normalized_cmdlines.value(), &ds.pids);
- if (config.min_anonymous_memory_kb() > 0)
- RemoveUnderAnonThreshold(config.min_anonymous_memory_kb(), &ds.pids);
-
- ds.config = std::move(config);
- ds.ds_config = std::move(ds_config);
- data_sources_.emplace(id, std::move(ds));
+ DataSource ds(ds_config, std::move(config), std::move(*normalized_cmdlines));
+ ds.CollectPids();
+ data_sources_.emplace(id, ds);
}
void JavaHprofProducer::StartDataSource(DataSourceInstanceID id,
@@ -131,7 +141,7 @@
return;
}
const DataSource& ds = it->second;
- const auto continuous_dump_config = ds.config.continuous_dump_config();
+ const auto& continuous_dump_config = ds.config().continuous_dump_config();
uint32_t dump_interval = continuous_dump_config.dump_interval_ms();
if (dump_interval) {
auto weak_producer = weak_factory_.GetWeakPtr();
@@ -143,7 +153,7 @@
},
continuous_dump_config.dump_phase_ms());
}
- SignalDataSource(ds);
+ ds.SendSignal();
}
void JavaHprofProducer::StopDataSource(DataSourceInstanceID id) {
diff --git a/src/profiling/memory/java_hprof_producer.h b/src/profiling/memory/java_hprof_producer.h
index 97ac07d..056c60a 100644
--- a/src/profiling/memory/java_hprof_producer.h
+++ b/src/profiling/memory/java_hprof_producer.h
@@ -69,11 +69,23 @@
kConnected,
};
- struct DataSource {
- DataSourceInstanceID id;
- std::set<pid_t> pids;
- JavaHprofConfig config;
- DataSourceConfig ds_config;
+ class DataSource {
+ public:
+ DataSource(DataSourceConfig ds_config,
+ JavaHprofConfig config,
+ std::vector<std::string> normalized_cmdlines);
+ void CollectPids();
+ void SendSignal() const;
+
+ const JavaHprofConfig& config() const { return config_; }
+ const DataSourceConfig& ds_config() const { return ds_config_; }
+
+ private:
+ DataSourceConfig ds_config_;
+ JavaHprofConfig config_;
+ std::vector<std::string> normalized_cmdlines_;
+
+ std::set<pid_t> pids_;
};
void ConnectService();
@@ -82,7 +94,6 @@
void IncreaseConnectionBackoff();
void DoContinuousDump(DataSourceInstanceID id, uint32_t dump_interval);
- static void SignalDataSource(const DataSource& ds);
// State of connection to the tracing service.
State state_ = kNotStarted;
diff --git a/src/profiling/perf/event_config.cc b/src/profiling/perf/event_config.cc
index 4195cc3..c45cb0b 100644
--- a/src/profiling/perf/event_config.cc
+++ b/src/profiling/perf/event_config.cc
@@ -183,6 +183,25 @@
}
}
+int32_t ToClockId(protos::gen::PerfEvents::PerfClock pb_enum) {
+ using protos::gen::PerfEvents;
+ switch (static_cast<int>(pb_enum)) { // cast to pacify -Wswitch-enum
+ case PerfEvents::PERF_CLOCK_REALTIME:
+ return CLOCK_REALTIME;
+ case PerfEvents::PERF_CLOCK_MONOTONIC:
+ return CLOCK_MONOTONIC;
+ case PerfEvents::PERF_CLOCK_MONOTONIC_RAW:
+ return CLOCK_MONOTONIC_RAW;
+ case PerfEvents::PERF_CLOCK_BOOTTIME:
+ return CLOCK_BOOTTIME;
+ // Default to a monotonic clock since it should be compatible with all types
+ // of events. Whereas boottime cannot be used with hardware events due to
+ // potential access within non-maskable interrupts.
+ default:
+ return CLOCK_MONOTONIC_RAW;
+ }
+}
+
} // namespace
// static
@@ -385,9 +404,7 @@
// What the samples will contain.
pe.sample_type = PERF_SAMPLE_TID | PERF_SAMPLE_TIME | PERF_SAMPLE_READ;
// PERF_SAMPLE_TIME:
- // We used to use CLOCK_BOOTTIME, but that is not nmi-safe, and therefore
- // works only for software events.
- pe.clockid = CLOCK_MONOTONIC_RAW;
+ pe.clockid = ToClockId(pb_config.timebase().timestamp_clock());
pe.use_clockid = true;
if (sample_callstacks) {
diff --git a/src/profiling/perf/event_config_unittest.cc b/src/profiling/perf/event_config_unittest.cc
index 30520f1..801d8ea 100644
--- a/src/profiling/perf/event_config_unittest.cc
+++ b/src/profiling/perf/event_config_unittest.cc
@@ -18,6 +18,7 @@
#include <linux/perf_event.h>
#include <stdint.h>
+#include <time.h>
#include "perfetto/base/logging.h"
#include "perfetto/ext/base/optional.h"
@@ -328,6 +329,40 @@
}
}
+TEST(EventConfigTest, TimestampClockId) {
+ { // if unset, a default is used
+ protos::gen::PerfEventConfig cfg;
+ base::Optional<EventConfig> event_config =
+ EventConfig::Create(AsDataSourceConfig(cfg));
+
+ ASSERT_TRUE(event_config.has_value());
+ EXPECT_TRUE(event_config->perf_attr()->use_clockid);
+ EXPECT_EQ(event_config->perf_attr()->clockid, CLOCK_MONOTONIC_RAW);
+ }
+ { // explicit boottime
+ protos::gen::PerfEventConfig cfg;
+ cfg.mutable_timebase()->set_timestamp_clock(
+ protos::gen::PerfEvents::PERF_CLOCK_BOOTTIME);
+ base::Optional<EventConfig> event_config =
+ EventConfig::Create(AsDataSourceConfig(cfg));
+
+ ASSERT_TRUE(event_config.has_value());
+ EXPECT_TRUE(event_config->perf_attr()->use_clockid);
+ EXPECT_EQ(event_config->perf_attr()->clockid, CLOCK_BOOTTIME);
+ }
+ { // explicit monotonic
+ protos::gen::PerfEventConfig cfg;
+ cfg.mutable_timebase()->set_timestamp_clock(
+ protos::gen::PerfEvents::PERF_CLOCK_MONOTONIC);
+ base::Optional<EventConfig> event_config =
+ EventConfig::Create(AsDataSourceConfig(cfg));
+
+ ASSERT_TRUE(event_config.has_value());
+ EXPECT_TRUE(event_config->perf_attr()->use_clockid);
+ EXPECT_EQ(event_config->perf_attr()->clockid, CLOCK_MONOTONIC);
+ }
+}
+
} // namespace
} // namespace profiling
} // namespace perfetto
diff --git a/src/profiling/perf/perf_producer.cc b/src/profiling/perf/perf_producer.cc
index f9ad9e2..0497b9b 100644
--- a/src/profiling/perf/perf_producer.cc
+++ b/src/profiling/perf/perf_producer.cc
@@ -80,6 +80,23 @@
return static_cast<size_t>(sysconf(_SC_NPROCESSORS_CONF));
}
+int32_t ToBuiltinClock(int32_t clockid) {
+ switch (clockid) {
+ case CLOCK_REALTIME:
+ return protos::pbzero::BUILTIN_CLOCK_REALTIME;
+ case CLOCK_MONOTONIC:
+ return protos::pbzero::BUILTIN_CLOCK_MONOTONIC;
+ case CLOCK_MONOTONIC_RAW:
+ return protos::pbzero::BUILTIN_CLOCK_MONOTONIC_RAW;
+ case CLOCK_BOOTTIME:
+ return protos::pbzero::BUILTIN_CLOCK_BOOTTIME;
+ // Should never get invalid input here as otherwise the syscall itself
+ // would've failed earlier.
+ default:
+ return protos::pbzero::BUILTIN_CLOCK_UNKNOWN;
+ }
+}
+
TraceWriter::TracePacketHandle StartTracePacket(TraceWriter* trace_writer) {
auto packet = trace_writer->NewTracePacket();
packet->set_sequence_flags(
@@ -97,16 +114,16 @@
packet->set_sequence_flags(
protos::pbzero::TracePacket::SEQ_INCREMENTAL_STATE_CLEARED);
- // default packet timestamp clockid:
+ // default packet timestamp clock for the samples:
+ perf_event_attr* perf_attr = event_config.perf_attr();
auto* defaults = packet->set_trace_packet_defaults();
- defaults->set_timestamp_clock_id(protos::pbzero::BUILTIN_CLOCK_MONOTONIC_RAW);
- PERFETTO_DCHECK(event_config.perf_attr()->clockid == CLOCK_MONOTONIC_RAW);
+ int32_t builtin_clock = ToBuiltinClock(perf_attr->clockid);
+ defaults->set_timestamp_clock_id(static_cast<uint32_t>(builtin_clock));
auto* perf_defaults = defaults->set_perf_sample_defaults();
auto* timebase_pb = perf_defaults->set_timebase();
// frequency/period:
- perf_event_attr* perf_attr = event_config.perf_attr();
if (perf_attr->freq) {
timebase_pb->set_frequency(perf_attr->sample_freq);
} else {
@@ -141,6 +158,9 @@
if (!timebase.name.empty()) {
timebase_pb->set_name(timebase.name);
}
+
+ // Not setting timebase.timestamp_clock since the field that matters during
+ // parsing is the root timestamp_clock_id set above.
}
uint32_t TimeToNextReadTickMs(DataSourceInstanceID ds_id, uint32_t period_ms) {
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index 65f84e6..4ce7cec 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -211,8 +211,6 @@
"importers/json/json_trace_parser.h",
"importers/json/json_trace_tokenizer.cc",
"importers/json/json_trace_tokenizer.h",
- "importers/json/json_tracker.cc",
- "importers/json/json_tracker.h",
"importers/proto/android_probes_module.cc",
"importers/proto/android_probes_module.h",
"importers/proto/android_probes_parser.cc",
@@ -462,7 +460,6 @@
if (enable_perfetto_trace_processor_json) {
sources += [
"importers/json/json_trace_tokenizer_unittest.cc",
- "importers/json/json_tracker_unittest.cc",
"importers/json/json_utils_unittest.cc",
]
deps += [ "../../gn:jsoncpp" ]
diff --git a/src/trace_processor/containers/string_pool.cc b/src/trace_processor/containers/string_pool.cc
index fd65195..0189054 100644
--- a/src/trace_processor/containers/string_pool.cc
+++ b/src/trace_processor/containers/string_pool.cc
@@ -53,8 +53,8 @@
StringPool::~StringPool() = default;
-StringPool::StringPool(StringPool&&) = default;
-StringPool& StringPool::operator=(StringPool&&) = default;
+StringPool::StringPool(StringPool&&) noexcept = default;
+StringPool& StringPool::operator=(StringPool&&) noexcept = default;
StringPool::Id StringPool::InsertString(base::StringView str, uint64_t hash) {
// Try and find enough space in the current block for the string and the
@@ -70,9 +70,8 @@
// new block to store the string.
if (str.size() + kMaxMetadataSize >= kMinLargeStringSizeBytes) {
return InsertLargeString(str, hash);
- } else {
- blocks_.emplace_back(kBlockSizeBytes);
}
+ blocks_.emplace_back(kBlockSizeBytes);
// Try and reserve space again - this time we should definitely succeed.
std::tie(success, offset) = blocks_.back().TryInsert(str);
@@ -82,7 +81,11 @@
// Compute the id from the block index and offset and add a mapping from the
// hash to the id.
Id string_id = Id::BlockString(blocks_.size() - 1, offset);
- string_index_.emplace(hash, string_id);
+
+ // Deliberately not adding |string_id| to |string_index_|. The caller
+ // (InternString()) must take care of this.
+ PERFETTO_DCHECK(string_index_.Find(hash));
+
return string_id;
}
@@ -91,7 +94,11 @@
large_strings_.emplace_back(new std::string(str.begin(), str.size()));
// Compute id from the index and add a mapping from the hash to the id.
Id string_id = Id::LargeString(large_strings_.size() - 1);
- string_index_.emplace(hash, string_id);
+
+ // Deliberately not adding |string_id| to |string_index_|. The caller
+ // (InternString()) must take care of this.
+ PERFETTO_DCHECK(string_index_.Find(hash));
+
return string_id;
}
diff --git a/src/trace_processor/containers/string_pool.h b/src/trace_processor/containers/string_pool.h
index 480c085..8d5e22f 100644
--- a/src/trace_processor/containers/string_pool.h
+++ b/src/trace_processor/containers/string_pool.h
@@ -21,9 +21,9 @@
#include <stdint.h>
#include <limits>
-#include <unordered_map>
#include <vector>
+#include "perfetto/ext/base/flat_hash_map.h"
#include "perfetto/ext/base/hash.h"
#include "perfetto/ext/base/optional.h"
#include "perfetto/ext/base/paged_memory.h"
@@ -107,8 +107,8 @@
~StringPool();
// Allow std::move().
- StringPool(StringPool&&);
- StringPool& operator=(StringPool&&);
+ StringPool(StringPool&&) noexcept;
+ StringPool& operator=(StringPool&&) noexcept;
// Disable implicit copy.
StringPool(const StringPool&) = delete;
@@ -119,12 +119,17 @@
return Id::Null();
auto hash = str.Hash();
- auto id_it = string_index_.find(hash);
- if (id_it != string_index_.end()) {
- PERFETTO_DCHECK(Get(id_it->second) == str);
- return id_it->second;
+
+ // Perform a hashtable insertion with a null ID just to check if the string
+ // is already inserted. If it's not, overwrite 0 with the actual Id.
+ auto it_and_inserted = string_index_.Insert(hash, Id());
+ Id* id = it_and_inserted.first;
+ if (!it_and_inserted.second) {
+ PERFETTO_DCHECK(Get(*id) == str);
+ return *id;
}
- return InsertString(str, hash);
+ *id = InsertString(str, hash);
+ return *id;
}
base::Optional<Id> GetId(base::StringView str) const {
@@ -132,10 +137,10 @@
return Id::Null();
auto hash = str.Hash();
- auto id_it = string_index_.find(hash);
- if (id_it != string_index_.end()) {
- PERFETTO_DCHECK(Get(id_it->second) == str);
- return id_it->second;
+ Id* id = string_index_.Find(hash);
+ if (id) {
+ PERFETTO_DCHECK(Get(*id) == str);
+ return *id;
}
return base::nullopt;
}
@@ -287,10 +292,12 @@
std::vector<std::unique_ptr<std::string>> large_strings_;
// Maps hashes of strings to the Id in the string pool.
- // TODO(lalitm): At some point we should benchmark just using a static
- // hashtable of 1M elements, we can afford paying a fixed 8MB here
- std::unordered_map<StringHash, Id, base::AlreadyHashed<StringHash>>
- string_index_;
+ base::FlatHashMap<StringHash,
+ Id,
+ base::AlreadyHashed<StringHash>,
+ base::LinearProbe,
+ /*AppendOnly=*/true>
+ string_index_{/*initial_capacity=*/1024u * 1024u};
};
} // namespace trace_processor
diff --git a/src/trace_processor/dynamic/thread_state_generator.cc b/src/trace_processor/dynamic/thread_state_generator.cc
index 4d33870..74115cd 100644
--- a/src/trace_processor/dynamic/thread_state_generator.cc
+++ b/src/trace_processor/dynamic/thread_state_generator.cc
@@ -91,7 +91,7 @@
uint32_t sched_idx = 0;
uint32_t waking_idx = 0;
uint32_t blocked_idx = 0;
- std::unordered_map<UniqueTid, ThreadSchedInfo> state_map;
+ TidInfoMap state_map(/*initial_capacity=*/1024);
while (sched_idx < sched.row_count() || waking_idx < waking.row_count() ||
blocked_idx < sched_blocked_reason.row_count()) {
int64_t sched_ts = sched_idx < sched.row_count()
@@ -117,21 +117,21 @@
}
// At the end, go through and flush any remaining pending events.
- for (const auto& utid_to_pending_info : state_map) {
- UniqueTid utid = utid_to_pending_info.first;
- const ThreadSchedInfo& pending_info = utid_to_pending_info.second;
+ for (auto it = state_map.GetIterator(); it; ++it) {
+ // for (const auto& utid_to_pending_info : state_map) {
+ UniqueTid utid = it.key();
+ const ThreadSchedInfo& pending_info = it.value();
FlushPendingEventsForThread(utid, pending_info, table.get(), base::nullopt);
}
return table;
}
-void ThreadStateGenerator::AddSchedEvent(
- const Table& sched,
- uint32_t sched_idx,
- std::unordered_map<UniqueTid, ThreadSchedInfo>& state_map,
- int64_t trace_end_ts,
- tables::ThreadStateTable* table) {
+void ThreadStateGenerator::AddSchedEvent(const Table& sched,
+ uint32_t sched_idx,
+ TidInfoMap& state_map,
+ int64_t trace_end_ts,
+ tables::ThreadStateTable* table) {
int64_t ts = sched.GetTypedColumnByName<int64_t>("ts")[sched_idx];
UniqueTid utid = sched.GetTypedColumnByName<uint32_t>("utid")[sched_idx];
ThreadSchedInfo* info = &state_map[utid];
@@ -201,10 +201,9 @@
info->scheduled_row = id_and_row.row;
}
-void ThreadStateGenerator::AddWakingEvent(
- const Table& waking,
- uint32_t waking_idx,
- std::unordered_map<UniqueTid, ThreadSchedInfo>& state_map) {
+void ThreadStateGenerator::AddWakingEvent(const Table& waking,
+ uint32_t waking_idx,
+ TidInfoMap& state_map) {
int64_t ts = waking.GetTypedColumnByName<int64_t>("ts")[waking_idx];
UniqueTid utid = static_cast<UniqueTid>(
waking.GetTypedColumnByName<int64_t>("ref")[waking_idx]);
@@ -299,10 +298,9 @@
}
}
-void ThreadStateGenerator::AddBlockedReasonEvent(
- const Table& blocked_reason,
- uint32_t blocked_idx,
- std::unordered_map<UniqueTid, ThreadSchedInfo>& state_map) {
+void ThreadStateGenerator::AddBlockedReasonEvent(const Table& blocked_reason,
+ uint32_t blocked_idx,
+ TidInfoMap& state_map) {
const auto& utid_col = blocked_reason.GetTypedColumnByName<int64_t>("ref");
const auto& arg_set_id_col =
blocked_reason.GetTypedColumnByName<uint32_t>("arg_set_id");
diff --git a/src/trace_processor/dynamic/thread_state_generator.h b/src/trace_processor/dynamic/thread_state_generator.h
index 75895ba..439d4bb 100644
--- a/src/trace_processor/dynamic/thread_state_generator.h
+++ b/src/trace_processor/dynamic/thread_state_generator.h
@@ -19,6 +19,7 @@
#include "src/trace_processor/sqlite/db_sqlite_table.h"
+#include "perfetto/ext/base/flat_hash_map.h"
#include "src/trace_processor/storage/trace_storage.h"
namespace perfetto {
@@ -54,22 +55,25 @@
base::Optional<int64_t> runnable_ts;
base::Optional<StringId> blocked_function;
};
+ using TidInfoMap = base::FlatHashMap<UniqueTid,
+ ThreadSchedInfo,
+ base::AlreadyHashed<UniqueTid>,
+ base::QuadraticProbe,
+ /*AppendOnly=*/true>;
void AddSchedEvent(const Table& sched,
uint32_t sched_idx,
- std::unordered_map<UniqueTid, ThreadSchedInfo>& state_map,
+ TidInfoMap& state_map,
int64_t trace_end_ts,
tables::ThreadStateTable* table);
- void AddWakingEvent(
- const Table& wakeup,
- uint32_t wakeup_idx,
- std::unordered_map<UniqueTid, ThreadSchedInfo>& state_map);
+ void AddWakingEvent(const Table& wakeup,
+ uint32_t wakeup_idx,
+ TidInfoMap& state_map);
- void AddBlockedReasonEvent(
- const Table& blocked_reason,
- uint32_t blocked_idx,
- std::unordered_map<UniqueTid, ThreadSchedInfo>& state_map);
+ void AddBlockedReasonEvent(const Table& blocked_reason,
+ uint32_t blocked_idx,
+ TidInfoMap& state_map);
void FlushPendingEventsForThread(UniqueTid utid,
const ThreadSchedInfo&,
diff --git a/src/trace_processor/forwarding_trace_parser.cc b/src/trace_processor/forwarding_trace_parser.cc
index b983d7b..619a9e6 100644
--- a/src/trace_processor/forwarding_trace_parser.cc
+++ b/src/trace_processor/forwarding_trace_parser.cc
@@ -158,7 +158,7 @@
if (size == 0)
return kUnknownTraceType;
std::string start(reinterpret_cast<const char*>(data),
- std::min<size_t>(size, 32));
+ std::min<size_t>(size, kGuessTraceMaxLookahead));
if (size >= 8) {
uint64_t first_word;
memcpy(&first_word, data, sizeof(first_word));
@@ -180,10 +180,16 @@
base::StartsWith(start, "<html>"))
return kSystraceTraceType;
- // Ctrace is deflate'ed systrace.
- if (base::Contains(start, "TRACE:"))
+ // Traces obtained from atrace -z (compress).
+ // They all have the string "TRACE:" followed by 78 9C which is a zlib header
+ // for "deflate, default compression, window size=32K" (see b/208691037)
+ if (base::Contains(start, "TRACE:\n\x78\x9c"))
return kCtraceTraceType;
+ // Traces obtained from atrace without -z (no compression).
+ if (base::Contains(start, "TRACE:\n"))
+ return kSystraceTraceType;
+
// Ninja's buils log (.ninja_log).
if (base::StartsWith(start, "# ninja log"))
return kNinjaLogTraceType;
diff --git a/src/trace_processor/forwarding_trace_parser.h b/src/trace_processor/forwarding_trace_parser.h
index a5164cf..566b3b8 100644
--- a/src/trace_processor/forwarding_trace_parser.h
+++ b/src/trace_processor/forwarding_trace_parser.h
@@ -24,6 +24,8 @@
namespace perfetto {
namespace trace_processor {
+constexpr size_t kGuessTraceMaxLookahead = 64;
+
enum TraceType {
kUnknownTraceType,
kProtoTraceType,
diff --git a/src/trace_processor/importers/common/args_tracker.cc b/src/trace_processor/importers/common/args_tracker.cc
index c7a5b97..1b6e0e9 100644
--- a/src/trace_processor/importers/common/args_tracker.cc
+++ b/src/trace_processor/importers/common/args_tracker.cc
@@ -79,7 +79,7 @@
}
ArgSetId set_id =
- context_->global_args_tracker->AddArgSet(args_, i, next_rid_idx);
+ context_->global_args_tracker->AddArgSet(&args_[0], i, next_rid_idx);
column->Set(row, SqlValue::Long(set_id));
i = next_rid_idx;
@@ -96,9 +96,9 @@
ArgsTracker::BoundInserter::~BoundInserter() {}
-ArgsTracker::BoundInserter::BoundInserter(BoundInserter&&) = default;
+ArgsTracker::BoundInserter::BoundInserter(BoundInserter&&) noexcept = default;
ArgsTracker::BoundInserter& ArgsTracker::BoundInserter::operator=(
- BoundInserter&&) = default;
+ BoundInserter&&) noexcept = default;
} // namespace trace_processor
} // namespace perfetto
diff --git a/src/trace_processor/importers/common/args_tracker.h b/src/trace_processor/importers/common/args_tracker.h
index f891537..9452b95 100644
--- a/src/trace_processor/importers/common/args_tracker.h
+++ b/src/trace_processor/importers/common/args_tracker.h
@@ -17,6 +17,7 @@
#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_ARGS_TRACKER_H_
#define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_ARGS_TRACKER_H_
+#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/types/trace_processor_context.h"
@@ -38,8 +39,8 @@
public:
virtual ~BoundInserter();
- BoundInserter(BoundInserter&&);
- BoundInserter& operator=(BoundInserter&&);
+ BoundInserter(BoundInserter&&) noexcept;
+ BoundInserter& operator=(BoundInserter&&) noexcept;
BoundInserter(const BoundInserter&) = delete;
BoundInserter& operator=(const BoundInserter&) = delete;
@@ -160,7 +161,7 @@
Variadic,
UpdatePolicy);
- std::vector<GlobalArgsTracker::Arg> args_;
+ base::SmallVector<GlobalArgsTracker::Arg, 16> args_;
TraceProcessorContext* const context_;
using ArrayKeyTuple =
diff --git a/src/trace_processor/importers/common/flow_tracker.cc b/src/trace_processor/importers/common/flow_tracker.cc
index 9e6e2d8..6e7dd2f 100644
--- a/src/trace_processor/importers/common/flow_tracker.cc
+++ b/src/trace_processor/importers/common/flow_tracker.cc
@@ -46,11 +46,11 @@
context_->storage->IncrementStats(stats::flow_no_enclosing_slice);
return;
}
- if (flow_to_slice_map_.count(flow_id) != 0) {
+ auto it_and_ins = flow_to_slice_map_.Insert(flow_id, open_slice_id.value());
+ if (!it_and_ins.second) {
context_->storage->IncrementStats(stats::flow_duplicate_id);
return;
}
- flow_to_slice_map_[flow_id] = open_slice_id.value();
}
void FlowTracker::Step(TrackId track_id, FlowId flow_id) {
@@ -60,13 +60,14 @@
context_->storage->IncrementStats(stats::flow_no_enclosing_slice);
return;
}
- if (flow_to_slice_map_.count(flow_id) == 0) {
+ auto* it = flow_to_slice_map_.Find(flow_id);
+ if (!it) {
context_->storage->IncrementStats(stats::flow_step_without_start);
return;
}
- SliceId slice_out_id = flow_to_slice_map_[flow_id];
+ SliceId slice_out_id = *it;
InsertFlow(flow_id, slice_out_id, open_slice_id.value());
- flow_to_slice_map_[flow_id] = open_slice_id.value();
+ *it = open_slice_id.value();
}
void FlowTracker::End(TrackId track_id,
@@ -83,29 +84,28 @@
context_->storage->IncrementStats(stats::flow_no_enclosing_slice);
return;
}
- if (flow_to_slice_map_.count(flow_id) == 0) {
+ auto* it = flow_to_slice_map_.Find(flow_id);
+ if (!it) {
context_->storage->IncrementStats(stats::flow_end_without_start);
return;
}
- SliceId slice_out_id = flow_to_slice_map_[flow_id];
- if (close_flow) {
- flow_to_slice_map_.erase(flow_to_slice_map_.find(flow_id));
- }
+ SliceId slice_out_id = *it;
+ if (close_flow)
+ flow_to_slice_map_.Erase(flow_id);
InsertFlow(flow_id, slice_out_id, open_slice_id.value());
}
bool FlowTracker::IsActive(FlowId flow_id) const {
- return flow_to_slice_map_.find(flow_id) != flow_to_slice_map_.end();
+ return flow_to_slice_map_.Find(flow_id) != nullptr;
}
FlowId FlowTracker::GetFlowIdForV1Event(uint64_t source_id,
StringId cat,
StringId name) {
V1FlowId v1_flow_id = {source_id, cat, name};
- auto iter = v1_flow_id_to_flow_id_map_.find(v1_flow_id);
- if (iter != v1_flow_id_to_flow_id_map_.end()) {
- return iter->second;
- }
+ auto* iter = v1_flow_id_to_flow_id_map_.Find(v1_flow_id);
+ if (iter)
+ return *iter;
FlowId new_id = v1_id_counter_++;
flow_id_to_v1_flow_id_map_[new_id] = v1_flow_id;
v1_flow_id_to_flow_id_map_[v1_flow_id] = new_id;
@@ -114,16 +114,16 @@
void FlowTracker::ClosePendingEventsOnTrack(TrackId track_id,
SliceId slice_id) {
- auto iter = pending_flow_ids_map_.find(track_id);
- if (iter == pending_flow_ids_map_.end())
+ auto* iter = pending_flow_ids_map_.Find(track_id);
+ if (!iter)
return;
- for (FlowId flow_id : iter->second) {
+ for (FlowId flow_id : *iter) {
SliceId slice_out_id = flow_to_slice_map_[flow_id];
InsertFlow(flow_id, slice_out_id, slice_id);
}
- pending_flow_ids_map_.erase(iter);
+ pending_flow_ids_map_.Erase(track_id);
}
void FlowTracker::InsertFlow(FlowId flow_id,
@@ -132,13 +132,13 @@
tables::FlowTable::Row row(slice_out_id, slice_in_id, kInvalidArgSetId);
auto id = context_->storage->mutable_flow_table()->Insert(row).id;
- auto it = flow_id_to_v1_flow_id_map_.find(flow_id);
- if (it != flow_id_to_v1_flow_id_map_.end()) {
+ auto* it = flow_id_to_v1_flow_id_map_.Find(flow_id);
+ if (it) {
// TODO(b/168007725): Add any args from v1 flow events and also export them.
auto args_tracker = ArgsTracker(context_);
auto inserter = context_->args_tracker->AddArgsTo(id);
- inserter.AddArg(name_key_id_, Variadic::String(it->second.name));
- inserter.AddArg(cat_key_id_, Variadic::String(it->second.cat));
+ inserter.AddArg(name_key_id_, Variadic::String(it->name));
+ inserter.AddArg(cat_key_id_, Variadic::String(it->cat));
context_->args_tracker->Flush();
}
}
diff --git a/src/trace_processor/importers/common/flow_tracker.h b/src/trace_processor/importers/common/flow_tracker.h
index f05d854..5def9b6 100644
--- a/src/trace_processor/importers/common/flow_tracker.h
+++ b/src/trace_processor/importers/common/flow_tracker.h
@@ -19,6 +19,7 @@
#include <stdint.h>
+#include "perfetto/ext/base/flat_hash_map.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"
@@ -78,11 +79,11 @@
}
};
- using FlowToSourceSliceMap = std::unordered_map<FlowId, SliceId>;
- using PendingFlowsMap = std::unordered_map<TrackId, std::vector<FlowId>>;
+ using FlowToSourceSliceMap = base::FlatHashMap<FlowId, SliceId>;
+ using PendingFlowsMap = base::FlatHashMap<TrackId, std::vector<FlowId>>;
using V1FlowIdToFlowIdMap =
- std::unordered_map<V1FlowId, FlowId, V1FlowIdHasher>;
- using FlowIdToV1FlowId = std::unordered_map<FlowId, V1FlowId>;
+ base::FlatHashMap<V1FlowId, FlowId, V1FlowIdHasher>;
+ using FlowIdToV1FlowId = base::FlatHashMap<FlowId, V1FlowId>;
void InsertFlow(FlowId flow_id,
SliceId outgoing_slice_id,
diff --git a/src/trace_processor/importers/common/global_args_tracker.h b/src/trace_processor/importers/common/global_args_tracker.h
index ee1867e..0d1c365 100644
--- a/src/trace_processor/importers/common/global_args_tracker.h
+++ b/src/trace_processor/importers/common/global_args_tracker.h
@@ -17,7 +17,9 @@
#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_GLOBAL_ARGS_TRACKER_H_
#define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_GLOBAL_ARGS_TRACKER_H_
+#include "perfetto/ext/base/flat_hash_map.h"
#include "perfetto/ext/base/hash.h"
+#include "perfetto/ext/base/small_vector.h"
#include "src/trace_processor/storage/trace_storage.h"
#include "src/trace_processor/types/trace_processor_context.h"
#include "src/trace_processor/types/variadic.h"
@@ -82,14 +84,11 @@
}
};
- GlobalArgsTracker(TraceProcessorContext* context);
+ explicit GlobalArgsTracker(TraceProcessorContext* context);
// Assumes that the interval [begin, end) of |args| is sorted by keys.
- ArgSetId AddArgSet(const std::vector<Arg>& args,
- uint32_t begin,
- uint32_t end) {
- std::vector<uint32_t> valid_indexes;
- valid_indexes.reserve(end - begin);
+ ArgSetId AddArgSet(const Arg* args, uint32_t begin, uint32_t end) {
+ base::SmallVector<uint32_t, 64> valid_indexes;
// TODO(eseckler): Also detect "invalid" key combinations in args sets (e.g.
// "foo" and "foo.bar" in the same arg set)?
@@ -107,7 +106,7 @@
}
}
- valid_indexes.push_back(i);
+ valid_indexes.emplace_back(i);
}
base::Hash hash;
@@ -118,13 +117,16 @@
auto* arg_table = context_->storage->mutable_arg_table();
ArgSetHash digest = hash.digest();
- auto it = arg_row_for_hash_.find(digest);
- if (it != arg_row_for_hash_.end())
- return arg_table->arg_set_id()[it->second];
+ auto it_and_inserted =
+ arg_row_for_hash_.Insert(digest, arg_table->row_count());
+ if (!it_and_inserted.second) {
+ // Already inserted.
+ return arg_table->arg_set_id()[*it_and_inserted.first];
+ }
- // The +1 ensures that nothing has an id == kInvalidArgSetId == 0.
- ArgSetId id = static_cast<uint32_t>(arg_row_for_hash_.size()) + 1;
- arg_row_for_hash_.emplace(digest, arg_table->row_count());
+ // Taking size() after the Insert() ensures that nothing has an id == 0
+ // (0 == kInvalidArgSetId).
+ ArgSetId id = static_cast<uint32_t>(arg_row_for_hash_.size());
for (uint32_t i : valid_indexes) {
const auto& arg = args[i];
@@ -163,10 +165,17 @@
return id;
}
+ // Exposed for making tests easier to write.
+ ArgSetId AddArgSet(const std::vector<Arg>& args,
+ uint32_t begin,
+ uint32_t end) {
+ return AddArgSet(args.data(), begin, end);
+ }
+
private:
using ArgSetHash = uint64_t;
- std::unordered_map<ArgSetHash, uint32_t, base::AlreadyHashed<ArgSetHash>>
+ base::FlatHashMap<ArgSetHash, uint32_t, base::AlreadyHashed<ArgSetHash>>
arg_row_for_hash_;
TraceProcessorContext* context_;
diff --git a/src/trace_processor/importers/common/process_tracker.cc b/src/trace_processor/importers/common/process_tracker.cc
index 0981063..47b1d7a 100644
--- a/src/trace_processor/importers/common/process_tracker.cc
+++ b/src/trace_processor/importers/common/process_tracker.cc
@@ -89,7 +89,7 @@
// of the process, we should also finish the process itself.
PERFETTO_DCHECK(thread_table->is_main_thread()[utid].value());
process_table->mutable_end_ts()->Set(*opt_upid, timestamp);
- pids_.erase(tid);
+ pids_.Erase(tid);
}
base::Optional<UniqueTid> ProcessTracker::GetThreadOrNull(uint32_t tid) {
@@ -156,8 +156,8 @@
// If the process has been replaced in |pids_|, this thread is dead.
uint32_t current_pid = processes->pid()[current_upid];
- auto pid_it = pids_.find(current_pid);
- if (pid_it != pids_.end() && pid_it->second != current_upid)
+ auto pid_it = pids_.Find(current_pid);
+ if (pid_it && *pid_it != current_upid)
return false;
return true;
@@ -169,13 +169,13 @@
auto* threads = context_->storage->mutable_thread_table();
auto* processes = context_->storage->mutable_process_table();
- auto vector_it = tids_.find(tid);
- if (vector_it == tids_.end())
+ auto vector_it = tids_.Find(tid);
+ if (!vector_it)
return base::nullopt;
// Iterate backwards through the threads so ones later in the trace are more
// likely to be picked.
- const auto& vector = vector_it->second;
+ const auto& vector = *vector_it;
for (auto it = vector.rbegin(); it != vector.rend(); it++) {
UniqueTid current_utid = *it;
@@ -225,7 +225,7 @@
uint32_t pid,
StringId main_thread_name,
ThreadNamePriority priority) {
- pids_.erase(pid);
+ pids_.Erase(pid);
// TODO(eseckler): Consider erasing all old entries in |tids_| that match the
// |pid| (those would be for an older process with the same pid). Right now,
// we keep them in |tids_| (if they weren't erased by EndThread()), but ignore
@@ -325,18 +325,20 @@
UniquePid ProcessTracker::GetOrCreateProcess(uint32_t pid) {
auto* process_table = context_->storage->mutable_process_table();
- auto it = pids_.find(pid);
- if (it != pids_.end()) {
+
+ // If the insertion succeeds, we'll fill the upid below.
+ auto it_and_ins = pids_.Insert(pid, UniquePid{0});
+ if (!it_and_ins.second) {
// Ensure that the process has not ended.
- PERFETTO_DCHECK(!process_table->end_ts()[it->second].has_value());
- return it->second;
+ PERFETTO_DCHECK(!process_table->end_ts()[*it_and_ins.first].has_value());
+ return *it_and_ins.first;
}
tables::ProcessTable::Row row;
row.pid = pid;
UniquePid upid = process_table->Insert(row).row;
- pids_.emplace(pid, upid);
+ *it_and_ins.first = upid; // Update the newly inserted hashmap entry.
// Create an entry for the main thread.
// We cannot call StartNewThread() here, because threads for this process
@@ -459,8 +461,8 @@
void ProcessTracker::SetPidZeroIgnoredForIdleProcess() {
// Create a mapping from (t|p)id 0 -> u(t|p)id 0 for the idle process.
- tids_.emplace(0, std::vector<UniqueTid>{0});
- pids_.emplace(0, 0);
+ tids_.Insert(0, std::vector<UniqueTid>{0});
+ pids_.Insert(0, 0);
auto swapper_id = context_->storage->InternString("swapper");
UpdateThreadName(0, swapper_id, ThreadNamePriority::kTraceProcessorConstant);
diff --git a/src/trace_processor/importers/common/process_tracker.h b/src/trace_processor/importers/common/process_tracker.h
index 86c4339..7cfe89d 100644
--- a/src/trace_processor/importers/common/process_tracker.h
+++ b/src/trace_processor/importers/common/process_tracker.h
@@ -19,6 +19,7 @@
#include <tuple>
+#include "perfetto/ext/base/flat_hash_map.h"
#include "perfetto/ext/base/string_view.h"
#include "src/trace_processor/importers/common/args_tracker.h"
#include "src/trace_processor/storage/trace_storage.h"
@@ -132,8 +133,8 @@
// Returns the upid for a given pid.
base::Optional<UniquePid> UpidForPidForTesting(uint32_t pid) {
- auto it = pids_.find(pid);
- return it == pids_.end() ? base::nullopt : base::make_optional(it->second);
+ auto it = pids_.Find(pid);
+ return it ? base::make_optional(*it) : base::nullopt;
}
// Returns the bounds of a range that includes all UniqueTids that have the
@@ -188,10 +189,10 @@
// simultaneously. This is no longer the case so this should be removed
// (though it seems like there are subtle things which break in Chrome if this
// changes).
- std::unordered_map<uint32_t /* tid */, std::vector<UniqueTid>> tids_;
+ base::FlatHashMap<uint32_t /* tid */, std::vector<UniqueTid>> tids_;
// Mapping of the most recently seen pid to the associated upid.
- std::unordered_map<uint32_t /* pid (aka tgid) */, UniquePid> pids_;
+ base::FlatHashMap<uint32_t /* pid (aka tgid) */, UniquePid> pids_;
// Pending thread associations. The meaning of a pair<ThreadA, ThreadB> in
// this vector is: we know that A and B belong to the same process, but we
diff --git a/src/trace_processor/importers/common/slice_tracker.cc b/src/trace_processor/importers/common/slice_tracker.cc
index dbb76a2..b160ec5 100644
--- a/src/trace_processor/importers/common/slice_tracker.cc
+++ b/src/trace_processor/importers/common/slice_tracker.cc
@@ -57,8 +57,8 @@
// Double check that if we've seen this track in the past, it was also
// marked as unnestable then.
#if PERFETTO_DCHECK_IS_ON()
- auto it = stacks_.find(row.track_id);
- PERFETTO_DCHECK(it == stacks_.end() || it->second.is_legacy_unnestable);
+ auto* it = stacks_.Find(row.track_id);
+ PERFETTO_DCHECK(!it || it->is_legacy_unnestable);
#endif
// Ensure that StartSlice knows that this track is unnestable.
@@ -98,11 +98,11 @@
StringId category,
StringId name,
SetArgsCallback args_callback) {
- auto it = stacks_.find(track_id);
- if (it == stacks_.end())
+ auto* it = stacks_.Find(track_id);
+ if (!it)
return base::nullopt;
- auto& stack = it->second.slice_stack;
+ auto& stack = it->slice_stack;
if (stack.empty())
return base::nullopt;
@@ -194,11 +194,11 @@
}
prev_timestamp_ = timestamp;
- auto it = stacks_.find(track_id);
- if (it == stacks_.end())
+ auto it = stacks_.Find(track_id);
+ if (!it)
return base::nullopt;
- TrackInfo& track_info = it->second;
+ TrackInfo& track_info = *it;
SlicesStack& stack = track_info.slice_stack;
MaybeCloseStack(timestamp, &stack, track_id);
if (stack.empty())
@@ -275,7 +275,7 @@
// TODO(eseckler): Reconsider whether we want to close pending slices by
// setting their duration to |trace_end - event_start|. Might still want some
// additional way of flagging these events as "incomplete" to the UI.
- stacks_.clear();
+ stacks_.Clear();
}
void SliceTracker::SetOnSliceBeginCallback(OnSliceBeginCallback callback) {
@@ -284,10 +284,10 @@
base::Optional<SliceId> SliceTracker::GetTopmostSliceOnTrack(
TrackId track_id) const {
- const auto iter = stacks_.find(track_id);
- if (iter == stacks_.end())
+ const auto* iter = stacks_.Find(track_id);
+ if (!iter)
return base::nullopt;
- const auto& stack = iter->second.slice_stack;
+ const auto& stack = iter->slice_stack;
if (stack.empty())
return base::nullopt;
uint32_t slice_idx = stack.back().row;
diff --git a/src/trace_processor/importers/common/slice_tracker.h b/src/trace_processor/importers/common/slice_tracker.h
index f574164..847bda6 100644
--- a/src/trace_processor/importers/common/slice_tracker.h
+++ b/src/trace_processor/importers/common/slice_tracker.h
@@ -19,6 +19,7 @@
#include <stdint.h>
+#include "perfetto/ext/base/flat_hash_map.h"
#include "src/trace_processor/importers/common/args_tracker.h"
#include "src/trace_processor/storage/trace_storage.h"
@@ -124,7 +125,7 @@
uint32_t legacy_unnestable_begin_count = 0;
int64_t legacy_unnestable_last_begin_ts = 0;
};
- using StackMap = std::unordered_map<TrackId, TrackInfo>;
+ using StackMap = base::FlatHashMap<TrackId, TrackInfo>;
// virtual for testing.
virtual base::Optional<SliceId> StartSlice(int64_t timestamp,
diff --git a/src/trace_processor/importers/ftrace/binder_tracker.cc b/src/trace_processor/importers/ftrace/binder_tracker.cc
index d866c0a..64eee90 100644
--- a/src/trace_processor/importers/ftrace/binder_tracker.cc
+++ b/src/trace_processor/importers/ftrace/binder_tracker.cc
@@ -15,14 +15,13 @@
*/
#include "src/trace_processor/importers/ftrace/binder_tracker.h"
+#include "perfetto/base/compiler.h"
+#include "perfetto/ext/base/string_utils.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/track_tracker.h"
#include "src/trace_processor/types/trace_processor_context.h"
-#include "perfetto/base/compiler.h"
-#include "perfetto/ext/base/string_utils.h"
-
namespace perfetto {
namespace trace_processor {
@@ -157,7 +156,8 @@
return;
}
- if (transaction_await_rcv.count(transaction_id) > 0) {
+ TrackId* rcv_track_id = transaction_await_rcv.Find(transaction_id);
+ if (rcv_track_id) {
// First begin the reply slice to get its slice id.
auto reply_slice_id = context_->slice_tracker->Begin(
ts, track_id, binder_category_id_, reply_id_);
@@ -171,9 +171,9 @@
Variadic::UnsignedInteger(reply_slice_id->value));
};
// Add the dest args to the current transaction slice and get the slice id.
- auto transaction_slice_id = context_->slice_tracker->AddArgs(
- transaction_await_rcv[transaction_id], binder_category_id_,
- transaction_slice_id_, args_inserter);
+ auto transaction_slice_id =
+ context_->slice_tracker->AddArgs(*rcv_track_id, binder_category_id_,
+ transaction_slice_id_, args_inserter);
// Add the dest slice id to the reply slice that has just begun.
auto reply_dest_inserter =
@@ -184,15 +184,15 @@
};
context_->slice_tracker->AddArgs(track_id, binder_category_id_, reply_id_,
reply_dest_inserter);
- transaction_await_rcv.erase(transaction_id);
+ transaction_await_rcv.Erase(transaction_id);
return;
}
- if (awaiting_async_rcv_.count(transaction_id) > 0) {
- auto args = awaiting_async_rcv_[transaction_id];
+ SetArgsCallback* args = awaiting_async_rcv_.Find(transaction_id);
+ if (args) {
context_->slice_tracker->Scoped(ts, track_id, binder_category_id_,
- async_rcv_id_, 0, args);
- awaiting_async_rcv_.erase(transaction_id);
+ async_rcv_id_, 0, *args);
+ awaiting_async_rcv_.Erase(transaction_id);
return;
}
}
@@ -209,7 +209,7 @@
void BinderTracker::Locked(int64_t ts, uint32_t pid) {
UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
- if (attempt_lock_.count(pid) == 0)
+ if (!attempt_lock_.Find(pid))
return;
TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
@@ -218,19 +218,19 @@
lock_held_id_);
lock_acquired_[pid] = ts;
- attempt_lock_.erase(pid);
+ attempt_lock_.Erase(pid);
}
void BinderTracker::Unlock(int64_t ts, uint32_t pid) {
UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
- if (lock_acquired_.count(pid) == 0)
+ if (!lock_acquired_.Find(pid))
return;
TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
context_->slice_tracker->End(ts, track_id, binder_category_id_,
lock_held_id_);
- lock_acquired_.erase(pid);
+ lock_acquired_.Erase(pid);
}
void BinderTracker::TransactionAllocBuf(int64_t ts,
diff --git a/src/trace_processor/importers/ftrace/binder_tracker.h b/src/trace_processor/importers/ftrace/binder_tracker.h
index a96816d..a9482de 100644
--- a/src/trace_processor/importers/ftrace/binder_tracker.h
+++ b/src/trace_processor/importers/ftrace/binder_tracker.h
@@ -18,9 +18,9 @@
#define SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_BINDER_TRACKER_H_
#include <stdint.h>
-#include <unordered_map>
-#include <unordered_set>
+#include "perfetto/base/flat_set.h"
+#include "perfetto/ext/base/flat_hash_map.h"
#include "src/trace_processor/importers/common/args_tracker.h"
#include "src/trace_processor/storage/trace_storage.h"
#include "src/trace_processor/types/destructible.h"
@@ -68,14 +68,14 @@
private:
TraceProcessorContext* const context_;
- std::unordered_set<int32_t> awaiting_rcv_for_reply_;
+ base::FlatSet<int32_t> awaiting_rcv_for_reply_;
- std::unordered_map<int32_t, TrackId> transaction_await_rcv;
- std::unordered_map<int32_t, SetArgsCallback> awaiting_async_rcv_;
+ base::FlatHashMap<int32_t, TrackId> transaction_await_rcv;
+ base::FlatHashMap<int32_t, SetArgsCallback> awaiting_async_rcv_;
- std::unordered_map<uint32_t, int64_t> attempt_lock_;
+ base::FlatHashMap<uint32_t, int64_t> attempt_lock_;
- std::unordered_map<uint32_t, int64_t> lock_acquired_;
+ base::FlatHashMap<uint32_t, int64_t> lock_acquired_;
const StringId binder_category_id_;
const StringId lock_waiting_id_;
diff --git a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
index 808d7bd..a6219d1 100644
--- a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
@@ -24,7 +24,7 @@
namespace trace_processor {
namespace {
-std::array<MessageDescriptor, 359> descriptors{{
+std::array<MessageDescriptor, 360> descriptors{{
{nullptr, 0, {}},
{nullptr, 0, {}},
{nullptr, 0, {}},
@@ -3843,6 +3843,17 @@
{"ib_quota", ProtoSchemaType::kUint64},
},
},
+ {
+ "rss_stat_throttled",
+ 4,
+ {
+ {},
+ {"curr", ProtoSchemaType::kUint32},
+ {"member", ProtoSchemaType::kInt32},
+ {"mm_id", ProtoSchemaType::kUint32},
+ {"size", ProtoSchemaType::kInt64},
+ },
+ },
}};
} // namespace
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index c25859e..155d0f5 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -145,7 +145,8 @@
sched_blocked_reason_id_(
context->storage->InternString("sched_blocked_reason")),
io_wait_id_(context->storage->InternString("io_wait")),
- function_id_(context->storage->InternString("function")) {
+ function_id_(context->storage->InternString("function")),
+ waker_utid_id_(context->storage->InternString("waker_utid")) {
// 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++) {
@@ -368,11 +369,11 @@
break;
}
case FtraceEvent::kSchedWakeupFieldNumber: {
- ParseSchedWakeup(ts, data);
+ ParseSchedWakeup(ts, pid, data);
break;
}
case FtraceEvent::kSchedWakingFieldNumber: {
- ParseSchedWaking(ts, data);
+ ParseSchedWaking(ts, pid, data);
break;
}
case FtraceEvent::kSchedProcessFreeFieldNumber: {
@@ -399,8 +400,9 @@
ParseZero(ts, pid, data);
break;
}
+ case FtraceEvent::kRssStatThrottledFieldNumber:
case FtraceEvent::kRssStatFieldNumber: {
- rss_stat_tracker_.ParseRssStat(ts, pid, data);
+ rss_stat_tracker_.ParseRssStat(ts, fld.id(), pid, data);
break;
}
case FtraceEvent::kIonHeapGrowFieldNumber: {
@@ -734,24 +736,36 @@
next_pid, ss.next_comm(), ss.next_prio());
}
-void FtraceParser::ParseSchedWakeup(int64_t timestamp, ConstBytes blob) {
+void FtraceParser::ParseSchedWakeup(int64_t timestamp,
+ uint32_t pid,
+ ConstBytes blob) {
protos::pbzero::SchedWakeupFtraceEvent::Decoder sw(blob.data, blob.size);
uint32_t wakee_pid = static_cast<uint32_t>(sw.pid());
StringId name_id = context_->storage->InternString(sw.comm());
- auto utid = context_->process_tracker->UpdateThreadName(
+ auto wakee_utid = context_->process_tracker->UpdateThreadName(
wakee_pid, name_id, ThreadNamePriority::kFtrace);
- context_->event_tracker->PushInstant(timestamp, sched_wakeup_name_id_, utid,
- RefType::kRefUtid);
+ InstantId id = context_->event_tracker->PushInstant(
+ timestamp, sched_wakeup_name_id_, wakee_utid, RefType::kRefUtid);
+
+ UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
+ context_->args_tracker->AddArgsTo(id).AddArg(waker_utid_id_,
+ Variadic::UnsignedInteger(utid));
}
-void FtraceParser::ParseSchedWaking(int64_t timestamp, ConstBytes blob) {
+void FtraceParser::ParseSchedWaking(int64_t timestamp,
+ uint32_t pid,
+ ConstBytes blob) {
protos::pbzero::SchedWakingFtraceEvent::Decoder sw(blob.data, blob.size);
uint32_t wakee_pid = static_cast<uint32_t>(sw.pid());
StringId name_id = context_->storage->InternString(sw.comm());
- auto utid = context_->process_tracker->UpdateThreadName(
+ auto wakee_utid = context_->process_tracker->UpdateThreadName(
wakee_pid, name_id, ThreadNamePriority::kFtrace);
- context_->event_tracker->PushInstant(timestamp, sched_waking_name_id_, utid,
- RefType::kRefUtid);
+ InstantId id = context_->event_tracker->PushInstant(
+ timestamp, sched_waking_name_id_, wakee_utid, RefType::kRefUtid);
+
+ UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
+ context_->args_tracker->AddArgsTo(id).AddArg(waker_utid_id_,
+ Variadic::UnsignedInteger(utid));
}
void FtraceParser::ParseSchedProcessFree(int64_t timestamp, ConstBytes blob) {
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.h b/src/trace_processor/importers/ftrace/ftrace_parser.h
index bf798a3..2f4cb0b 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.h
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.h
@@ -48,8 +48,8 @@
protozero::ConstBytes,
PacketSequenceStateGeneration*);
void ParseSchedSwitch(uint32_t cpu, int64_t timestamp, protozero::ConstBytes);
- void ParseSchedWakeup(int64_t timestamp, protozero::ConstBytes);
- void ParseSchedWaking(int64_t timestamp, protozero::ConstBytes);
+ void ParseSchedWakeup(int64_t timestamp, uint32_t pid, protozero::ConstBytes);
+ void ParseSchedWaking(int64_t timestamp, uint32_t pid, protozero::ConstBytes);
void ParseSchedProcessFree(int64_t timestamp, protozero::ConstBytes);
void ParseCpuFreq(int64_t timestamp, protozero::ConstBytes);
void ParseGpuFreq(int64_t timestamp, protozero::ConstBytes);
@@ -191,6 +191,7 @@
const StringId sched_blocked_reason_id_;
const StringId io_wait_id_;
const StringId function_id_;
+ const StringId waker_utid_id_;
struct FtraceMessageStrings {
// The string id of name of the event field (e.g. sched_switch's id).
diff --git a/src/trace_processor/importers/ftrace/rss_stat_tracker.cc b/src/trace_processor/importers/ftrace/rss_stat_tracker.cc
index 214bd75..80ca10f 100644
--- a/src/trace_processor/importers/ftrace/rss_stat_tracker.cc
+++ b/src/trace_processor/importers/ftrace/rss_stat_tracker.cc
@@ -20,11 +20,15 @@
#include "src/trace_processor/importers/common/process_tracker.h"
#include "src/trace_processor/types/trace_processor_context.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
#include "protos/perfetto/trace/ftrace/kmem.pbzero.h"
+#include "protos/perfetto/trace/ftrace/synthetic.pbzero.h"
namespace perfetto {
namespace trace_processor {
+using FtraceEvent = protos::pbzero::FtraceEvent;
+
RssStatTracker::RssStatTracker(TraceProcessorContext* context)
: context_(context) {
rss_members_.emplace_back(context->storage->InternString("mem.rss.file"));
@@ -37,19 +41,41 @@
context->storage->InternString("mem.unknown")); // Keep this last.
}
-void RssStatTracker::ParseRssStat(int64_t ts, uint32_t pid, ConstBytes blob) {
- protos::pbzero::RssStatFtraceEvent::Decoder rss(blob.data, blob.size);
- uint32_t member = static_cast<uint32_t>(rss.member());
- int64_t size = rss.size();
+void RssStatTracker::ParseRssStat(int64_t ts,
+ int32_t field_id,
+ uint32_t pid,
+ ConstBytes blob) {
+ uint32_t member;
+ int64_t size;
base::Optional<bool> curr;
base::Optional<int64_t> mm_id;
- if (rss.has_curr()) {
+
+ if (field_id == FtraceEvent::kRssStatFieldNumber) {
+ protos::pbzero::RssStatFtraceEvent::Decoder rss(blob.data, blob.size);
+
+ member = static_cast<uint32_t>(rss.member());
+ size = rss.size();
+ if (rss.has_curr()) {
+ curr = base::make_optional(static_cast<bool>(rss.curr()));
+ }
+ if (rss.has_mm_id()) {
+ mm_id = base::make_optional(rss.mm_id());
+ }
+
+ ParseRssStat(ts, pid, size, member, curr, mm_id);
+ } else if (field_id == FtraceEvent::kRssStatThrottledFieldNumber) {
+ protos::pbzero::RssStatThrottledFtraceEvent::Decoder rss(blob.data,
+ blob.size);
+
+ member = static_cast<uint32_t>(rss.member());
+ size = rss.size();
curr = base::make_optional(static_cast<bool>(rss.curr()));
- }
- if (rss.has_mm_id()) {
mm_id = base::make_optional(rss.mm_id());
+
+ ParseRssStat(ts, pid, size, member, curr, mm_id);
+ } else {
+ PERFETTO_DFATAL("Unexpected field id");
}
- ParseRssStat(ts, pid, size, member, curr, mm_id);
}
void RssStatTracker::ParseRssStat(int64_t ts,
@@ -97,32 +123,33 @@
// If curr is false, try and lookup the utid we previously saw for this
// mm id.
- auto it = mm_id_to_utid_.find(mm_id);
- if (it == mm_id_to_utid_.end())
+ auto* it = mm_id_to_utid_.Find(mm_id);
+ if (!it)
return base::nullopt;
// If the utid in the map is the same as our current utid but curr is false,
// that means we are in the middle of a process changing mm structs (i.e. in
// the middle of a vfork + exec). Therefore, we should discard the association
// of this vm struct with this thread.
- UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
- if (it->second == utid) {
- mm_id_to_utid_.erase(it);
+ const UniqueTid mm_utid = *it;
+ const UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
+ if (mm_utid == utid) {
+ mm_id_to_utid_.Erase(mm_id);
return base::nullopt;
}
// Verify that the utid in the map is still alive. This can happen if an mm
// struct we saw in the past is about to be reused after thread but we don't
// know the new process that struct will be associated with.
- if (!context_->process_tracker->IsThreadAlive(it->second)) {
- mm_id_to_utid_.erase(it);
+ if (!context_->process_tracker->IsThreadAlive(mm_utid)) {
+ mm_id_to_utid_.Erase(mm_id);
return base::nullopt;
}
// This case happens when a process is changing the VM of another process and
// we know that the utid corresponding to the target process. Just return that
// utid.
- return it->second;
+ return mm_utid;
}
} // namespace trace_processor
diff --git a/src/trace_processor/importers/ftrace/rss_stat_tracker.h b/src/trace_processor/importers/ftrace/rss_stat_tracker.h
index fff203c..b3ba29a 100644
--- a/src/trace_processor/importers/ftrace/rss_stat_tracker.h
+++ b/src/trace_processor/importers/ftrace/rss_stat_tracker.h
@@ -17,8 +17,7 @@
#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_RSS_STAT_TRACKER_H_
#define SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_RSS_STAT_TRACKER_H_
-#include <unordered_map>
-
+#include "perfetto/ext/base/flat_hash_map.h"
#include "perfetto/protozero/field.h"
#include "src/trace_processor/storage/trace_storage.h"
@@ -33,7 +32,10 @@
explicit RssStatTracker(TraceProcessorContext*);
- void ParseRssStat(int64_t ts, uint32_t pid, ConstBytes blob);
+ void ParseRssStat(int64_t ts,
+ int32_t field_id,
+ uint32_t pid,
+ ConstBytes blob);
void ParseRssStat(int64_t ts,
uint32_t pid,
int64_t size,
@@ -46,7 +48,7 @@
bool is_curr,
uint32_t pid);
- std::unordered_map<int64_t, UniqueTid> mm_id_to_utid_;
+ base::FlatHashMap<int64_t, UniqueTid> mm_id_to_utid_;
std::vector<StringId> rss_members_;
TraceProcessorContext* const context_;
};
diff --git a/src/trace_processor/importers/ftrace/sched_event_tracker.cc b/src/trace_processor/importers/ftrace/sched_event_tracker.cc
index 2fc7fc3..00fc93f 100644
--- a/src/trace_processor/importers/ftrace/sched_event_tracker.cc
+++ b/src/trace_processor/importers/ftrace/sched_event_tracker.cc
@@ -36,7 +36,8 @@
namespace trace_processor {
SchedEventTracker::SchedEventTracker(TraceProcessorContext* context)
- : context_(context) {
+ : waker_utid_id_(context->storage->InternString("waker_utid")),
+ context_(context) {
// pre-parse sched_switch
auto* switch_descriptor = GetMessageDescriptorForId(
protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber);
@@ -318,7 +319,11 @@
auto* instants = context_->storage->mutable_instant_table();
auto ref_type_id = context_->storage->InternString(
GetRefTypeStringMap()[static_cast<size_t>(RefType::kRefUtid)]);
- instants->Insert({ts, sched_waking_id_, wakee_utid, ref_type_id});
+ tables::InstantTable::Id id =
+ instants->Insert({ts, sched_waking_id_, wakee_utid, ref_type_id}).id;
+
+ context_->args_tracker->AddArgsTo(id).AddArg(
+ waker_utid_id_, Variadic::UnsignedInteger(curr_utid));
}
void SchedEventTracker::FlushPendingEvents() {
diff --git a/src/trace_processor/importers/ftrace/sched_event_tracker.h b/src/trace_processor/importers/ftrace/sched_event_tracker.h
index eae4bac..eff442c 100644
--- a/src/trace_processor/importers/ftrace/sched_event_tracker.h
+++ b/src/trace_processor/importers/ftrace/sched_event_tracker.h
@@ -129,6 +129,8 @@
std::array<StringId, kSchedWakingMaxFieldId + 1> sched_waking_field_ids_;
StringId sched_waking_id_;
+ StringId waker_utid_id_;
+
TraceProcessorContext* const context_;
};
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc
index d215b19..6628c41 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc
+++ b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc
@@ -17,7 +17,6 @@
#include "src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h"
#include <cinttypes>
-#include <unordered_map>
#include "perfetto/base/logging.h"
#include "perfetto/ext/base/string_view.h"
diff --git a/src/trace_processor/importers/json/json_trace_parser.cc b/src/trace_processor/importers/json/json_trace_parser.cc
index cc9f76f..91cac7a 100644
--- a/src/trace_processor/importers/json/json_trace_parser.cc
+++ b/src/trace_processor/importers/json/json_trace_parser.cc
@@ -30,7 +30,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_tracker.h"
-#include "src/trace_processor/importers/json/json_tracker.h"
#include "src/trace_processor/importers/json/json_utils.h"
#include "src/trace_processor/tables/slice_tables.h"
#include "src/trace_processor/types/trace_processor_context.h"
@@ -137,11 +136,9 @@
row.track_id = track_id;
row.category = cat_id;
row.name = name_id;
- row.thread_ts =
- JsonTracker::GetOrCreate(context_)->CoerceToTs(value["tts"]);
+ row.thread_ts = json::CoerceToTs(value["tts"]);
// tdur will only exist on 'X' events.
- row.thread_dur =
- JsonTracker::GetOrCreate(context_)->CoerceToTs(value["tdur"]);
+ row.thread_dur = json::CoerceToTs(value["tdur"]);
// JSON traces don't report these counters as part of slices.
row.thread_instruction_count = base::nullopt;
row.thread_instruction_delta = base::nullopt;
@@ -161,8 +158,7 @@
auto opt_slice_id = slice_tracker->End(timestamp, track_id, cat_id,
name_id, args_inserter);
// Now try to update thread_dur if we have a tts field.
- auto opt_tts =
- JsonTracker::GetOrCreate(context_)->CoerceToTs(value["tts"]);
+ auto opt_tts = json::CoerceToTs(value["tts"]);
if (opt_slice_id.has_value() && opt_tts) {
auto* thread_slice = storage->mutable_thread_slice_table();
auto maybe_row = thread_slice->id().IndexOf(*opt_slice_id);
@@ -176,8 +172,7 @@
break;
}
case 'X': { // TRACE_EVENT (scoped event).
- base::Optional<int64_t> opt_dur =
- JsonTracker::GetOrCreate(context_)->CoerceToTs(value["dur"]);
+ base::Optional<int64_t> opt_dur = json::CoerceToTs(value["dur"]);
if (!opt_dur.has_value())
return;
TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
diff --git a/src/trace_processor/importers/json/json_trace_parser.h b/src/trace_processor/importers/json/json_trace_parser.h
index 2084c4b..20d1e85 100644
--- a/src/trace_processor/importers/json/json_trace_parser.h
+++ b/src/trace_processor/importers/json/json_trace_parser.h
@@ -21,10 +21,8 @@
#include <memory>
#include <tuple>
-#include <unordered_map>
#include "src/trace_processor/importers/common/trace_parser.h"
-#include "src/trace_processor/importers/json/json_tracker.h"
#include "src/trace_processor/importers/systrace/systrace_line_parser.h"
#include "src/trace_processor/timestamped_trace_piece.h"
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer.cc b/src/trace_processor/importers/json/json_trace_tokenizer.cc
index bcfe02e..3677033 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer.cc
+++ b/src/trace_processor/importers/json/json_trace_tokenizer.cc
@@ -22,7 +22,6 @@
#include "perfetto/ext/base/string_utils.h"
#include "perfetto/trace_processor/trace_blob_view.h"
-#include "src/trace_processor/importers/json/json_tracker.h"
#include "src/trace_processor/importers/json/json_utils.h"
#include "src/trace_processor/storage/stats.h"
#include "src/trace_processor/trace_sorter.h"
@@ -364,54 +363,6 @@
return ReadSystemLineRes::kNeedsMoreData;
}
-base::Optional<json::TimeUnit> MaybeParseDisplayTimeUnit(
- base::StringView buffer) {
- size_t idx = buffer.find("\"displayTimeUnit\"");
- if (idx == base::StringView::npos)
- return base::nullopt;
-
- base::StringView time_unit = buffer.substr(idx);
- std::string key;
- const char* value_start = nullptr;
- ReadKeyRes res =
- ReadOneJsonKey(time_unit.data(), time_unit.end(), &key, &value_start);
-
- // If we didn't read the key successfully, it could have been that
- // displayTimeUnit is used somewhere else in the dict (maybe as a value?) so
- // just ignore it.
- if (res == ReadKeyRes::kEndOfDictionary || res == ReadKeyRes::kFatalError ||
- res == ReadKeyRes::kNeedsMoreData) {
- return base::nullopt;
- }
-
- // Double check that we did actually get the displayTimeUnit key.
- if (key != "displayTimeUnit")
- return base::nullopt;
-
- // If the value isn't a string, we can do little except bail.
- if (value_start[0] != '"')
- return base::nullopt;
-
- std::string value;
- const char* unused;
- ReadStringRes value_res =
- ReadOneJsonString(value_start + 1, time_unit.end(), &value, &unused);
-
- // If we didn't read the value successfully, we could be in another key
- // in a nested dictionary. Just ignore the error.
- if (value_res == ReadStringRes::kFatalError ||
- value_res == ReadStringRes::kNeedsMoreData) {
- return base::nullopt;
- }
-
- if (value == "ns") {
- return json::TimeUnit::kNs;
- } else if (value == "ms") {
- return json::TimeUnit::kMs;
- }
- return base::nullopt;
-}
-
JsonTraceTokenizer::JsonTraceTokenizer(TraceProcessorContext* ctx)
: context_(ctx) {}
JsonTraceTokenizer::~JsonTraceTokenizer() = default;
@@ -425,17 +376,6 @@
const char* end = buf + buffer_.size();
if (offset_ == 0) {
- // It's possible the displayTimeUnit key is at the end of the json
- // file so to be correct we ought to parse the whole file looking
- // for this key before parsing any events however this would require
- // two passes on the file so for now we only handle displayTimeUnit
- // correctly if it is at the beginning of the file.
- base::Optional<json::TimeUnit> timeunit =
- MaybeParseDisplayTimeUnit(base::StringView(buf, blob.size()));
- if (timeunit) {
- JsonTracker::GetOrCreate(context_)->SetTimeUnit(*timeunit);
- }
-
// Strip leading whitespace.
while (next != end && isspace(*next)) {
next++;
@@ -481,7 +421,6 @@
const char* end,
const char** out) {
PERFETTO_DCHECK(json::IsJsonSupported());
- JsonTracker* json_tracker = JsonTracker::GetOrCreate(context_);
auto* trace_sorter = context_->sorter.get();
const char* next = start;
@@ -513,19 +452,10 @@
return ParseInternal(next + 1, end, out);
} else if (key == "displayTimeUnit") {
std::string time_unit;
- auto string_res = ReadOneJsonString(next + 1, end, &time_unit, &next);
- if (string_res == ReadStringRes::kFatalError)
+ auto result = ReadOneJsonString(next + 1, end, &time_unit, &next);
+ if (result == ReadStringRes::kFatalError)
return util::ErrStatus("Could not parse displayTimeUnit");
- if (string_res == ReadStringRes::kNeedsMoreData)
- return util::ErrStatus("displayTimeUnit too large");
- if (time_unit != "ms" && time_unit != "ns")
- return util::ErrStatus("displayTimeUnit unknown");
- // If the displayTimeUnit came after the first chunk, the increment the
- // too late stat.
- if (offset_ != 0) {
- context_->storage->IncrementStats(
- stats::json_display_time_unit_too_late);
- }
+ context_->storage->IncrementStats(stats::json_display_time_unit);
return ParseInternal(next, end, out);
} else if (key == "otherData") {
base::StringView unparsed;
@@ -618,7 +548,7 @@
base::Optional<std::string> opt_raw_ts;
RETURN_IF_ERROR(ExtractValueForJsonKey(unparsed, "ts", &opt_raw_ts));
base::Optional<int64_t> opt_ts =
- opt_raw_ts ? json_tracker->CoerceToTs(*opt_raw_ts) : base::nullopt;
+ opt_raw_ts ? json::CoerceToTs(*opt_raw_ts) : base::nullopt;
int64_t ts = 0;
if (opt_ts.has_value()) {
ts = opt_ts.value();
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer.h b/src/trace_processor/importers/json/json_trace_tokenizer.h
index d25954d..8074b61 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer.h
+++ b/src/trace_processor/importers/json/json_trace_tokenizer.h
@@ -94,11 +94,6 @@
std::string* line,
const char** next);
-// Parses the "displayTimeUnit" key from the given trace buffer
-// and returns the associated time unit if one exists.
-base::Optional<json::TimeUnit> MaybeParseDisplayTimeUnit(
- base::StringView buffer);
-
// Reads a JSON trace in chunks and extracts top level json objects.
class JsonTraceTokenizer : public ChunkedTraceReader {
public:
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer_unittest.cc b/src/trace_processor/importers/json/json_trace_tokenizer_unittest.cc
index 6a11a44..051f52a 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer_unittest.cc
+++ b/src/trace_processor/importers/json/json_trace_tokenizer_unittest.cc
@@ -272,58 +272,6 @@
ASSERT_EQ(*line, R"({"ts": 149029, "foo": "bar"})");
}
-TEST(JsonTraceTokenizerTest, DisplayTimeUnit) {
- ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
- {
- }
- )"),
- base::nullopt);
- ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
- {
- "displayTimeUnit": 0
- }
- )"),
- base::nullopt);
- ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
- {
- "str": "displayTimeUnit"
- }
- )"),
- base::nullopt);
-
- ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
- {
- "traceEvents": [
- {
- "pid": 1, "tid": 1, "name": "test",
- "ts": 1, "dur": 1000, "ph": "X", "cat": "fee"
- }
- ],
- "displayTimeUnit": "ms"
- }
- )"),
- json::TimeUnit::kMs);
- ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
- {
- "traceEvents": [
- {
- "pid": 1, "tid": 1, "name": "test",
- "ts": 1, "dur": 1000, "ph": "X", "cat": "fee"
- }
- ],
- "displayTimeUnit": "ns"
- }
- )"),
- json::TimeUnit::kNs);
-
- ASSERT_EQ(MaybeParseDisplayTimeUnit(R"(
- {
- "displayTimeUnit":"ms"
- }
- )"),
- json::TimeUnit::kMs);
-}
-
} // namespace
} // namespace trace_processor
} // namespace perfetto
diff --git a/src/trace_processor/importers/json/json_tracker.h b/src/trace_processor/importers/json/json_tracker.h
deleted file mode 100644
index 167881e..0000000
--- a/src/trace_processor/importers/json/json_tracker.h
+++ /dev/null
@@ -1,61 +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_IMPORTERS_JSON_JSON_TRACKER_H_
-#define SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACKER_H_
-
-#include "src/trace_processor/importers/json/json_utils.h"
-#include "src/trace_processor/types/destructible.h"
-#include "src/trace_processor/types/trace_processor_context.h"
-
-namespace Json {
-class Value;
-}
-
-namespace perfetto {
-namespace trace_processor {
-
-class JsonTracker : public Destructible {
- public:
- JsonTracker(const JsonTracker&) = delete;
- JsonTracker& operator=(const JsonTracker&) = delete;
- explicit JsonTracker(TraceProcessorContext*);
- ~JsonTracker() override;
-
- static JsonTracker* GetOrCreate(TraceProcessorContext* context) {
- if (!context->json_tracker) {
- context->json_tracker.reset(new JsonTracker(context));
- }
- return static_cast<JsonTracker*>(context->json_tracker.get());
- }
-
- void SetTimeUnit(json::TimeUnit time_unit) { time_unit_ = time_unit; }
-
- base::Optional<int64_t> CoerceToTs(const Json::Value& value) {
- return json::CoerceToTs(time_unit_, value);
- }
- base::Optional<int64_t> CoerceToTs(const std::string& value) {
- return json::CoerceToTs(time_unit_, value);
- }
-
- private:
- json::TimeUnit time_unit_ = json::TimeUnit::kUs;
-};
-
-} // namespace trace_processor
-} // namespace perfetto
-
-#endif // SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACKER_H_
diff --git a/src/trace_processor/importers/json/json_tracker_unittest.cc b/src/trace_processor/importers/json/json_tracker_unittest.cc
deleted file mode 100644
index 4ae6009..0000000
--- a/src/trace_processor/importers/json/json_tracker_unittest.cc
+++ /dev/null
@@ -1,40 +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/importers/json/json_tracker.h"
-
-#include "test/gtest_and_gmock.h"
-
-#include <json/value.h>
-
-namespace perfetto {
-namespace trace_processor {
-namespace {
-
-TEST(JsonTrackerTest, Ns) {
- JsonTracker tracker(nullptr);
- tracker.SetTimeUnit(json::TimeUnit::kNs);
- ASSERT_EQ(tracker.CoerceToTs(Json::Value(42)).value_or(-1), 42);
-}
-
-TEST(JsonTraceUtilsTest, Us) {
- JsonTracker tracker(nullptr);
- ASSERT_EQ(tracker.CoerceToTs(Json::Value(42)).value_or(-1), 42000);
-}
-
-} // namespace
-} // namespace trace_processor
-} // namespace perfetto
diff --git a/src/trace_processor/importers/json/json_utils.cc b/src/trace_processor/importers/json/json_utils.cc
index c44efa1..432ddbe 100644
--- a/src/trace_processor/importers/json/json_utils.cc
+++ b/src/trace_processor/importers/json/json_utils.cc
@@ -28,15 +28,6 @@
namespace perfetto {
namespace trace_processor {
namespace json {
-namespace {
-
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
-int64_t TimeUnitToNs(TimeUnit unit) {
- return static_cast<int64_t>(unit);
-}
-#endif
-
-} // namespace
bool IsJsonSupported() {
#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
@@ -46,30 +37,28 @@
#endif
}
-base::Optional<int64_t> CoerceToTs(TimeUnit unit, const Json::Value& value) {
+base::Optional<int64_t> CoerceToTs(const Json::Value& value) {
PERFETTO_DCHECK(IsJsonSupported());
#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
switch (static_cast<size_t>(value.type())) {
case Json::realValue:
- return static_cast<int64_t>(value.asDouble() *
- static_cast<double>(TimeUnitToNs(unit)));
+ return static_cast<int64_t>(value.asDouble() * 1000.0);
case Json::uintValue:
case Json::intValue:
- return value.asInt64() * TimeUnitToNs(unit);
+ return value.asInt64() * 1000;
case Json::stringValue:
- return CoerceToTs(unit, value.asString());
+ return CoerceToTs(value.asString());
default:
return base::nullopt;
}
#else
- perfetto::base::ignore_result(unit);
perfetto::base::ignore_result(value);
return base::nullopt;
#endif
}
-base::Optional<int64_t> CoerceToTs(TimeUnit unit, const std::string& s) {
+base::Optional<int64_t> CoerceToTs(const std::string& s) {
PERFETTO_DCHECK(IsJsonSupported());
#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
@@ -82,11 +71,9 @@
(!rhs.has_value() && rhs_start < s.size())) {
return base::nullopt;
}
- int64_t factor = TimeUnitToNs(unit);
- return lhs.value_or(0) * factor +
- static_cast<int64_t>(rhs.value_or(0) * static_cast<double>(factor));
+ return lhs.value_or(0) * 1000 +
+ static_cast<int64_t>(rhs.value_or(0) * 1000.0);
#else
- perfetto::base::ignore_result(unit);
perfetto::base::ignore_result(s);
return base::nullopt;
#endif
diff --git a/src/trace_processor/importers/json/json_utils.h b/src/trace_processor/importers/json/json_utils.h
index 74a2c3a..1cbcae8 100644
--- a/src/trace_processor/importers/json/json_utils.h
+++ b/src/trace_processor/importers/json/json_utils.h
@@ -40,9 +40,8 @@
// build flags.
bool IsJsonSupported();
-enum class TimeUnit { kNs = 1, kUs = 1000, kMs = 1000000 };
-base::Optional<int64_t> CoerceToTs(TimeUnit unit, const Json::Value& value);
-base::Optional<int64_t> CoerceToTs(TimeUnit unit, const std::string& value);
+base::Optional<int64_t> CoerceToTs(const Json::Value& value);
+base::Optional<int64_t> CoerceToTs(const std::string& value);
base::Optional<int64_t> CoerceToInt64(const Json::Value& value);
base::Optional<uint32_t> CoerceToUint32(const Json::Value& value);
diff --git a/src/trace_processor/importers/json/json_utils_unittest.cc b/src/trace_processor/importers/json/json_utils_unittest.cc
index a3846ea..7f01ad6 100644
--- a/src/trace_processor/importers/json/json_utils_unittest.cc
+++ b/src/trace_processor/importers/json/json_utils_unittest.cc
@@ -43,26 +43,17 @@
}
TEST(JsonTraceUtilsTest, CoerceToTs) {
- ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value(42)).value_or(-1), 42000);
- ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value("42")).value_or(-1), 42000);
- ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value(42.1)).value_or(-1), 42100);
- ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value("42.1")).value_or(-1), 42100);
- ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value(".42")).value_or(-1), 420);
- ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value("42.")).value_or(-1), 42000);
- ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value("42.0")).value_or(-1), 42000);
- ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value("0.2")).value_or(-1), 200);
- ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value("0.2e-1")).value_or(-1), 20);
- ASSERT_EQ(CoerceToTs(TimeUnit::kNs, Json::Value(42)).value_or(-1), 42);
- ASSERT_EQ(CoerceToTs(TimeUnit::kMs, Json::Value(42)).value_or(-1), 42000000);
- ASSERT_EQ(CoerceToTs(TimeUnit::kUs, Json::Value(".")).value_or(-1), 0);
- ASSERT_FALSE(CoerceToTs(TimeUnit::kNs, Json::Value("foo")).has_value());
- ASSERT_FALSE(CoerceToTs(TimeUnit::kNs, Json::Value(".foo")).has_value());
- ASSERT_FALSE(CoerceToTs(TimeUnit::kNs, Json::Value("0.foo")).has_value());
- ASSERT_FALSE(CoerceToTs(TimeUnit::kNs, Json::Value("foo0.23")).has_value());
- ASSERT_FALSE(CoerceToTs(TimeUnit::kNs, Json::Value("23.12foo")).has_value());
- ASSERT_FALSE(CoerceToTs(TimeUnit::kNs, Json::Value("1234!")).has_value());
- ASSERT_FALSE(CoerceToTs(TimeUnit::kUs, Json::Value("1234!")).has_value());
- ASSERT_FALSE(CoerceToTs(TimeUnit::kMs, Json::Value("1234!")).has_value());
+ ASSERT_EQ(CoerceToTs(Json::Value(42)).value_or(-1), 42000);
+ ASSERT_EQ(CoerceToTs(Json::Value("42")).value_or(-1), 42000);
+ ASSERT_EQ(CoerceToTs(Json::Value(42.1)).value_or(-1), 42100);
+ ASSERT_EQ(CoerceToTs(Json::Value("42.1")).value_or(-1), 42100);
+ ASSERT_EQ(CoerceToTs(Json::Value(".42")).value_or(-1), 420);
+ ASSERT_EQ(CoerceToTs(Json::Value("42.")).value_or(-1), 42000);
+ ASSERT_EQ(CoerceToTs(Json::Value("42.0")).value_or(-1), 42000);
+ ASSERT_EQ(CoerceToTs(Json::Value("0.2")).value_or(-1), 200);
+ ASSERT_EQ(CoerceToTs(Json::Value("0.2e-1")).value_or(-1), 20);
+ ASSERT_EQ(CoerceToTs(Json::Value(".")).value_or(-1), 0);
+ ASSERT_FALSE(CoerceToTs(Json::Value("1234!")).has_value());
}
} // namespace
diff --git a/src/trace_processor/importers/memory_tracker/graph_processor_unittest.cc b/src/trace_processor/importers/memory_tracker/graph_processor_unittest.cc
index 8d1cc31..4275b36 100644
--- a/src/trace_processor/importers/memory_tracker/graph_processor_unittest.cc
+++ b/src/trace_processor/importers/memory_tracker/graph_processor_unittest.cc
@@ -18,8 +18,6 @@
#include <stddef.h>
-#include <unordered_map>
-
#include "perfetto/base/build_config.h"
#include "test/gtest_and_gmock.h"
diff --git a/src/trace_processor/importers/memory_tracker/raw_process_memory_node_unittest.cc b/src/trace_processor/importers/memory_tracker/raw_process_memory_node_unittest.cc
index 91a27ec..653dfe0 100644
--- a/src/trace_processor/importers/memory_tracker/raw_process_memory_node_unittest.cc
+++ b/src/trace_processor/importers/memory_tracker/raw_process_memory_node_unittest.cc
@@ -18,8 +18,6 @@
#include <stddef.h>
-#include <unordered_map>
-
#include "perfetto/base/build_config.h"
#include "test/gtest_and_gmock.h"
diff --git a/src/trace_processor/importers/proto/async_track_set_tracker.h b/src/trace_processor/importers/proto/async_track_set_tracker.h
index b3dd836..6e7fa81 100644
--- a/src/trace_processor/importers/proto/async_track_set_tracker.h
+++ b/src/trace_processor/importers/proto/async_track_set_tracker.h
@@ -17,8 +17,6 @@
#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_ASYNC_TRACK_SET_TRACKER_H_
#define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_ASYNC_TRACK_SET_TRACKER_H_
-#include <unordered_map>
-
#include "src/trace_processor/storage/trace_storage.h"
namespace perfetto {
diff --git a/src/trace_processor/importers/proto/chrome_string_lookup.cc b/src/trace_processor/importers/proto/chrome_string_lookup.cc
index 7858dde..7de67f0 100644
--- a/src/trace_processor/importers/proto/chrome_string_lookup.cc
+++ b/src/trace_processor/importers/proto/chrome_string_lookup.cc
@@ -167,6 +167,7 @@
{ChromeThreadDescriptor::THREAD_NETWORKCONFIGWATCHER,
"NetworkConfigWatcher"},
{ChromeThreadDescriptor::THREAD_WASAPI_RENDER, "wasapi_render_thread"},
+ {ChromeThreadDescriptor::THREAD_LOADER_LOCK_SAMPLER, "LoaderLockSampler"},
};
} // namespace
diff --git a/src/trace_processor/importers/proto/heap_graph_tracker.cc b/src/trace_processor/importers/proto/heap_graph_tracker.cc
index 062bd1f..f34377f 100644
--- a/src/trace_processor/importers/proto/heap_graph_tracker.cc
+++ b/src/trace_processor/importers/proto/heap_graph_tracker.cc
@@ -855,7 +855,7 @@
size_t parent_id; // id of parent node in the result tree.
size_t i; // Index of the next child of this node to handle.
uint32_t depth; // Depth in the resulting tree
- // (including artifical root).
+ // (including artificial root).
std::vector<tables::HeapGraphObjectTable::Id> children;
};
@@ -912,9 +912,31 @@
GetChildren(*storage, n);
children.assign(children_set.cbegin(), children_set.cend());
PERFETTO_CHECK(children.size() == children_set.size());
+
+ if (storage->heap_graph_object_table().native_size()[row]) {
+ StringPool::Id native_class_name_id = storage->InternString(
+ base::StringView(std::string("[native] ") +
+ storage->GetString(class_name_id).ToStdString()));
+ std::map<StringId, size_t>::iterator native_it;
+ bool inserted_new_node;
+ std::tie(native_it, inserted_new_node) =
+ path->nodes[path_id].children.insert({native_class_name_id, 0});
+ if (inserted_new_node) {
+ native_it->second = path->nodes.size();
+ path->nodes.emplace_back(PathFromRoot::Node{});
+
+ path->nodes.back().class_name_id = native_class_name_id;
+ path->nodes.back().depth = depth + 1;
+ path->nodes.back().parent_id = path_id;
+ }
+ PathFromRoot::Node* new_output_tree_node = &path->nodes[native_it->second];
+
+ new_output_tree_node->size += storage->heap_graph_object_table().native_size()[row];
+ new_output_tree_node->count++;
+ }
}
- // Otherwise we have already handled this node and just need to get its
- // i-th child.
+
+ // We have already handled this node and just need to get its i-th child.
if (!children.empty()) {
PERFETTO_CHECK(i < children.size());
tables::HeapGraphObjectTable::Id child = children[i];
diff --git a/src/trace_processor/importers/proto/track_event_parser.cc b/src/trace_processor/importers/proto/track_event_parser.cc
index 2af9a04..136c5de 100644
--- a/src/trace_processor/importers/proto/track_event_parser.cc
+++ b/src/trace_processor/importers/proto/track_event_parser.cc
@@ -1120,9 +1120,14 @@
}
TrackEventArgsParser args_writer(*inserter, *storage_, *sequence_state_);
+ int unknown_extensions = 0;
log_errors(parser_->args_parser_.ParseMessage(
blob_, ".perfetto.protos.TrackEvent", &parser_->reflect_fields_,
- args_writer));
+ args_writer, &unknown_extensions));
+ if (unknown_extensions > 0) {
+ context_->storage->IncrementStats(stats::unknown_extension_fields,
+ unknown_extensions);
+ }
{
auto key = parser_->args_parser_.EnterDictionary("debug");
diff --git a/src/trace_processor/importers/systrace/systrace_line_parser.cc b/src/trace_processor/importers/systrace/systrace_line_parser.cc
index 15f6827..9120e65 100644
--- a/src/trace_processor/importers/systrace/systrace_line_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_line_parser.cc
@@ -16,6 +16,7 @@
#include "src/trace_processor/importers/systrace/systrace_line_parser.h"
+#include "perfetto/ext/base/flat_hash_map.h"
#include "perfetto/ext/base/string_splitter.h"
#include "perfetto/ext/base/string_utils.h"
#include "src/trace_processor/importers/common/args_tracker.h"
@@ -31,7 +32,6 @@
#include <cctype>
#include <cinttypes>
#include <string>
-#include <unordered_map>
namespace perfetto {
namespace trace_processor {
@@ -40,11 +40,13 @@
: context_(ctx),
rss_stat_tracker_(context_),
sched_wakeup_name_id_(ctx->storage->InternString("sched_wakeup")),
+ sched_waking_name_id_(ctx->storage->InternString("sched_waking")),
cpuidle_name_id_(ctx->storage->InternString("cpuidle")),
workqueue_name_id_(ctx->storage->InternString("workqueue")),
sched_blocked_reason_id_(
ctx->storage->InternString("sched_blocked_reason")),
- io_wait_id_(ctx->storage->InternString("io_wait")) {}
+ io_wait_id_(ctx->storage->InternString("io_wait")),
+ waker_utid_id_(ctx->storage->InternString("waker_utid")) {}
util::Status SystraceLineParser::ParseLine(const SystraceLine& line) {
auto utid = context_->process_tracker->UpdateThreadName(
@@ -58,14 +60,14 @@
}
}
- std::unordered_map<std::string, std::string> args;
+ base::FlatHashMap<std::string, std::string> args;
for (base::StringSplitter ss(line.args_str, ' '); ss.Next();) {
std::string key;
std::string value;
if (!base::Contains(ss.cur_token(), "=")) {
key = "name";
value = ss.cur_token();
- args.emplace(std::move(key), std::move(value));
+ args.Insert(std::move(key), std::move(value));
continue;
}
for (base::StringSplitter inner(ss.cur_token(), '='); inner.Next();) {
@@ -75,7 +77,7 @@
value = inner.cur_token();
}
}
- args.emplace(std::move(key), std::move(value));
+ args.Insert(std::move(key), std::move(value));
}
if (line.event_name == "sched_switch") {
auto prev_state_str = args["prev_state"];
@@ -101,7 +103,8 @@
line.event_name == "0" || line.event_name == "print") {
SystraceParser::GetOrCreate(context_)->ParsePrintEvent(
line.ts, line.pid, line.args_str.c_str());
- } else if (line.event_name == "sched_wakeup") {
+ } else if (line.event_name == "sched_wakeup" ||
+ line.event_name == "sched_waking") {
auto comm = args["comm"];
base::Optional<uint32_t> wakee_pid = base::StringToUInt32(args["pid"]);
if (!wakee_pid.has_value()) {
@@ -111,8 +114,15 @@
StringId name_id = context_->storage->InternString(base::StringView(comm));
auto wakee_utid = context_->process_tracker->UpdateThreadName(
wakee_pid.value(), name_id, ThreadNamePriority::kFtrace);
- context_->event_tracker->PushInstant(line.ts, sched_wakeup_name_id_,
- wakee_utid, RefType::kRefUtid);
+
+ StringId event_name_id = line.event_name == "sched_wakeup"
+ ? sched_wakeup_name_id_
+ : sched_waking_name_id_;
+ InstantId instant_id = context_->event_tracker->PushInstant(
+ line.ts, event_name_id, wakee_utid, RefType::kRefUtid);
+ context_->args_tracker->AddArgsTo(instant_id)
+ .AddArg(waker_utid_id_, Variadic::UnsignedInteger(utid));
+
} else if (line.event_name == "cpu_idle") {
base::Optional<uint32_t> event_cpu = base::StringToUInt32(args["cpu_id"]);
base::Optional<double> new_state = base::StringToDouble(args["state"]);
diff --git a/src/trace_processor/importers/systrace/systrace_line_parser.h b/src/trace_processor/importers/systrace/systrace_line_parser.h
index 071ee31..24331fe 100644
--- a/src/trace_processor/importers/systrace/systrace_line_parser.h
+++ b/src/trace_processor/importers/systrace/systrace_line_parser.h
@@ -36,11 +36,14 @@
private:
TraceProcessorContext* const context_;
RssStatTracker rss_stat_tracker_;
+
const StringId sched_wakeup_name_id_ = kNullStringId;
+ const StringId sched_waking_name_id_ = kNullStringId;
const StringId cpuidle_name_id_ = kNullStringId;
const StringId workqueue_name_id_ = kNullStringId;
const StringId sched_blocked_reason_id_ = kNullStringId;
const StringId io_wait_id_ = kNullStringId;
+ const StringId waker_utid_id_ = kNullStringId;
};
} // namespace trace_processor
diff --git a/src/trace_processor/importers/systrace/systrace_trace_parser.cc b/src/trace_processor/importers/systrace/systrace_trace_parser.cc
index 445dda1..0fe8781 100644
--- a/src/trace_processor/importers/systrace/systrace_trace_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_trace_parser.cc
@@ -19,6 +19,7 @@
#include "perfetto/base/logging.h"
#include "perfetto/ext/base/string_splitter.h"
#include "perfetto/ext/base/string_utils.h"
+#include "src/trace_processor/forwarding_trace_parser.h"
#include "src/trace_processor/importers/common/process_tracker.h"
#include "src/trace_processor/trace_sorter.h"
@@ -71,6 +72,18 @@
blob.data() + blob.size());
if (state_ == ParseState::kBeforeParse) {
+ // Remove anything before the TRACE:\n marker, which is emitted when
+ // obtaining traces via `adb shell "atrace -t 1 sched" > out.txt`.
+ std::array<uint8_t, 7> kAtraceMarker = {'T', 'R', 'A', 'C', 'E', ':', '\n'};
+ auto search_end = partial_buf_.begin() +
+ static_cast<int>(std::min(partial_buf_.size(),
+ kGuessTraceMaxLookahead));
+ auto it = std::search(partial_buf_.begin(), search_end,
+ kAtraceMarker.begin(), kAtraceMarker.end());
+ if (it != search_end)
+ partial_buf_.erase(partial_buf_.begin(), it + kAtraceMarker.size());
+
+ // Deal with HTML traces.
state_ = partial_buf_[0] == '<' ? ParseState::kHtmlBeforeSystrace
: ParseState::kSystrace;
}
diff --git a/src/trace_processor/metrics/sql/android/android_multiuser_populator.sql b/src/trace_processor/metrics/sql/android/android_multiuser_populator.sql
index 98a4b16..c1726ff 100644
--- a/src/trace_processor/metrics/sql/android/android_multiuser_populator.sql
+++ b/src/trace_processor/metrics/sql/android/android_multiuser_populator.sql
@@ -14,6 +14,9 @@
-- limitations under the License.
--
+-- Create the base tables and views containing the launch spans.
+SELECT RUN_METRIC('android/startup/launches.sql');
+
-- Collect the important timestamps for Multiuser events.
DROP VIEW IF EXISTS multiuser_events;
CREATE VIEW multiuser_events AS
@@ -31,9 +34,9 @@
)
),
(
- SELECT slice.ts + slice.dur AS launcher_end_time_ns
- FROM slice
- WHERE (slice.name = "launching: com.google.android.apps.nexuslauncher")
+ SELECT ts_end AS launcher_end_time_ns
+ FROM launches
+ WHERE (package = 'com.android.launcher3' OR package = 'com.google.android.apps.nexuslauncher')
),
(
SELECT MIN(slice.ts) AS user_create_time_ns
diff --git a/src/trace_processor/metrics/sql/android/java_heap_stats.sql b/src/trace_processor/metrics/sql/android/java_heap_stats.sql
index dfafbbd..9f22a8a 100644
--- a/src/trace_processor/metrics/sql/android/java_heap_stats.sql
+++ b/src/trace_processor/metrics/sql/android/java_heap_stats.sql
@@ -26,9 +26,11 @@
upid,
graph_sample_ts,
SUM(self_size) AS total_size,
+ SUM(native_size) AS total_native_size,
COUNT(1) AS total_obj_count,
- SUM(CASE reachable WHEN TRUE THEN self_size ELSE 0 END) AS reachable_size,
- SUM(CASE reachable WHEN TRUE THEN 1 ELSE 0 END) AS reachable_obj_count
+ SUM(IIF(reachable, self_size, 0)) AS reachable_size,
+ SUM(IIF(reachable, native_size, 0)) AS reachable_native_size,
+ SUM(IIF(reachable, 1, 0)) AS reachable_obj_count
FROM heap_graph_object
GROUP BY 1, 2
),
@@ -92,8 +94,10 @@
RepeatedField(JavaHeapStats_Sample(
'ts', graph_sample_ts,
'heap_size', total_size,
+ 'heap_native_size', total_native_size,
'obj_count', total_obj_count,
'reachable_heap_size', reachable_size,
+ 'reachable_heap_native_size', reachable_native_size,
'reachable_obj_count', reachable_obj_count,
'roots', roots,
'anon_rss_and_swap_size', closest_anon_swap.val
diff --git a/src/trace_processor/metrics/sql/experimental/chrome_dropped_frames.sql b/src/trace_processor/metrics/sql/experimental/chrome_dropped_frames.sql
index cd1b809..595491d 100644
--- a/src/trace_processor/metrics/sql/experimental/chrome_dropped_frames.sql
+++ b/src/trace_processor/metrics/sql/experimental/chrome_dropped_frames.sql
@@ -46,14 +46,28 @@
ON dropped_frames_with_upid.upid = process.upid;
-- Create the derived event track for dropped frames.
+-- All tracks generated from chrome_dropped_frames_event are
+-- placed under a track group named 'Dropped Frames', whose summary
+-- track is the first track ('All Processes') in chrome_dropped_frames_event.
DROP VIEW IF EXISTS chrome_dropped_frames_event;
CREATE VIEW chrome_dropped_frames_event AS
SELECT
'slice' AS track_type,
- 'Dropped Frames' AS track_name,
+ 'All Processes' AS track_name,
ts,
0 AS dur,
- 'Dropped Frame' AS slice_name
+ 'Dropped Frame' AS slice_name,
+ 'Dropped Frames' AS group_name
+FROM dropped_frames_with_process_info
+GROUP BY ts
+UNION ALL
+SELECT
+ 'slice' AS track_type,
+ process_name || ' ' || process_id AS track_name,
+ ts,
+ 0 AS dur,
+ 'Dropped Frame' AS slice_name,
+ 'Dropped Frames' AS group_name
FROM dropped_frames_with_process_info
GROUP BY ts;
diff --git a/src/trace_processor/python/perfetto/trace_processor/__init__.py b/src/trace_processor/python/perfetto/trace_processor/__init__.py
index fbbffcd..7106a6c 100644
--- a/src/trace_processor/python/perfetto/trace_processor/__init__.py
+++ b/src/trace_processor/python/perfetto/trace_processor/__init__.py
@@ -13,5 +13,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from .api import TraceProcessor
-from .http import TraceProcessorHttp
\ No newline at end of file
+from .api import TraceProcessor, TraceProcessorException
+from .http import TraceProcessorHttp
diff --git a/src/trace_processor/python/perfetto/trace_processor/api.py b/src/trace_processor/python/perfetto/trace_processor/api.py
index 1bf499f..56d9fc9 100644
--- a/src/trace_processor/python/perfetto/trace_processor/api.py
+++ b/src/trace_processor/python/perfetto/trace_processor/api.py
@@ -135,7 +135,7 @@
except ModuleNotFoundError:
raise TraceProcessorException(
- 'The sufficient libraries are not installed')
+ 'Python dependencies missing. Please pip3 install pandas numpy')
def __len__(self):
return self.__count
@@ -238,8 +238,13 @@
return response.metatrace
- # TODO(@aninditaghosh): Investigate context managers for
- # cleaner usage
+ def __enter__(self):
+ return self
+
+ def __exit__(self, _, __, ___):
+ self.close()
+ return False
+
def close(self):
if hasattr(self, 'subprocess'):
self.subprocess.kill()
diff --git a/src/trace_processor/python/perfetto/trace_processor/metrics.descriptor b/src/trace_processor/python/perfetto/trace_processor/metrics.descriptor
index 0eb8e34..167645f 100644
--- a/src/trace_processor/python/perfetto/trace_processor/metrics.descriptor
+++ b/src/trace_processor/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor b/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor
index 1212bcd..c7b0668 100644
--- a/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor
+++ b/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor
Binary files differ
diff --git a/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor.sha1 b/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor.sha1
index ecdef65..23712a1 100644
--- a/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor.sha1
+++ b/src/trace_processor/python/perfetto/trace_processor/trace_processor.descriptor.sha1
@@ -2,5 +2,5 @@
// SHA1(tools/gen_binary_descriptors)
// 9fc6d77de57ec76a80b76aa282f4c7cf5ce55eec
// SHA1(protos/perfetto/trace_processor/trace_processor.proto)
-// 8ed3e45d389366979465140f6dcdbbb1356f323a
+// e303e1fc877a9fe4f8dd8413c03266ee68dfd3aa
\ No newline at end of file
diff --git a/src/trace_processor/rpc/BUILD.gn b/src/trace_processor/rpc/BUILD.gn
index 09b9061..7246ec1 100644
--- a/src/trace_processor/rpc/BUILD.gn
+++ b/src/trace_processor/rpc/BUILD.gn
@@ -66,7 +66,7 @@
"../../../include/perfetto/trace_processor",
"../../../protos/perfetto/trace_processor:zero",
"../../base",
- "../../base:unix_socket",
+ "../../base/http",
"../../protozero",
]
}
diff --git a/src/trace_processor/rpc/httpd.cc b/src/trace_processor/rpc/httpd.cc
index 654c566..72c0f12 100644
--- a/src/trace_processor/rpc/httpd.cc
+++ b/src/trace_processor/rpc/httpd.cc
@@ -20,14 +20,11 @@
#include "src/trace_processor/rpc/httpd.h"
-#include <map>
-#include <string>
-
-#include "perfetto/ext/base/paged_memory.h"
+#include "perfetto/ext/base/http/http_server.h"
#include "perfetto/ext/base/string_utils.h"
#include "perfetto/ext/base/string_view.h"
-#include "perfetto/ext/base/unix_socket.h"
#include "perfetto/ext/base/unix_task_runner.h"
+#include "perfetto/ext/base/utils.h"
#include "perfetto/protozero/scattered_heap_buffer.h"
#include "perfetto/trace_processor/trace_processor.h"
#include "src/trace_processor/rpc/rpc.h"
@@ -39,8 +36,7 @@
namespace {
-constexpr char kBindPort[] = "9001";
-constexpr size_t kOmitContentLength = static_cast<size_t>(-1);
+constexpr int kBindPort = 9001;
// Sets the Access-Control-Allow-Origin: $origin on the following origins.
// This affects only browser clients that use CORS. Other HTTP clients (e.g. the
@@ -51,350 +47,146 @@
"http://127.0.0.1:10000",
};
-// 32 MiB payload + 128K for HTTP headers.
-constexpr size_t kMaxRequestSize = (32 * 1024 + 128) * 1024;
-
-// Owns the socket and data for one HTTP client connection.
-struct Client {
- Client(std::unique_ptr<base::UnixSocket> s)
- : sock(std::move(s)),
- rxbuf(base::PagedMemory::Allocate(kMaxRequestSize)) {}
- size_t rxbuf_avail() { return rxbuf.size() - rxbuf_used; }
-
- std::unique_ptr<base::UnixSocket> sock;
- base::PagedMemory rxbuf;
- size_t rxbuf_used = 0;
-};
-
-struct HttpRequest {
- base::StringView method;
- base::StringView uri;
- base::StringView origin;
- base::StringView body;
- int id = 0;
-};
-
-class HttpServer : public base::UnixSocket::EventListener {
+class Httpd : public base::HttpRequestHandler {
public:
- explicit HttpServer(std::unique_ptr<TraceProcessor>);
- ~HttpServer() override;
- void Run(const char*, const char*);
-
- // This is non-null only while serving an HTTP request.
- Client* active_client() { return active_client_; }
+ explicit Httpd(std::unique_ptr<TraceProcessor>);
+ ~Httpd() override;
+ void Run(int port);
private:
- size_t ParseOneHttpRequest(Client* client);
- void HandleRequest(Client*, const HttpRequest&);
- void ServeHelpPage(Client*);
+ // HttpRequestHandler implementation.
+ void OnHttpRequest(const base::HttpRequest&) override;
+ void OnWebsocketMessage(const base::WebsocketMessage&) override;
- void OnNewIncomingConnection(base::UnixSocket*,
- std::unique_ptr<base::UnixSocket>) override;
- void OnConnect(base::UnixSocket* self, bool connected) override;
- void OnDisconnect(base::UnixSocket* self) override;
- void OnDataAvailable(base::UnixSocket* self) override;
+ void ServeHelpPage(const base::HttpRequest&);
Rpc trace_processor_rpc_;
base::UnixTaskRunner task_runner_;
- std::unique_ptr<base::UnixSocket> sock4_;
- std::unique_ptr<base::UnixSocket> sock6_;
- std::list<Client> clients_;
- Client* active_client_ = nullptr;
- bool origin_error_logged_ = false;
+ base::HttpServer http_srv_;
};
-HttpServer* g_httpd_instance;
+base::HttpServerConnection* g_cur_conn;
-void Append(std::vector<char>& buf, const char* str) {
- buf.insert(buf.end(), str, str + strlen(str));
+base::StringView Vec2Sv(const std::vector<uint8_t>& v) {
+ return base::StringView(reinterpret_cast<const char*>(v.data()), v.size());
}
-void Append(std::vector<char>& buf, const std::string& str) {
- buf.insert(buf.end(), str.begin(), str.end());
-}
-
-void HttpReply(base::UnixSocket* sock,
- const char* http_code,
- std::initializer_list<const char*> headers = {},
- const uint8_t* content = nullptr,
- size_t content_length = 0) {
- std::vector<char> response;
- response.reserve(4096);
- Append(response, "HTTP/1.1 ");
- Append(response, http_code);
- Append(response, "\r\n");
- for (const char* hdr : headers) {
- if (strlen(hdr) == 0)
- continue;
- Append(response, hdr);
- Append(response, "\r\n");
+// Used both by websockets and /rpc chunked HTTP endpoints.
+void SendRpcChunk(const void* data, uint32_t len) {
+ if (data == nullptr) {
+ // Unrecoverable RPC error case.
+ if (!g_cur_conn->is_websocket())
+ g_cur_conn->SendResponseBody("0\r\n\r\n", 5);
+ g_cur_conn->Close();
+ return;
}
- if (content_length != kOmitContentLength) {
- Append(response, "Content-Length: ");
- Append(response, std::to_string(content_length));
- Append(response, "\r\n");
+ if (g_cur_conn->is_websocket()) {
+ g_cur_conn->SendWebsocketMessage(data, len);
+ } else {
+ base::StackString<32> chunk_hdr("%x\r\n", len);
+ g_cur_conn->SendResponseBody(chunk_hdr.c_str(), chunk_hdr.len());
+ g_cur_conn->SendResponseBody(data, len);
+ g_cur_conn->SendResponseBody("\r\n", 2);
}
- Append(response, "\r\n"); // End-of-headers marker.
- sock->Send(response.data(), response.size()); // Send response headers.
- if (content_length > 0 && content_length != kOmitContentLength)
- sock->Send(content, content_length); // Send response payload.
}
-void ShutdownBadRequest(base::UnixSocket* sock, const char* reason) {
- HttpReply(sock, "500 Bad Request", {},
- reinterpret_cast<const uint8_t*>(reason), strlen(reason));
- sock->Shutdown(/*notify=*/true);
-}
+Httpd::Httpd(std::unique_ptr<TraceProcessor> preloaded_instance)
+ : trace_processor_rpc_(std::move(preloaded_instance)),
+ http_srv_(&task_runner_, this) {}
+Httpd::~Httpd() = default;
-HttpServer::HttpServer(std::unique_ptr<TraceProcessor> preloaded_instance)
- : trace_processor_rpc_(std::move(preloaded_instance)) {}
-HttpServer::~HttpServer() = default;
-
-void HttpServer::Run(const char* kBindAddr4, const char* kBindAddr6) {
- PERFETTO_ILOG("[HTTP] Starting RPC server on %s and %s", kBindAddr4,
- kBindAddr6);
+void Httpd::Run(int port) {
+ PERFETTO_ILOG("[HTTP] Starting RPC server on localhost:%d", port);
PERFETTO_LOG(
"[HTTP] This server can be used by reloading https://ui.perfetto.dev and "
"clicking on YES on the \"Trace Processor native acceleration\" dialog "
"or through the Python API (see "
"https://perfetto.dev/docs/analysis/trace-processor#python-api).");
- sock4_ = base::UnixSocket::Listen(kBindAddr4, this, &task_runner_,
- base::SockFamily::kInet,
- base::SockType::kStream);
- bool ipv4_listening = sock4_ && sock4_->is_listening();
- if (!ipv4_listening) {
- PERFETTO_ILOG("Failed to listen on IPv4 socket");
- }
-
- sock6_ = base::UnixSocket::Listen(kBindAddr6, this, &task_runner_,
- base::SockFamily::kInet6,
- base::SockType::kStream);
- bool ipv6_listening = sock6_ && sock6_->is_listening();
- if (!ipv6_listening) {
- PERFETTO_ILOG("Failed to listen on IPv6 socket");
- }
-
- PERFETTO_CHECK(ipv4_listening || ipv6_listening);
-
+ for (size_t i = 0; i < base::ArraySize(kAllowedCORSOrigins); ++i)
+ http_srv_.AddAllowedOrigin(kAllowedCORSOrigins[i]);
+ http_srv_.Start(port);
task_runner_.Run();
}
-void HttpServer::OnNewIncomingConnection(
- base::UnixSocket*,
- std::unique_ptr<base::UnixSocket> sock) {
- PERFETTO_LOG("[HTTP] New connection");
- clients_.emplace_back(std::move(sock));
-}
-
-void HttpServer::OnConnect(base::UnixSocket*, bool) {}
-
-void HttpServer::OnDisconnect(base::UnixSocket* sock) {
- PERFETTO_LOG("[HTTP] Client disconnected");
- for (auto it = clients_.begin(); it != clients_.end(); ++it) {
- if (it->sock.get() == sock) {
- clients_.erase(it);
- return;
- }
- }
- PERFETTO_DFATAL("[HTTP] untracked client in OnDisconnect()");
-}
-
-void HttpServer::OnDataAvailable(base::UnixSocket* sock) {
- Client* client = nullptr;
- for (auto it = clients_.begin(); it != clients_.end() && !client; ++it)
- client = (it->sock.get() == sock) ? &*it : nullptr;
- PERFETTO_CHECK(client);
-
- char* rxbuf = reinterpret_cast<char*>(client->rxbuf.Get());
- for (;;) {
- size_t avail = client->rxbuf_avail();
- PERFETTO_CHECK(avail <= kMaxRequestSize);
- if (avail == 0)
- return ShutdownBadRequest(sock, "Request body too big");
- size_t rsize = sock->Receive(&rxbuf[client->rxbuf_used], avail);
- client->rxbuf_used += rsize;
- if (rsize == 0 || client->rxbuf_avail() == 0)
- break;
- }
-
- // At this point |rxbuf| can contain a partial HTTP request, a full one or
- // more (in case of HTTP Keepalive pipelining).
- for (;;) {
- active_client_ = client;
- size_t bytes_consumed = ParseOneHttpRequest(client);
- active_client_ = nullptr;
- if (bytes_consumed == 0)
- break;
- memmove(rxbuf, &rxbuf[bytes_consumed], client->rxbuf_used - bytes_consumed);
- client->rxbuf_used -= bytes_consumed;
- }
-}
-
-// Parses the HTTP request and invokes HandleRequest(). It returns the size of
-// the HTTP header + body that has been processed or 0 if there isn't enough
-// data for a full HTTP request in the buffer.
-size_t HttpServer::ParseOneHttpRequest(Client* client) {
- auto* rxbuf = reinterpret_cast<char*>(client->rxbuf.Get());
- base::StringView buf_view(rxbuf, client->rxbuf_used);
- size_t pos = 0;
- size_t body_offset = 0;
- size_t body_size = 0;
- bool has_parsed_first_line = false;
- HttpRequest http_req;
-
- // This loop parses the HTTP request headers and sets the |body_offset|.
- for (;;) {
- size_t next = buf_view.find("\r\n", pos);
- size_t col;
- if (next == std::string::npos)
- break;
-
- if (!has_parsed_first_line) {
- // Parse the "GET /xxx HTTP/1.1" line.
- has_parsed_first_line = true;
- size_t space = buf_view.find(' ');
- if (space == std::string::npos || space + 2 >= client->rxbuf_used) {
- ShutdownBadRequest(client->sock.get(), "Malformed HTTP request");
- return 0;
- }
- http_req.method = buf_view.substr(0, space);
- size_t uri_size = buf_view.find(' ', space + 1) - space - 1;
- http_req.uri = buf_view.substr(space + 1, uri_size);
- } else if (next == pos) {
- // The CR-LF marker that separates headers from body.
- body_offset = next + 2;
- break;
- } else if ((col = buf_view.find(':', pos)) < next) {
- // Parse HTTP headers. They look like: "Content-Length: 1234".
- auto hdr_name = buf_view.substr(pos, col - pos);
- auto hdr_value = buf_view.substr(col + 2, next - col - 2);
- if (hdr_name.CaseInsensitiveEq("content-length")) {
- body_size = static_cast<size_t>(atoi(hdr_value.ToStdString().c_str()));
- } else if (hdr_name.CaseInsensitiveEq("origin")) {
- http_req.origin = hdr_value;
- } else if (hdr_name.CaseInsensitiveEq("x-seq-id")) {
- http_req.id = atoi(hdr_value.ToStdString().c_str());
- }
- }
- pos = next + 2;
- }
-
- // If we have a full header but not yet the full body, return and try again
- // next time we receive some more data.
- size_t http_req_size = body_offset + body_size;
- if (!body_offset || client->rxbuf_used < http_req_size)
- return 0;
-
- http_req.body = base::StringView(&rxbuf[body_offset], body_size);
- HandleRequest(client, http_req);
- return http_req_size;
-}
-
-void HttpServer::HandleRequest(Client* client, const HttpRequest& req) {
+void Httpd::OnHttpRequest(const base::HttpRequest& req) {
+ base::HttpServerConnection& conn = *req.conn;
if (req.uri == "/") {
// If a user tries to open http://127.0.0.1:9001/ show a minimal help page.
- return ServeHelpPage(client);
+ return ServeHelpPage(req);
}
static int last_req_id = 0;
- if (req.id) {
- if (last_req_id && req.id != last_req_id + 1 && req.id != 1)
+ auto seq_hdr = req.GetHeader("x-seq-id").value_or(base::StringView());
+ int seq_id = base::StringToInt32(seq_hdr.ToStdString()).value_or(0);
+
+ if (seq_id) {
+ if (last_req_id && seq_id != last_req_id + 1 && seq_id != 1)
PERFETTO_ELOG("HTTP Request out of order");
- last_req_id = req.id;
- }
-
- PERFETTO_LOG("[HTTP] %04d %s %s (body: %zu bytes).", req.id,
- req.method.ToStdString().c_str(), req.uri.ToStdString().c_str(),
- req.body.size());
-
- std::string allow_origin_hdr;
- for (const char* allowed_origin : kAllowedCORSOrigins) {
- if (req.origin != base::StringView(allowed_origin))
- continue;
- allow_origin_hdr =
- "Access-Control-Allow-Origin: " + req.origin.ToStdString();
- break;
- }
- if (allow_origin_hdr.empty() && !origin_error_logged_) {
- origin_error_logged_ = true;
- PERFETTO_ELOG(
- "The HTTP origin \"%s\" is not trusted, no Access-Control-Allow-Origin "
- "will be emitted. If this request comes from a browser it will fail. "
- "For the list of allowed origins see kAllowedCORSOrigins.",
- req.origin.ToStdString().c_str());
+ last_req_id = seq_id;
}
// This is the default. Overridden by the /query handler for chunked replies.
char transfer_encoding_hdr[255] = "Transfer-Encoding: identity";
std::initializer_list<const char*> headers = {
- "Connection: Keep-Alive", //
"Cache-Control: no-cache", //
- "Keep-Alive: timeout=5, max=1000", //
"Content-Type: application/x-protobuf", //
- "Vary: Origin", //
transfer_encoding_hdr, //
- allow_origin_hdr.c_str(),
};
- if (req.method == "OPTIONS") {
- // CORS headers.
- return HttpReply(client->sock.get(), "204 No Content",
- {
- "Access-Control-Allow-Methods: POST, GET, OPTIONS",
- "Access-Control-Allow-Headers: *",
- "Access-Control-Max-Age: 86400",
- "Vary: Origin",
- allow_origin_hdr.c_str(),
- });
+ if (req.uri == "/status") {
+ auto status = trace_processor_rpc_.GetStatus();
+ return conn.SendResponse("200 OK", headers, Vec2Sv(status));
}
+ if (req.uri == "/websocket" && req.is_websocket_handshake) {
+ // Will trigger OnWebsocketMessage() when is received.
+ // It returns a 403 if the origin is not in kAllowedCORSOrigins.
+ return conn.UpgradeToWebsocket(req);
+ }
+
+ // --- Everything below this line is a legacy endpoint not used by the UI.
+ // There are two generations of pre-websocket legacy-ness:
+ // 1. The /rpc based endpoint. This is based on a chunked transfer, doing one
+ // POST request for each RPC invocation. All RPC methods are multiplexed
+ // into this one. This is still used by the python API.
+ // 2. The REST API, with one enpoint per RPC method (/parse, /query, ...).
+ // This is unused and will be removed at some point.
+
if (req.uri == "/rpc") {
// Start the chunked reply.
base::StringCopy(transfer_encoding_hdr, "Transfer-Encoding: chunked",
sizeof(transfer_encoding_hdr));
- base::UnixSocket* cli_sock = client->sock.get();
- HttpReply(cli_sock, "200 OK", headers, nullptr, kOmitContentLength);
-
- static auto resp_fn = [](const void* data, uint32_t len) {
- char chunk_hdr[32];
- auto hdr_len = static_cast<size_t>(sprintf(chunk_hdr, "%x\r\n", len));
- auto* http_client = g_httpd_instance->active_client();
- PERFETTO_CHECK(http_client);
- if (data == nullptr) {
- // Unrecoverable RPC error case.
- http_client->sock->Send("0\r\n\r\n", 5);
- http_client->sock->Shutdown(/*notify=*/true);
- return;
- }
- http_client->sock->Send(chunk_hdr, hdr_len);
- http_client->sock->Send(data, len);
- http_client->sock->Send("\r\n", 2);
- };
-
- trace_processor_rpc_.SetRpcResponseFunction(resp_fn);
+ conn.SendResponseHeaders("200 OK", headers,
+ base::HttpServerConnection::kOmitContentLength);
+ PERFETTO_CHECK(g_cur_conn == nullptr);
+ g_cur_conn = req.conn;
+ trace_processor_rpc_.SetRpcResponseFunction(SendRpcChunk);
+ // OnRpcRequest() will call SendRpcChunk() one or more times.
trace_processor_rpc_.OnRpcRequest(req.body.data(), req.body.size());
trace_processor_rpc_.SetRpcResponseFunction(nullptr);
+ g_cur_conn = nullptr;
// Terminate chunked stream.
- cli_sock->Send("0\r\n\r\n", 5);
+ conn.SendResponseBody("0\r\n\r\n", 5);
+ conn.Close();
return;
}
if (req.uri == "/parse") {
trace_processor_rpc_.Parse(
reinterpret_cast<const uint8_t*>(req.body.data()), req.body.size());
- return HttpReply(client->sock.get(), "200 OK", headers);
+ return conn.SendResponse("200 OK", headers);
}
if (req.uri == "/notify_eof") {
trace_processor_rpc_.NotifyEndOfFile();
- return HttpReply(client->sock.get(), "200 OK", headers);
+ return conn.SendResponse("200 OK", headers);
}
if (req.uri == "/restore_initial_tables") {
trace_processor_rpc_.RestoreInitialTables();
- return HttpReply(client->sock.get(), "200 OK", headers);
+ return conn.SendResponse("200 OK", headers);
}
// New endpoint, returns data in batches using chunked transfer encoding.
@@ -407,8 +199,8 @@
// Start the chunked reply.
base::StringCopy(transfer_encoding_hdr, "Transfer-Encoding: chunked",
sizeof(transfer_encoding_hdr));
- base::UnixSocket* cli_sock = client->sock.get();
- HttpReply(cli_sock, "200 OK", headers, nullptr, kOmitContentLength);
+ conn.SendResponseHeaders("200 OK", headers,
+ base::HttpServerConnection::kOmitContentLength);
// |on_result_chunk| will be called nested within the same callstack of the
// rpc.Query() call. No further calls will be made once Query() returns.
@@ -416,12 +208,12 @@
PERFETTO_DLOG("Sending response chunk, len=%zu eof=%d", len, !has_more);
char chunk_hdr[32];
auto hdr_len = static_cast<size_t>(sprintf(chunk_hdr, "%zx\r\n", len));
- cli_sock->Send(chunk_hdr, hdr_len);
- cli_sock->Send(buf, len);
- cli_sock->Send("\r\n", 2);
+ conn.SendResponseBody(chunk_hdr, hdr_len);
+ conn.SendResponseBody(buf, len);
+ conn.SendResponseBody("\r\n", 2);
if (!has_more) {
hdr_len = static_cast<size_t>(sprintf(chunk_hdr, "0\r\n\r\n"));
- cli_sock->Send(chunk_hdr, hdr_len);
+ conn.SendResponseBody(chunk_hdr, hdr_len);
}
};
trace_processor_rpc_.Query(
@@ -436,50 +228,49 @@
if (req.uri == "/raw_query") {
std::vector<uint8_t> response = trace_processor_rpc_.RawQuery(
reinterpret_cast<const uint8_t*>(req.body.data()), req.body.size());
- return HttpReply(client->sock.get(), "200 OK", headers, response.data(),
- response.size());
- }
-
- if (req.uri == "/status") {
- auto status = trace_processor_rpc_.GetStatus();
- return HttpReply(client->sock.get(), "200 OK", headers, status.data(),
- status.size());
+ return conn.SendResponse("200 OK", headers, Vec2Sv(response));
}
if (req.uri == "/compute_metric") {
std::vector<uint8_t> res = trace_processor_rpc_.ComputeMetric(
reinterpret_cast<const uint8_t*>(req.body.data()), req.body.size());
- return HttpReply(client->sock.get(), "200 OK", headers, res.data(),
- res.size());
+ return conn.SendResponse("200 OK", headers, Vec2Sv(res));
}
if (req.uri == "/enable_metatrace") {
trace_processor_rpc_.EnableMetatrace();
- return HttpReply(client->sock.get(), "200 OK", headers);
+ return conn.SendResponse("200 OK", headers);
}
if (req.uri == "/disable_and_read_metatrace") {
std::vector<uint8_t> res = trace_processor_rpc_.DisableAndReadMetatrace();
- return HttpReply(client->sock.get(), "200 OK", headers, res.data(),
- res.size());
+ return conn.SendResponse("200 OK", headers, Vec2Sv(res));
}
- return HttpReply(client->sock.get(), "404 Not Found", headers);
+ return conn.SendResponseAndClose("404 Not Found", headers);
+}
+
+void Httpd::OnWebsocketMessage(const base::WebsocketMessage& msg) {
+ PERFETTO_CHECK(g_cur_conn == nullptr);
+ g_cur_conn = msg.conn;
+ trace_processor_rpc_.SetRpcResponseFunction(SendRpcChunk);
+ // OnRpcRequest() will call SendRpcChunk() one or more times.
+ trace_processor_rpc_.OnRpcRequest(msg.data.data(), msg.data.size());
+ trace_processor_rpc_.SetRpcResponseFunction(nullptr);
+ g_cur_conn = nullptr;
}
} // namespace
void RunHttpRPCServer(std::unique_ptr<TraceProcessor> preloaded_instance,
std::string port_number) {
- HttpServer srv(std::move(preloaded_instance));
- g_httpd_instance = &srv;
- std::string port = port_number.empty() ? kBindPort : port_number;
- std::string ipv4_addr = "127.0.0.1:" + port;
- std::string ipv6_addr = "[::1]:" + port;
- srv.Run(ipv4_addr.c_str(), ipv6_addr.c_str());
+ Httpd srv(std::move(preloaded_instance));
+ base::Optional<int> port_opt = base::StringToInt32(port_number);
+ int port = port_opt.has_value() ? *port_opt : kBindPort;
+ srv.Run(port);
}
-void HttpServer::ServeHelpPage(Client* client) {
+void Httpd::ServeHelpPage(const base::HttpRequest& req) {
static const char kPage[] = R"(Perfetto Trace Processor RPC Server
@@ -503,12 +294,8 @@
https://perfetto.dev/docs/contributing/getting-started#community
)";
- char content_length[255];
- sprintf(content_length, "Content-Length: %zu", sizeof(kPage) - 1);
- std::initializer_list<const char*> headers{"Content-Type: text/plain",
- content_length};
- HttpReply(client->sock.get(), "200 OK", headers,
- reinterpret_cast<const uint8_t*>(kPage), sizeof(kPage) - 1);
+ std::initializer_list<const char*> headers{"Content-Type: text/plain"};
+ req.conn->SendResponse("200 OK", headers, kPage);
}
} // namespace trace_processor
diff --git a/src/trace_processor/sqlite/span_join_operator_table.h b/src/trace_processor/sqlite/span_join_operator_table.h
index 6fba96d..a6256f8 100644
--- a/src/trace_processor/sqlite/span_join_operator_table.h
+++ b/src/trace_processor/sqlite/span_join_operator_table.h
@@ -25,9 +25,9 @@
#include <map>
#include <memory>
#include <string>
-#include <unordered_map>
#include <vector>
+#include "perfetto/ext/base/flat_hash_map.h"
#include "perfetto/trace_processor/basic_types.h"
#include "perfetto/trace_processor/status.h"
#include "src/trace_processor/sqlite/scoped_db.h"
@@ -428,7 +428,7 @@
TableDefinition t1_defn_;
TableDefinition t2_defn_;
PartitioningType partitioning_;
- std::unordered_map<size_t, ColumnLocator> global_index_to_column_locator_;
+ base::FlatHashMap<size_t, ColumnLocator> global_index_to_column_locator_;
sqlite3* const db_;
};
diff --git a/src/trace_processor/sqlite/sqlite_raw_table.h b/src/trace_processor/sqlite/sqlite_raw_table.h
index ce1b9ac..a3ec5c2 100644
--- a/src/trace_processor/sqlite/sqlite_raw_table.h
+++ b/src/trace_processor/sqlite/sqlite_raw_table.h
@@ -18,6 +18,7 @@
#define SRC_TRACE_PROCESSOR_SQLITE_SQLITE_RAW_TABLE_H_
#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/flat_hash_map.h"
#include "perfetto/ext/base/string_writer.h"
#include "src/trace_processor/sqlite/db_sqlite_table.h"
#include "src/trace_processor/storage/trace_storage.h"
@@ -31,13 +32,13 @@
public:
using ScopedCString = std::unique_ptr<char, void (*)(void*)>;
- SystraceSerializer(TraceProcessorContext* context);
+ explicit SystraceSerializer(TraceProcessorContext* context);
ScopedCString SerializeToString(uint32_t raw_row);
private:
using StringIdMap =
- std::unordered_map<StringId, std::vector<base::Optional<uint32_t>>>;
+ base::FlatHashMap<StringId, std::vector<base::Optional<uint32_t>>>;
void SerializePrefix(uint32_t raw_row, base::StringWriter* writer);
diff --git a/src/trace_processor/storage/stats.h b/src/trace_processor/storage/stats.h
index ea71ef0..7e537ca 100644
--- a/src/trace_processor/storage/stats.h
+++ b/src/trace_processor/storage/stats.h
@@ -124,10 +124,11 @@
F(process_tracker_errors, kSingle, kError, kAnalysis, ""), \
F(json_tokenizer_failure, kSingle, kError, kTrace, ""), \
F(json_parser_failure, kSingle, kError, kTrace, ""), \
- F(json_display_time_unit_too_late, kSingle, kError, kTrace, \
- "The displayTimeUnit key came too late in the JSON trace so was " \
- "ignored. Trace processor only supports displayTimeUnit appearing " \
- "at the start of JSON traces"), \
+ F(json_display_time_unit, kSingle, kInfo, kTrace, \
+ "The displayTimeUnit key was set in the JSON trace. In some prior " \
+ "versions of trace processor this key could effect how the trace " \
+ "processor parsed timestamps and durations. In this version the key is " \
+ "ignored which more closely matches the bavahiour of catapult."), \
F(heap_graph_invalid_string_id, kIndexed, kError, kTrace, ""), \
F(heap_graph_non_finalized_graph, kSingle, kError, kTrace, ""), \
F(heap_graph_malformed_packet, kIndexed, kError, kTrace, ""), \
@@ -187,9 +188,13 @@
"processor."), \
F(perf_guardrail_stop_ts, kIndexed, kDataLoss, kTrace, ""), \
F(sorter_push_event_out_of_order, kSingle, kError, kTrace, \
- "Trace events are out of order event after sorting. This can happen " \
- "due to many factors including clock sync drift, producers emitting " \
- "events out of order or a bug in trace processor's logic of sorting.")
+ "Trace events are out of order event after sorting. This can happen " \
+ "due to many factors including clock sync drift, producers emitting " \
+ "events out of order or a bug in trace processor's logic of sorting."), \
+ F(unknown_extension_fields, kSingle, kError, kTrace, \
+ "TraceEvent had unknown extension fields, which might result in " \
+ "missing some arguments. You may need a newer version of trace " \
+ "processor to parse them.")
// clang-format on
enum Type {
diff --git a/src/trace_processor/trace_database_integrationtest.cc b/src/trace_processor/trace_database_integrationtest.cc
index c6c8095..6a59ca6 100644
--- a/src/trace_processor/trace_database_integrationtest.cc
+++ b/src/trace_processor/trace_database_integrationtest.cc
@@ -58,9 +58,24 @@
ASSERT_EQ(it.Get(0).long_value, 1);
}
+TEST(TraceProcessorCustomConfigTest, EmptyStringSkipsAllMetrics) {
+ auto config = Config();
+ config.skip_builtin_metric_paths = {""};
+ auto processor = TraceProcessor::CreateInstance(config);
+ processor->NotifyEndOfFile();
+
+ // Check that other metrics have been loaded.
+ auto it = processor->ExecuteQuery(
+ "select count(*) from trace_metrics "
+ "where name = 'trace_metadata';");
+ ASSERT_TRUE(it.Next());
+ ASSERT_EQ(it.Get(0).type, SqlValue::kLong);
+ ASSERT_EQ(it.Get(0).long_value, 0);
+}
+
TEST(TraceProcessorCustomConfigTest, HandlesMalformedMountPath) {
auto config = Config();
- config.skip_builtin_metric_paths = {"", "androi"};
+ config.skip_builtin_metric_paths = {"androi"};
auto processor = TraceProcessor::CreateInstance(config);
processor->NotifyEndOfFile();
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index ca3725d..2976e2c 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -687,10 +687,17 @@
tp->ExtendMetricsProto(kAllChromeMetricsDescriptor.data(),
kAllChromeMetricsDescriptor.size(), skip_prefixes);
- for (const auto& file_to_sql : metrics::sql_metrics::kFileToSql) {
- if (base::StartsWithAny(file_to_sql.path, sanitized_extension_paths))
- continue;
- tp->RegisterMetric(file_to_sql.path, file_to_sql.sql);
+ // TODO(lalitm): remove this special casing and change
+ // SanitizeMetricMountPaths if/when we move all protos for builtin metrics to
+ // match extension protos.
+ bool skip_all_sql = std::find(extension_paths.begin(), extension_paths.end(),
+ "") != extension_paths.end();
+ if (!skip_all_sql) {
+ for (const auto& file_to_sql : metrics::sql_metrics::kFileToSql) {
+ if (base::StartsWithAny(file_to_sql.path, sanitized_extension_paths))
+ continue;
+ tp->RegisterMetric(file_to_sql.path, file_to_sql.sql);
+ }
}
RegisterFunction<metrics::NullIfEmpty>(db, "NULL_IF_EMPTY", 1);
@@ -1054,6 +1061,23 @@
if (IsRootMetricField(no_ext_name)) {
metric.proto_field_name = no_ext_name;
metric.output_table_name = no_ext_name + "_output";
+
+ auto field_it_and_inserted =
+ proto_field_to_sql_metric_path_.emplace(*metric.proto_field_name, path);
+ if (!field_it_and_inserted.second) {
+ // We already had a metric with this field name in the map. However, if
+ // this was the case, we should have found the metric in
+ // |path_to_sql_metric_file_| above if we are simply overriding the
+ // metric. Return an error since this means we have two different SQL
+ // files which are trying to output the same metric.
+ const auto& prev_path = field_it_and_inserted.first->second;
+ PERFETTO_DCHECK(prev_path != path);
+ return base::ErrStatus(
+ "RegisterMetric Error: Metric paths %s (which is already registered) "
+ "and %s are both trying to output the proto field %s",
+ prev_path.c_str(), path.c_str(), metric.proto_field_name->c_str());
+ }
+
InsertIntoTraceMetricsTable(*db_, no_ext_name);
}
diff --git a/src/trace_processor/trace_processor_impl.h b/src/trace_processor/trace_processor_impl.h
index 90a4393..33ec127 100644
--- a/src/trace_processor/trace_processor_impl.h
+++ b/src/trace_processor/trace_processor_impl.h
@@ -125,6 +125,7 @@
DescriptorPool pool_;
std::vector<metrics::SqlMetricFile> sql_metrics_;
+ std::unordered_map<std::string, std::string> proto_field_to_sql_metric_path_;
// This is atomic because it is set by the CTRL-C signal handler and we need
// to prevent single-flow compiler optimizations in ExecuteQuery().
diff --git a/src/trace_processor/trace_processor_shell.cc b/src/trace_processor/trace_processor_shell.cc
index 3fe3486..1eede6f 100644
--- a/src/trace_processor/trace_processor_shell.cc
+++ b/src/trace_processor/trace_processor_shell.cc
@@ -342,7 +342,6 @@
base::ReadFile(register_metric, &sql);
std::string path = "shell/" + BaseName(register_metric);
-
return g_tp->RegisterMetric(path, sql);
}
@@ -387,9 +386,19 @@
kNone,
};
-util::Status RunMetrics(const std::vector<std::string>& metric_names,
+struct MetricNameAndPath {
+ std::string name;
+ base::Optional<std::string> no_ext_path;
+};
+
+util::Status RunMetrics(const std::vector<MetricNameAndPath>& metrics,
OutputFormat format,
const google::protobuf::DescriptorPool& pool) {
+ std::vector<std::string> metric_names(metrics.size());
+ for (size_t i = 0; i < metrics.size(); ++i) {
+ metric_names[i] = metrics[i].name;
+ }
+
if (format == OutputFormat::kTextProto) {
std::string out;
util::Status status =
@@ -416,10 +425,10 @@
google::protobuf::DynamicMessageFactory factory(&pool);
auto* descriptor =
pool.FindMessageTypeByName("perfetto.protos.TraceMetrics");
- std::unique_ptr<google::protobuf::Message> metrics(
+ std::unique_ptr<google::protobuf::Message> metric_msg(
factory.GetPrototype(descriptor)->New());
- metrics->ParseFromArray(metric_result.data(),
- static_cast<int>(metric_result.size()));
+ metric_msg->ParseFromArray(metric_result.data(),
+ static_cast<int>(metric_result.size()));
// We need to instantiate field options from dynamic message factory
// because otherwise it cannot parse our custom extensions.
@@ -427,7 +436,7 @@
factory.GetPrototype(
pool.FindMessageTypeByName("google.protobuf.FieldOptions"));
auto out = proto_to_json::MessageToJsonWithAnnotations(
- *metrics, field_options_prototype, 0);
+ *metric_msg, field_options_prototype, 0);
fwrite(out.c_str(), sizeof(char), out.size(), stdout);
break;
}
@@ -708,6 +717,7 @@
bool wide = false;
bool force_full_sort = false;
std::string metatrace_path;
+ bool dev = false;
};
void PrintUsage(char** argv) {
@@ -759,7 +769,12 @@
Loads metric proto and sql files from
DISK_PATH/protos and DISK_PATH/sql
respectively, and mounts them onto
- VIRTUAL_PATH.)",
+ VIRTUAL_PATH.
+ --dev Enables features which are reserved for
+ local development use only and
+ *should not* be enabled on production
+ builds. The features behind this flag can
+ break at any time without any warning.)",
argv[0]);
}
@@ -772,6 +787,7 @@
OPT_FORCE_FULL_SORT,
OPT_HTTP_PORT,
OPT_METRIC_EXTENSION,
+ OPT_DEV,
};
static const option long_options[] = {
@@ -791,6 +807,7 @@
{"full-sort", no_argument, nullptr, OPT_FORCE_FULL_SORT},
{"http-port", required_argument, nullptr, OPT_HTTP_PORT},
{"metric-extension", required_argument, nullptr, OPT_METRIC_EXTENSION},
+ {"dev", no_argument, nullptr, OPT_DEV},
{nullptr, 0, nullptr, 0}};
bool explicit_interactive = false;
@@ -882,6 +899,11 @@
continue;
}
+ if (option == OPT_DEV) {
+ command_line_options.dev = true;
+ continue;
+ }
+
PrintUsage(argv);
exit(option == 'h' ? 0 : 1);
}
@@ -995,7 +1017,8 @@
return util::OkStatus();
}
-base::Status ParseSingleMetricExtensionPath(const std::string& raw_extension,
+base::Status ParseSingleMetricExtensionPath(bool dev,
+ const std::string& raw_extension,
MetricExtension& parsed_extension) {
// We cannot easily use ':' as a path separator because windows paths can have
// ':' in them (e.g. C:\foo\bar).
@@ -1008,6 +1031,15 @@
parsed_extension.SetDiskPath(std::move(parts[0]));
parsed_extension.SetVirtualPath(std::move(parts[1]));
+ if (parsed_extension.virtual_path() == "/") {
+ if (!dev) {
+ return base::ErrStatus(
+ "Local development features must be enabled (using the "
+ "--dev flag) to override built-in metrics");
+ }
+ parsed_extension.SetVirtualPath("");
+ }
+
if (parsed_extension.virtual_path() == "shell/") {
return base::Status(
"Cannot have 'shell/' as metric extension virtual path.");
@@ -1037,11 +1069,12 @@
}
base::Status ParseMetricExtensionPaths(
+ bool dev,
const std::vector<std::string>& raw_metric_extensions,
std::vector<MetricExtension>& metric_extensions) {
for (const auto& raw_extension : raw_metric_extensions) {
metric_extensions.push_back({});
- RETURN_IF_ERROR(ParseSingleMetricExtensionPath(raw_extension,
+ RETURN_IF_ERROR(ParseSingleMetricExtensionPath(dev, raw_extension,
metric_extensions.back()));
}
return CheckForDuplicateMetricExtension(metric_extensions);
@@ -1123,13 +1156,9 @@
return base::OkStatus();
}
-util::Status RunMetrics(const CommandLineOptions& options,
- std::vector<MetricExtension>& metric_extensions) {
- // Descriptor pool used for printing output as textproto. Building on top of
- // generated pool so default protos in google.protobuf.descriptor.proto are
- // available.
- google::protobuf::DescriptorPool pool(
- google::protobuf::DescriptorPool::generated_pool());
+util::Status PopulateDescriptorPool(
+ google::protobuf::DescriptorPool& pool,
+ const std::vector<MetricExtension>& metric_extensions) {
// TODO(b/182165266): There is code duplication here with trace_processor_impl
// SetupMetrics. This will be removed when we switch the output formatter to
// use internal DescriptorPool.
@@ -1143,65 +1172,100 @@
ExtendPoolWithBinaryDescriptor(pool, kAllChromeMetricsDescriptor.data(),
kAllChromeMetricsDescriptor.size(),
skip_prefixes);
+ return base::OkStatus();
+}
- std::vector<std::string> metrics;
- for (base::StringSplitter ss(options.metric_names, ','); ss.Next();) {
- metrics.emplace_back(ss.cur_token());
+util::Status LoadMetrics(const std::string& raw_metric_names,
+ google::protobuf::DescriptorPool& pool,
+ std::vector<MetricNameAndPath>& name_and_path) {
+ std::vector<std::string> split;
+ for (base::StringSplitter ss(raw_metric_names, ','); ss.Next();) {
+ split.emplace_back(ss.cur_token());
}
// For all metrics which are files, register them and extend the metrics
// proto.
- for (size_t i = 0; i < metrics.size(); ++i) {
- const std::string& metric_or_path = metrics[i];
-
+ for (const std::string& metric_or_path : split) {
// If there is no extension, we assume it is a builtin metric.
auto ext_idx = metric_or_path.rfind('.');
- if (ext_idx == std::string::npos)
+ if (ext_idx == std::string::npos) {
+ name_and_path.emplace_back(
+ MetricNameAndPath{metric_or_path, base::nullopt});
continue;
+ }
- std::string no_ext_name = metric_or_path.substr(0, ext_idx);
+ std::string no_ext_path = metric_or_path.substr(0, ext_idx);
// The proto must be extended before registering the metric.
- util::Status status = ExtendMetricsProto(no_ext_name + ".proto", &pool);
+ util::Status status = ExtendMetricsProto(no_ext_path + ".proto", &pool);
if (!status.ok()) {
return util::ErrStatus("Unable to extend metrics proto %s: %s",
metric_or_path.c_str(), status.c_message());
}
- status = RegisterMetric(no_ext_name + ".sql");
+ status = RegisterMetric(no_ext_path + ".sql");
if (!status.ok()) {
return util::ErrStatus("Unable to register metric %s: %s",
metric_or_path.c_str(), status.c_message());
}
+ name_and_path.emplace_back(
+ MetricNameAndPath{BaseName(no_ext_path), no_ext_path});
+ }
+ return base::OkStatus();
+}
- metrics[i] = BaseName(no_ext_name);
+OutputFormat ParseOutputFormat(const CommandLineOptions& options) {
+ if (!options.query_file_path.empty())
+ return OutputFormat::kNone;
+ if (options.metric_output == "binary")
+ return OutputFormat::kBinaryProto;
+ if (options.metric_output == "json")
+ return OutputFormat::kJson;
+ return OutputFormat::kTextProto;
+}
+
+base::Status LoadMetricsAndExtensionsSql(
+ const std::vector<MetricNameAndPath>& metrics,
+ const std::vector<MetricExtension>& extensions) {
+ for (const MetricExtension& extension : extensions) {
+ const std::string& disk_path = extension.disk_path();
+ const std::string& virtual_path = extension.virtual_path();
+
+ RETURN_IF_ERROR(LoadMetricExtensionSql(disk_path + "sql/", virtual_path));
}
- OutputFormat format;
- if (!options.query_file_path.empty()) {
- format = OutputFormat::kNone;
- } else if (options.metric_output == "binary") {
- format = OutputFormat::kBinaryProto;
- } else if (options.metric_output == "json") {
- format = OutputFormat::kJson;
- } else {
- format = OutputFormat::kTextProto;
+ for (const MetricNameAndPath& metric : metrics) {
+ // Ignore builtin metrics.
+ if (!metric.no_ext_path.has_value())
+ continue;
+ RETURN_IF_ERROR(RegisterMetric(metric.no_ext_path.value() + ".sql"));
}
-
- return RunMetrics(std::move(metrics), format, pool);
+ return base::OkStatus();
}
void PrintShellUsage() {
PERFETTO_ELOG(
"Available commands:\n"
- ".quit, .q Exit the shell.\n"
- ".help This text.\n"
- ".dump FILE Export the trace as a sqlite database.\n"
- ".read FILE Executes the queries in the FILE.\n"
- ".reset Destroys all tables/view created by the user.\n");
+ ".quit, .q Exit the shell.\n"
+ ".help This text.\n"
+ ".dump FILE Export the trace as a sqlite database.\n"
+ ".read FILE Executes the queries in the FILE.\n"
+ ".reset Destroys all tables/view created by the user.\n"
+ ".load-metrics-sql Reloads SQL from extension and custom metric paths\n"
+ " specified in command line args.\n"
+ ".run-metrics Runs metrics specified in command line args\n"
+ " and prints the result.\n");
}
-util::Status StartInteractiveShell(uint32_t column_width) {
+struct InteractiveOptions {
+ uint32_t column_width;
+ OutputFormat metric_format;
+ std::vector<MetricExtension> extensions;
+ std::vector<MetricNameAndPath> metrics;
+ const google::protobuf::DescriptorPool* pool;
+};
+
+util::Status StartInteractiveShell(const InteractiveOptions& options) {
SetupLineEditor();
for (;;) {
@@ -1230,6 +1294,23 @@
if (!status.ok()) {
PERFETTO_ELOG("%s", status.c_message());
}
+ } else if (strcmp(command, "load-metrics-sql") == 0) {
+ base::Status status =
+ LoadMetricsAndExtensionsSql(options.metrics, options.extensions);
+ if (!status.ok()) {
+ PERFETTO_ELOG("%s", status.c_message());
+ }
+ } else if (strcmp(command, "run-metrics") == 0) {
+ if (options.metrics.empty()) {
+ PERFETTO_ELOG("No metrics specified on command line");
+ continue;
+ }
+
+ base::Status status =
+ RunMetrics(options.metrics, options.metric_format, *options.pool);
+ if (!status.ok()) {
+ PERFETTO_ELOG("%s", status.c_message());
+ }
} else {
PrintShellUsage();
}
@@ -1238,7 +1319,7 @@
base::TimeNanos t_start = base::GetWallTimeNs();
auto it = g_tp->ExecuteQuery(line.get());
- PrintQueryResultInteractively(&it, t_start, column_width);
+ PrintQueryResultInteractively(&it, t_start, options.column_width);
}
return util::OkStatus();
}
@@ -1252,8 +1333,8 @@
: SortingMode::kDefaultHeuristics;
std::vector<MetricExtension> metric_extensions;
- RETURN_IF_ERROR(ParseMetricExtensionPaths(options.raw_metric_extensions,
- metric_extensions));
+ RETURN_IF_ERROR(ParseMetricExtensionPaths(
+ options.dev, options.raw_metric_extensions, metric_extensions));
for (const auto& extension : metric_extensions) {
config.skip_builtin_metric_paths.push_back(extension.virtual_path());
@@ -1282,8 +1363,8 @@
t_load = base::GetWallTimeNs() - t_load_start;
double t_load_s = static_cast<double>(t_load.count()) / 1E9;
- PERFETTO_ILOG("Trace loaded: %.2f MB (%.1f MB/s)", size_mb,
- size_mb / t_load_s);
+ PERFETTO_ILOG("Trace loaded: %.2f MB in %.2fs (%.1f MB/s)", size_mb,
+ t_load_s, size_mb / t_load_s);
RETURN_IF_ERROR(PrintStats());
}
@@ -1304,8 +1385,23 @@
RETURN_IF_ERROR(RunQueries(options.pre_metrics_path, false));
}
+ // Descriptor pool used for printing output as textproto. Building on top of
+ // generated pool so default protos in google.protobuf.descriptor.proto are
+ // available.
+ // For some insane reason, the descriptor pool is not movable so we need to
+ // create it here so we can create references and pass it everywhere.
+ google::protobuf::DescriptorPool pool(
+ google::protobuf::DescriptorPool::generated_pool());
+ RETURN_IF_ERROR(PopulateDescriptorPool(pool, metric_extensions));
+
+ std::vector<MetricNameAndPath> metrics;
if (!options.metric_names.empty()) {
- RETURN_IF_ERROR(RunMetrics(options, metric_extensions));
+ RETURN_IF_ERROR(LoadMetrics(options.metric_names, pool, metrics));
+ }
+
+ OutputFormat metric_format = ParseOutputFormat(options);
+ if (!metrics.empty()) {
+ RETURN_IF_ERROR(RunMetrics(metrics, metric_format, pool));
}
if (!options.query_file_path.empty()) {
@@ -1318,7 +1414,9 @@
}
if (options.launch_shell) {
- RETURN_IF_ERROR(StartInteractiveShell(options.wide ? 40 : 20));
+ RETURN_IF_ERROR(StartInteractiveShell(
+ InteractiveOptions{options.wide ? 40u : 20u, metric_format,
+ metric_extensions, metrics, &pool}));
} else if (!options.perf_file_path.empty()) {
RETURN_IF_ERROR(PrintPerfFile(options.perf_file_path, t_load, t_query));
}
diff --git a/src/trace_processor/types/trace_processor_context.h b/src/trace_processor/types/trace_processor_context.h
index 1cf723c..4bc0e2b 100644
--- a/src/trace_processor/types/trace_processor_context.h
+++ b/src/trace_processor/types/trace_processor_context.h
@@ -48,7 +48,6 @@
class TraceSorter;
class TraceStorage;
class TrackTracker;
-class JsonTracker;
class DescriptorPool;
class TraceProcessorContext {
@@ -92,7 +91,6 @@
std::unique_ptr<Destructible> binder_tracker; // BinderTracker
std::unique_ptr<Destructible> systrace_parser; // SystraceParser
std::unique_ptr<Destructible> heap_graph_tracker; // HeapGraphTracker
- std::unique_ptr<Destructible> json_tracker; // JsonTracker
std::unique_ptr<Destructible> system_info_tracker; // SystemInfoTracker
// These fields are trace readers which will be called by |forwarding_parser|
diff --git a/src/trace_processor/util/interned_message_view.h b/src/trace_processor/util/interned_message_view.h
index 4f5b993..3422b8b 100644
--- a/src/trace_processor/util/interned_message_view.h
+++ b/src/trace_processor/util/interned_message_view.h
@@ -17,10 +17,9 @@
#ifndef SRC_TRACE_PROCESSOR_UTIL_INTERNED_MESSAGE_VIEW_H_
#define SRC_TRACE_PROCESSOR_UTIL_INTERNED_MESSAGE_VIEW_H_
+#include "perfetto/ext/base/flat_hash_map.h"
#include "perfetto/trace_processor/trace_blob_view.h"
-#include <unordered_map>
-
namespace perfetto {
namespace trace_processor {
@@ -49,7 +48,7 @@
this->message_ = view.message_.copy();
this->decoder_ = nullptr;
this->decoder_type_ = nullptr;
- this->submessages_.clear();
+ this->submessages_.Clear();
return *this;
}
@@ -87,9 +86,9 @@
// TODO(eseckler): Support repeated fields.
template <typename MessageType, uint32_t FieldId>
InternedMessageView* GetOrCreateSubmessageView() {
- auto it = submessages_.find(FieldId);
- if (it != submessages_.end())
- return it->second.get();
+ auto it_and_ins = submessages_.Insert(FieldId, nullptr);
+ if (!it_and_ins.second)
+ return it_and_ins.first->get();
auto* decoder = GetOrCreateDecoder<MessageType>();
// Calls the at() template method on the decoder.
auto field = decoder->template at<FieldId>().as_bytes();
@@ -98,8 +97,7 @@
TraceBlobView submessage = message_.slice(field.data, field.size);
InternedMessageView* submessage_view =
new InternedMessageView(std::move(submessage));
- submessages_.emplace_hint(
- it, FieldId, std::unique_ptr<InternedMessageView>(submessage_view));
+ it_and_ins.first->reset(submessage_view);
return submessage_view;
}
@@ -107,8 +105,8 @@
private:
using SubMessageViewMap =
- std::unordered_map<uint32_t /*field_id*/,
- std::unique_ptr<InternedMessageView>>;
+ base::FlatHashMap<uint32_t /*field_id*/,
+ std::unique_ptr<InternedMessageView>>;
TraceBlobView message_;
diff --git a/src/trace_processor/util/proto_to_args_parser.cc b/src/trace_processor/util/proto_to_args_parser.cc
index a17b5dc..8e3e8d1 100644
--- a/src/trace_processor/util/proto_to_args_parser.cc
+++ b/src/trace_processor/util/proto_to_args_parser.cc
@@ -79,9 +79,11 @@
const protozero::ConstBytes& cb,
const std::string& type,
const std::vector<uint16_t>* allowed_fields,
- Delegate& delegate) {
+ Delegate& delegate,
+ int* unknown_extensions) {
ScopedNestedKeyContext key_context(key_prefix_);
- return ParseMessageInternal(key_context, cb, type, allowed_fields, delegate);
+ return ParseMessageInternal(key_context, cb, type, allowed_fields, delegate,
+ unknown_extensions);
}
base::Status ProtoToArgsParser::ParseMessageInternal(
@@ -89,7 +91,8 @@
const protozero::ConstBytes& cb,
const std::string& type,
const std::vector<uint16_t>* allowed_fields,
- Delegate& delegate) {
+ Delegate& delegate,
+ int* unknown_extensions) {
if (auto override_result =
MaybeApplyOverrideForType(type, key_context, cb, delegate)) {
return override_result.value();
@@ -112,6 +115,9 @@
empty_message = false;
auto field = descriptor.FindFieldByTag(f.id());
if (!field) {
+ if (unknown_extensions != nullptr) {
+ (*unknown_extensions)++;
+ }
// Unknown field, possibly an unknown extension.
continue;
}
@@ -127,8 +133,8 @@
// reflected.
continue;
}
- RETURN_IF_ERROR(
- ParseField(*field, repeated_field_index[f.id()], f, delegate));
+ RETURN_IF_ERROR(ParseField(*field, repeated_field_index[f.id()], f,
+ delegate, unknown_extensions));
if (field->is_repeated()) {
repeated_field_index[f.id()]++;
}
@@ -145,7 +151,8 @@
const FieldDescriptor& field_descriptor,
int repeated_field_number,
protozero::Field field,
- Delegate& delegate) {
+ Delegate& delegate,
+ int* unknown_extensions) {
std::string prefix_part = field_descriptor.name();
if (field_descriptor.is_repeated()) {
std::string number = std::to_string(repeated_field_number);
@@ -176,7 +183,7 @@
protos::pbzero::FieldDescriptorProto::TYPE_MESSAGE) {
return ParseMessageInternal(key_context, field.as_bytes(),
field_descriptor.resolved_type_name(), nullptr,
- delegate);
+ delegate, unknown_extensions);
}
return ParseSimpleField(field_descriptor, field, delegate);
diff --git a/src/trace_processor/util/proto_to_args_parser.h b/src/trace_processor/util/proto_to_args_parser.h
index 2ec4f1c..2329edb 100644
--- a/src/trace_processor/util/proto_to_args_parser.h
+++ b/src/trace_processor/util/proto_to_args_parser.h
@@ -136,7 +136,8 @@
base::Status ParseMessage(const protozero::ConstBytes& cb,
const std::string& type,
const std::vector<uint16_t>* allowed_fields,
- Delegate& delegate);
+ Delegate& delegate,
+ int* unknown_extensions = nullptr);
// This class is responsible for resetting the current key prefix to the old
// value when deleted or reset.
@@ -237,7 +238,8 @@
base::Status ParseField(const FieldDescriptor& field_descriptor,
int repeated_field_number,
protozero::Field field,
- Delegate& delegate);
+ Delegate& delegate,
+ int* unknown_extensions);
base::Optional<base::Status> MaybeApplyOverrideForField(
const protozero::Field&,
@@ -255,7 +257,8 @@
const protozero::ConstBytes& cb,
const std::string& type,
const std::vector<uint16_t>* fields,
- Delegate& delegate);
+ Delegate& delegate,
+ int* unknown_extensions);
base::Status ParseSimpleField(const FieldDescriptor& desciptor,
const protozero::Field& field,
diff --git a/src/tracebox/BUILD.gn b/src/tracebox/BUILD.gn
index 647ad5e..9000732 100644
--- a/src/tracebox/BUILD.gn
+++ b/src/tracebox/BUILD.gn
@@ -24,6 +24,7 @@
"../perfetto_cmd:trigger_perfetto_cmd",
"../traced/probes",
"../traced/service",
+ "../websocket_bridge:lib",
]
sources = [ "tracebox.cc" ]
}
diff --git a/src/tracebox/tracebox.cc b/src/tracebox/tracebox.cc
index ed75b31..a421b97 100644
--- a/src/tracebox/tracebox.cc
+++ b/src/tracebox/tracebox.cc
@@ -20,6 +20,7 @@
#include "perfetto/ext/base/utils.h"
#include "perfetto/ext/traced/traced.h"
#include "src/perfetto_cmd/perfetto_cmd.h"
+#include "src/websocket_bridge/websocket_bridge.h"
#include <stdio.h>
@@ -39,6 +40,7 @@
{"traced_probes", ProbesMain},
{"perfetto", PerfettoCmdMain},
{"trigger_perfetto", TriggerPerfettoMain},
+ {"websocket_bridge", WebsocketBridgeMain},
};
void PrintUsage() {
diff --git a/src/traced/probes/BUILD.gn b/src/traced/probes/BUILD.gn
index db547fc..4c4b65a 100644
--- a/src/traced/probes/BUILD.gn
+++ b/src/traced/probes/BUILD.gn
@@ -101,6 +101,7 @@
"filesystem:unittests",
"initial_display_state:unittests",
"packages_list:unittests",
+ "power:unittests",
"ps:unittests",
"sys_stats:unittests",
"system_info:unittests",
diff --git a/src/traced/probes/ftrace/cpu_reader.cc b/src/traced/probes/ftrace/cpu_reader.cc
index 10e9788..6e09c01 100644
--- a/src/traced/probes/ftrace/cpu_reader.cc
+++ b/src/traced/probes/ftrace/cpu_reader.cc
@@ -28,6 +28,7 @@
#include "perfetto/ext/base/crash_keys.h"
#include "perfetto/ext/base/metatrace.h"
#include "perfetto/ext/base/optional.h"
+#include "perfetto/ext/base/string_splitter.h"
#include "perfetto/ext/base/string_utils.h"
#include "perfetto/ext/base/utils.h"
#include "perfetto/ext/tracing/core/trace_writer.h"
@@ -141,6 +142,16 @@
return fcntl(fd, F_SETFL, flags) == 0;
}
+void LogInvalidPage(const void* start, size_t size) {
+ PERFETTO_ELOG("Invalid ftrace page");
+ std::string hexdump = base::HexDump(start, size);
+ // Only a single line per log message, because log message size might be
+ // limited.
+ for (base::StringSplitter ss(std::move(hexdump), '\n'); ss.Next();) {
+ PERFETTO_ELOG("%s", ss.cur_token());
+ }
+}
+
} // namespace
using protos::pbzero::GenericFtraceEvent;
@@ -411,6 +422,7 @@
if (!page_header.has_value() || page_header->size == 0 ||
parse_pos >= curr_page_end ||
parse_pos + page_header->size > curr_page_end) {
+ LogInvalidPage(curr_page, base::kPageSize);
PERFETTO_DFATAL("invalid page header");
return false;
}
@@ -436,6 +448,7 @@
if (evt_size != page_header->size) {
pages_parsed_ok = false;
+ LogInvalidPage(curr_page, base::kPageSize);
PERFETTO_DFATAL("could not parse ftrace page");
}
}
diff --git a/src/traced/probes/ftrace/event_info.cc b/src/traced/probes/ftrace/event_info.cc
index 9d6ac9d..dbb9364 100644
--- a/src/traced/probes/ftrace/event_info.cc
+++ b/src/traced/probes/ftrace/event_info.cc
@@ -6481,6 +6481,25 @@
kUnsetFtraceId,
40,
kUnsetSize},
+ {"rss_stat_throttled",
+ "synthetic",
+ {
+ {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+ "curr", 1, ProtoSchemaType::kUint32,
+ TranslationStrategy::kInvalidTranslationStrategy},
+ {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+ "member", 2, ProtoSchemaType::kInt32,
+ TranslationStrategy::kInvalidTranslationStrategy},
+ {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+ "mm_id", 3, ProtoSchemaType::kUint32,
+ TranslationStrategy::kInvalidTranslationStrategy},
+ {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+ "size", 4, ProtoSchemaType::kInt64,
+ TranslationStrategy::kInvalidTranslationStrategy},
+ },
+ kUnsetFtraceId,
+ 359,
+ kUnsetSize},
{"0",
"systrace",
{
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.cc b/src/traced/probes/ftrace/ftrace_config_muxer.cc
index a19564f..4ebd30f 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.cc
@@ -93,6 +93,12 @@
*out = std::move(v);
}
+bool SupportsRssStatThrottled(const FtraceProcfs& ftrace_procfs) {
+ const auto trigger_info = ftrace_procfs.ReadEventTrigger("kmem", "rss_stat");
+
+ return trigger_info.find("rss_stat_throttled") != std::string::npos;
+}
+
// This is just to reduce binary size and stack frame size of the insertions.
// It effectively undoes STL's set::insert inlining.
void PERFETTO_NO_INLINE InsertEvent(const char* group,
@@ -407,13 +413,19 @@
}
if (category == "memory") {
- InsertEvent("kmem", "rss_stat", &events);
+ // Use rss_stat_throttled if supported
+ if (SupportsRssStatThrottled(*ftrace_)) {
+ InsertEvent("synthetic", "rss_stat_throttled", &events);
+ } else {
+ InsertEvent("kmem", "rss_stat", &events);
+ }
InsertEvent("kmem", "ion_heap_grow", &events);
InsertEvent("kmem", "ion_heap_shrink", &events);
// ion_stat supersedes ion_heap_grow / shrink for kernel 4.19+
InsertEvent("ion", "ion_stat", &events);
InsertEvent("mm_event", "mm_event_record", &events);
InsertEvent("dmabuf_heap", "dma_heap_stat", &events);
+ InsertEvent("gpu_mem", "gpu_mem_total", &events);
continue;
}
@@ -424,6 +436,20 @@
}
}
}
+
+ // If throttle_rss_stat: true, use the rss_stat_throttled event if supported
+ if (request.throttle_rss_stat() && SupportsRssStatThrottled(*ftrace_)) {
+ auto it = std::find_if(
+ events.begin(), events.end(), [](const GroupAndName& event) {
+ return event.group() == "kmem" && event.name() == "rss_stat";
+ });
+
+ if (it != events.end()) {
+ events.erase(it);
+ InsertEvent("synthetic", "rss_stat_throttled", &events);
+ }
+ }
+
return events;
}
diff --git a/src/traced/probes/ftrace/ftrace_procfs.cc b/src/traced/probes/ftrace/ftrace_procfs.cc
index c6d21d5..e393648 100644
--- a/src/traced/probes/ftrace/ftrace_procfs.cc
+++ b/src/traced/probes/ftrace/ftrace_procfs.cc
@@ -130,6 +130,12 @@
return ReadFileIntoString(path);
}
+std::string FtraceProcfs::ReadEventTrigger(const std::string& group,
+ const std::string& name) const {
+ std::string path = root_ + "events/" + group + "/" + name + "/trigger";
+ return ReadFileIntoString(path);
+}
+
std::string FtraceProcfs::ReadPrintkFormats() const {
std::string path = root_ + "printk_formats";
return ReadFileIntoString(path);
diff --git a/src/traced/probes/ftrace/ftrace_procfs.h b/src/traced/probes/ftrace/ftrace_procfs.h
index 81d3063..6e0791b 100644
--- a/src/traced/probes/ftrace/ftrace_procfs.h
+++ b/src/traced/probes/ftrace/ftrace_procfs.h
@@ -58,6 +58,10 @@
virtual std::string ReadPageHeaderFormat() const;
+ // Read the triggers for event with the given |group| and |name|.
+ std::string ReadEventTrigger(const std::string& group,
+ const std::string& name) const;
+
// Read the printk formats file.
std::string ReadPrintkFormats() const;
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/synthetic/rss_stat_throttled/format b/src/traced/probes/ftrace/test/data/synthetic/events/synthetic/rss_stat_throttled/format
new file mode 100644
index 0000000..1b8185f
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/synthetic/rss_stat_throttled/format
@@ -0,0 +1,14 @@
+name: rss_stat_throttled
+ID: 1471
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:unsigned int mm_id; offset:8; size:4; signed:0;
+ field:unsigned int curr; offset:16; size:4; signed:0;
+ field:int member; offset:24; size:4; signed:1;
+ field:long size; offset:32; size:8; signed:1;
+
+print fmt: "mm_id=%u, curr=%u, member=%d, size=%ld", REC->mm_id, REC->curr, REC->member, REC->size
diff --git a/src/traced/probes/power/BUILD.gn b/src/traced/probes/power/BUILD.gn
index fc28f67..0131a92 100644
--- a/src/traced/probes/power/BUILD.gn
+++ b/src/traced/probes/power/BUILD.gn
@@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import("../../../../gn/test.gni")
+
source_set("power") {
public_deps = [ "../../../tracing/core" ]
deps = [
@@ -28,5 +30,19 @@
sources = [
"android_power_data_source.cc",
"android_power_data_source.h",
+ "linux_power_sysfs_data_source.cc",
+ "linux_power_sysfs_data_source.h",
]
}
+
+perfetto_unittest_source_set("unittests") {
+ testonly = true
+ deps = [
+ ":power",
+ "../../../../gn:default_deps",
+ "../../../../gn:gtest_and_gmock",
+ "../../../../src/tracing/test:test_support",
+ "../../../base:test_support",
+ ]
+ sources = [ "linux_power_sysfs_data_source_unittest.cc" ]
+}
diff --git a/src/traced/probes/power/linux_power_sysfs_data_source.cc b/src/traced/probes/power/linux_power_sysfs_data_source.cc
new file mode 100644
index 0000000..ff1e8b4
--- /dev/null
+++ b/src/traced/probes/power/linux_power_sysfs_data_source.cc
@@ -0,0 +1,173 @@
+/*
+ * 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/traced/probes/power/linux_power_sysfs_data_source.h"
+
+#include <dirent.h>
+#include <sys/types.h>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/task_runner.h"
+#include "perfetto/base/time.h"
+#include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/optional.h"
+#include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/tracing/core/trace_packet.h"
+#include "perfetto/ext/tracing/core/trace_writer.h"
+#include "perfetto/tracing/core/data_source_config.h"
+
+#include "protos/perfetto/trace/power/battery_counters.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto {
+
+namespace {
+constexpr uint32_t kDefaultPollIntervalMs = 1000;
+
+base::Optional<int64_t> ReadFileAsInt64(std::string path) {
+ std::string buf;
+ if (!base::ReadFile(path, &buf))
+ return base::nullopt;
+ return base::StringToInt64(base::StripSuffix(buf, "\n"));
+}
+} // namespace
+
+LinuxPowerSysfsDataSource::BatteryInfo::BatteryInfo(
+ const char* power_supply_dir_path) {
+ base::ScopedDir power_supply_dir(opendir(power_supply_dir_path));
+ if (!power_supply_dir)
+ return;
+
+ for (auto* ent = readdir(power_supply_dir.get()); ent;
+ ent = readdir(power_supply_dir.get())) {
+ if (ent->d_name[0] == '.')
+ continue;
+ std::string dir_name =
+ std::string(power_supply_dir_path) + "/" + ent->d_name;
+ std::string buf;
+ if (!base::ReadFile(dir_name + "/type", &buf) ||
+ base::StripSuffix(buf, "\n") != "Battery")
+ continue;
+ buf.clear();
+ if (!base::ReadFile(dir_name + "/present", &buf) ||
+ base::StripSuffix(buf, "\n") != "1")
+ continue;
+ sysfs_battery_dirs_.push_back(dir_name);
+ }
+}
+LinuxPowerSysfsDataSource::BatteryInfo::~BatteryInfo() = default;
+
+size_t LinuxPowerSysfsDataSource::BatteryInfo::num_batteries() const {
+ return sysfs_battery_dirs_.size();
+}
+
+base::Optional<int64_t>
+LinuxPowerSysfsDataSource::BatteryInfo::GetChargeCounterUah(
+ size_t battery_idx) {
+ PERFETTO_CHECK(battery_idx < sysfs_battery_dirs_.size());
+ return ReadFileAsInt64(sysfs_battery_dirs_[battery_idx] + "/charge_now");
+}
+
+base::Optional<int64_t>
+LinuxPowerSysfsDataSource::BatteryInfo::GetCapacityPercent(size_t battery_idx) {
+ PERFETTO_CHECK(battery_idx < sysfs_battery_dirs_.size());
+ return ReadFileAsInt64(sysfs_battery_dirs_[battery_idx] + "/capacity");
+}
+
+base::Optional<int64_t> LinuxPowerSysfsDataSource::BatteryInfo::GetCurrentNowUa(
+ size_t battery_idx) {
+ PERFETTO_CHECK(battery_idx < sysfs_battery_dirs_.size());
+ return ReadFileAsInt64(sysfs_battery_dirs_[battery_idx] + "/current_now");
+}
+
+base::Optional<int64_t>
+LinuxPowerSysfsDataSource::BatteryInfo::GetAverageCurrentUa(
+ size_t battery_idx) {
+ PERFETTO_CHECK(battery_idx < sysfs_battery_dirs_.size());
+ return ReadFileAsInt64(sysfs_battery_dirs_[battery_idx] + "/current_avg");
+}
+
+// static
+const ProbesDataSource::Descriptor LinuxPowerSysfsDataSource::descriptor = {
+ /*name*/ "linux.sysfs_power",
+ /*flags*/ Descriptor::kFlagsNone,
+};
+
+LinuxPowerSysfsDataSource::LinuxPowerSysfsDataSource(
+ DataSourceConfig cfg,
+ base::TaskRunner* task_runner,
+ TracingSessionID session_id,
+ std::unique_ptr<TraceWriter> writer)
+ : ProbesDataSource(session_id, &descriptor),
+ poll_interval_ms_(kDefaultPollIntervalMs),
+ task_runner_(task_runner),
+ writer_(std::move(writer)),
+ weak_factory_(this) {
+ base::ignore_result(cfg); // The data source doesn't need any config yet.
+}
+
+LinuxPowerSysfsDataSource::~LinuxPowerSysfsDataSource() = default;
+
+void LinuxPowerSysfsDataSource::Start() {
+ battery_info_.reset(new BatteryInfo());
+ Tick();
+}
+
+void LinuxPowerSysfsDataSource::Tick() {
+ // Post next task.
+ auto now_ms = base::GetWallTimeMs().count();
+ auto weak_this = weak_factory_.GetWeakPtr();
+ task_runner_->PostDelayedTask(
+ [weak_this] {
+ if (weak_this)
+ weak_this->Tick();
+ },
+ poll_interval_ms_ - static_cast<uint32_t>(now_ms % poll_interval_ms_));
+
+ WriteBatteryCounters();
+}
+
+void LinuxPowerSysfsDataSource::WriteBatteryCounters() {
+ auto packet = writer_->NewTracePacket();
+ packet->set_timestamp(static_cast<uint64_t>(base::GetBootTimeNs().count()));
+
+ // Query battery counters from sysfs. Report the battery counters for each
+ // battery.
+ for (size_t battery_idx = 0; battery_idx < battery_info_->num_batteries();
+ battery_idx++) {
+ auto* counters_proto = packet->set_battery();
+ auto value = battery_info_->GetChargeCounterUah(battery_idx);
+ if (value)
+ counters_proto->set_charge_counter_uah(*value);
+ value = battery_info_->GetCapacityPercent(battery_idx);
+ if (value)
+ counters_proto->set_capacity_percent(static_cast<float>(*value));
+ value = battery_info_->GetCurrentNowUa(battery_idx);
+ if (value)
+ counters_proto->set_current_ua(*value);
+ value = battery_info_->GetAverageCurrentUa(battery_idx);
+ if (value)
+ counters_proto->set_current_ua(*value);
+ }
+}
+
+void LinuxPowerSysfsDataSource::Flush(FlushRequestID,
+ std::function<void()> callback) {
+ writer_->Flush(callback);
+}
+
+} // namespace perfetto
diff --git a/src/traced/probes/power/linux_power_sysfs_data_source.h b/src/traced/probes/power/linux_power_sysfs_data_source.h
new file mode 100644
index 0000000..9838a15
--- /dev/null
+++ b/src/traced/probes/power/linux_power_sysfs_data_source.h
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+
+#ifndef SRC_TRACED_PROBES_POWER_LINUX_POWER_SYSFS_DATA_SOURCE_H_
+#define SRC_TRACED_PROBES_POWER_LINUX_POWER_SYSFS_DATA_SOURCE_H_
+
+#include "perfetto/ext/base/optional.h"
+#include "perfetto/ext/base/weak_ptr.h"
+#include "perfetto/tracing/core/data_source_config.h"
+#include "src/traced/probes/probes_data_source.h"
+
+namespace perfetto {
+class BatteryInfo;
+class TraceWriter;
+
+namespace base {
+class TaskRunner;
+}
+
+class LinuxPowerSysfsDataSource : public ProbesDataSource {
+ public:
+ class BatteryInfo {
+ public:
+ explicit BatteryInfo(
+ const char* power_supply_dir_path = "/sys/class/power_supply");
+ ~BatteryInfo();
+
+ // The current coloumb counter value in µAh.
+ base::Optional<int64_t> GetChargeCounterUah(size_t battery_idx);
+
+ // The battery capacity in percent.
+ base::Optional<int64_t> GetCapacityPercent(size_t battery_idx);
+
+ // The current reading of the battery in µA.
+ base::Optional<int64_t> GetCurrentNowUa(size_t battery_idx);
+
+ // The smoothed current reading of the battery in µA.
+ base::Optional<int64_t> GetAverageCurrentUa(size_t battery_idx);
+
+ size_t num_batteries() const;
+
+ private:
+ // The subdirectories that contain info of a battery power supply, not
+ // USBPD, AC or other types of power supplies.
+ std::vector<std::string> sysfs_battery_dirs_;
+ };
+ static const ProbesDataSource::Descriptor descriptor;
+
+ LinuxPowerSysfsDataSource(DataSourceConfig,
+ base::TaskRunner*,
+ TracingSessionID,
+ std::unique_ptr<TraceWriter> writer);
+
+ ~LinuxPowerSysfsDataSource() override;
+
+ base::WeakPtr<LinuxPowerSysfsDataSource> GetWeakPtr() const;
+
+ // ProbesDataSource implementation.
+ void Start() override;
+ void Flush(FlushRequestID, std::function<void()> callback) override;
+ // Use the default ClearIncrementalState() implementation: this data source
+ // doesn't have any incremental state.
+
+ private:
+ void Tick();
+ void WriteBatteryCounters();
+
+ uint32_t poll_interval_ms_ = 0;
+
+ base::TaskRunner* const task_runner_;
+ std::unique_ptr<TraceWriter> writer_;
+ std::unique_ptr<BatteryInfo> battery_info_;
+ base::WeakPtrFactory<LinuxPowerSysfsDataSource> weak_factory_; // Keep last.
+};
+
+} // namespace perfetto
+
+#endif // SRC_TRACED_PROBES_POWER_LINUX_POWER_SYSFS_DATA_SOURCE_H_
diff --git a/src/traced/probes/power/linux_power_sysfs_data_source_unittest.cc b/src/traced/probes/power/linux_power_sysfs_data_source_unittest.cc
new file mode 100644
index 0000000..590c926
--- /dev/null
+++ b/src/traced/probes/power/linux_power_sysfs_data_source_unittest.cc
@@ -0,0 +1,68 @@
+/*
+ * 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/traced/probes/power/linux_power_sysfs_data_source.h"
+#include "src/base/test/tmp_dir_tree.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace {
+
+TEST(LinuxPowerSysfsDataSourceTest, BatteryCounters) {
+ base::TmpDirTree tmpdir;
+ std::unique_ptr<LinuxPowerSysfsDataSource::BatteryInfo> battery_info_;
+
+ tmpdir.AddDir("BAT0");
+ tmpdir.AddFile("BAT0/type", "Battery\n");
+ tmpdir.AddFile("BAT0/present", "1\n");
+ tmpdir.AddFile("BAT0/capacity", "95\n"); // 95 percent.
+ tmpdir.AddFile("BAT0/charge_now", "3074000\n"); // 3074000 µAh.
+ tmpdir.AddFile("BAT0/current_now", "245000\n"); // 245000 µA.
+ tmpdir.AddFile("BAT0/current_avg", "240000\n"); // 240000 µA.
+
+ battery_info_.reset(
+ new LinuxPowerSysfsDataSource::BatteryInfo(tmpdir.path().c_str()));
+
+ EXPECT_EQ(battery_info_->num_batteries(), 1u);
+ EXPECT_EQ(*battery_info_->GetCapacityPercent(0), 95);
+ EXPECT_EQ(*battery_info_->GetCurrentNowUa(0), 245000);
+ EXPECT_EQ(*battery_info_->GetAverageCurrentUa(0), 240000);
+ EXPECT_EQ(*battery_info_->GetChargeCounterUah(0), 3074000);
+}
+
+TEST(LinuxPowerSysfsDataSourceTest, HidDeviceCounters) {
+ base::TmpDirTree tmpdir;
+ std::unique_ptr<LinuxPowerSysfsDataSource::BatteryInfo> battery_info_;
+
+ // Some HID devices (e.g. stylus) can also report battery info.
+ tmpdir.AddDir("hid-0001-battery");
+ tmpdir.AddFile("hid-0001-battery/type", "Battery\n");
+ tmpdir.AddFile("hid-0001-battery/present", "1\n");
+ tmpdir.AddFile("hid-0001-battery/capacity", "88\n"); // 88 percent.
+ // The HID device only reports the battery capacity in percent.
+
+ battery_info_.reset(
+ new LinuxPowerSysfsDataSource::BatteryInfo(tmpdir.path().c_str()));
+
+ EXPECT_EQ(battery_info_->num_batteries(), 1u);
+ EXPECT_EQ(*battery_info_->GetCapacityPercent(0), 88);
+ EXPECT_EQ(battery_info_->GetCurrentNowUa(0), base::nullopt);
+ EXPECT_EQ(battery_info_->GetAverageCurrentUa(0), base::nullopt);
+ EXPECT_EQ(battery_info_->GetChargeCounterUah(0), base::nullopt);
+}
+
+} // namespace
+} // namespace perfetto
diff --git a/src/traced/probes/probes_producer.cc b/src/traced/probes/probes_producer.cc
index e33a2a0..b2c4297 100644
--- a/src/traced/probes/probes_producer.cc
+++ b/src/traced/probes/probes_producer.cc
@@ -42,6 +42,7 @@
#include "src/traced/probes/metatrace/metatrace_data_source.h"
#include "src/traced/probes/packages_list/packages_list_data_source.h"
#include "src/traced/probes/power/android_power_data_source.h"
+#include "src/traced/probes/power/linux_power_sysfs_data_source.h"
#include "src/traced/probes/probes_data_source.h"
#include "src/traced/probes/ps/process_stats_data_source.h"
#include "src/traced/probes/sys_stats/sys_stats_data_source.h"
@@ -71,6 +72,7 @@
&InodeFileDataSource::descriptor, //
&SysStatsDataSource::descriptor, //
&AndroidPowerDataSource::descriptor, //
+ &LinuxPowerSysfsDataSource::descriptor, //
&AndroidLogDataSource::descriptor, //
&PackagesListDataSource::descriptor, //
&MetatraceDataSource::descriptor, //
@@ -176,6 +178,8 @@
data_source = CreateSysStatsDataSource(session_id, config);
} else if (config.name() == AndroidPowerDataSource::descriptor.name) {
data_source = CreateAndroidPowerDataSource(session_id, config);
+ } else if (config.name() == LinuxPowerSysfsDataSource::descriptor.name) {
+ data_source = CreateLinuxPowerSysfsDataSource(session_id, config);
} else if (config.name() == AndroidLogDataSource::descriptor.name) {
data_source = CreateAndroidLogDataSource(session_id, config);
} else if (config.name() == PackagesListDataSource::descriptor.name) {
@@ -289,6 +293,16 @@
endpoint_->CreateTraceWriter(buffer_id)));
}
+std::unique_ptr<ProbesDataSource>
+ProbesProducer::CreateLinuxPowerSysfsDataSource(
+ TracingSessionID session_id,
+ const DataSourceConfig& config) {
+ auto buffer_id = static_cast<BufferID>(config.target_buffer());
+ return std::unique_ptr<ProbesDataSource>(
+ new LinuxPowerSysfsDataSource(config, task_runner_, session_id,
+ endpoint_->CreateTraceWriter(buffer_id)));
+}
+
std::unique_ptr<ProbesDataSource> ProbesProducer::CreateAndroidLogDataSource(
TracingSessionID session_id,
const DataSourceConfig& config) {
diff --git a/src/traced/probes/probes_producer.h b/src/traced/probes/probes_producer.h
index dda1759..6650501 100644
--- a/src/traced/probes/probes_producer.h
+++ b/src/traced/probes/probes_producer.h
@@ -87,6 +87,9 @@
std::unique_ptr<ProbesDataSource> CreateAndroidLogDataSource(
TracingSessionID session_id,
const DataSourceConfig& config);
+ std::unique_ptr<ProbesDataSource> CreateLinuxPowerSysfsDataSource(
+ TracingSessionID session_id,
+ const DataSourceConfig& config);
std::unique_ptr<ProbesDataSource> CreatePackagesListDataSource(
TracingSessionID session_id,
const DataSourceConfig& config);
diff --git a/src/tracing/core/trace_buffer.cc b/src/tracing/core/trace_buffer.cc
index 4b72d42..acdcaa9 100644
--- a/src/tracing/core/trace_buffer.cc
+++ b/src/tracing/core/trace_buffer.cc
@@ -27,25 +27,6 @@
#define TRACE_BUFFER_VERBOSE_LOGGING() 0 // Set to 1 when debugging unittests.
#if TRACE_BUFFER_VERBOSE_LOGGING()
#define TRACE_BUFFER_DLOG PERFETTO_DLOG
-namespace {
-constexpr char kHexDigits[] = "0123456789abcdef";
-std::string HexDump(const uint8_t* src, size_t size) {
- std::string buf;
- buf.reserve(4096 * 4);
- char line[64];
- char* c = line;
- for (size_t i = 0; i < size; i++) {
- *c++ = kHexDigits[(src[i] >> 4) & 0x0f];
- *c++ = kHexDigits[(src[i] >> 0) & 0x0f];
- if (i % 16 == 15) {
- buf.append("\n");
- buf.append(line);
- c = line;
- }
- }
- return buf;
-}
-} // namespace
#else
#define TRACE_BUFFER_DLOG(...) void()
#endif
@@ -224,7 +205,8 @@
TRACE_BUFFER_DLOG(" copying @ [%lu - %lu] %zu", wptr - begin(),
uintptr_t(wptr - begin()) + record_size, record_size);
WriteChunkRecord(wptr, record, src, size);
- TRACE_BUFFER_DLOG("Chunk raw: %s", HexDump(wptr, record_size).c_str());
+ TRACE_BUFFER_DLOG("Chunk raw: %s",
+ base::HexDump(wptr, record_size).c_str());
stats_.set_chunks_rewritten(stats_.chunks_rewritten() + 1);
return;
}
@@ -281,7 +263,7 @@
TRACE_BUFFER_DLOG(" copying @ [%lu - %lu] %zu", wptr_ - begin(),
uintptr_t(wptr_ - begin()) + record_size, record_size);
WriteChunkRecord(wptr_, record, src, size);
- TRACE_BUFFER_DLOG("Chunk raw: %s", HexDump(wptr_, record_size).c_str());
+ TRACE_BUFFER_DLOG("Chunk raw: %s", base::HexDump(wptr_, record_size).c_str());
wptr_ += record_size;
if (wptr_ >= end()) {
PERFETTO_DCHECK(padding_size == 0);
@@ -456,7 +438,7 @@
}
TRACE_BUFFER_DLOG(
"Chunk raw (after patch): %s",
- HexDump(chunk_begin, chunk_meta.chunk_record->size).c_str());
+ base::HexDump(chunk_begin, chunk_meta.chunk_record->size).c_str());
stats_.set_patches_succeeded(stats_.patches_succeeded() + patches_size);
if (!other_patches_pending) {
diff --git a/src/tracing/core/tracing_service_impl.cc b/src/tracing/core/tracing_service_impl.cc
index 1121ed9..b898a1a 100644
--- a/src/tracing/core/tracing_service_impl.cc
+++ b/src/tracing/core/tracing_service_impl.cc
@@ -53,6 +53,7 @@
#include "perfetto/base/status.h"
#include "perfetto/base/task_runner.h"
#include "perfetto/ext/base/android_utils.h"
+#include "perfetto/ext/base/crash_keys.h"
#include "perfetto/ext/base/file_utils.h"
#include "perfetto/ext/base/metatrace.h"
#include "perfetto/ext/base/string_utils.h"
@@ -126,6 +127,11 @@
constexpr uint32_t kGuardrailsMaxTracingBufferSizeKb = 128 * 1024;
constexpr uint32_t kGuardrailsMaxTracingDurationMillis = 24 * kMillisPerHour;
+// TODO(primiano): this is to investigate b/191600928. Remove in Jan 2022.
+base::CrashKey g_crash_key_prod_name("producer_name");
+base::CrashKey g_crash_key_ds_count("ds_count");
+base::CrashKey g_crash_key_ds_clear_count("ds_clear_count");
+
#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) || PERFETTO_BUILDFLAG(PERFETTO_OS_NACL)
struct iovec {
void* iov_base; // Address
@@ -1172,7 +1178,7 @@
task_runner_->PostDelayedTask(
[weak_this, tsid] {
if (weak_this)
- weak_this->ReadBuffers(tsid, nullptr);
+ weak_this->ReadBuffersIntoFile(tsid);
},
tracing_session->delay_to_next_write_period_ms());
}
@@ -1592,7 +1598,7 @@
if (tracing_session->write_into_file) {
tracing_session->write_period_ms = 0;
- ReadBuffers(tracing_session->id, nullptr);
+ ReadBuffersIntoFile(tracing_session->id);
}
if (tracing_session->on_disable_callback_for_bugreport) {
@@ -1930,13 +1936,18 @@
// Queue the IPCs to producers with active data sources that opted in.
std::map<ProducerID, std::vector<DataSourceInstanceID>> clear_map;
+ int ds_clear_count = 0;
for (const auto& kv : tracing_session->data_source_instances) {
ProducerID producer_id = kv.first;
const DataSourceInstance& data_source = kv.second;
- if (data_source.handles_incremental_state_clear)
+ if (data_source.handles_incremental_state_clear) {
clear_map[producer_id].push_back(data_source.instance_id);
+ ++ds_clear_count;
+ }
}
+ g_crash_key_ds_clear_count.Set(ds_clear_count);
+
for (const auto& kv : clear_map) {
ProducerID producer_id = kv.first;
const std::vector<DataSourceInstanceID>& data_sources = kv.second;
@@ -1947,44 +1958,25 @@
}
producer->ClearIncrementalState(data_sources);
}
+
+ // ClearIncrementalState internally posts a task for each data source. Clear
+ // the crash key in a task queued at the end of the tasks atove.
+ task_runner_->PostTask([] { g_crash_key_ds_clear_count.Clear(); });
}
-// Note: when this is called to write into a file passed when starting tracing
-// |consumer| will be == nullptr (as opposite to the case of a consumer asking
-// to send the trace data back over IPC).
-bool TracingServiceImpl::ReadBuffers(TracingSessionID tsid,
- ConsumerEndpointImpl* consumer) {
+bool TracingServiceImpl::ReadBuffersIntoConsumer(
+ TracingSessionID tsid,
+ ConsumerEndpointImpl* consumer) {
+ PERFETTO_DCHECK(consumer);
PERFETTO_DCHECK_THREAD(thread_checker_);
TracingSession* tracing_session = GetTracingSession(tsid);
if (!tracing_session) {
- // This will be hit systematically from the PostDelayedTask when directly
- // writing into the file (in which case consumer == nullptr). Suppress the
- // log in this case as it's just spam.
- if (consumer) {
- PERFETTO_DLOG("Cannot ReadBuffers(): no tracing session is active");
- }
- return false;
- }
-
- // When a tracing session is waiting for a trigger it is considered empty. If
- // a tracing session finishes and moves into DISABLED without ever receiving a
- // trigger the trace should never return any data. This includes the synthetic
- // packets like TraceConfig and Clock snapshots. So we bail out early and let
- // the consumer know there is no data.
- if (!tracing_session->config.trigger_config().triggers().empty() &&
- tracing_session->received_triggers.empty() &&
- !tracing_session->seized_for_bugreport) {
PERFETTO_DLOG(
- "ReadBuffers(): tracing session has not received a trigger yet.");
+ "Cannot ReadBuffersIntoConsumer(): no tracing session is active");
return false;
}
- // This can happen if the file is closed by a previous task because it reaches
- // |max_file_size_bytes|.
- if (!tracing_session->write_into_file && !consumer)
- return false;
-
- if (tracing_session->write_into_file && consumer) {
+ if (tracing_session->write_into_file) {
// If the consumer enabled tracing and asked to save the contents into the
// passed file makes little sense to also try to read the buffers over IPC,
// as that would just steal data from the periodic draining task.
@@ -1992,14 +1984,12 @@
return false;
}
- std::vector<TracePacket> packets;
- packets.reserve(1024); // Just an educated guess to avoid trivial expansions.
-
// If a bugreport request happened and the trace was stolen for that, give
// an empty trace with a clear signal to the consumer. This deals only with
// the case of readback-from-IPC. A similar code-path deals with the
// write_into_file case in MaybeSaveTraceForBugreport().
- if (tracing_session->seized_for_bugreport && consumer) {
+ if (tracing_session->seized_for_bugreport) {
+ std::vector<TracePacket> packets;
if (!tracing_session->config.builtin_data_sources()
.disable_service_events()) {
EmitSeizedForBugreportLifecycleEvent(&packets);
@@ -2009,6 +1999,66 @@
return true;
}
+ if (IsWaitingForTrigger(tracing_session))
+ return false;
+
+ return ReadBuffers(tsid, tracing_session, consumer);
+}
+
+bool TracingServiceImpl::ReadBuffersIntoFile(TracingSessionID tsid) {
+ PERFETTO_DCHECK_THREAD(thread_checker_);
+ TracingSession* tracing_session = GetTracingSession(tsid);
+ if (!tracing_session) {
+ // This will be hit systematically from the PostDelayedTask. Avoid logging,
+ // it would be just spam.
+ return false;
+ }
+
+ // This can happen if the file is closed by a previous task because it reaches
+ // |max_file_size_bytes|.
+ if (!tracing_session->write_into_file)
+ return false;
+
+ if (!tracing_session->seized_for_bugreport &&
+ IsWaitingForTrigger(tracing_session))
+ return false;
+
+ return ReadBuffers(tsid, tracing_session, nullptr);
+}
+
+bool TracingServiceImpl::IsWaitingForTrigger(TracingSession* tracing_session) {
+ // When a tracing session is waiting for a trigger, it is considered empty. If
+ // a tracing session finishes and moves into DISABLED without ever receiving a
+ // trigger, the trace should never return any data. This includes the
+ // synthetic packets like TraceConfig and Clock snapshots. So we bail out
+ // early and let the consumer know there is no data.
+ if (!tracing_session->config.trigger_config().triggers().empty() &&
+ tracing_session->received_triggers.empty()) {
+ PERFETTO_DLOG(
+ "ReadBuffers(): tracing session has not received a trigger yet.");
+ return true;
+ }
+ return false;
+}
+
+// Note: when this is called to write into a file passed when starting tracing
+// |consumer| will be == nullptr (as opposite to the case of a consumer asking
+// to send the trace data back over IPC).
+bool TracingServiceImpl::ReadBuffers(TracingSessionID tsid,
+ TracingSession* tracing_session,
+ ConsumerEndpointImpl* consumer) {
+ PERFETTO_DCHECK_THREAD(thread_checker_);
+ PERFETTO_DCHECK(tracing_session);
+
+ // Speculative fix for the memory watchdog crash in b/195145848. This function
+ // uses the heap extensively and might need a M_PURGE. window.gc() is back.
+ // TODO(primiano): if this fixes the crash we might want to coalesce the purge
+ // and throttle it.
+ auto on_ret = base::OnScopeExit([] { base::MaybeReleaseAllocatorMemToOS(); });
+
+ std::vector<TracePacket> packets;
+ packets.reserve(1024); // Just an educated guess to avoid trivial expansions.
+
if (!tracing_session->initial_clock_snapshot.empty()) {
EmitClockSnapshot(tracing_session,
std::move(tracing_session->initial_clock_snapshot),
@@ -2040,12 +2090,10 @@
EmitLifecycleEvents(tracing_session, &packets);
size_t packets_bytes = 0; // SUM(slice.size() for each slice in |packets|).
- size_t total_slices = 0; // SUM(#slices in |packets|).
// Add up size for packets added by the Maybe* calls above.
for (const TracePacket& packet : packets) {
packets_bytes += packet.size();
- total_slices += packet.slices().size();
}
// This is a rough threshold to determine how much to read from the buffer in
@@ -2116,7 +2164,6 @@
// Append the packet (inclusive of the trusted uid) to |packets|.
packets_bytes += packet.size();
- total_slices += packet.slices().size();
did_hit_threshold = packets_bytes >= kApproxBytesPerTask &&
!tracing_session->write_into_file;
packets.emplace_back(std::move(packet));
@@ -2125,7 +2172,6 @@
const bool has_more = did_hit_threshold;
- size_t prev_packets_size = packets.size();
if (!tracing_session->config.builtin_data_sources()
.disable_service_events()) {
// We don't bother snapshotting clocks here because we wouldn't be able to
@@ -2148,12 +2194,6 @@
tracing_session->should_emit_stats = false;
}
- // Add sizes of packets emitted by the EmitLifecycleEvents + EmitStats.
- for (size_t i = prev_packets_size; i < packets.size(); ++i) {
- packets_bytes += packets[i].size();
- total_slices += packets[i].slices().size();
- }
-
// +-------------------------------------------------------------------------+
// | NO MORE CHANGES TO |packets| AFTER THIS POINT. |
// +-------------------------------------------------------------------------+
@@ -2205,13 +2245,17 @@
? tracing_session->max_file_size_bytes
: std::numeric_limits<size_t>::max();
+ size_t total_slices = 0;
+ for (const TracePacket& packet : packets) {
+ total_slices += packet.slices().size();
+ }
// When writing into a file, the file should look like a root trace.proto
// message. Each packet should be prepended with a proto preamble stating
// its field id (within trace.proto) and size. Hence the addition below.
const size_t max_iovecs = total_slices + packets.size();
size_t num_iovecs = 0;
- bool stop_writing_into_file = tracing_session->write_period_ms == 0;
+ bool stop_writing_into_file = false;
std::unique_ptr<struct iovec[]> iovecs(new struct iovec[max_iovecs]);
size_t num_iovecs_at_last_packet = 0;
uint64_t bytes_about_to_be_written = 0;
@@ -2261,7 +2305,7 @@
PERFETTO_DLOG("Draining into file, written: %" PRIu64 " KB, stop: %d",
(total_wr_size + 1023) / 1024, stop_writing_into_file);
- if (stop_writing_into_file) {
+ if (stop_writing_into_file || tracing_session->write_period_ms == 0) {
// Ensure all data was written to the file before we close it.
base::FlushFile(fd);
tracing_session->write_into_file.reset();
@@ -2275,7 +2319,7 @@
task_runner_->PostDelayedTask(
[weak_this, tsid] {
if (weak_this)
- weak_this->ReadBuffers(tsid, nullptr);
+ weak_this->ReadBuffersIntoFile(tsid);
},
tracing_session->delay_to_next_write_period_ms());
return true;
@@ -2287,7 +2331,7 @@
task_runner_->PostTask([weak_this, weak_consumer, tsid] {
if (!weak_this || !weak_consumer)
return;
- weak_this->ReadBuffers(tsid, weak_consumer.get());
+ weak_this->ReadBuffersIntoConsumer(tsid, weak_consumer.get());
});
}
@@ -2379,6 +2423,7 @@
auto reg_ds = data_sources_.emplace(desc.name(),
RegisteredDataSource{producer_id, desc});
+ g_crash_key_ds_count.Set(static_cast<int64_t>(data_sources_.size()));
// If there are existing tracing sessions, we need to check if the new
// data source is enabled by any of them.
@@ -2498,6 +2543,7 @@
if (it->second.producer_id == producer_id &&
it->second.descriptor.name() == name) {
data_sources_.erase(it);
+ g_crash_key_ds_count.Set(static_cast<int64_t>(data_sources_.size()));
return;
}
}
@@ -3437,7 +3483,7 @@
consumer_->OnTraceData({}, /* has_more = */ false);
return;
}
- if (!service_->ReadBuffers(tracing_session_id_, this)) {
+ if (!service_->ReadBuffersIntoConsumer(tracing_session_id_, this)) {
consumer_->OnTraceData({}, /* has_more = */ false);
}
}
@@ -3931,6 +3977,8 @@
auto weak_this = weak_ptr_factory_.GetWeakPtr();
task_runner_->PostTask([weak_this, data_sources] {
if (weak_this) {
+ base::StringView producer_name(weak_this->name_);
+ auto scoped_crash_key = g_crash_key_prod_name.SetScoped(producer_name);
weak_this->producer_->ClearIncrementalState(data_sources.data(),
data_sources.size());
}
diff --git a/src/tracing/core/tracing_service_impl.h b/src/tracing/core/tracing_service_impl.h
index 97371f3..8100968 100644
--- a/src/tracing/core/tracing_service_impl.h
+++ b/src/tracing/core/tracing_service_impl.h
@@ -285,7 +285,30 @@
uint32_t timeout_ms,
ConsumerEndpoint::FlushCallback);
void FlushAndDisableTracing(TracingSessionID);
- bool ReadBuffers(TracingSessionID, ConsumerEndpointImpl*);
+
+ // Starts reading the internal tracing buffers from the tracing session `tsid`
+ // and sends them to `*consumer` (which must be != nullptr).
+ //
+ // Only reads a limited amount of data in one call. If there's more data,
+ // immediately schedules itself on a PostTask.
+ //
+ // Returns false in case of error.
+ bool ReadBuffersIntoConsumer(TracingSessionID tsid,
+ ConsumerEndpointImpl* consumer);
+
+ // Reads all the tracing buffers from the tracing session `tsid` and writes
+ // them into the associated file.
+ //
+ // Reads all the data in the buffers (or until the file is full) before
+ // returning.
+ //
+ // If the tracing session write_period_ms is 0, the file is full or there has
+ // been an error, flushes the file and closes it. Otherwise, schedules itself
+ // to be executed after write_period_ms.
+ //
+ // Returns false in case of error.
+ bool ReadBuffersIntoFile(TracingSessionID);
+
void FreeBuffers(TracingSessionID);
// Service implementation.
@@ -654,6 +677,10 @@
void ScrapeSharedMemoryBuffers(TracingSession*, ProducerEndpointImpl*);
void PeriodicClearIncrementalStateTask(TracingSessionID, bool post_next_only);
TraceBuffer* GetBufferByID(BufferID);
+ bool ReadBuffers(TracingSessionID, TracingSession*, ConsumerEndpointImpl*);
+ // Returns true if `*tracing_session` is waiting for a trigger that hasn't
+ // happened.
+ static bool IsWaitingForTrigger(TracingSession*);
void OnStartTriggersTimeout(TracingSessionID tsid);
void MaybeLogUploadEvent(const TraceConfig&,
PerfettoStatsdAtom atom,
diff --git a/src/tracing/ipc/BUILD.gn b/src/tracing/ipc/BUILD.gn
index f86aeb5..dfd0eda 100644
--- a/src/tracing/ipc/BUILD.gn
+++ b/src/tracing/ipc/BUILD.gn
@@ -27,7 +27,6 @@
"../../../include/perfetto/ext/tracing/ipc",
]
sources = [
- "default_socket.cc",
"memfd.cc",
"memfd.h",
"posix_shared_memory.cc",
@@ -36,6 +35,7 @@
"shared_memory_windows.h",
]
deps = [
+ ":default_socket",
"../../../gn:default_deps",
"../../../include/perfetto/ext/ipc",
"../../base",
@@ -43,6 +43,17 @@
]
}
+source_set("default_socket") {
+ sources = [ "default_socket.cc" ]
+ public_deps = [ "../../../include/perfetto/ext/tracing/ipc" ]
+ deps = [
+ "../../../gn:default_deps",
+ "../../../include/perfetto/ext/ipc",
+ "../../../include/perfetto/ext/tracing/core",
+ "../../base",
+ ]
+}
+
perfetto_unittest_source_set("unittests") {
testonly = true
deps = [
diff --git a/src/websocket_bridge/BUILD.gn b/src/websocket_bridge/BUILD.gn
new file mode 100644
index 0000000..38f3e66
--- /dev/null
+++ b/src/websocket_bridge/BUILD.gn
@@ -0,0 +1,35 @@
+# 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.
+
+source_set("lib") {
+ deps = [
+ "../../gn:default_deps",
+ "../base",
+ "../base:unix_socket",
+ "../base/http",
+ "../tracing/ipc:default_socket",
+ ]
+ sources = [
+ "websocket_bridge.cc",
+ "websocket_bridge.h",
+ ]
+}
+
+executable("websocket_bridge") {
+ deps = [
+ ":lib",
+ "../../gn:default_deps",
+ ]
+ sources = [ "websocket_bridge_main.cc" ]
+}
diff --git a/src/websocket_bridge/websocket_bridge.cc b/src/websocket_bridge/websocket_bridge.cc
new file mode 100644
index 0000000..eb26506
--- /dev/null
+++ b/src/websocket_bridge/websocket_bridge.cc
@@ -0,0 +1,190 @@
+/*
+ * 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/websocket_bridge/websocket_bridge.h"
+
+#include <stdint.h>
+
+#include <map>
+#include <memory>
+#include <vector>
+
+#include "perfetto/ext/base/http/http_server.h"
+#include "perfetto/ext/base/unix_socket.h"
+#include "perfetto/ext/base/unix_task_runner.h"
+#include "perfetto/ext/tracing/ipc/default_socket.h"
+
+namespace perfetto {
+namespace {
+
+constexpr int kWebsocketPort = 8037;
+
+struct Endpoint {
+ const char* uri;
+ const char* endpoint;
+ base::SockFamily family;
+};
+
+class WSBridge : public base::HttpRequestHandler,
+ public base::UnixSocket::EventListener {
+ public:
+ void Main(int argc, char** argv);
+
+ // base::HttpRequestHandler implementation.
+ void OnHttpRequest(const base::HttpRequest&) override;
+ void OnWebsocketMessage(const base::WebsocketMessage&) override;
+ void OnHttpConnectionClosed(base::HttpServerConnection*) override;
+
+ // base::UnixSocket::EventListener implementation.
+ void OnNewIncomingConnection(base::UnixSocket*,
+ std::unique_ptr<base::UnixSocket>) override;
+ void OnConnect(base::UnixSocket*, bool) override;
+ void OnDisconnect(base::UnixSocket*) override;
+ void OnDataAvailable(base::UnixSocket* self) override;
+
+ private:
+ base::HttpServerConnection* GetWebsocket(base::UnixSocket*);
+
+ base::UnixTaskRunner task_runner_;
+ std::vector<Endpoint> endpoints_;
+ std::map<base::HttpServerConnection*, std::unique_ptr<base::UnixSocket>>
+ conns_;
+};
+
+void WSBridge::Main(int, char**) {
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+ // On Windows traced used a TCP socket.
+ const auto kTracedFamily = base::SockFamily::kInet;
+#else
+ const auto kTracedFamily = base::SockFamily::kUnix;
+#endif
+ endpoints_.push_back({"/traced", GetConsumerSocket(), kTracedFamily});
+ endpoints_.push_back({"/adb", "127.0.0.1:5037", base::SockFamily::kInet});
+
+ base::HttpServer srv(&task_runner_, this);
+ srv.AddAllowedOrigin("http://localhost:10000");
+ srv.AddAllowedOrigin("http://127.0.0.1:10000");
+ srv.AddAllowedOrigin("https://ui.perfetto.dev");
+
+ srv.Start(kWebsocketPort);
+ PERFETTO_LOG("[WSBridge] Listening on 127.0.0.1:%d", kWebsocketPort);
+ task_runner_.Run();
+}
+
+void WSBridge::OnHttpRequest(const base::HttpRequest& req) {
+ for (const auto& ep : endpoints_) {
+ if (req.uri != ep.uri || !req.is_websocket_handshake)
+ continue;
+
+ // Connect to the endpoint in blocking mode.
+ auto sock_raw =
+ base::UnixSocketRaw::CreateMayFail(ep.family, base::SockType::kStream);
+ if (!sock_raw) {
+ PERFETTO_PLOG("socket() failed");
+ req.conn->SendResponseAndClose("500 Server Error");
+ return;
+ }
+ PERFETTO_LOG("[WSBridge] New connection from \"%.*s\"",
+ static_cast<int>(req.origin.size()), req.origin.data());
+ sock_raw.SetTxTimeout(3000);
+ sock_raw.SetBlocking(true);
+
+ if (!sock_raw.Connect(ep.endpoint)) {
+ PERFETTO_ELOG("[WSBridge] Connection to %s failed", ep.endpoint);
+ req.conn->SendResponseAndClose("503 Service Unavailable");
+ return;
+ }
+ sock_raw.SetBlocking(false);
+
+ PERFETTO_DLOG("[WSBridge] Connected to %s", ep.endpoint);
+ conns_[req.conn] = base::UnixSocket::AdoptConnected(
+ sock_raw.ReleaseFd(), this, &task_runner_, ep.family,
+ base::SockType::kStream);
+
+ req.conn->UpgradeToWebsocket(req);
+ return;
+ } // for (endpoint)
+ req.conn->SendResponseAndClose("404 Not Found");
+}
+
+// Called when an inbound websocket message is received from the browser.
+void WSBridge::OnWebsocketMessage(const base::WebsocketMessage& msg) {
+ auto it = conns_.find(msg.conn);
+ PERFETTO_CHECK(it != conns_.end());
+ // Pass through the websocket message onto the endpoint TCP socket.
+ base::UnixSocket& sock = *it->second;
+ sock.Send(msg.data.data(), msg.data.size());
+}
+
+// Called when a TCP message is received from the endpoint.
+void WSBridge::OnDataAvailable(base::UnixSocket* sock) {
+ base::HttpServerConnection* websocket = GetWebsocket(sock);
+ PERFETTO_CHECK(websocket);
+
+ char buf[8192];
+ auto rsize = sock->Receive(buf, sizeof(buf));
+ if (rsize > 0) {
+ websocket->SendWebsocketMessage(buf, static_cast<size_t>(rsize));
+ } else {
+ // Connection closed or errored.
+ sock->Shutdown(/*notify=*/true); // Will trigger OnDisconnect().
+ websocket->Close();
+ }
+}
+
+// Called when the browser terminates the websocket connection.
+void WSBridge::OnHttpConnectionClosed(base::HttpServerConnection* websocket) {
+ PERFETTO_DLOG("[WSBridge] Websocket connection closed");
+ auto it = conns_.find(websocket);
+ if (it == conns_.end())
+ return; // Can happen if ADB closed first.
+ base::UnixSocket& sock = *it->second;
+ sock.Shutdown(/*notify=*/true);
+ conns_.erase(websocket);
+}
+
+void WSBridge::OnDisconnect(base::UnixSocket* sock) {
+ base::HttpServerConnection* websocket = GetWebsocket(sock);
+ if (!websocket)
+ return;
+ websocket->Close();
+ sock->Shutdown(/*notify=*/false);
+ conns_.erase(websocket);
+ PERFETTO_DLOG("[WSBridge] Socket connection closed");
+}
+
+base::HttpServerConnection* WSBridge::GetWebsocket(base::UnixSocket* sock) {
+ for (const auto& it : conns_) {
+ if (it.second.get() == sock) {
+ return it.first;
+ }
+ }
+ return nullptr;
+}
+
+void WSBridge::OnConnect(base::UnixSocket*, bool) {}
+void WSBridge::OnNewIncomingConnection(base::UnixSocket*,
+ std::unique_ptr<base::UnixSocket>) {}
+
+} // namespace
+
+int PERFETTO_EXPORT_ENTRYPOINT WebsocketBridgeMain(int argc, char** argv) {
+ perfetto::WSBridge ws_bridge;
+ ws_bridge.Main(argc, argv);
+ return 0;
+}
+
+} // namespace perfetto
diff --git a/src/trace_processor/importers/json/json_tracker.cc b/src/websocket_bridge/websocket_bridge.h
similarity index 66%
rename from src/trace_processor/importers/json/json_tracker.cc
rename to src/websocket_bridge/websocket_bridge.h
index 971042a..d93bd61 100644
--- a/src/trace_processor/importers/json/json_tracker.cc
+++ b/src/websocket_bridge/websocket_bridge.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-#include "src/trace_processor/importers/json/json_tracker.h"
+#ifndef SRC_WEBSOCKET_BRIDGE_WEBSOCKET_BRIDGE_H_
+#define SRC_WEBSOCKET_BRIDGE_WEBSOCKET_BRIDGE_H_
+
+#include "perfetto/base/compiler.h"
namespace perfetto {
-namespace trace_processor {
+int WebsocketBridgeMain(int argc, char** argv);
+}
-JsonTracker::JsonTracker(TraceProcessorContext*) {}
-JsonTracker::~JsonTracker() = default;
-
-} // namespace trace_processor
-} // namespace perfetto
+#endif // SRC_WEBSOCKET_BRIDGE_WEBSOCKET_BRIDGE_H_
diff --git a/src/trace_processor/importers/json/json_tracker.cc b/src/websocket_bridge/websocket_bridge_main.cc
similarity index 62%
copy from src/trace_processor/importers/json/json_tracker.cc
copy to src/websocket_bridge/websocket_bridge_main.cc
index 971042a..480522b 100644
--- a/src/trace_processor/importers/json/json_tracker.cc
+++ b/src/websocket_bridge/websocket_bridge_main.cc
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -14,13 +14,10 @@
* limitations under the License.
*/
-#include "src/trace_processor/importers/json/json_tracker.h"
+#include "src/websocket_bridge/websocket_bridge.h"
-namespace perfetto {
-namespace trace_processor {
-
-JsonTracker::JsonTracker(TraceProcessorContext*) {}
-JsonTracker::~JsonTracker() = default;
-
-} // namespace trace_processor
-} // namespace perfetto
+// This is split in a dedicated translation unit so that tracebox can refer to
+// WebsocketBridgeMain() without pulling in a main() symbol.
+int main(int argc, char** argv) {
+ return perfetto::WebsocketBridgeMain(argc, argv);
+}
diff --git a/test/data/atrace_compressed.ctrace.sha256 b/test/data/atrace_compressed.ctrace.sha256
new file mode 100644
index 0000000..d39c5ea
--- /dev/null
+++ b/test/data/atrace_compressed.ctrace.sha256
@@ -0,0 +1 @@
+db92be3d78ab0619af5068240b88bad78a655f3e403c5f7c7876956eb5b260b0
\ No newline at end of file
diff --git a/test/data/atrace_uncompressed_b_208691037.sha256 b/test/data/atrace_uncompressed_b_208691037.sha256
new file mode 100644
index 0000000..7348172
--- /dev/null
+++ b/test/data/atrace_uncompressed_b_208691037.sha256
@@ -0,0 +1 @@
+9872b827df72895a98af2977c117ad3bc31f8c62be79d211bdf254170db4e1de
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
index 5ddebaa..86c5f5e 100644
--- a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
+++ b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
@@ -1 +1 @@
-f7ff6334f50962bde39b2000a2fc331ac48341782916c62969d026c81570814e
\ No newline at end of file
+0c1ab8b9a3d60c59e43fc4be2f7d6f41a5a0bc81b7a5e5693d8d933952965783
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
index 7d5af5a..c44a1c0 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
@@ -1 +1 @@
-1eaf9d88a907eea4517052cba5bc06fda521d84086cf761b2710c3ae2dda3d13
\ No newline at end of file
+babc1a1b16f85e2746e157705987fddd90b680ac24ed69600c4709acca997d33
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
index 5c7c86d..0813ae8 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
@@ -1 +1 @@
-f63513514da003a0f24d9df61a5516f1961b3fa88ca1cdf74ad50cd916571222
\ No newline at end of file
+11a73e2b898d70785c9480500e0bf35c9426c49340bee1df3eab672a72bd91a2
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
index eb52a0a..6211277 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
@@ -1 +1 @@
-6241ed1b7972f4cb709c391d383513b6e2999b46386b3dedb4e2565baa9677e1
\ No newline at end of file
+5c76ac76334433ef5ca847f47f0020d57d0bb78a6bc3741c7e4e1e9ad4e32c81
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
index 6c7bbde..4efc85f 100644
--- a/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
@@ -1 +1 @@
-498e62690012328f236403968b31aab16a63db000c29b6f38bcec36ea0726a06
\ No newline at end of file
+99911b5f7e6db8081ecbca7d7e2c47bf57b8a54434c639a17830dd4d84b67009
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256 b/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256
index f565bae..2104823 100644
--- a/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256
@@ -1 +1 @@
-80e9b51139060f6c3a0fe7a6d35d91d2fdd9b600cf6c8e667236b8aa8498cbfe
\ No newline at end of file
+459f3bcd870dc4d697510e380983e1daf39355732766e754f6649f066c60007e
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_trace_and_go_back_to_landing_page.png.sha256 b/test/data/ui-screenshots/ui-routing_open_trace_and_go_back_to_landing_page.png.sha256
index b57e638..815741c 100644
--- a/test/data/ui-screenshots/ui-routing_open_trace_and_go_back_to_landing_page.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_trace_and_go_back_to_landing_page.png.sha256
@@ -1 +1 @@
-8ac1dbfa90bdf508de0ffda2d37c0a55200dc579fbb8515dc38dd01711735fff
\ No newline at end of file
+f5f87e3e2bbb7026e797dcd3fd8c25f4776ed03aaa50e399115fbe92d0008e75
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
index 6c7bbde..4efc85f 100644
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
@@ -1 +1 @@
-498e62690012328f236403968b31aab16a63db000c29b6f38bcec36ea0726a06
\ No newline at end of file
+99911b5f7e6db8081ecbca7d7e2c47bf57b8a54434c639a17830dd4d84b67009
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256
index 9e91e29..89e28ad 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256
@@ -1 +1 @@
-cf77763da2a3cc43b56e08debd14a09b1e3fe13ad9a5dc25b599d81bc0d5e236
\ No newline at end of file
+c64e2df83124c5e42b7552d8e673fa390ee31d1b6c5a8acd07eb022afd97b84d
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
index f01dadb..a461053 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
@@ -1 +1 @@
-46abbeaf901c8c60d22d3bf3b77bf820a382ec6a2023d7efd85a740c549c5093
\ No newline at end of file
+ffb8037bb4c40d392cd0d99220fc1330ef53ca0c912dbf104dcb5c4900dc9e05
\ No newline at end of file
diff --git a/test/end_to_end_integrationtest.cc b/test/end_to_end_integrationtest.cc
index 1ec13d3..3d2b65e 100644
--- a/test/end_to_end_integrationtest.cc
+++ b/test/end_to_end_integrationtest.cc
@@ -296,10 +296,30 @@
#define TEST_PRODUCER_SOCK_NAME ::perfetto::GetProducerSocket()
#endif
+// Defining this macro out-of-line works around C/C++'s macro rules (see
+// https://stackoverflow.com/questions/26284393/nested-operator-in-c-preprocessor).
+#define DisableTest(x) DISABLED_##x
+
#if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
#define TreeHuggerOnly(x) x
#else
-#define TreeHuggerOnly(x) DISABLED_##x
+#define TreeHuggerOnly(x) DisableTest(x)
+#endif
+
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
+#define AndroidOnly(x) x
+#else
+#define AndroidOnly(x) DisableTest(x)
+#endif
+
+// Disable cmdline tests on sanitizets because they use fork() and that messes
+// up leak / races detections, which has been fixed only recently (see
+// https://github.com/google/sanitizers/issues/836 ).
+#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER) || \
+ defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER)
+#define NoSanitizers(X) DisableTest(X)
+#else
+#define NoSanitizers(X) X
#endif
// TODO(b/73453011): reenable on more platforms (including standalone Android).
@@ -1190,16 +1210,6 @@
Property(&protos::gen::TestEvent::str, SizeIs(kMsgSize)))));
}
-// Disable cmdline tests on sanitizets because they use fork() and that messes
-// up leak / races detections, which has been fixed only recently (see
-// https://github.com/google/sanitizers/issues/836 ).
-#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER) || \
- defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER)
-#define NoSanitizers(X) DISABLED_##X
-#else
-#define NoSanitizers(X) X
-#endif
-
TEST_F(PerfettoCmdlineTest, NoSanitizers(InvalidCases)) {
std::string cfg("duration_ms: 100");
@@ -1518,11 +1528,8 @@
// Dropbox on the commandline client only works on android builds. So disable
// this test on all other builds.
-#if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
-TEST_F(PerfettoCmdlineTest, NoSanitizers(NoDataNoFileWithoutTrigger)) {
-#else
-TEST_F(PerfettoCmdlineTest, DISABLED_NoDataNoFileWithoutTrigger) {
-#endif
+TEST_F(PerfettoCmdlineTest,
+ NoSanitizers(TreeHuggerOnly(NoDataNoFileWithoutTrigger))) {
// See |message_count| and |message_size| in the TraceConfig above.
constexpr size_t kMessageCount = 11;
constexpr size_t kMessageSize = 32;
@@ -1746,4 +1753,86 @@
EXPECT_EQ(0, query_raw.Run(&stderr_)) << stderr_;
}
+TEST_F(PerfettoCmdlineTest,
+ NoSanitizers(AndroidOnly(CmdTriggerWithUploadFlag))) {
+ // See |message_count| and |message_size| in the TraceConfig above.
+ constexpr size_t kMessageCount = 2;
+ constexpr size_t kMessageSize = 2;
+ protos::gen::TraceConfig trace_config;
+ trace_config.add_buffers()->set_size_kb(1024);
+ auto* ds_config = trace_config.add_data_sources()->mutable_config();
+ ds_config->set_name("android.perfetto.FakeProducer");
+ ds_config->mutable_for_testing()->set_message_count(kMessageCount);
+ ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+ auto* trigger_cfg = trace_config.mutable_trigger_config();
+ trigger_cfg->set_trigger_mode(
+ protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
+ trigger_cfg->set_trigger_timeout_ms(15000);
+ auto* trigger = trigger_cfg->add_triggers();
+ trigger->set_name("trigger_name");
+ // |stop_delay_ms| must be long enough that we can write the packets in
+ // before the trace finishes. This has to be long enough for the slowest
+ // emulator. But as short as possible to prevent the test running a long
+ // time.
+ trigger->set_stop_delay_ms(500);
+
+ // We have to construct all the processes we want to fork before we start the
+ // service with |StartServiceIfRequired()|. this is because it is unsafe
+ // (could deadlock) to fork after we've spawned some threads which might
+ // printf (and thus hold locks).
+ const std::string path = RandomTraceFileName();
+ auto perfetto_proc = ExecPerfetto(
+ {
+ "-o",
+ path,
+ "-c",
+ "-",
+ },
+ trace_config.SerializeAsString());
+
+ std::string triggers = R"(
+ activate_triggers: "trigger_name"
+ )";
+ auto perfetto_proc_2 = ExecPerfetto(
+ {
+ "--upload",
+ "-c",
+ "-",
+ "--txt",
+ },
+ triggers);
+
+ // Start the service and connect a simple fake producer.
+ StartServiceIfRequiredNoNewExecsAfterThis();
+ auto* fake_producer = ConnectFakeProducer();
+ EXPECT_TRUE(fake_producer);
+
+ std::thread background_trace([&perfetto_proc]() {
+ std::string stderr_str;
+ EXPECT_EQ(0, perfetto_proc.Run(&stderr_str)) << stderr_str;
+ });
+
+ WaitForProducerEnabled();
+ // Wait for the producer to start, and then write out 11 packets, before the
+ // trace actually starts (the trigger is seen).
+ auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
+ fake_producer->ProduceEventBatch(WrapTask(on_data_written));
+ task_runner_.RunUntilCheckpoint("data_written_1");
+
+ EXPECT_EQ(0, perfetto_proc_2.Run(&stderr_)) << "stderr: " << stderr_;
+
+ background_trace.join();
+
+ std::string trace_str;
+ base::ReadFile(path, &trace_str);
+ protos::gen::Trace trace;
+ ASSERT_TRUE(trace.ParseFromString(trace_str));
+ EXPECT_LT(static_cast<int>(kMessageCount), trace.packet_size());
+ for (const auto& packet : trace.packet()) {
+ if (packet.has_trigger()) {
+ EXPECT_EQ("trigger_name", packet.trigger().trigger_name());
+ }
+ }
+}
+
} // namespace perfetto
diff --git a/test/trace_processor/parsing/android_multiuser_switch.textproto b/test/trace_processor/parsing/android_multiuser_switch.textproto
index 4798139..d2a836c 100644
--- a/test/trace_processor/parsing/android_multiuser_switch.textproto
+++ b/test/trace_processor/parsing/android_multiuser_switch.textproto
@@ -1,11 +1,29 @@
packet {
+ process_tree {
+ processes {
+ pid: 2609
+ ppid: 129
+ cmdline: "system_server"
+ uid: 1000
+ }
+ }
+ process_tree {
+ processes {
+ pid: 5993
+ ppid: 129
+ cmdline: "com.android.systemui"
+ uid: 10216
+ }
+ }
+}
+packet {
ftrace_events {
cpu: 3
event {
timestamp: 3000000000 # 3e9
pid: 4064
print {
- buf: "S|1204|UserDetailView.Adapter#onClick|0\n"
+ buf: "S|5993|UserDetailView.Adapter#onClick|0\n"
}
}
}
@@ -17,7 +35,31 @@
timestamp: 3100000000
pid: 4064
print {
- buf: "F|1204|UserDetailView.Adapter#onClick|0\n"
+ buf: "F|5993|UserDetailView.Adapter#onClick|0\n"
+ }
+ }
+ }
+}
+packet {
+ ftrace_events {
+ cpu: 2
+ event {
+ timestamp: 5186970000
+ pid: 4032
+ print {
+ buf: "S|2609|MetricsLogger:launchObserverNotifyIntentStarted|0\n"
+ }
+ }
+ }
+}
+packet {
+ ftrace_events {
+ cpu: 2
+ event {
+ timestamp: 5187000000
+ pid: 4032
+ print {
+ buf: "F|2609|MetricsLogger:launchObserverNotifyIntentStarted|0\n"
}
}
}
@@ -29,7 +71,7 @@
timestamp: 5200000000
pid: 4065
print {
- buf: "S|1204|launching: com.google.android.apps.nexuslauncher|0\n"
+ buf: "S|2609|launching: com.google.android.apps.nexuslauncher|0\n"
}
}
}
@@ -41,8 +83,32 @@
timestamp: 7900000000 # 7.9e9
pid: 4065
print {
- buf: "F|1204|launching: com.google.android.apps.nexuslauncher|0\n"
+ buf: "F|2609|launching: com.google.android.apps.nexuslauncher|0\n"
}
}
}
-}
\ No newline at end of file
+}
+packet {
+ ftrace_events {
+ cpu: 2
+ event {
+ timestamp: 7900016000
+ pid: 4075
+ print {
+ buf: "S|2609|MetricsLogger:launchObserverNotifyActivityLaunchFinished|0\n"
+ }
+ }
+ }
+}
+packet {
+ ftrace_events {
+ cpu: 2
+ event {
+ timestamp: 7900516000
+ pid: 4075
+ print {
+ buf: "F|2609|MetricsLogger:launchObserverNotifyActivityLaunchFinished|0\n"
+ }
+ }
+ }
+}
diff --git a/test/trace_processor/parsing/atrace_compressed_sched_count.out b/test/trace_processor/parsing/atrace_compressed_sched_count.out
new file mode 100644
index 0000000..c03bd11
--- /dev/null
+++ b/test/trace_processor/parsing/atrace_compressed_sched_count.out
@@ -0,0 +1,2 @@
+"COUNT(1)"
+1120
diff --git a/test/trace_processor/parsing/atrace_uncompressed_sched_count.out b/test/trace_processor/parsing/atrace_uncompressed_sched_count.out
new file mode 100644
index 0000000..41c340b
--- /dev/null
+++ b/test/trace_processor/parsing/atrace_uncompressed_sched_count.out
@@ -0,0 +1,2 @@
+"COUNT(1)"
+9
diff --git a/test/trace_processor/parsing/display_time_unit_slices.out b/test/trace_processor/parsing/display_time_unit_slices.out
index 4343a9e..cee485b 100644
--- a/test/trace_processor/parsing/display_time_unit_slices.out
+++ b/test/trace_processor/parsing/display_time_unit_slices.out
@@ -1,2 +1,2 @@
"ts","dur","name"
-1597071955492308000,211463000,"add_graph"
+-7794778920422990592,211463000000,"add_graph"
diff --git a/test/trace_processor/parsing/index b/test/trace_processor/parsing/index
index b586e28..ce6f074 100644
--- a/test/trace_processor/parsing/index
+++ b/test/trace_processor/parsing/index
@@ -163,3 +163,10 @@
# Multiuser
android_multiuser_switch.textproto android_multiuser android_multiuser_switch.out
+
+# Output of atrace -z.
+../../data/atrace_compressed.ctrace sched_smoke.sql atrace_compressed_sched_count.out
+
+# Output of adb shell "atrace -t 1 sched" > out.txt". It has extra garbage
+# coming from stderr before the TRACE: marker. See b/208691037.
+../../data/atrace_uncompressed_b_208691037 sched_smoke.sql atrace_uncompressed_sched_count.out
diff --git a/test/trace_processor/profiling/heap_stats_closest_proc.out b/test/trace_processor/profiling/heap_stats_closest_proc.out
index ddcdc95..1ee09bf 100644
--- a/test/trace_processor/profiling/heap_stats_closest_proc.out
+++ b/test/trace_processor/profiling/heap_stats_closest_proc.out
@@ -8,7 +8,9 @@
samples {
ts: 200000000
heap_size: 64
+ heap_native_size: 0
reachable_heap_size: 64
+ reachable_heap_native_size: 0
obj_count: 1
reachable_obj_count: 1
anon_rss_and_swap_size: 2048
@@ -30,7 +32,9 @@
samples {
ts: 1500000000
heap_size: 64
+ heap_native_size: 0
reachable_heap_size: 64
+ reachable_heap_native_size: 0
obj_count: 1
reachable_obj_count: 1
anon_rss_and_swap_size: 3072
@@ -41,4 +45,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/test/trace_processor/profiling/java_heap_stats.out b/test/trace_processor/profiling/java_heap_stats.out
index a4ff25c..4cecef7 100644
--- a/test/trace_processor/profiling/java_heap_stats.out
+++ b/test/trace_processor/profiling/java_heap_stats.out
@@ -8,7 +8,9 @@
samples {
ts: 10
heap_size: 992
+ heap_native_size: 0
reachable_heap_size: 352
+ reachable_heap_native_size: 0
obj_count: 6
reachable_obj_count: 3
anon_rss_and_swap_size: 4096000
diff --git a/tools/batch_trace_processor/main.py b/tools/batch_trace_processor/main.py
index 36a4d53..843e5f1 100644
--- a/tools/batch_trace_processor/main.py
+++ b/tools/batch_trace_processor/main.py
@@ -23,46 +23,9 @@
import pandas as pd
import plotille
-from concurrent.futures import ThreadPoolExecutor
-from dataclasses import dataclass
-from perfetto.trace_processor import TraceProcessor
-from perfetto.trace_processor.api import TraceProcessorException
+from perfetto.trace_processor import TraceProcessorException
-
-@dataclass
-class TpArg:
- shell_path: str
- verbose: bool
- file: str
-
-
-def create_tp_args(args, files):
- return [TpArg(args.shell_path, args.verbose, f) for f in files]
-
-
-def create_tp(arg):
- return TraceProcessor(
- file_path=arg.file, bin_path=arg.shell_path, verbose=arg.verbose)
-
-
-def close_tp(tp):
- tp.close()
-
-
-def query_single_result(tp, query):
- df = tp.query(query).as_pandas_dataframe()
- if len(df.index) != 1:
- raise TraceProcessorException("Query should only return a single row")
-
- if len(df.columns) != 1:
- raise TraceProcessorException("Query should only return a single column")
-
- return df.iloc[0, 0]
-
-
-def query_file_and_return_last(tp, queries_str):
- queries = [q.strip() for q in queries_str.split(";\n")]
- return [tp.query(q).as_pandas_dataframe() for q in queries if q][-1]
+from perfetto.batch_trace_processor.api import BatchTraceProcessor
def prefix_path_column(path, df):
@@ -72,16 +35,14 @@
class TpBatchShell(cmd.Cmd):
- def __init__(self, executor, files, tps):
+ def __init__(self, files, batch_tp):
super().__init__()
- self.executor = executor
self.files = files
- self.tps = tps
+ self.batch_tp = batch_tp
def do_histogram(self, arg):
try:
- data = list(
- self.executor.map(lambda tp: query_single_result(tp, arg), self.tps))
+ data = self.batch_tp.query_single_result(arg)
print(plotille.histogram(data))
self.print_percentiles(data)
except TraceProcessorException as ex:
@@ -89,8 +50,7 @@
def do_vhistogram(self, arg):
try:
- data = list(
- self.executor.map(lambda tp: query_single_result(tp, arg), self.tps))
+ data = self.batch_tp.query_single_result(arg)
print(plotille.hist(data))
self.print_percentiles(data)
except TraceProcessorException as ex:
@@ -98,8 +58,7 @@
def do_count(self, arg):
try:
- data = list(
- self.executor.map(lambda tp: query_single_result(tp, arg), self.tps))
+ data = self.batch_tp.query_single_result(arg)
counts = dict()
for i in data:
counts[i] = counts.get(i, 0) + 1
@@ -145,32 +104,28 @@
if not files:
logging.info("At least one file must be specified in files or file list")
- executor = ThreadPoolExecutor()
-
logging.info('Loading traces...')
- tps = [tp for tp in executor.map(create_tp, create_tp_args(args, files))]
+ with BatchTraceProcessor(
+ files, bin_path=args.shell_path, verbose=args.verbose) as batch_tp:
+ if args.query_file:
+ logging.info('Running query file...')
- if args.query_file:
- logging.info('Running query file...')
+ with open(args.query_file, 'r') as f:
+ queries_str = f.read()
- with open(args.query_file, 'r') as f:
- query = f.read()
+ queries = [q.strip() for q in queries_str.split(";\n")]
+ out = [batch_tp.query(q) for q in queries if q][-1]
+ res = pd.concat(
+ [prefix_path_column(path, df) for (path, df) in zip(files, out)])
+ print(res.to_csv(index=False))
- out = list(
- executor.map(lambda tp: query_file_and_return_last(tp, query), tps))
- res = pd.concat(
- [prefix_path_column(path, df) for (path, df) in zip(files, out)])
- print(res.to_csv(index=False))
+ if args.interactive or not args.query_file:
+ try:
+ TpBatchShell(files, batch_tp).cmdloop()
+ except KeyboardInterrupt:
+ pass
- if args.interactive or not args.query_file:
- try:
- TpBatchShell(executor, files, tps).cmdloop()
- except KeyboardInterrupt:
- pass
-
- logging.info("Closing; please wait...")
- executor.map(close_tp, tps)
- executor.shutdown()
+ logging.info("Closing; please wait...")
if __name__ == '__main__':
diff --git a/tools/batch_trace_processor/perfetto/batch_trace_processor/__init__.py b/tools/batch_trace_processor/perfetto/batch_trace_processor/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/batch_trace_processor/perfetto/batch_trace_processor/__init__.py
diff --git a/tools/batch_trace_processor/perfetto/batch_trace_processor/api.py b/tools/batch_trace_processor/perfetto/batch_trace_processor/api.py
new file mode 100644
index 0000000..f492641
--- /dev/null
+++ b/tools/batch_trace_processor/perfetto/batch_trace_processor/api.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+# 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.
+
+from concurrent.futures import ThreadPoolExecutor
+from dataclasses import dataclass
+from perfetto.trace_processor import TraceProcessor, TraceProcessorException
+
+
+@dataclass
+class TpArg:
+ bin_path: str
+ verbose: bool
+ file: str
+
+
+class BatchTraceProcessor:
+ """BatchTraceProcessor is the blessed way of running ad-hoc queries on
+ Python across many Perfetto traces.
+
+ Usage:
+ with BatchTraceProcessor(file_paths=files) as btp:
+ dfs = btp.query('select * from slice')
+ for df in dfs:
+ print(df)
+ """
+
+ def __init__(self, file_paths, bin_path=None, verbose=False):
+ """Creates a batch trace processor instance: the blessed way of running
+ ad-hoc queries on Python across many traces.
+
+ Args:
+ file_paths: List of trace file paths to load into this batch trace
+ processor instance.
+ bin_path: Optional path to a trace processor shell binary to use to
+ load the traces.
+ verbose: Optional flag indiciating whether verbose trace processor
+ output should be printed to stderr.
+ """
+ self.executor = ThreadPoolExecutor()
+ self.paths = file_paths
+
+ def create_tp(arg):
+ return TraceProcessor(
+ file_path=arg.file, bin_path=arg.bin_path, verbose=arg.verbose)
+
+ tp_args = [TpArg(bin_path, verbose, file) for file in file_paths]
+ self.tps = list(self.executor.map(create_tp, tp_args))
+
+ def query(self, sql):
+ """Executes the provided SQL statement in parallel across all the traces.
+
+ Args:
+ sql: The SQL statement to execute.
+
+ Returns:
+ A list of Pandas dataframes with the result of executing the query (one
+ per trace).
+
+ Raises:
+ TraceProcessorException: An error occurred running the query.
+ """
+ return self.__execute_on_tps(lambda tp: tp.query(sql).as_pandas_dataframe())
+
+ def query_single_result(self, sql):
+ """Executes the provided SQL statement (which should return a single row)
+ in parallel across all the traces.
+
+ Args:
+ sql: The SQL statement to execute. This statement should return exactly
+ one row on any trace.
+
+ Returns:
+ A list of values with the result of executing the query (one per ftrace).
+
+ Raises:
+ TraceProcessorException: An error occurred running the query or more than
+ one result was returned.
+ """
+
+ def query_single_result_inner(tp):
+ df = tp.query(sql).as_pandas_dataframe()
+ if len(df.index) != 1:
+ raise TraceProcessorException("Query should only return a single row")
+
+ if len(df.columns) != 1:
+ raise TraceProcessorException(
+ "Query should only return a single column")
+
+ return df.iloc[0, 0]
+
+ return self.__execute_on_tps(query_single_result_inner)
+
+ def close(self):
+ """Closes this batch trace processor instance: this closes all spawned
+ trace processor instances, releasing all the memory and resources those
+ instances take.
+
+ No further calls to other methods in this class should be made after
+ calling this method.
+ """
+ self.executor.map(lambda tp: tp.close(), self.tps)
+ self.executor.shutdown()
+
+ def __execute_on_tps(self, fn):
+ return list(self.executor.map(fn, self.tps))
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, _, __, ___):
+ self.close()
+ return False
diff --git a/tools/build_all_configs.py b/tools/build_all_configs.py
index 95fe044..0c0ec5d 100755
--- a/tools/build_all_configs.py
+++ b/tools/build_all_configs.py
@@ -100,6 +100,11 @@
else:
assert False, 'Unsupported system %r' % system
+ machine = platform.machine()
+ if machine == 'arm64':
+ for name, config in configs.items():
+ configs[name] = config + ('host_cpu="arm64"',)
+
if args.ccache:
for config_name, gn_args in iteritems(configs):
configs[config_name] = gn_args + ('cc_wrapper="ccache"',)
diff --git a/tools/ftrace_proto_gen/event_list b/tools/ftrace_proto_gen/event_list
index a4193d4..a3378a2 100644
--- a/tools/ftrace_proto_gen/event_list
+++ b/tools/ftrace_proto_gen/event_list
@@ -353,3 +353,4 @@
sde/sde_perf_crtc_update
sde/sde_perf_set_qos_luts
sde/sde_perf_update_bus
+synthetic/rss_stat_throttled
diff --git a/tools/install-build-deps b/tools/install-build-deps
index 492d03d..e714cb8 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -296,7 +296,7 @@
# This variable is updated by tools/roll-catapult-trace-viewer.
CATAPULT_SHA256 = 'b30108e05268ce6c65bb4126b65f6bfac165d17f5c1fd285046e7e6fd76c209f'
-TYPEFACES_SHA256 = 'b3f0f14eeecd4555ae94f897ec246b2c6e046ce0ea417407553f5767e7812575'
+TYPEFACES_SHA256 = 'f5f78f8f4395db65cdf5fdc1bf51da65f2161e6d61a305091d6ea54d3094a1f0'
UI_DEPS = [
Dependency(
diff --git a/tools/run_python_api_tests.py b/tools/run_python_api_tests.py
index 789a1f0..78a78a1 100755
--- a/tools/run_python_api_tests.py
+++ b/tools/run_python_api_tests.py
@@ -22,6 +22,13 @@
def main():
+ try:
+ import numpy
+ import pandas
+ except ModuleNotFoundError:
+ print('Cannot proceed. Please `pip3 install pandas numpy`', file=sys.stderr)
+ return 1
+
# Append test and src paths so that all imports are loaded in correctly
sys.path.append(os.path.join(ROOT_DIR, 'test', 'trace_processor', 'python'))
sys.path.append(
diff --git a/tools/test_data b/tools/test_data
index 4aa2139..cf8ae14 100755
--- a/tools/test_data
+++ b/tools/test_data
@@ -136,7 +136,7 @@
assert (fs.actual_digest is not None)
dst_name = '%s/%s-%s' % (args.bucket, os.path.basename(
fs.path), fs.actual_digest)
- cmd = ['gsutil', '-q', 'cp', '-a', 'public-read', fs.path, dst_name]
+ cmd = ['gsutil', '-q', 'cp', '-n', '-a', 'public-read', fs.path, dst_name]
logging.debug(' '.join(cmd))
subprocess.check_call(cmd)
with open(fs.path + SUFFIX + '.swp', 'w') as f:
diff --git a/ui/README.md b/ui/README.md
index 3fed469..a89b0f6 100644
--- a/ui/README.md
+++ b/ui/README.md
@@ -29,7 +29,7 @@
Finally run:
```bash
-$ ./ui/run-dev-server out/debug
+$ ./ui/run-dev-server
```
and navigate to `localhost:10000`.
diff --git a/ui/config/rollup.config.js b/ui/config/rollup.config.js
index 42f5afe..7a28ba4 100644
--- a/ui/config/rollup.config.js
+++ b/ui/config/rollup.config.js
@@ -65,6 +65,16 @@
// Translate source maps to point back to the .ts sources.
sourcemaps(),
],
+ onwarn: function(warning, warn) {
+ // Ignore circular dependency warnings coming from third party code.
+ if (warning.code === 'CIRCULAR_DEPENDENCY' &&
+ warning.importer.includes('node_modules')) {
+ return;
+ }
+
+ // Call the default warning handler for all remaining warnings.
+ warn(warning);
+ }
};
}
diff --git a/ui/release/channels.json b/ui/release/channels.json
index 5ee2472..359228c 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -2,11 +2,11 @@
"channels": [
{
"name": "stable",
- "rev": "6f7273f220d1812e315be627605be2561c617ed7"
+ "rev": "b4bd17f12d544c7b09cc93c6e7ed22e80ed73e48"
},
{
"name": "canary",
- "rev": "78d87a701cdcab64ca0937baed175d27ef38ef9c"
+ "rev": "9e3a964c585f3108b1ba2a1b58d4bb6ab96dc1ba"
},
{
"name": "autopush",
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 3e36ee2..42a3460 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -70,7 +70,7 @@
* {
box-sizing: border-box;
- -webkit-tap-highlight-color: none;
+ -webkit-tap-highlight-color: transparent;
touch-action: none;
}
@@ -91,6 +91,10 @@
overscroll-behavior: none;
}
+pre, code {
+ font-family: var(--monospace-font);
+}
+
// This is to minimize Mac vs Linux Chrome Headless rendering differences
// when running UI intergrationtests via puppeteer.
body.testing {
@@ -155,17 +159,13 @@
.full-page-loading-screen {
position: absolute;
- background: #3e4a5a;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
- background-image: url('assets/logo-3d.png');
- background-attachment: fixed;
- background-repeat: no-repeat;
- background-position: center;
+ background: #3e4a5a url('assets/logo-3d.png') no-repeat fixed center;
}
.page {
@@ -484,7 +484,7 @@
.debug-panel-border {
position: absolute;
- top: 0px;
+ top: 0;
height: 100%;
width: 100%;
border: 1px solid rgba(69, 187, 73, 0.5);
@@ -498,7 +498,7 @@
left: 0;
width: 600px;
color: var(--stroke-color);
- font-family: monospace;
+ font-family: var(--monospace-font);
padding: 10px 24px;
z-index: 100;
background-color: rgba(27, 28, 29, 0.90);
@@ -522,13 +522,13 @@
border-bottom: 1px solid var(--stroke-color);
}
div {
- margin: 2px 0px;
+ margin: 2px 0;
}
table, td, th {
border: 1px solid var(--stroke-color);
text-align: center;
padding: 4px;
- margin: 4px 0px;
+ margin: 4px 0;
}
table {
border-collapse: collapse;
@@ -541,6 +541,7 @@
--expanded-background: hsl(215, 22%, 19%);
--expanded-transparent: hsl(215, 22%, 19%, 0);
display: grid;
+ align-items: center;
grid-template-columns: auto 1fr;
grid-template-rows: 1fr;
transition: background-color .4s, color .4s;
@@ -581,6 +582,7 @@
align-items: center;
line-height: 1;
width: var(--track-shell-width);
+ min-height: 40px;
transition: background-color .4s;
h1 {
grid-area: title;
@@ -605,6 +607,9 @@
background-color: #ebeef9;
}
}
+ .track-content {
+ @include track_shell_title();
+ }
}
.time-selection-panel {
@@ -750,7 +755,7 @@
margin-left: 10px;
border-radius: 50%;
border: 2px solid #408ee0;
- border-color: #408ee0 transparent #408ee0 transparent;
+ border-color: #408ee0 transparent;
animation: spinner 1.25s linear infinite;
@keyframes spinner {
0% { transform: rotate(0deg); }
diff --git a/ui/src/assets/details.scss b/ui/src/assets/details.scss
index 1545e4d..b7bfad8 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -35,10 +35,10 @@
.tab {
font-family: 'Roboto Condensed', sans-serif;
color: #3c4b5d;
- padding: 3px 10px 0px 10px;
+ padding: 3px 10px 0 10px;
margin-top: 3px;
font-size: 13px;
- border-radius: 5px 5px 0px 0px;
+ border-radius: 5px 5px 0 0;
background-color: hsla(0, 0%, 75%, 1);
border-top: solid 1px hsla(0, 0%, 75%, 1);
border-left: solid 1px hsla(0, 0%, 75%, 1);
@@ -120,7 +120,7 @@
.details-panel-heading {
padding: 10px 0 5px 0;
position: sticky;
- top: 0px;
+ top: 0;
display: flex;
background: white;
&.aggregation {
@@ -176,7 +176,7 @@
justify-content: space-between;
align-content: center;
height: 30px;
- padding: 0px;
+ padding: 0;
font-size: 12px;
* {
align-self: center;
@@ -193,7 +193,7 @@
width: fit-content;
height: 20px;
padding: 3px;
- padding-top: 0px;
+ padding-top: 0;
margin: 2px;
font-size: 12px;
opacity: 0.5;
@@ -240,7 +240,7 @@
// which is done by using fixed table layout.
table-layout: fixed;
word-wrap: break-word;
- padding: 0px 10px;
+ padding: 0 10px;
tr:hover {
background-color: hsl(214, 22%, 90%);
}
@@ -385,7 +385,7 @@
header {
position: sticky;
- top: 0px;
+ top: 0;
z-index: 1;
background-color: white;
color: #3c4b5d;
diff --git a/ui/src/assets/home_page.scss b/ui/src/assets/home_page.scss
index 8eed245..f76137c 100644
--- a/ui/src/assets/home_page.scss
+++ b/ui/src/assets/home_page.scss
@@ -86,7 +86,8 @@
z-index: 2;
text-transform: uppercase;
font-size: 16px;
- font-weight: 200;
+ font-family: 'Raleway';
+ font-weight: 400;
letter-spacing: 0.3px;
}
@@ -108,7 +109,7 @@
opacity: 0;
color: #da4534;
font-weight: 400;
- @include transition;
+ @include transition();
&.show { opacity: 1; }
}
} // .channel-select
diff --git a/ui/src/assets/modal.scss b/ui/src/assets/modal.scss
index 0f54b00..fe1bc87 100644
--- a/ui/src/assets/modal.scss
+++ b/ui/src/assets/modal.scss
@@ -94,15 +94,11 @@
-moz-osx-font-smoothing: grayscale;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
- -webkit-transform: translateZ(0);
transform: translateZ(0);
- transition: -webkit-transform .25s ease-out;
transition: transform .25s ease-out;
- transition: transform .25s ease-out,-webkit-transform .25s ease-out;
}
.modal-btn:focus, .modal-btn:hover {
- -webkit-transform: scale(1.05);
transform: scale(1.05);
}
@@ -195,7 +191,7 @@
border: 1px solid #999;
background: #eee;
font-size: 10px;
- font-family: monospace;
+ font-family: var(--monospace-font);
-webkit-user-select: text;
margin-top: 10px;
margin-bottom: 10px;
diff --git a/ui/src/assets/record.scss b/ui/src/assets/record.scss
index 110920d..df6935d 100644
--- a/ui/src/assets/record.scss
+++ b/ui/src/assets/record.scss
@@ -90,12 +90,12 @@
&:hover {
background-color: hsl(88, 50%, 84%);
- box-shadow: 0 0 4px 0px #999;
+ box-shadow: 0 0 4px 0 #999;
}
&.selected {
background-color: hsl(88, 50%, 67%);
- box-shadow: 0 0 4px 0px #999;
+ box-shadow: 0 0 4px 0 #999;
}
&.disabled {
@@ -116,7 +116,7 @@
}
label, select, button {
- font-weight: 100;
+ font-weight: 300;
margin: 3px;
color: #333;
font-size: 17px;
@@ -140,7 +140,7 @@
padding: 4px;
height: 30px;
&:hover, &:active {
- box-shadow: 0 0 4px 0px #ccc;
+ box-shadow: 0 0 4px 0 #ccc;
background-color: #fafafa;
}
i {
@@ -290,7 +290,7 @@
height: auto;
width: 100%;
- padding: 0px;
+ padding: 0;
display: flex;
align-items: center;
}
@@ -317,7 +317,7 @@
padding: 7px;
&:hover:enabled {
- box-shadow: 0 0 3px 0px #aaa;
+ box-shadow: 0 0 3px 0 #aaa;
}
&:not(:enabled) {
@@ -358,7 +358,7 @@
margin-bottom: 20px;
display: flex;
align-items: center;
- padding: 0px;
+ padding: 0;
input {
border-radius: 20px;
@@ -484,7 +484,7 @@
height: 26px;
border-radius: 100px;
background: #f5f5f5;
- box-shadow: 0px 3px 3px rgba(0,0,0,0.15);
+ box-shadow: 0 3px 3px rgba(0,0,0,0.15);
content: '';
transition: all 0.3s ease;
}
@@ -582,7 +582,7 @@
height: 20px;
border-radius: 100px;
background: #f5f5f5;
- box-shadow: 0px 3px 3px rgba(0,0,0,0.15);
+ box-shadow: 0 3px 3px rgba(0,0,0,0.15);
content: '';
transition: all 0.3s ease;
}
@@ -636,12 +636,12 @@
&:hover {
background-color: hsl(88, 50%, 84%);
- box-shadow: 0 0 4px 0px #999;
+ box-shadow: 0 0 4px 0 #999;
}
&.selected {
background-color: hsl(88, 50%, 67%);
- box-shadow: 0 0 4px 0px #999;
+ box-shadow: 0 0 4px 0 #999;
}
img {
@@ -716,7 +716,7 @@
width: 100%;
appearance: none;
-webkit-appearance: none;
- scroll-snap-type: mandatory;
+ scroll-snap-type: x mandatory;
background-color : transparent;
outline: none;
margin-left: -10px;
@@ -947,7 +947,7 @@
}
&.no-top-bar {
- white-space: 'pre';
+ white-space: pre;
&::before {
height: 0;
}
@@ -1054,12 +1054,12 @@
&:hover {
background-color: hsl(88, 50%, 84%);
- box-shadow: 0 0 4px 0px #999;
+ box-shadow: 0 0 4px 0 #999;
}
&.selected {
background-color: hsl(88, 50%, 67%);
- box-shadow: 0 0 4px 0px #999;
+ box-shadow: 0 0 4px 0 #999;
}
}
}
@@ -1076,7 +1076,7 @@
background-color: hsl(88, 50%, 67%);
&:hover {
- box-shadow: 0 0 4px 0px #999;
+ box-shadow: 0 0 4px 0 #999;
}
}
@@ -1097,10 +1097,10 @@
}
} // record-section
-.inline-chip {
+.inline-chip {
@include transition();
&:hover, &:active {
- box-shadow: 0 0 2px 0px #ccc;
+ box-shadow: 0 0 2px 0 #ccc;
background-color: #fafafa;
}
diff --git a/ui/src/assets/sidebar.scss b/ui/src/assets/sidebar.scss
index 7bd6dc9..db8428f 100644
--- a/ui/src/assets/sidebar.scss
+++ b/ui/src/assets/sidebar.scss
@@ -29,26 +29,28 @@
}
input[type=file] { display:none; }
>header {
- font-family: 'Raleway', sans-serif;
+ font-family: 'Roboto Condensed', sans-serif;
+ font-weight: 700;
+ font-size: 24px;
height: var(--topbar-height);
line-height: var(--topbar-height);
vertical-align: middle;
padding: 0 20px;
color: #fff;
- font-weight: 400;
- font-size: 24px;
- letter-spacing: 0.5px;
overflow: visible;
.brand {
height: 40px;
margin-top: 4px;
}
+ &::before {
+ z-index: 10;
+ }
&.canary::before, &.autopush::before {
display: block;
position: absolute;
font-size: 10px;
line-height: 10px;
- font-family: 'Roboto', sans-serif;
+ font-family: 'Raleway', sans-serif;
left: 155px;
top: 7px;
}
@@ -183,7 +185,7 @@
margin-left: 10px;
border-radius: 50%;
border: 2px solid #b4b7ba;
- border-color: #b4b7ba transparent #b4b7ba transparent;
+ border-color: #b4b7ba transparent;
animation: pending-spinner 1.25s linear infinite;
}
@keyframes pending-spinner {
@@ -225,6 +227,7 @@
height: - var(--sidebar-padding-bottom);
grid-template-columns: repeat(4, min-content);
grid-gap: 10px;
+ user-select: text;
> button {
color: hsl(217, 39%, 94%);
@@ -270,7 +273,7 @@
right: 8px;
bottom: 3px;
font-size: 12px;
- font-family: 'Roboto Condensed', 'Roboto', sans-serif;
+ font-family: 'Roboto Condensed', sans-serif;
a {
color: rgba(255, 255, 255, 0.5);
text-decoration: none;
diff --git a/ui/src/assets/topbar.scss b/ui/src/assets/topbar.scss
index 4ab3907..27dd63c 100644
--- a/ui/src/assets/topbar.scss
+++ b/ui/src/assets/topbar.scss
@@ -22,7 +22,7 @@
z-index: 3;
overflow: visible;
background-color: hsl(215, 1%, 95%);
- box-shadow: 0px -3px 14px 2px #bbb;
+ box-shadow: 0 -3px 14px 2px #bbb;
min-height: var(--topbar-height);
display: flex;
justify-content: center;
diff --git a/ui/src/assets/typefaces.scss b/ui/src/assets/typefaces.scss
index 46ad9b4..a8661b9 100644
--- a/ui/src/assets/typefaces.scss
+++ b/ui/src/assets/typefaces.scss
@@ -26,6 +26,42 @@
/* latin */
@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 100;
+ src: url(assets/Roboto-100.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* latin */
+@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 300;
+ src: url(assets/Roboto-300.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* latin */
+@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 400;
+ src: url(assets/Roboto-400.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* latin */
+@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 500;
+ src: url(assets/Roboto-500.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* latin */
+@font-face {
font-family: 'Roboto Condensed';
font-style: normal;
font-weight: 300;
@@ -52,7 +88,6 @@
}
.material-icons {
- font-display: block;
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 4df3cb7..8ab5034 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -16,6 +16,7 @@
import {assertExists, assertTrue} from '../base/logging';
import {randomColor} from '../common/colorizer';
+import {RecordConfig} from '../controller/record_config_types';
import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../tracks/actual_frames/common';
import {ASYNC_SLICE_TRACK_KIND} from '../tracks/async_slices/common';
import {COUNTER_TRACK_KIND} from '../tracks/counter/common';
@@ -32,6 +33,7 @@
} from '../tracks/process_scheduling/common';
import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary/common';
+import {createEmptyState} from './empty_state';
import {DEFAULT_VIEWING_OPTION, PERF_SAMPLES_KEY} from './flamegraph_util';
import {
AggregationAttrs,
@@ -43,14 +45,12 @@
AdbRecordingTarget,
Area,
CallsiteInfo,
- createEmptyState,
EngineMode,
FlamegraphStateViewingOption,
LoadedConfig,
LogsPagination,
NewEngineMode,
OmniboxState,
- RecordConfig,
RecordingTarget,
SCROLLING_TRACK_GROUP,
State,
@@ -83,6 +83,7 @@
engineId: string;
kind: string;
name: string;
+ labels?: string[];
trackKindPriority: TrackKindPriority;
trackGroup?: string;
config: {};
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
index 85eaba8..53e8f65 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -23,8 +23,8 @@
import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state/common';
import {StateActions} from './actions';
+import {createEmptyState} from './empty_state';
import {
- createEmptyState,
SCROLLING_TRACK_GROUP,
State,
TraceUrlSource,
diff --git a/ui/src/common/canvas_utils.ts b/ui/src/common/canvas_utils.ts
index 0835bad..aef36a0 100644
--- a/ui/src/common/canvas_utils.ts
+++ b/ui/src/common/canvas_utils.ts
@@ -72,10 +72,8 @@
x: number,
y: number,
width: number,
- height: number,
- color: string) {
+ height: number) {
ctx.beginPath();
- ctx.fillStyle = color;
const triangleSize = height / 4;
ctx.moveTo(x, y);
ctx.lineTo(x + width, y);
@@ -89,4 +87,4 @@
ctx.lineTo(x + width, y + 4 * triangleSize);
ctx.lineTo(x, y + height);
ctx.fill();
-}
\ No newline at end of file
+}
diff --git a/ui/src/common/colorizer.ts b/ui/src/common/colorizer.ts
index 5ff5e78..0c30df8 100644
--- a/ui/src/common/colorizer.ts
+++ b/ui/src/common/colorizer.ts
@@ -43,7 +43,7 @@
{c: 'yellow', h: 54, s: 100, l: 62},
];
-const GREY_COLOR: Color = {
+export const GRAY_COLOR: Color = {
c: 'grey',
h: 0,
s: 0,
@@ -129,7 +129,7 @@
export function colorForThread(thread?: {pid?: number, tid: number}): Color {
if (thread === undefined) {
- return Object.assign({}, GREY_COLOR);
+ return Object.assign({}, GRAY_COLOR);
}
const tid = thread.pid ? thread.pid : thread.tid;
return colorForTid(tid);
@@ -172,3 +172,14 @@
newLightness = Math.min(newLightness, 88);
return [hue, saturation, newLightness];
}
+
+export function colorToStr(color: Color) {
+ if (color.a !== undefined) {
+ return `hsla(${color.h}, ${color.s}%, ${color.l}%, ${color.a})`;
+ }
+ return `hsl(${color.h}, ${color.s}%, ${color.l}%)`;
+}
+
+export function colorCompare(x: Color, y: Color) {
+ return (x.h - y.h) || (x.s - y.s) || (x.l - y.l);
+}
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
new file mode 100644
index 0000000..a3b1fdc
--- /dev/null
+++ b/ui/src/common/empty_state.ts
@@ -0,0 +1,106 @@
+// 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 {createEmptyRecordConfig} from '../controller/record_config_types';
+import {
+ autosaveConfigStore,
+ recordTargetStore
+} from '../frontend/record_config';
+
+import {featureFlags} from './feature_flags';
+import {defaultTraceTime, State, STATE_VERSION} from './state';
+
+const AUTOLOAD_STARTED_CONFIG_FLAG = featureFlags.register({
+ id: 'autoloadStartedConfig',
+ name: 'Auto-load last used recording config',
+ description: 'Starting a recording automatically saves its configuration. ' +
+ 'This flag controls whether this config is automatically loaded.',
+ defaultValue: true,
+});
+
+export function createEmptyState(): State {
+ return {
+ version: STATE_VERSION,
+ nextId: 0,
+ nextNoteId: 1, // 0 is reserved for ephemeral area marking.
+ nextAreaId: 0,
+ newEngineMode: 'USE_HTTP_RPC_IF_AVAILABLE',
+ engines: {},
+ traceTime: {...defaultTraceTime},
+ tracks: {},
+ uiTrackIdByTraceTrackId: new Map<number, string>(),
+ aggregatePreferences: {},
+ trackGroups: {},
+ visibleTracks: [],
+ pinnedTracks: [],
+ scrollingTracks: [],
+ areas: {},
+ queries: {},
+ metrics: {},
+ permalink: {},
+ notes: {},
+ pivotTableConfig: {},
+ pivotTable: {},
+
+ recordConfig: AUTOLOAD_STARTED_CONFIG_FLAG.get() ?
+ autosaveConfigStore.get() :
+ createEmptyRecordConfig(),
+ displayConfigAsPbtxt: false,
+ lastLoadedConfig: {type: 'NONE'},
+
+ frontendLocalState: {
+ omniboxState: {
+ lastUpdate: 0,
+ omnibox: '',
+ mode: 'SEARCH',
+ },
+
+ visibleState: {
+ ...defaultTraceTime,
+ lastUpdate: 0,
+ resolution: 0,
+ },
+ },
+
+ logsPagination: {
+ offset: 0,
+ count: 0,
+ },
+
+ status: {msg: '', timestamp: 0},
+ currentSelection: null,
+ currentFlamegraphState: null,
+ traceConversionInProgress: false,
+
+ perfDebug: false,
+ sidebarVisible: true,
+ hoveredUtid: -1,
+ hoveredPid: -1,
+ hoveredLogsTimestamp: -1,
+ hoveredNoteTimestamp: -1,
+ highlightedSliceId: -1,
+ focusedFlowIdLeft: -1,
+ focusedFlowIdRight: -1,
+ searchIndex: -1,
+
+ recordingInProgress: false,
+ recordingCancelled: false,
+ extensionInstalled: false,
+ recordingTarget: recordTargetStore.getValidTarget(),
+ availableAdbDevices: [],
+
+ fetchChromeCategories: false,
+ chromeCategories: undefined,
+ };
+}
\ No newline at end of file
diff --git a/ui/src/common/pivot_table_query_generator.ts b/ui/src/common/pivot_table_query_generator.ts
index 1e55eb6..dd6f71d 100644
--- a/ui/src/common/pivot_table_query_generator.ts
+++ b/ui/src/common/pivot_table_query_generator.ts
@@ -121,7 +121,6 @@
private generateJoinQuery(
pivots: PivotAttrs[], aggregations: AggregationAttrs[],
whereFilters: string[], joinTables: string[]): string {
- let joinQuery = 'SELECT\n';
const pivotCols = [];
for (const pivot of pivots) {
@@ -147,15 +146,14 @@
`${getSqlAggregationAlias(aggregation)}`);
}
- joinQuery += pivotCols.concat(aggCols).join(',\n ');
- joinQuery += '\n';
- joinQuery += 'FROM\n';
- joinQuery += joinTables.join(',\n ');
- joinQuery += '\n';
- joinQuery += 'WHERE\n';
- joinQuery += whereFilters.join(' AND\n ');
- joinQuery += '\n';
- return joinQuery;
+ return `
+ SELECT
+ ${pivotCols.concat(aggCols).join(',\n ')}
+ FROM
+ ${joinTables.join(',\n ')}
+ WHERE
+ ${whereFilters.join(' AND\n ')}
+ `;
}
// Partitions the aggregations from the subquery generateJoinQuery over
@@ -170,7 +168,6 @@
pivots, aggregations, whereFilters, joinTables);
}
- let aggQuery = 'SELECT\n';
const pivotCols = getSqlAliasedPivotColumns(pivots);
let partitionByPivotCols = pivotCols;
if (pivots.length > 0 && pivots[0].isStackPivot) {
@@ -211,13 +208,14 @@
`${getSqlAggregationAlias(aggregation)}`);
}
- aggQuery += pivotCols.concat(aggCols).join(',\n ');
- aggQuery += '\n';
- aggQuery += 'FROM (\n';
- aggQuery +=
- this.generateJoinQuery(pivots, aggregations, whereFilters, joinTables);
- aggQuery += ')\n';
- return aggQuery;
+ return `
+ SELECT
+ ${pivotCols.concat(aggCols).join(',\n ')}
+ FROM (
+ ${
+ this.generateJoinQuery(pivots, aggregations, whereFilters, joinTables)}
+ )
+ `;
}
// Takes a list of pivots and aggregations and generates a query that
@@ -233,7 +231,6 @@
return '';
}
- let query = '\nSELECT\n';
const pivotCols = getSqlAliasedPivotColumns(pivots);
const aggCols = getSqlAliasedAggregationsColumns(
@@ -241,19 +238,20 @@
/* has_pivots_selected = */ pivots.length > 0,
isStackQuery);
- query += pivotCols.concat(aggCols).join(',\n ');
- query += '\n';
- query += 'FROM (\n';
- query += this.generateAggregationQuery(
- pivots, aggregations, whereFilters, joinTables, isStackQuery);
- query += ')\n';
- query += 'GROUP BY ';
-
- // Grouping by each pivot, additional pivots, and aggregations.
const aggregationsGroupBy =
aggregations.map(aggregation => getSqlAggregationAlias(aggregation));
- query += pivotCols.concat(aggregationsGroupBy).join(', ');
- query += '\n';
+
+ let query = `
+ SELECT
+ ${pivotCols.concat(aggCols).join(',\n ')}
+ FROM (
+ ${
+ this.generateAggregationQuery(
+ pivots, aggregations, whereFilters, joinTables, isStackQuery)}
+ )
+ GROUP BY
+ ${pivotCols.concat(aggregationsGroupBy).join(', ')}
+ `;
const pivotsOrderBy = [];
@@ -274,9 +272,10 @@
aggregations.map(aggregation => orderString(aggregation));
if (orderBy && pivotsOrderBy.length + aggregationsOrderBy.length > 0) {
- query += 'ORDER BY ';
- query += pivotsOrderBy.concat(aggregationsOrderBy).join(', ');
- query += '\n';
+ query += `
+ ORDER BY
+ ${pivotsOrderBy.concat(aggregationsOrderBy).join(', ')}
+ `;
}
return query;
}
diff --git a/ui/src/common/pivot_table_query_generator_unittest.ts b/ui/src/common/pivot_table_query_generator_unittest.ts
index 93681c7..42b95af 100644
--- a/ui/src/common/pivot_table_query_generator_unittest.ts
+++ b/ui/src/common/pivot_table_query_generator_unittest.ts
@@ -22,6 +22,16 @@
const TABLES = ['slice'];
+// Normalize query for comparison by replacing repeated whitespace characters
+// with a single space.
+function normalize(s: string): string {
+ return s.replace(/\s+/g, ' ');
+}
+
+function expectQueryEqual(actual: string, expected: string) {
+ expect(normalize(actual)).toEqual(normalize(expected));
+}
+
test('Generate query with pivots and aggregations', () => {
const pivotTableQueryGenerator = new PivotTableQueryGenerator();
const selectedPivots: PivotAttrs[] = [
@@ -31,34 +41,38 @@
const selectedAggregations: AggregationAttrs[] = [
{aggregation: 'SUM', tableName: 'slice', columnName: 'dur', order: 'DESC'}
];
- const expectedQuery = '\nSELECT\n' +
- '"slice type",\n' +
- ' "slice id",\n' +
- ' "slice dur (SUM)",\n' +
- ' "slice dur (SUM) (total)"\n' +
- 'FROM (\n' +
- 'SELECT\n' +
- '"slice type",\n' +
- ' "slice id",\n' +
- ' SUM("slice dur (SUM)") OVER () AS "slice dur (SUM) (total)",\n' +
- ' SUM("slice dur (SUM)") OVER (PARTITION BY "slice type", "slice id")' +
- ' AS "slice dur (SUM)"\n' +
- 'FROM (\n' +
- 'SELECT\n' +
- 'slice.type AS "slice type",\n' +
- ' slice.id AS "slice id",\n' +
- ' slice.dur AS "slice dur (SUM)"\n' +
- 'FROM\n' +
- 'slice\n' +
- 'WHERE\n' +
- 'slice.dur != -1\n' +
- ')\n' +
- ')\n' +
- 'GROUP BY "slice type", "slice id", "slice dur (SUM)"\n' +
- 'ORDER BY "slice dur (SUM)" DESC\n';
- expect(pivotTableQueryGenerator.generateQuery(
- selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES))
- .toEqual(expectedQuery);
+ const expectedQuery = `
+ SELECT
+ "slice type",
+ "slice id",
+ "slice dur (SUM)",
+ "slice dur (SUM) (total)"
+ FROM (
+ SELECT
+ "slice type",
+ "slice id",
+ SUM("slice dur (SUM)") OVER () AS "slice dur (SUM) (total)",
+ SUM("slice dur (SUM)")
+ OVER (PARTITION BY "slice type", "slice id")
+ AS "slice dur (SUM)"
+ FROM (
+ SELECT
+ slice.type AS "slice type",
+ slice.id AS "slice id",
+ slice.dur AS "slice dur (SUM)"
+ FROM
+ slice
+ WHERE
+ slice.dur != -1
+ )
+ )
+ GROUP BY "slice type", "slice id", "slice dur (SUM)"
+ ORDER BY "slice dur (SUM)" DESC
+ `;
+ expectQueryEqual(
+ pivotTableQueryGenerator.generateQuery(
+ selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES),
+ expectedQuery);
});
test('Generate query with pivots', () => {
@@ -68,22 +82,25 @@
{tableName: 'slice', columnName: 'id', isStackPivot: false}
];
const selectedAggregations: AggregationAttrs[] = [];
- const expectedQuery = '\nSELECT\n' +
- '"slice type",\n' +
- ' "slice id"\n' +
- 'FROM (\n' +
- 'SELECT\n' +
- 'slice.type AS "slice type",\n' +
- ' slice.id AS "slice id"\n' +
- 'FROM\n' +
- 'slice\n' +
- 'WHERE\n' +
- 'slice.dur != -1\n' +
- ')\n' +
- 'GROUP BY "slice type", "slice id"\n';
- expect(pivotTableQueryGenerator.generateQuery(
- selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES))
- .toEqual(expectedQuery);
+ const expectedQuery = `
+ SELECT
+ "slice type",
+ "slice id"
+ FROM (
+ SELECT
+ slice.type AS "slice type",
+ slice.id AS "slice id"
+ FROM
+ slice
+ WHERE
+ slice.dur != -1
+ )
+ GROUP BY "slice type", "slice id"
+ `;
+ expectQueryEqual(
+ pivotTableQueryGenerator.generateQuery(
+ selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES),
+ expectedQuery);
});
test('Generate query with aggregations', () => {
@@ -93,28 +110,31 @@
{aggregation: 'SUM', tableName: 'slice', columnName: 'dur', order: 'DESC'},
{aggregation: 'MAX', tableName: 'slice', columnName: 'dur', order: 'ASC'}
];
- const expectedQuery = '\nSELECT\n' +
- '"slice dur (SUM)",\n' +
- ' "slice dur (MAX)"\n' +
- 'FROM (\n' +
- 'SELECT\n' +
- 'SUM("slice dur (SUM)") AS "slice dur (SUM)",\n' +
- ' MAX("slice dur (MAX)") AS "slice dur (MAX)"\n' +
- 'FROM (\n' +
- 'SELECT\n' +
- 'slice.dur AS "slice dur (SUM)",\n' +
- ' slice.dur AS "slice dur (MAX)"\n' +
- 'FROM\n' +
- 'slice\n' +
- 'WHERE\n' +
- 'slice.dur != -1\n' +
- ')\n' +
- ')\n' +
- 'GROUP BY "slice dur (SUM)", "slice dur (MAX)"\n' +
- 'ORDER BY "slice dur (SUM)" DESC, "slice dur (MAX)" ASC\n';
- expect(pivotTableQueryGenerator.generateQuery(
- selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES))
- .toEqual(expectedQuery);
+ const expectedQuery = `
+ SELECT
+ "slice dur (SUM)",
+ "slice dur (MAX)"
+ FROM (
+ SELECT
+ SUM("slice dur (SUM)") AS "slice dur (SUM)",
+ MAX("slice dur (MAX)") AS "slice dur (MAX)"
+ FROM (
+ SELECT
+ slice.dur AS "slice dur (SUM)",
+ slice.dur AS "slice dur (MAX)"
+ FROM
+ slice
+ WHERE
+ slice.dur != -1
+ )
+ )
+ GROUP BY "slice dur (SUM)", "slice dur (MAX)"
+ ORDER BY "slice dur (SUM)" DESC, "slice dur (MAX)" ASC
+ `;
+ expectQueryEqual(
+ pivotTableQueryGenerator.generateQuery(
+ selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES),
+ expectedQuery);
});
test('Generate a query with stack pivot', () => {
@@ -126,45 +146,52 @@
const selectedAggregations: AggregationAttrs[] = [
{aggregation: 'COUNT', tableName: 'slice', columnName: 'id', order: 'DESC'},
];
- const expectedQuery = '\nSELECT\n' +
- '"slice name (stack)",\n' +
- ' "slice depth (hidden)",\n' +
- ' "slice stack_id (hidden)",\n' +
- ' "slice parent_stack_id (hidden)",\n' +
- ' "slice category",\n' +
- ' "slice id (COUNT)",\n' +
- ' "slice id (COUNT) (total)"\n' +
- 'FROM (\n' +
- 'SELECT\n' +
- '"slice name (stack)",\n' +
- ' "slice depth (hidden)",\n' +
- ' "slice stack_id (hidden)",\n' +
- ' "slice parent_stack_id (hidden)",\n' +
- ' "slice category",\n' +
- ' COUNT("slice id (COUNT)") OVER () AS "slice id (COUNT) (total)",\n' +
- ' COUNT("slice id (COUNT)") OVER (PARTITION BY' +
- ' "slice stack_id (hidden)", "slice category") AS "slice id (COUNT)"\n' +
- 'FROM (\n' +
- 'SELECT\n' +
- 'slice.name AS "slice name (stack)",\n' +
- ' slice.depth AS "slice depth (hidden)",\n' +
- ' slice.stack_id AS "slice stack_id (hidden)",\n' +
- ' slice.parent_stack_id AS "slice parent_stack_id (hidden)",\n' +
- ' slice.category AS "slice category",\n' +
- ' slice.id AS "slice id (COUNT)"\n' +
- 'FROM\n' +
- 'slice\n' +
- 'WHERE\n' +
- 'slice.dur != -1\n' +
- ')\n' +
- ')\n' +
- 'GROUP BY "slice name (stack)", "slice depth (hidden)", ' +
- '"slice stack_id (hidden)", "slice parent_stack_id (hidden)", ' +
- '"slice category", "slice id (COUNT)"\n' +
- 'ORDER BY "slice id (COUNT)" DESC\n';
- expect(pivotTableQueryGenerator.generateQuery(
- selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES))
- .toEqual(expectedQuery);
+ const expectedQuery = `
+ SELECT
+ "slice name (stack)",
+ "slice depth (hidden)",
+ "slice stack_id (hidden)",
+ "slice parent_stack_id (hidden)",
+ "slice category",
+ "slice id (COUNT)",
+ "slice id (COUNT) (total)"
+ FROM (
+ SELECT
+ "slice name (stack)",
+ "slice depth (hidden)",
+ "slice stack_id (hidden)",
+ "slice parent_stack_id (hidden)",
+ "slice category",
+ COUNT("slice id (COUNT)") OVER () AS "slice id (COUNT) (total)",
+ COUNT("slice id (COUNT)")
+ OVER (PARTITION BY "slice stack_id (hidden)", "slice category")
+ AS "slice id (COUNT)"
+ FROM (
+ SELECT
+ slice.name AS "slice name (stack)",
+ slice.depth AS "slice depth (hidden)",
+ slice.stack_id AS "slice stack_id (hidden)",
+ slice.parent_stack_id AS "slice parent_stack_id (hidden)",
+ slice.category AS "slice category",
+ slice.id AS "slice id (COUNT)"
+ FROM
+ slice
+ WHERE
+ slice.dur != -1
+ )
+ )
+ GROUP BY "slice name (stack)",
+ "slice depth (hidden)",
+ "slice stack_id (hidden)",
+ "slice parent_stack_id (hidden)",
+ "slice category",
+ "slice id (COUNT)"
+ ORDER BY "slice id (COUNT)" DESC
+ `;
+ expectQueryEqual(
+ pivotTableQueryGenerator.generateQuery(
+ selectedPivots, selectedAggregations, WHERE_FILTERS, TABLES),
+ expectedQuery);
});
test('Generate a descendant stack query', () => {
@@ -173,34 +200,39 @@
{tableName: 'slice', columnName: SLICE_STACK_COLUMN, isStackPivot: true},
];
const selectedAggregations: AggregationAttrs[] = [];
- const expectedQuery = '\nSELECT\n' +
- '"slice name (stack)",\n' +
- ' "slice depth (hidden)",\n' +
- ' "slice stack_id (hidden)",\n' +
- ' "slice parent_stack_id (hidden)"\n' +
- 'FROM (\n' +
- 'SELECT\n' +
- 'slice.name AS "slice name (stack)",\n' +
- ' slice.depth AS "slice depth (hidden)",\n' +
- ' slice.stack_id AS "slice stack_id (hidden)",\n' +
- ' slice.parent_stack_id AS "slice parent_stack_id (hidden)"\n' +
- 'FROM\n' +
- 'descendant_slice_by_stack(stack_id) AS slice\n' +
- 'WHERE\n' +
- 'slice.dur != -1\n' +
- ')\n' +
- 'GROUP BY "slice name (stack)", "slice depth (hidden)", ' +
- '"slice stack_id (hidden)", "slice parent_stack_id (hidden)"\n' +
- 'ORDER BY "slice depth (hidden)" ASC\n';
+ const expectedQuery = `
+ SELECT
+ "slice name (stack)",
+ "slice depth (hidden)",
+ "slice stack_id (hidden)",
+ "slice parent_stack_id (hidden)"
+ FROM (
+ SELECT
+ slice.name AS "slice name (stack)",
+ slice.depth AS "slice depth (hidden)",
+ slice.stack_id AS "slice stack_id (hidden)",
+ slice.parent_stack_id AS "slice parent_stack_id (hidden)"
+ FROM
+ descendant_slice_by_stack(stack_id) AS slice
+ WHERE
+ slice.dur != -1
+ )
+ GROUP BY "slice name (stack)",
+ "slice depth (hidden)",
+ "slice stack_id (hidden)",
+ "slice parent_stack_id (hidden)"
+ ORDER BY "slice depth (hidden)" ASC
+ `;
const table = ['descendant_slice_by_stack(stack_id) AS slice'];
- expect(pivotTableQueryGenerator.generateStackQuery(
- selectedPivots,
- selectedAggregations,
- WHERE_FILTERS,
- table,
- /* stack_id = */ 'stack_id'))
- .toEqual(expectedQuery);
+ expectQueryEqual(
+ pivotTableQueryGenerator.generateStackQuery(
+ selectedPivots,
+ selectedAggregations,
+ WHERE_FILTERS,
+ table,
+ /* stack_id = */ 'stack_id'),
+ expectedQuery);
});
test('Generate a descendant stack query with another pivot', () => {
@@ -210,58 +242,65 @@
{tableName: 'slice', columnName: 'category', isStackPivot: false}
];
const selectedAggregations: AggregationAttrs[] = [];
- const expectedQuery = '\nSELECT\n' +
- '"slice name (stack)",\n' +
- ' "slice depth (hidden)",\n' +
- ' "slice stack_id (hidden)",\n' +
- ' "slice parent_stack_id (hidden)",\n' +
- ' "slice category"\n' +
- 'FROM (\n' +
- 'SELECT\n' +
- 'slice.name AS "slice name (stack)",\n' +
- ' slice.depth AS "slice depth (hidden)",\n' +
- ' slice.stack_id AS "slice stack_id (hidden)",\n' +
- ' slice.parent_stack_id AS "slice parent_stack_id (hidden)",\n' +
- ' slice.category AS "slice category"\n' +
- 'FROM\n' +
- 'slice\n' +
- 'WHERE\n' +
- 'slice.dur != -1 AND\n' +
- ' slice.stack_id = stack_id\n' +
- ')\n' +
- 'GROUP BY "slice name (stack)", "slice depth (hidden)", ' +
- '"slice stack_id (hidden)", "slice parent_stack_id (hidden)", ' +
- '"slice category"\n' +
- ' UNION ALL \n' +
- 'SELECT\n' +
- '"slice name (stack)",\n' +
- ' "slice depth (hidden)",\n' +
- ' "slice stack_id (hidden)",\n' +
- ' "slice parent_stack_id (hidden)",\n' +
- ' "slice category"\n' +
- 'FROM (\n' +
- 'SELECT\n' +
- 'slice.name AS "slice name (stack)",\n' +
- ' slice.depth AS "slice depth (hidden)",\n' +
- ' slice.stack_id AS "slice stack_id (hidden)",\n' +
- ' slice.parent_stack_id AS "slice parent_stack_id (hidden)",\n' +
- ' slice.category AS "slice category"\n' +
- 'FROM\n' +
- 'descendant_slice_by_stack(stack_id) AS slice\n' +
- 'WHERE\n' +
- 'slice.dur != -1\n' +
- ')\n' +
- 'GROUP BY "slice name (stack)", "slice depth (hidden)", ' +
- '"slice stack_id (hidden)", "slice parent_stack_id (hidden)", ' +
- '"slice category"\n' +
- 'ORDER BY "slice depth (hidden)" ASC\n';
+ const expectedQuery = `
+ SELECT
+ "slice name (stack)",
+ "slice depth (hidden)",
+ "slice stack_id (hidden)",
+ "slice parent_stack_id (hidden)",
+ "slice category"
+ FROM (
+ SELECT
+ slice.name AS "slice name (stack)",
+ slice.depth AS "slice depth (hidden)",
+ slice.stack_id AS "slice stack_id (hidden)",
+ slice.parent_stack_id AS "slice parent_stack_id (hidden)",
+ slice.category AS "slice category"
+ FROM
+ slice
+ WHERE
+ slice.dur != -1 AND
+ slice.stack_id = stack_id
+ )
+ GROUP BY "slice name (stack)",
+ "slice depth (hidden)",
+ "slice stack_id (hidden)",
+ "slice parent_stack_id (hidden)",
+ "slice category"
+ UNION ALL
+ SELECT
+ "slice name (stack)",
+ "slice depth (hidden)",
+ "slice stack_id (hidden)",
+ "slice parent_stack_id (hidden)",
+ "slice category"
+ FROM (
+ SELECT
+ slice.name AS "slice name (stack)",
+ slice.depth AS "slice depth (hidden)",
+ slice.stack_id AS "slice stack_id (hidden)",
+ slice.parent_stack_id AS "slice parent_stack_id (hidden)",
+ slice.category AS "slice category"
+ FROM
+ descendant_slice_by_stack(stack_id) AS slice
+ WHERE
+ slice.dur != -1
+ )
+ GROUP BY "slice name (stack)",
+ "slice depth (hidden)",
+ "slice stack_id (hidden)",
+ "slice parent_stack_id (hidden)",
+ "slice category"
+ ORDER BY "slice depth (hidden)" ASC
+ `;
const table = ['descendant_slice_by_stack(stack_id) AS slice'];
- expect(pivotTableQueryGenerator.generateStackQuery(
- selectedPivots,
- selectedAggregations,
- WHERE_FILTERS,
- table,
- /* stack_id = */ 'stack_id'))
- .toEqual(expectedQuery);
+ expectQueryEqual(
+ pivotTableQueryGenerator.generateStackQuery(
+ selectedPivots,
+ selectedAggregations,
+ WHERE_FILTERS,
+ table,
+ /* stack_id = */ 'stack_id'),
+ expectedQuery);
});
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 4d0c3f7..865982f 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -12,13 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {createEmptyRecordConfig} from '../controller/validate_config';
-import {
- autosaveConfigStore,
- recordTargetStore
-} from '../frontend/record_config';
+import {RecordConfig} from '../controller/record_config_types';
-import {featureFlags} from './feature_flags';
import {
AggregationAttrs,
PivotAttrs,
@@ -149,6 +144,7 @@
engineId: string;
kind: string;
name: string;
+ labels?: string[];
trackKindPriority: TrackKindPriority;
trackGroup?: string;
config: {};
@@ -280,6 +276,7 @@
(NoteSelection|SliceSelection|CounterSelection|HeapProfileSelection|
CpuProfileSampleSelection|ChromeSliceSelection|ThreadStateSelection|
AreaSelection|PerfSamplesSelection)&{trackId?: string};
+export type SelectionKind = Selection['kind']; // 'THREAD_STATE' | 'SLICE' ...
export interface LogsPagination {
offset: number;
@@ -481,87 +478,6 @@
return config.chromeHighOverheadCategoriesSelected.length > 0;
}
-export interface RecordConfig {
- // Global settings
- mode: RecordMode;
- durationMs: number;
- bufferSizeMb: number;
- maxFileSizeMb: number; // Only for mode == 'LONG_TRACE'.
- fileWritePeriodMs: number; // Only for mode == 'LONG_TRACE'.
-
- cpuSched: boolean;
- cpuFreq: boolean;
- cpuCoarse: boolean;
- cpuCoarsePollMs: number;
- cpuSyscall: boolean;
-
- gpuFreq: boolean;
- gpuMemTotal: boolean;
-
- ftrace: boolean;
- atrace: boolean;
- ftraceEvents: string[];
- ftraceExtraEvents: string;
- atraceCats: string[];
- atraceApps: string;
- ftraceBufferSizeKb: number;
- ftraceDrainPeriodMs: number;
- androidLogs: boolean;
- androidLogBuffers: string[];
- androidFrameTimeline: boolean;
-
- batteryDrain: boolean;
- batteryDrainPollMs: number;
-
- boardSensors: boolean;
-
- memHiFreq: boolean;
- memLmk: boolean;
- meminfo: boolean;
- meminfoPeriodMs: number;
- meminfoCounters: string[];
- vmstat: boolean;
- vmstatPeriodMs: number;
- vmstatCounters: string[];
-
- heapProfiling: boolean;
- hpSamplingIntervalBytes: number;
- hpProcesses: string;
- hpContinuousDumpsPhase: number;
- hpContinuousDumpsInterval: number;
- hpSharedMemoryBuffer: number;
- hpBlockClient: boolean;
- hpAllHeaps: boolean;
-
- javaHeapDump: boolean;
- jpProcesses: string;
- jpContinuousDumpsPhase: number;
- jpContinuousDumpsInterval: number;
-
- procStats: boolean;
- procStatsPeriodMs: number;
-
- chromeCategoriesSelected: string[];
- chromeHighOverheadCategoriesSelected: string[];
-
- chromeLogs: boolean;
- taskScheduling: boolean;
- ipcFlows: boolean;
- jsExecution: boolean;
- webContentRendering: boolean;
- uiRendering: boolean;
- inputEvents: boolean;
- navigationAndLoading: boolean;
-
- symbolizeKsyms: boolean;
-}
-
-export interface NamedRecordConfig {
- title: string;
- config: RecordConfig;
- key: string;
-}
-
export function getDefaultRecordingTargets(): RecordingTarget[] {
return [
{os: 'Q', name: 'Android Q+'},
@@ -817,90 +733,6 @@
];
}
-const AUTOLOAD_STARTED_CONFIG_FLAG = featureFlags.register({
- id: 'autoloadStartedConfig',
- name: 'Auto-load last used recording config',
- description: 'Starting a recording automatically saves its configuration. ' +
- 'This flag controls whether this config is automatically loaded.',
- defaultValue: false,
-});
-
-export function createEmptyState(): State {
- return {
- version: STATE_VERSION,
- nextId: 0,
- nextNoteId: 1, // 0 is reserved for ephemeral area marking.
- nextAreaId: 0,
- newEngineMode: 'USE_HTTP_RPC_IF_AVAILABLE',
- engines: {},
- traceTime: {...defaultTraceTime},
- tracks: {},
- uiTrackIdByTraceTrackId: new Map<number, string>(),
- aggregatePreferences: {},
- trackGroups: {},
- visibleTracks: [],
- pinnedTracks: [],
- scrollingTracks: [],
- areas: {},
- queries: {},
- metrics: {},
- permalink: {},
- notes: {},
- pivotTableConfig: {},
- pivotTable: {},
-
- recordConfig: AUTOLOAD_STARTED_CONFIG_FLAG.get() ?
- autosaveConfigStore.get() :
- createEmptyRecordConfig(),
- displayConfigAsPbtxt: false,
- lastLoadedConfig: {type: 'NONE'},
-
- frontendLocalState: {
- omniboxState: {
- lastUpdate: 0,
- omnibox: '',
- mode: 'SEARCH',
- },
-
- visibleState: {
- ...defaultTraceTime,
- lastUpdate: 0,
- resolution: 0,
- },
- },
-
- logsPagination: {
- offset: 0,
- count: 0,
- },
-
- status: {msg: '', timestamp: 0},
- currentSelection: null,
- currentFlamegraphState: null,
- traceConversionInProgress: false,
-
- perfDebug: false,
- sidebarVisible: true,
- hoveredUtid: -1,
- hoveredPid: -1,
- hoveredLogsTimestamp: -1,
- hoveredNoteTimestamp: -1,
- highlightedSliceId: -1,
- focusedFlowIdLeft: -1,
- focusedFlowIdRight: -1,
- searchIndex: -1,
-
- recordingInProgress: false,
- recordingCancelled: false,
- extensionInstalled: false,
- recordingTarget: recordTargetStore.getValidTarget(),
- availableAdbDevices: [],
-
- fetchChromeCategories: false,
- chromeCategories: undefined,
- };
-}
-
export function getContainingTrackId(state: State, trackId: string): null|
string {
const track = state.tracks[trackId];
diff --git a/ui/src/common/state_unittest.ts b/ui/src/common/state_unittest.ts
index 2f94a6e..046f864 100644
--- a/ui/src/common/state_unittest.ts
+++ b/ui/src/common/state_unittest.ts
@@ -12,12 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {
- createEmptyState,
- getContainingTrackId,
- State,
- TrackKindPriority
-} from './state';
+import {createEmptyState} from './empty_state';
+import {getContainingTrackId, State, TrackKindPriority} from './state';
test('createEmptyState', () => {
const state: State = createEmptyState();
diff --git a/ui/src/common/upload_utils.ts b/ui/src/common/upload_utils.ts
index 03a9e39..d817084 100644
--- a/ui/src/common/upload_utils.ts
+++ b/ui/src/common/upload_utils.ts
@@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {RecordConfig} from '../controller/record_config_types';
+
export const BUCKET_NAME = 'perfetto-ui-data';
import * as uuidv4 from 'uuid/v4';
-import {State, RecordConfig} from './state';
+import {State} from './state';
export async function saveTrace(trace: File|ArrayBuffer): Promise<string> {
// TODO(hjd): This should probably also be a hash but that requires
diff --git a/ui/src/controller/globals.ts b/ui/src/controller/globals.ts
index f479f37..84564a8 100644
--- a/ui/src/controller/globals.ts
+++ b/ui/src/controller/globals.ts
@@ -16,8 +16,10 @@
import {assertExists} from '../base/logging';
import {DeferredAction} from '../common/actions';
-import {createEmptyState, State} from '../common/state';
+import {createEmptyState} from '../common/empty_state';
+import {State} from '../common/state';
import {globals as frontendGlobals} from '../frontend/globals';
+
import {ControllerAny} from './controller';
export interface App {
diff --git a/ui/src/controller/permalink_controller.ts b/ui/src/controller/permalink_controller.ts
index 99b962c..cfb87dd 100644
--- a/ui/src/controller/permalink_controller.ts
+++ b/ui/src/controller/permalink_controller.ts
@@ -17,8 +17,9 @@
import {assertExists} from '../base/logging';
import {Actions} from '../common/actions';
import {ConversionJobStatus} from '../common/conversion_jobs';
-import {createEmptyState, State} from '../common/state';
-import {RecordConfig, STATE_VERSION} from '../common/state';
+import {createEmptyState} from '../common/empty_state';
+import {State} from '../common/state';
+import {STATE_VERSION} from '../common/state';
import {
BUCKET_NAME,
saveState,
@@ -30,7 +31,8 @@
import {Controller} from './controller';
import {globals} from './globals';
-import {JsonObject, runParser, validateRecordConfig} from './validate_config';
+import {RecordConfig, recordConfigValidator} from './record_config_types';
+import {runValidator} from './validators';
export class PermalinkController extends Controller<'main'> {
private lastRequestId?: string;
@@ -76,10 +78,9 @@
if (PermalinkController.isRecordConfig(stateOrConfig)) {
// This permalink state only contains a RecordConfig. Show the
// recording page with the config, but keep other state as-is.
- const validConfig = runParser(
- validateRecordConfig,
- stateOrConfig as unknown as JsonObject)
- .result;
+ const validConfig =
+ runValidator(recordConfigValidator, stateOrConfig as unknown)
+ .result;
globals.dispatch(Actions.setRecordConfig({config: validConfig}));
Router.navigate('#!/record');
return;
diff --git a/ui/src/controller/record_config_types.ts b/ui/src/controller/record_config_types.ts
new file mode 100644
index 0000000..95df440
--- /dev/null
+++ b/ui/src/controller/record_config_types.ts
@@ -0,0 +1,111 @@
+// 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 {
+ oneOf,
+ num,
+ bool,
+ arrayOf,
+ str,
+ requiredStr,
+ record,
+ runValidator,
+ ValidatedType
+} from './validators';
+
+const recordModes = ['STOP_WHEN_FULL', 'RING_BUFFER', 'LONG_TRACE'] as const;
+export const recordConfigValidator = record({
+ mode: oneOf(recordModes, 'STOP_WHEN_FULL'),
+ durationMs: num(10000.0),
+ maxFileSizeMb: num(100),
+ fileWritePeriodMs: num(2500),
+ bufferSizeMb: num(64.0),
+
+ cpuSched: bool(),
+ cpuFreq: bool(),
+ cpuSyscall: bool(),
+
+ gpuFreq: bool(),
+ gpuMemTotal: bool(),
+
+ ftrace: bool(),
+ atrace: bool(),
+ ftraceEvents: arrayOf(str()),
+ ftraceExtraEvents: str(),
+ atraceCats: arrayOf(str()),
+ atraceApps: str(),
+ ftraceBufferSizeKb: num(2 * 1024),
+ ftraceDrainPeriodMs: num(250),
+ androidLogs: bool(),
+ androidLogBuffers: arrayOf(str()),
+ androidFrameTimeline: bool(),
+
+ cpuCoarse: bool(),
+ cpuCoarsePollMs: num(1000),
+
+ batteryDrain: bool(),
+ batteryDrainPollMs: num(1000),
+
+ boardSensors: bool(),
+
+ memHiFreq: bool(),
+ meminfo: bool(),
+ meminfoPeriodMs: num(1000),
+ meminfoCounters: arrayOf(str()),
+
+ vmstat: bool(),
+ vmstatPeriodMs: num(1000),
+ vmstatCounters: arrayOf(str()),
+
+ heapProfiling: bool(),
+ hpSamplingIntervalBytes: num(4096),
+ hpProcesses: str(),
+ hpContinuousDumpsPhase: num(),
+ hpContinuousDumpsInterval: num(),
+ hpSharedMemoryBuffer: num(8 * 1048576),
+ hpBlockClient: bool(true),
+ hpAllHeaps: bool(),
+
+ javaHeapDump: bool(),
+ jpProcesses: str(),
+ jpContinuousDumpsPhase: num(),
+ jpContinuousDumpsInterval: num(),
+
+ memLmk: bool(),
+ procStats: bool(),
+ procStatsPeriodMs: num(1000),
+
+ chromeCategoriesSelected: arrayOf(str()),
+ chromeHighOverheadCategoriesSelected: arrayOf(str()),
+
+ chromeLogs: bool(),
+ taskScheduling: bool(),
+ ipcFlows: bool(),
+ jsExecution: bool(),
+ webContentRendering: bool(),
+ uiRendering: bool(),
+ inputEvents: bool(),
+ navigationAndLoading: bool(),
+
+ symbolizeKsyms: bool(),
+});
+export const namedRecordConfigValidator = record(
+ {title: requiredStr, key: requiredStr, config: recordConfigValidator});
+export type NamedRecordConfig =
+ ValidatedType<typeof namedRecordConfigValidator>;
+export type RecordConfig = ValidatedType<typeof recordConfigValidator>;
+
+export function createEmptyRecordConfig(): RecordConfig {
+ return runValidator(recordConfigValidator, {}).result;
+}
diff --git a/ui/src/controller/record_controller.ts b/ui/src/controller/record_controller.ts
index 4518e87..0868940 100644
--- a/ui/src/controller/record_controller.ts
+++ b/ui/src/controller/record_controller.ts
@@ -43,7 +43,7 @@
isAndroidP,
isChromeTarget,
isCrOSTarget,
- RecordConfig,
+ isLinuxTarget,
RecordingTarget
} from '../common/state';
import {publishBufferUsage, publishTrackData} from '../frontend/publish';
@@ -63,6 +63,7 @@
} from './consumer_port_types';
import {Controller} from './controller';
import {App, globals} from './globals';
+import {RecordConfig} from './record_config_types';
import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
type RPCImplMethod = (Method|rpc.ServiceMethod<Message<{}>, Message<{}>>);
@@ -164,15 +165,19 @@
if (uiCfg.batteryDrain) {
const ds = new TraceConfig.DataSource();
ds.config = new DataSourceConfig();
- ds.config.name = 'android.power';
- ds.config.androidPowerConfig = new AndroidPowerConfig();
- ds.config.androidPowerConfig.batteryPollMs = uiCfg.batteryDrainPollMs;
- ds.config.androidPowerConfig.batteryCounters = [
- AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CAPACITY_PERCENT,
- AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CHARGE,
- AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CURRENT,
- ];
- ds.config.androidPowerConfig.collectPowerRails = true;
+ if (isCrOSTarget(target) || isLinuxTarget(target)) {
+ ds.config.name = 'linux.sysfs_power';
+ } else {
+ ds.config.name = 'android.power';
+ ds.config.androidPowerConfig = new AndroidPowerConfig();
+ ds.config.androidPowerConfig.batteryPollMs = uiCfg.batteryDrainPollMs;
+ ds.config.androidPowerConfig.batteryCounters = [
+ AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CAPACITY_PERCENT,
+ AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CHARGE,
+ AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CURRENT,
+ ];
+ ds.config.androidPowerConfig.collectPowerRails = true;
+ }
if (!isChromeTarget(target) || isCrOSTarget(target)) {
protoCfg.dataSources.push(ds);
}
diff --git a/ui/src/controller/record_controller_jsdomtest.ts b/ui/src/controller/record_controller_jsdomtest.ts
index 09916a2..d263cf0 100644
--- a/ui/src/controller/record_controller_jsdomtest.ts
+++ b/ui/src/controller/record_controller_jsdomtest.ts
@@ -15,8 +15,8 @@
import {assertExists} from '../base/logging';
import {TraceConfig} from '../common/protos';
+import {createEmptyRecordConfig} from './record_config_types';
import {genConfigProto, toPbtxt} from './record_controller';
-import {createEmptyRecordConfig} from './validate_config';
test('encodeConfig', () => {
const config = createEmptyRecordConfig();
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index 3726947..82f6e22 100644
--- a/ui/src/controller/selection_controller.ts
+++ b/ui/src/controller/selection_controller.ts
@@ -416,10 +416,22 @@
}
// Find the sched slice with the utid of the waker running when the
// sched wakeup occurred. This is the waker.
- const queryWaker = `select utid, cpu from sched where utid =
- (select utid from raw where name = '${event}' and ts = ${wakeupTs})
+ let queryWaker = `select utid, cpu from sched where utid =
+ (select EXTRACT_ARG(arg_set_id, 'waker_utid') from instants where name =
+ '${event}' and ts = ${wakeupTs})
and ts < ${wakeupTs} and ts + dur >= ${wakeupTs};`;
- const wakerResult = await this.args.engine.query(queryWaker);
+ let wakerResult = await this.args.engine.query(queryWaker);
+ if (wakerResult.numRows() === 0) {
+ // An old version of trace processor (that does not populate the
+ // 'waker_utid' arg) might be in use. Try getting the same info from the
+ // raw table).
+ // TODO(b/206390308): Remove this workaround when
+ // TRACE_PROCESSOR_CURRENT_API_VERSION is incremented.
+ queryWaker = `select utid, cpu from sched where utid =
+ (select utid from raw where name = '${event}' and ts = ${wakeupTs})
+ and ts < ${wakeupTs} and ts + dur >= ${wakeupTs};`;
+ wakerResult = await this.args.engine.query(queryWaker);
+ }
if (wakerResult.numRows() === 0) {
return undefined;
}
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 74576b0..1686210 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -616,7 +616,8 @@
id INTEGER PRIMARY KEY,
name STRING,
__metric_name STRING,
- upid INTEGER
+ upid INTEGER,
+ group_name STRING
);
`);
@@ -672,6 +673,7 @@
let hasDur = false;
let hasUpid = false;
let hasValue = false;
+ let hasGroupName = false;
const it = result.iter({name: STR});
for (; it.valid(); it.next()) {
const name = it.name;
@@ -679,17 +681,22 @@
hasDur = hasDur || name === 'dur';
hasUpid = hasUpid || name === 'upid';
hasValue = hasValue || name === 'value';
+ hasGroupName = hasGroupName || name === 'group_name';
}
const upidColumnSelect = hasUpid ? 'upid' : '0 AS upid';
const upidColumnWhere = hasUpid ? 'upid' : '0';
+ const groupNameColumn =
+ hasGroupName ? 'group_name' : 'NULL AS group_name';
if (hasSliceName && hasDur) {
await engine.query(`
- INSERT INTO annotation_slice_track(name, __metric_name, upid)
+ INSERT INTO annotation_slice_track(
+ name, __metric_name, upid, group_name)
SELECT DISTINCT
track_name,
'${metric}' as metric_name,
- ${upidColumnSelect}
+ ${upidColumnSelect},
+ ${groupNameColumn}
FROM ${metric}_event
WHERE track_type = 'slice'
`);
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index dbb7bb6..f1765fc 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -21,7 +21,7 @@
DeferredAction,
} from '../common/actions';
import {Engine} from '../common/engine';
-import {PERF_SAMPLE_FLAG} from '../common/feature_flags';
+import {featureFlags, PERF_SAMPLE_FLAG} from '../common/feature_flags';
import {
NUM,
NUM_NULL,
@@ -50,6 +50,13 @@
import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary/common';
import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state/common';
+const TRACKS_V2_FLAG = featureFlags.register({
+ id: 'tracksV2',
+ name: 'Tracks V2',
+ description: 'Show tracks built on top of the Track V2 API.',
+ defaultValue: false,
+});
+
const MEM_DMA_COUNTER_NAME = 'mem.dma_heap';
const MEM_DMA = 'mem.dma_buffer';
const MEM_ION = 'mem.ion';
@@ -420,25 +427,56 @@
async addAnnotationTracks(): Promise<void> {
const sliceResult = await this.engine.query(`
- SELECT id, name, upid FROM annotation_slice_track`);
+ SELECT id, name, upid, group_name FROM annotation_slice_track`);
const sliceIt = sliceResult.iter({
id: NUM,
name: STR,
upid: NUM,
+ group_name: STR_NULL,
});
+ interface GroupIds {
+ id: string;
+ summaryTrackId: string;
+ }
+
+ const groupNameToIds = new Map<string, GroupIds>();
+
for (; sliceIt.valid(); sliceIt.next()) {
const id = sliceIt.id;
const name = sliceIt.name;
const upid = sliceIt.upid;
+ const groupName = sliceIt.group_name;
+
+ let trackId = undefined;
+ let trackGroupId =
+ upid === 0 ? SCROLLING_TRACK_GROUP : this.upidToUuid.get(upid);
+
+ if (groupName) {
+ // If this is the first track encountered for a certain group,
+ // create an id for the group and use this track as the group's
+ // summary track.
+ const groupIds = groupNameToIds.get(groupName);
+ if (groupIds) {
+ trackGroupId = groupIds.id;
+ } else {
+ trackGroupId = uuidv4();
+ trackId = uuidv4();
+ groupNameToIds.set(groupName, {
+ id: trackGroupId,
+ summaryTrackId: trackId,
+ });
+ }
+ }
+
this.tracksToAdd.push({
+ id: trackId,
engineId: this.engineId,
kind: SLICE_TRACK_KIND,
name,
trackKindPriority: TrackDecider.inferTrackKindPriority(name),
- trackGroup: upid === 0 ? SCROLLING_TRACK_GROUP :
- this.upidToUuid.get(upid),
+ trackGroup: trackGroupId,
config: {
maxDepth: 0,
namespace: 'annotation',
@@ -447,6 +485,17 @@
});
}
+ for (const [groupName, groupIds] of groupNameToIds) {
+ const addGroup = Actions.addTrackGroup({
+ engineId: this.engineId,
+ summaryTrackId: groupIds.summaryTrackId,
+ name: groupName,
+ id: groupIds.id,
+ collapsed: true,
+ });
+ this.addTrackGroupActions.push(addGroup);
+ }
+
const counterResult = await this.engine.query(`
SELECT
id,
@@ -869,6 +918,17 @@
trackKindPriority,
config: {trackId, maxDepth, tid, isThreadSlice: onlyThreadSlice === 1}
});
+
+ if (TRACKS_V2_FLAG.get()) {
+ this.tracksToAdd.push({
+ engineId: this.engineId,
+ kind: 'GenericSliceTrack',
+ name,
+ trackGroup: uuid,
+ trackKindPriority,
+ config: {sqlTrackId: trackId},
+ });
+ }
}
}
@@ -1003,7 +1063,8 @@
process.pid as pid,
thread.tid as tid,
process.name as processName,
- thread.name as threadName
+ thread.name as threadName,
+ process.arg_set_id as argSetId
from (
select upid, 0 as utid from process_track
union
@@ -1065,6 +1126,7 @@
processName: STR_NULL,
hasSched: NUM_NULL,
hasHeapProfiles: NUM_NULL,
+ argSetId: NUM_NULL
});
for (; it.valid(); it.next()) {
const utid = it.utid;
@@ -1076,6 +1138,21 @@
const hasSched = !!it.hasSched;
const hasHeapProfiles = !!it.hasHeapProfiles;
+ const labels = [];
+ if (it.argSetId !== null) {
+ const result = await this.engine.query(`
+ select string_value as label
+ from args
+ where arg_set_id = ${it.argSetId}
+ `);
+ const argIt = result.iter({label: STR_NULL});
+ for (; argIt.valid(); argIt.next()) {
+ if (argIt.label !== null) {
+ labels.push(argIt.label);
+ }
+ }
+ }
+
// Group by upid if present else by utid.
let pUuid =
upid === null ? this.utidToUuid.get(utid) : this.upidToUuid.get(upid);
@@ -1095,6 +1172,7 @@
trackKindPriority: TrackDecider.inferTrackKindPriority(threadName),
name: `${upid === null ? tid : pid} summary`,
config: {pidForColor, upid, utid, tid},
+ labels,
});
const name = TrackDecider.getTrackName(
@@ -1123,6 +1201,7 @@
await this.addGpuFreqTracks();
await this.addGlobalCounterTracks();
await this.addCpuPerfCounterTracks();
+ await this.addAnnotationTracks();
await this.groupGlobalIonTracks();
// Create the per-process track groups. Note that this won't necessarily
@@ -1144,7 +1223,6 @@
await this.addThreadSliceTracks();
await this.addThreadCpuSampleTracks();
await this.addLogsTrack();
- await this.addAnnotationTracks();
this.addTrackGroupActions.push(
Actions.addTracks({tracks: this.tracksToAdd}));
diff --git a/ui/src/controller/validate_config.ts b/ui/src/controller/validate_config.ts
deleted file mode 100644
index 0d9e81e..0000000
--- a/ui/src/controller/validate_config.ts
+++ /dev/null
@@ -1,279 +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.
-
-import {NamedRecordConfig, RecordConfig, RecordMode} from '../common/state';
-
-type Json = JsonObject|Json[]|null|number|boolean|string;
-
-export interface JsonObject {
- [key: string]: Json;
-}
-
-class ObjectValidator {
- raw: JsonObject;
- invalidKeys: string[];
- prefix: string;
-
- constructor(raw: JsonObject, prefix?: string, invalidKeys?: string[]) {
- this.raw = raw;
- this.prefix = prefix || '';
- this.invalidKeys = invalidKeys || [];
- }
-
- private reportInvalidKey(key: string) {
- this.invalidKeys.push(this.prefix + key);
- }
-
- number(key: string, def = 0): number {
- if (!(key in this.raw)) {
- return def;
- }
-
- const val = this.raw[key];
- delete this.raw[key];
- if (typeof val === 'number') {
- return val;
- } else {
- this.reportInvalidKey(key);
- return def;
- }
- }
-
- string(key: string, def = ''): string {
- if (!(key in this.raw)) {
- return def;
- }
-
- const val = this.raw[key];
- delete this.raw[key];
- if (typeof val === 'string') {
- return val;
- } else {
- this.reportInvalidKey(key);
- return def;
- }
- }
-
- requiredString(key: string): string {
- if (!(key in this.raw)) {
- throw new Error(`key ${this.prefix + key} not found`);
- }
-
- const val = this.raw[key];
- delete this.raw[key];
- if (typeof val === 'string') {
- return val;
- } else {
- throw new Error(`key ${this.prefix + key} not found`);
- }
- }
-
- stringArray(key: string, def: string[] = []): string[] {
- if (!(key in this.raw)) {
- return def;
- }
-
- const val = this.raw[key];
- delete this.raw[key];
- if (Array.isArray(val)) {
- for (let i = 0; i < val.length; i++) {
- if (typeof val[i] !== 'string') {
- this.reportInvalidKey(key);
- return def;
- }
- }
- return val as string[];
- } else {
- this.reportInvalidKey(key);
- return def;
- }
- }
-
- boolean(key: string, def = false): boolean {
- if (!(key in this.raw)) {
- return def;
- }
-
- const val = this.raw[key];
- delete this.raw[key];
- if (typeof val === 'boolean') {
- return val;
- } else {
- this.reportInvalidKey(key);
- return def;
- }
- }
-
- recordMode(key: string, def: RecordMode): RecordMode {
- if (!(key in this.raw)) {
- return def;
- }
-
- const mode = this.raw[key];
- delete this.raw[key];
- if (typeof mode !== 'string') {
- this.reportInvalidKey(key);
- return def;
- }
-
- if (mode === 'STOP_WHEN_FULL') {
- return mode;
- } else if (mode === 'RING_BUFFER') {
- return mode;
- } else if (mode === 'LONG_TRACE') {
- return mode;
- } else {
- this.reportInvalidKey(key);
- return def;
- }
- }
-
- private childObject(key: string): JsonObject {
- if (!(key in this.raw)) {
- return {};
- }
-
- const result = this.raw[key];
- delete this.raw[key];
- if (typeof result === 'object' && !Array.isArray(result) &&
- result !== null) {
- return result;
- } else {
- this.reportInvalidKey(key);
- return {};
- }
- }
-
- object(key: string): ObjectValidator {
- return new ObjectValidator(
- this.childObject(key), key + '.', this.invalidKeys);
- }
-}
-
-export interface ValidationResult<T> {
- result: T;
- invalidKeys: string[];
- extraKeys: string[];
-}
-
-// Run the parser that takes raw JSON and outputs (potentially reconstructed
-// with default values) typed object, together with additional information,
-// such as fields with invalid values and extraneous keys.
-//
-// Parsers modify input JSON objects destructively.
-export function runParser<T>(
- parser: (validator: ObjectValidator) => T,
- input: JsonObject): ValidationResult<T> {
- const validator = new ObjectValidator(input);
- const valid = parser(validator);
-
- return {
- result: valid,
- // Validator removes all the parsed keys, therefore the only ones that
- // remain are extraneous.
- extraKeys: Object.keys(input),
- invalidKeys: validator.invalidKeys
- };
-}
-
-export function validateNamedRecordConfig(v: ObjectValidator):
- NamedRecordConfig {
- return {
- title: v.requiredString('title'),
- config: validateRecordConfig(v.object('config')),
- key: v.requiredString('key')
- };
-}
-
-export function validateRecordConfig(v: ObjectValidator): RecordConfig {
- return {
- mode: v.recordMode('mode', 'STOP_WHEN_FULL'),
- durationMs: v.number('durationMs', 10000.0),
- maxFileSizeMb: v.number('maxFileSizeMb', 100),
- fileWritePeriodMs: v.number('fileWritePeriodMs', 2500),
- bufferSizeMb: v.number('bufferSizeMb', 64.0),
-
- cpuSched: v.boolean('cpuSched'),
- cpuFreq: v.boolean('cpuFreq'),
- cpuSyscall: v.boolean('cpuSyscall'),
-
- gpuFreq: v.boolean('gpuFreq'),
- gpuMemTotal: v.boolean('gpuMemTotal'),
-
- ftrace: v.boolean('ftrace'),
- atrace: v.boolean('atrace'),
- ftraceEvents: v.stringArray('ftraceEvents'),
- ftraceExtraEvents: v.string('ftraceExtraEvents'),
- atraceCats: v.stringArray('atraceCats'),
- atraceApps: v.string('atraceApps'),
- ftraceBufferSizeKb: v.number('ftraceBufferSizeKb', 2 * 1024),
- ftraceDrainPeriodMs: v.number('ftraceDrainPerionMs', 250),
- androidLogs: v.boolean('androidLogs'),
- androidLogBuffers: v.stringArray('androidLogBuffers'),
- androidFrameTimeline: v.boolean('androidFrameTimeline'),
-
- cpuCoarse: v.boolean('cpuCoarse'),
- cpuCoarsePollMs: v.number('cpuCoarsePollMs', 1000),
-
- batteryDrain: v.boolean('batteryDrain'),
- batteryDrainPollMs: v.number('batteryDrainPollMs', 1000),
-
- boardSensors: v.boolean('boardSensors'),
-
- memHiFreq: v.boolean('memHiFreq'),
- meminfo: v.boolean('meminfo'),
- meminfoPeriodMs: v.number('meminfoPeriodMs', 1000),
- meminfoCounters: v.stringArray('meminfoCounters'),
-
- vmstat: v.boolean('vmstat'),
- vmstatPeriodMs: v.number('vmstatPeriodMs', 1000),
- vmstatCounters: v.stringArray('vmstatCounters'),
-
- heapProfiling: v.boolean('heapProfiling'),
- hpSamplingIntervalBytes: v.number('hpSamplingIntervalBytes', 4096),
- hpProcesses: v.string('hpProcesses'),
- hpContinuousDumpsPhase: v.number('hpContinuousDumpsPhase'),
- hpContinuousDumpsInterval: v.number('hpContinuousDumpsInterval'),
- hpSharedMemoryBuffer: v.number('hpSharedMemoryBuffer', 8 * 1048576),
- hpBlockClient: v.boolean('hpBlockClient', true),
- hpAllHeaps: v.boolean('hpAllHeaps'),
-
- javaHeapDump: v.boolean('javaHeapDump'),
- jpProcesses: v.string('jpProcesses'),
- jpContinuousDumpsPhase: v.number('jpContinuousDumpsPhase'),
- jpContinuousDumpsInterval: v.number('jpContinuousDumpsInterval'),
-
- memLmk: v.boolean('memLmk'),
- procStats: v.boolean('procStats'),
- procStatsPeriodMs: v.number('procStatsPeriodMs', 1000),
-
- chromeCategoriesSelected: v.stringArray('chromeCategoriesSelected'),
-
- chromeLogs: v.boolean('chromeLogs'),
- taskScheduling: v.boolean('taskScheduling'),
- ipcFlows: v.boolean('ipcFlows'),
- jsExecution: v.boolean('jsExecution'),
- webContentRendering: v.boolean('webContentRendering'),
- uiRendering: v.boolean('uiRendering'),
- inputEvents: v.boolean('inputEvents'),
- navigationAndLoading: v.boolean('navigationAndLoading'),
- chromeHighOverheadCategoriesSelected:
- v.stringArray('chromeHighOverheadCategoriesSelected'),
- symbolizeKsyms: v.boolean('symbolizeKsyms'),
- };
-}
-
-export function createEmptyRecordConfig(): RecordConfig {
- return runParser(validateRecordConfig, {}).result;
-}
diff --git a/ui/src/controller/validate_config_jsdomtest.ts b/ui/src/controller/validate_config_jsdomtest.ts
deleted file mode 100644
index f8a6546..0000000
--- a/ui/src/controller/validate_config_jsdomtest.ts
+++ /dev/null
@@ -1,75 +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.
-
-import {RecordConfig} from '../common/state';
-
-import {
- createEmptyRecordConfig,
- runParser,
- validateNamedRecordConfig,
- validateRecordConfig,
- ValidationResult
-} from './validate_config';
-
-test('validateRecordConfig does not keep invalid keys', () => {
- const key = 'Invalid key';
- const config: ValidationResult<RecordConfig> =
- runParser(validateRecordConfig, {[key]: 'Some random value'});
-
- expect((config.result as object).hasOwnProperty(key)).toEqual(false);
-
- // Information about an extra key is available in validation result.
- expect(config.extraKeys.includes(key)).toEqual(true);
-});
-
-test('validateRecordConfig keeps provided values', () => {
- const value = 31337;
- const config: ValidationResult<RecordConfig> =
- runParser(validateRecordConfig, {'durationMs': value});
-
- expect(config.result.durationMs).toEqual(value);
-
- // Check that the valid keys do not show as extra keys in validation result.
- expect(config.extraKeys.includes('durationMs')).toEqual(false);
-});
-
-test(
- 'validateRecordConfig tracks invalid keys while using default values',
- () => {
- const config: ValidationResult<RecordConfig> = runParser(
- validateRecordConfig,
- {'durationMs': 'a string, this should not be a string'});
- const defaultConfig = createEmptyRecordConfig();
-
- expect(config.result.durationMs).toEqual(defaultConfig.durationMs);
- expect(config.invalidKeys.includes('durationMs')).toEqual(true);
- });
-
-test(
- 'validateNamedRecordConfig throws exception on required field missing',
- () => {
- const unparsedConfig = {
- title: 'Invalid config'
- // Key is missing
- };
-
- let thrown = false;
- try {
- runParser(validateNamedRecordConfig, unparsedConfig);
- } catch {
- thrown = true;
- }
-
- expect(thrown).toBeTruthy();
- });
diff --git a/ui/src/controller/validators.ts b/ui/src/controller/validators.ts
new file mode 100644
index 0000000..c35c69d
--- /dev/null
+++ b/ui/src/controller/validators.ts
@@ -0,0 +1,267 @@
+// 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.
+
+// Execution context of object validator
+interface ValidatorContext {
+ // Path to the current value starting from the root. Object field names are
+ // stored as is, array indices are wrapped to square brackets. Represented
+ // as an array to avoid unnecessary string concatenation: parts are going to
+ // be concatenated into a single string when reporting errors, which should
+ // not happen on a happy path.
+ // Example: ["config", "androidLogBuffers", "1"] when parsing object
+ // accessible through expression `root.config.androidLogBuffers[1]`
+ path: string[];
+
+ // Paths from the root to extraneous keys in a validated object.
+ extraKeys: string[];
+
+ // Paths from the root to keys containing values of wrong type in validated
+ // object.
+ invalidKeys: string[];
+}
+
+// Validator accepting arbitrary data structure and returning a typed value.
+// Can throw an error if a part of the value does not have a reasonable
+// default.
+export interface Validator<T> {
+ validate(input: unknown, context: ValidatorContext): T;
+}
+
+// Helper function to flatten array of path chunks into a single string
+// Example: ["config", "androidLogBuffers", "1"] is mapped to
+// "config.androidLogBuffers[1]".
+function renderPath(path: string[]): string {
+ let result = '';
+ for (let i = 0; i < path.length; i++) {
+ if (i > 0 && !path[i].startsWith('[')) {
+ result += '.';
+ }
+ result += path[i];
+ }
+ return result;
+}
+
+export class ValidationError extends Error {}
+
+// Abstract class for validating simple values, such as strings and booleans.
+// Allows to avoid repetition of most of the code related to validation of
+// these.
+abstract class PrimitiveValidator<T> implements Validator<T> {
+ defaultValue: T;
+ required: boolean;
+
+ constructor(defaultValue: T, required: boolean) {
+ this.defaultValue = defaultValue;
+ this.required = required;
+ }
+
+ // Abstract method that checks whether passed input has correct type.
+ abstract predicate(input: unknown): input is T;
+
+ validate(input: unknown, context: ValidatorContext): T {
+ if (this.predicate(input)) {
+ return input;
+ }
+ if (this.required) {
+ throw new ValidationError(renderPath(context.path));
+ }
+ if (input !== undefined) {
+ // The value is defined, but does not conform to the expected type;
+ // proceed with returning the default value but report the key.
+ context.invalidKeys.push(renderPath(context.path));
+ }
+ return this.defaultValue;
+ }
+}
+
+
+class StringValidator extends PrimitiveValidator<string> {
+ predicate(input: unknown): input is string {
+ return typeof input === 'string';
+ }
+}
+
+class NumberValidator extends PrimitiveValidator<number> {
+ predicate(input: unknown): input is number {
+ return typeof input === 'number';
+ }
+}
+
+class BooleanValidator extends PrimitiveValidator<boolean> {
+ predicate(input: unknown): input is boolean {
+ return typeof input === 'boolean';
+ }
+}
+
+// Type-level function returning resulting type of a validator.
+export type ValidatedType<T> = T extends Validator<infer S>? S : never;
+
+// Type-level function traversing a record of validator and returning record
+// with the same keys and valid types.
+export type RecordValidatedType<T> = {
+ [k in keyof T]: ValidatedType<T[k]>
+};
+
+// Combinator for validators: takes a record of validators, and returns a
+// validator for a record where record's fields passed to validator with the
+// same name.
+//
+// Generic parameter T is instantiated to type of record of validators, and
+// should be provided implicitly by type inference due to verbosity of its
+// instantiations.
+class RecordValidator<T extends Record<string, Validator<unknown>>> implements
+ Validator<RecordValidatedType<T>> {
+ validators: T;
+
+ constructor(validators: T) {
+ this.validators = validators;
+ }
+
+ validate(input: unknown, context: ValidatorContext): RecordValidatedType<T> {
+ // If value is missing or of incorrect type, empty record is still processed
+ // in the loop below to initialize default fields of the nested object.
+ let o: object = {};
+ if (typeof input === 'object' && input !== null) {
+ o = input;
+ } else if (input !== undefined) {
+ context.invalidKeys.push(renderPath(context.path));
+ }
+
+ const result: Partial<RecordValidatedType<T>> = {};
+ // Separate declaration is required to avoid assigning `string` type to `k`.
+ for (const k in this.validators) {
+ if (this.validators.hasOwnProperty(k)) {
+ context.path.push(k);
+ const validator = this.validators[k];
+
+ // Accessing value of `k` of `o` is safe because `undefined` values are
+ // considered to indicate a missing value and handled appropriately by
+ // every provided validator.
+ const valid =
+ validator.validate((o as Record<string, unknown>)[k], context);
+
+ result[k] = valid as ValidatedType<T[string]>;
+ context.path.pop();
+ }
+ }
+
+ // Check if passed object has any extra keys to be reported as such.
+ for (const key of Object.keys(o)) {
+ if (!this.validators.hasOwnProperty(key)) {
+ context.path.push(key);
+ context.extraKeys.push(renderPath(context.path));
+ context.path.pop();
+ }
+ }
+ return result as RecordValidatedType<T>;
+ }
+}
+
+// Validator checking whether a value is one of preset values. Used in order to
+// provide easy validation for union of literal types.
+class OneOfValidator<T> implements Validator<T> {
+ validValues: readonly T[];
+ defaultValue: T;
+
+ constructor(validValues: readonly T[], defaultValue: T) {
+ this.defaultValue = defaultValue;
+ this.validValues = validValues;
+ }
+
+ validate(input: unknown, context: ValidatorContext): T {
+ if (this.validValues.includes(input as T)) {
+ return input as T;
+ } else if (input !== undefined) {
+ context.invalidKeys.push(renderPath(context.path));
+ }
+ return this.defaultValue;
+ }
+}
+
+// Validator for an array of elements, applying the same element validator for
+// each element of an array. Uses empty array as a default value.
+class ArrayValidator<T> implements Validator<T[]> {
+ elementValidator: Validator<T>;
+
+ constructor(elementValidator: Validator<T>) {
+ this.elementValidator = elementValidator;
+ }
+
+ validate(input: unknown, context: ValidatorContext): T[] {
+ const result: T[] = [];
+ if (Array.isArray(input)) {
+ for (let i = 0; i < input.length; i++) {
+ context.path.push(`[${i}]`);
+ result.push(this.elementValidator.validate(input[i], context));
+ context.path.pop();
+ }
+ } else if (input !== undefined) {
+ context.invalidKeys.push(renderPath(context.path));
+ }
+ return result;
+ }
+}
+
+// Wrapper container for validation result contaiting diagnostic information in
+// addition to the resulting typed value.
+export interface ValidationResult<T> {
+ result: T;
+ invalidKeys: string[];
+ extraKeys: string[];
+}
+
+// Wrapper for running a validator initializing the context.
+export function runValidator<T>(
+ validator: Validator<T>, input: unknown): ValidationResult<T> {
+ const context: ValidatorContext = {
+ path: [],
+ invalidKeys: [],
+ extraKeys: [],
+ };
+ const result = validator.validate(input, context);
+ return {
+ result,
+ invalidKeys: context.invalidKeys,
+ extraKeys: context.extraKeys,
+ };
+}
+
+// Shorthands for the validator classes above enabling concise notation.
+export function str(defaultValue = ''): StringValidator {
+ return new StringValidator(defaultValue, false);
+}
+
+export const requiredStr = new StringValidator('', true);
+
+export function num(defaultValue = 0): NumberValidator {
+ return new NumberValidator(defaultValue, false);
+}
+
+export function bool(defaultValue = false): BooleanValidator {
+ return new BooleanValidator(defaultValue, false);
+}
+
+export function record<T extends Record<string, Validator<unknown>>>(
+ validators: T): RecordValidator<T> {
+ return new RecordValidator<T>(validators);
+}
+
+export function oneOf<T>(
+ values: readonly T[], defaultValue: T): OneOfValidator<T> {
+ return new OneOfValidator<T>(values, defaultValue);
+}
+
+export function arrayOf<T>(elementValidator: Validator<T>): ArrayValidator<T> {
+ return new ArrayValidator<T>(elementValidator);
+}
diff --git a/ui/src/controller/validators_unittest.ts b/ui/src/controller/validators_unittest.ts
new file mode 100644
index 0000000..4d54c5c
--- /dev/null
+++ b/ui/src/controller/validators_unittest.ts
@@ -0,0 +1,109 @@
+// 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 {
+ arrayOf,
+ num,
+ oneOf,
+ record,
+ requiredStr,
+ runValidator,
+ ValidatedType,
+ ValidationError
+} from './validators';
+
+const colors = ['RED', 'GREEN', 'BLUE'] as const;
+
+type Color = typeof colors[number];
+
+const point = record({
+ id: requiredStr,
+ color: oneOf<Color>(colors, 'RED'),
+ x: num(),
+ y: num(1),
+ properties: record({mass: num(10)})
+});
+
+type Point = ValidatedType<typeof point>;
+
+const nested =
+ record({deeply: record({nested: record({array: arrayOf(point)})})});
+
+test('validator ensures presence of required fields', () => {
+ expect(() => {
+ runValidator(point, {});
+ }).toThrow(ValidationError);
+});
+
+test('validator ensures correct type of required fields', () => {
+ expect(() => {
+ runValidator(point, {id: 0});
+ }).toThrow(ValidationError);
+});
+
+test('validator fills default values', () => {
+ const p: Point = runValidator(point, {id: 'test'}).result;
+
+ expect(p.color).toEqual('RED');
+ expect(p.x).toEqual(0);
+ expect(p.y).toEqual(1);
+ expect(p.properties.mass).toEqual(10);
+});
+
+test('validator uses provided values', () => {
+ const p: Point =
+ runValidator(
+ point,
+ {id: 'test', x: 100, y: 200, color: 'GREEN', properties: {mass: 20}})
+ .result;
+
+ expect(p.color).toEqual('GREEN');
+ expect(p.x).toEqual(100);
+ expect(p.y).toEqual(200);
+ expect(p.properties.mass).toEqual(20);
+});
+
+test('validator keeps information about extra and invalid keys', () => {
+ const result = runValidator(point, {
+ id: 'test',
+ x: 'should not be a string',
+ extra: 'should not be here',
+ properties: {mass: 'should be a number', weight: 'should not be here'}
+ });
+
+ expect(result.extraKeys).toContain('extra');
+ expect(result.extraKeys).toContain('properties.weight');
+ expect(result.invalidKeys).toContain('x');
+ expect(result.invalidKeys).toContain('properties.mass');
+});
+
+test('validator correctly keeps track of path when reporting keys', () => {
+ const result = runValidator(nested, {
+ extra1: 0,
+ deeply: {
+ extra2: 1,
+ nested: {
+ array: [
+ {id: 'point1', x: 'should not be a string'},
+ {id: 'point2', extra3: 'should not be here'}
+ ]
+ }
+ }
+ });
+
+ expect(result.extraKeys).toContain('extra1');
+ expect(result.extraKeys).toContain('deeply.extra2');
+ expect(result.extraKeys).toContain('deeply.nested.array[1].extra3');
+ expect(result.invalidKeys).toContain('deeply.nested.array[0].x');
+});
diff --git a/ui/src/frontend/analyze_page.ts b/ui/src/frontend/analyze_page.ts
index 2585ebc..c464651 100644
--- a/ui/src/frontend/analyze_page.ts
+++ b/ui/src/frontend/analyze_page.ts
@@ -26,6 +26,7 @@
const INPUT_MAX_LINES = 10;
const INPUT_LINE_HEIGHT_EM = 1.2;
const TAB_SPACES = 2;
+const TAB_SPACES_STRING = ' '.repeat(TAB_SPACES);
const QUERY_ID = 'analyze-page-query';
class QueryInput implements m.ClassComponent {
@@ -35,10 +36,14 @@
static onKeyDown(e: Event) {
const event = e as KeyboardEvent;
const target = e.target as HTMLTextAreaElement;
+ const {selectionStart, selectionEnd} = target;
if (event.code === 'Enter' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
- const query = target.value;
+ let query = target.value;
+ if (selectionEnd > selectionStart) {
+ query = query.substring(selectionStart, selectionEnd);
+ }
if (!query) return;
globals.dispatch(
Actions.executeQuery({engineId: '0', queryId: QUERY_ID, query}));
@@ -47,14 +52,62 @@
if (event.code === 'Tab') {
// Handle tabs to insert spaces.
event.preventDefault();
- const whitespace = ' '.repeat(TAB_SPACES);
- const {selectionStart, selectionEnd} = target;
- target.value = target.value.substring(0, selectionStart) + whitespace +
- target.value.substring(selectionEnd);
- target.selectionEnd = selectionStart + TAB_SPACES;
+ const lastLineBreak = target.value.lastIndexOf('\n', selectionEnd);
+
+ if (selectionStart === selectionEnd || lastLineBreak < selectionStart) {
+ // Selection does not contain line breaks, therefore is on a single
+ // line. In this case, replace the selection with spaces. Replacement is
+ // done via document.execCommand as opposed to direct manipulation of
+ // element's value attribute because modifying latter programmatically
+ // drops the edit history which breaks undo/redo functionality.
+ document.execCommand('insertText', false, TAB_SPACES_STRING);
+ } else {
+ this.handleMultilineTab(target, event);
+ }
}
}
+ // Handle Tab press when the current selection is multiline: find all the
+ // lines intersecting with the selection, and either indent or dedent (if
+ // Shift key is held) them.
+ private static handleMultilineTab(
+ target: HTMLTextAreaElement, event: KeyboardEvent) {
+ const {selectionStart, selectionEnd} = target;
+ const firstLineBreak = target.value.lastIndexOf('\n', selectionStart - 1);
+
+ // If no line break is found (selection begins at the first line),
+ // replacementStart would have the correct value of 0.
+ const replacementStart = firstLineBreak + 1;
+ const replacement = target.value.substring(replacementStart, selectionEnd)
+ .split('\n')
+ .map((line) => {
+ if (event.shiftKey) {
+ // When Shift is held, remove whitespace at the
+ // beginning
+ return this.dedent(line);
+ } else {
+ return TAB_SPACES_STRING + line;
+ }
+ })
+ .join('\n');
+ // Select the range to be replaced.
+ target.setSelectionRange(replacementStart, selectionEnd);
+ document.execCommand('insertText', false, replacement);
+ // Restore the selection to match the previous selection, allowing to chain
+ // indent operations by just pressing Tab several times.
+ target.setSelectionRange(
+ replacementStart, replacementStart + replacement.length);
+ }
+
+ // Chop off up to TAB_SPACES leading spaces from a string.
+ private static dedent(line: string): string {
+ let i = 0;
+ while (i < line.length && i < TAB_SPACES && line[i] === ' ') {
+ i++;
+ }
+ return line.substring(i);
+ }
+
onInput(textareaValue: string) {
const textareaLines = textareaValue.split('\n').length;
const clampedNumLines =
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
new file mode 100644
index 0000000..dc818d3
--- /dev/null
+++ b/ui/src/frontend/base_slice_track.ts
@@ -0,0 +1,696 @@
+// 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 {Actions} from '../common/actions';
+import {cropText, drawIncompleteSlice} from '../common/canvas_utils';
+import {colorCompare, colorToStr, GRAY_COLOR} from '../common/colorizer';
+import {NUM, QueryResult} from '../common/query_result';
+import {SelectionKind} from '../common/state';
+import {fromNs, toNs} from '../common/time';
+
+import {checkerboardExcept} from './checkerboard';
+import {globals} from './globals';
+import {Slice} from './slice';
+import {DEFAULT_SLICE_LAYOUT, SliceLayout} from './slice_layout';
+import {NewTrackArgs, SliceRect, Track} from './track';
+
+// The common class that underpins all tracks drawing slices.
+
+export const SLICE_FLAGS_INCOMPLETE = 1;
+export const SLICE_FLAGS_INSTANT = 2;
+
+// Slices smaller than this don't get any text:
+const SLICE_MIN_WIDTH_FOR_TEXT_PX = 5;
+// Slices smaller than this aren't rendered at all.
+const SLICE_MIN_WIDTH_PX = 0.1;
+const CHEVRON_WIDTH_PX = 10;
+const DEFAULT_SLICE_COLOR = GRAY_COLOR;
+
+// TODO(hjd): Implement caching.
+
+// The minimal set of columns that any table/view must expose to render tracks.
+// Note: this class assumes that, at the SQL level, slices are:
+// - Not temporally overlapping (unless they are nested at inner depth).
+// - Strictly stacked (i.e. a slice at depth N+1 cannot be larger than any
+// slices at depth 0..N.
+// If you need temporally overlapping slices, look at AsyncSliceTrack, which
+// merges several tracks into one visual track.
+export const BASE_SLICE_ROW = {
+ id: NUM, // The slice ID, for selection / lookups.
+ tsq: NUM, // Quantized |ts|. This class owns the quantization logic.
+ ts: NUM, // Start time in nanoseconds.
+ dur: NUM, // Duration in nanoseconds. -1 = incomplete, 0 = instant.
+ depth: NUM, // Vertical depth.
+};
+
+export type BaseSliceRow = typeof BASE_SLICE_ROW;
+
+// The meta-type which describes the types used to extend the BaseSliceTrack.
+// Derived classes can extend this interface to override these types if needed.
+export interface BaseSliceTrackTypes {
+ slice: Slice;
+ row: BaseSliceRow;
+ config: {};
+}
+
+export abstract class BaseSliceTrack<T extends BaseSliceTrackTypes =
+ BaseSliceTrackTypes> extends
+ Track<T['config']> {
+ // This is the slice cache.
+ private slices = new Array<T['slice']>();
+ protected sliceLayout: SliceLayout = {...DEFAULT_SLICE_LAYOUT};
+
+ // These are the over-skirted cached bounds.
+ private slicesStartNs = -1;
+ private slicesEndNs = -1;
+ private slicesBucketNs = -1;
+
+ private readonly tableName: string;
+ private maxDurNs = 0;
+ private sqlState: 'UNINITIALIZED'|'INITIALIZING'|'QUERY_PENDING'|
+ 'QUERY_DONE' = 'UNINITIALIZED';
+ private extraSqlColumns: string[];
+
+ private charWidth = -1;
+ private hoverPos?: {x: number, y: number};
+ protected hoveredSlice?: T['slice'];
+ private hoverTooltip: string[] = [];
+ private maxDataDepth = 0;
+
+ // Computed layout.
+ private computedTrackHeight = 0;
+ private computedSliceHeight = 0;
+ private computedRowSpacing = 0;
+
+ // TODO(hjd): Remove when updating selection.
+ // We shouldn't know here about CHROME_SLICE. Maybe should be set by
+ // whatever deals with that. Dunno the namespace of selection is weird. For
+ // most cases in non-ambiguous (because most things are a 'slice'). But some
+ // others (e.g. THREAD_SLICE) have their own ID namespace so we need this.
+ protected selectionKinds: SelectionKind[] = ['SLICE', 'CHROME_SLICE'];
+
+ // Extension points.
+ // Each extension point should take a dedicated argument type (e.g.,
+ // OnSliceOverArgs {slice?: T['slice']}) so it makes future extensions
+ // non-API-breaking (e.g. if we want to add the X position).
+ abstract initSqlTable(_tableName: string): Promise<void>;
+ getRowSpec(): T['row'] {
+ return BASE_SLICE_ROW;
+ }
+ onSliceOver(_args: OnSliceOverArgs<T['slice']>): void {}
+ onSliceOut(_args: OnSliceOutArgs<T['slice']>): void {}
+ onSliceClick(_args: OnSliceClickArgs<T['slice']>): void {}
+ prepareSlices(slices: Array<T['slice']>): void {
+ this.highlightHovererdAndSameTitle(slices);
+ }
+
+ // TODO(hjd): Remove.
+ drawSchedLatencyArrow(
+ _: CanvasRenderingContext2D, _selectedSlice?: T['slice']): void {}
+
+ constructor(args: NewTrackArgs) {
+ super(args);
+ this.frontendOnly = true; // Disable auto checkerboarding.
+ this.tableName = `track_${this.trackId}`.replace(/[^a-zA-Z0-9_]+/g, '_');
+
+ // Work out the extra columns.
+ // This is the union of the embedder-defined columns and the base columns
+ // we know about (ts, dur, ...).
+ const allCols = Object.keys(this.getRowSpec());
+ const baseCols = Object.keys(BASE_SLICE_ROW);
+ this.extraSqlColumns = allCols.filter(key => !baseCols.includes(key));
+ }
+
+ setSliceLayout(sliceLayout: SliceLayout) {
+ if (sliceLayout.minDepth > sliceLayout.maxDepth) {
+ const {maxDepth, minDepth} = sliceLayout;
+ throw new Error(`minDepth ${minDepth} must be <= maxDepth ${maxDepth}`);
+ }
+ this.sliceLayout = sliceLayout;
+ }
+
+ onFullRedraw(): void {
+ // TODO(hjd): Call this only when cache changes. See discussion:
+ // What we want to do here is give the Impl a chance to colour the slice,
+ // e.g. depending on the currently selected thread or process.
+ // Here's an interesting thought. We have two options here:
+ // A) We could pass only the vizSlices, but then we'd have to call this
+ // @ 60FPS (because vizSlices changes as we pan).
+ // B) We could call this only on full redraws (when the state changes),
+ // but then the track needs to process *all* cached slices, not just
+ // the visible ones. It's okay now (it's a 2x factor) but might get
+ // worse if we cache several layers of slices at various resolutions.
+ // But there's an escape, I think. I think the right thing to do is:
+ // - For now call it on the full slices, but only on full redraws.
+ // - When we get caching, call it every time we switch "cached quantization
+ // level", which is a way in the middle between 60FPS and full redraws..
+ // Overall the API contract of this prepareSlices() call is:
+ // - I am going to draw these slices in the near future.
+ // - I am not going to draw any slice that I haven't passed here first.
+ // - This is guaranteed to be called at least on every state change.
+ // - This is NOT guaranteed to be called on every frame. For instance you
+ // cannot use this to do some colour-based animation.
+
+ // Give a chance to the embedder to change colors and other stuff.
+ this.prepareSlices(this.slices);
+ }
+
+ renderCanvas(ctx: CanvasRenderingContext2D): void {
+ // TODO(hjd): fonts and colors should come from the CSS and not hardcoded
+ // here.
+ const {timeScale} = globals.frontendLocalState;
+ const vizTime = globals.frontendLocalState.visibleWindowTime;
+
+ // If the visible time range is outside the cached area, requests
+ // asynchronously new data from the SQL engine.
+ this.maybeRequestData();
+
+ // In any case, draw whatever we have (which might be stale/incomplete).
+
+ // If the cached trace slices don't fully cover the visible time range,
+ // show a gray rectangle with a "Loading..." label.
+ checkerboardExcept(
+ ctx,
+ this.getHeight(),
+ timeScale.timeToPx(vizTime.start),
+ timeScale.timeToPx(vizTime.end),
+ timeScale.timeToPx(fromNs(this.slicesStartNs)),
+ timeScale.timeToPx(fromNs(this.slicesEndNs)));
+
+ let charWidth = this.charWidth;
+ if (charWidth < 0) {
+ // TODO(hjd): Centralize font measurement/invalidation.
+ ctx.font = '12px Roboto Condensed';
+ charWidth = this.charWidth = ctx.measureText('dbpqaouk').width / 8;
+ }
+
+ // Filter only the visible slices. |this.slices| will have more slices than
+ // needed because maybeRequestData() over-fetches to handle small pan/zooms.
+ // We don't want to waste time drawing slices that are off screen.
+ const vizSlices = this.getVisibleSlices(vizTime.start, vizTime.end);
+
+ let selection = globals.state.currentSelection;
+
+ if (!selection || !this.selectionKinds.includes(selection.kind)) {
+ selection = null;
+ }
+
+ // Believe it or not, doing 4xO(N) passes is ~2x faster than trying to draw
+ // everything in one go. The key is that state changes operations on the
+ // canvas (e.g., color, fonts) dominate any number crunching we do in JS.
+
+ this.updateSliceAndTrackHeight();
+ const sliceHeight = this.computedSliceHeight;
+ const padding = this.sliceLayout.padding;
+ const rowSpacing = this.computedRowSpacing;
+
+ // First pass: compute geometry of slices.
+ let selSlice: T['slice']|undefined;
+
+ // pxEnd is the last visible pixel in the visible viewport. Drawing
+ // anything < 0 or > pxEnd doesn't produce any visible effect as it goes
+ // beyond the visible portion of the canvas.
+ const pxEnd = Math.floor(timeScale.timeToPx(vizTime.end));
+
+ for (const slice of vizSlices) {
+ // Compute the basic geometry for any visible slice, even if only
+ // partially visible. This might end up with a negative x if the
+ // slice starts before the visible time or with a width that overflows
+ // pxEnd.
+ slice.x = timeScale.timeToPx(slice.startS);
+ slice.w = timeScale.deltaTimeToPx(slice.durationS);
+ if (slice.flags & SLICE_FLAGS_INSTANT) {
+ // In the case of an instant slice, set the slice geometry on the
+ // bounding box that will contain the chevron.
+ slice.x -= CHEVRON_WIDTH_PX / 2;
+ slice.w = CHEVRON_WIDTH_PX;
+ } else {
+ // If the slice is an actual slice, intersect the slice geometry with
+ // the visible viewport (this affects only the first and last slice).
+ // This is so that text is always centered even if we are zoomed in.
+ // Visually if we have
+ // [ visible viewport ]
+ // [ slice ]
+ // The resulting geometry will be:
+ // [slice]
+ // So that the slice title stays within the visible region.
+ const sliceVizLimit = Math.min(slice.x + slice.w, pxEnd);
+ slice.x = Math.max(slice.x, 0);
+ slice.w = sliceVizLimit - slice.x;
+ }
+
+ if (selection && (selection as {id: number}).id === slice.id) {
+ selSlice = slice;
+ }
+ }
+
+ // Second pass: fill slices by color.
+ // The .slice() turned out to be an unintended pun.
+ const vizSlicesByColor = vizSlices.slice();
+ vizSlicesByColor.sort((a, b) => colorCompare(a.color, b.color));
+ let lastColor = undefined;
+ for (const slice of vizSlicesByColor) {
+ if (slice.color !== lastColor) {
+ lastColor = slice.color;
+ ctx.fillStyle = colorToStr(slice.color);
+ }
+ const y = padding + slice.depth * (sliceHeight + rowSpacing);
+ if (slice.flags & SLICE_FLAGS_INSTANT) {
+ this.drawChevron(ctx, slice.x, y, sliceHeight);
+ } else if (slice.flags & SLICE_FLAGS_INCOMPLETE) {
+ const w = Math.max(slice.w - 2, 2);
+ drawIncompleteSlice(ctx, slice.x, y, w, sliceHeight);
+ } else if (slice.w > SLICE_MIN_WIDTH_PX) {
+ ctx.fillRect(slice.x, y, slice.w, sliceHeight);
+ }
+ }
+
+ // Third pass, draw the titles (e.g., process name for sched slices).
+ ctx.fillStyle = '#fff';
+ ctx.textAlign = 'center';
+ ctx.font = '12px Roboto Condensed';
+ ctx.textBaseline = 'middle';
+ for (const slice of vizSlices) {
+ if ((slice.flags & SLICE_FLAGS_INSTANT) || !slice.title ||
+ slice.w < SLICE_MIN_WIDTH_FOR_TEXT_PX) {
+ continue;
+ }
+
+ const title = cropText(slice.title, charWidth, slice.w);
+ const rectXCenter = slice.x + slice.w / 2;
+ const y = padding + slice.depth * (sliceHeight + rowSpacing);
+ const yDiv = slice.subTitle ? 3 : 2;
+ const yMidPoint = Math.floor(y + sliceHeight / yDiv) - 0.5;
+ ctx.fillText(title, rectXCenter, yMidPoint);
+ }
+
+ // Fourth pass, draw the subtitles (e.g., thread name for sched slices).
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
+ ctx.font = '10px Roboto Condensed';
+ for (const slice of vizSlices) {
+ if (slice.w < SLICE_MIN_WIDTH_FOR_TEXT_PX || !slice.subTitle ||
+ (slice.flags & SLICE_FLAGS_INSTANT)) {
+ continue;
+ }
+ const rectXCenter = slice.x + slice.w / 2;
+ const subTitle = cropText(slice.subTitle, charWidth, slice.w);
+ const y = padding + slice.depth * (sliceHeight + rowSpacing);
+ const yMidPoint = Math.ceil(y + sliceHeight * 2 / 3) + 1.5;
+ ctx.fillText(subTitle, rectXCenter, yMidPoint);
+ }
+
+ // Draw a thicker border around the selected slice (or chevron).
+ if (selSlice !== undefined) {
+ const color = selSlice.color;
+ const y = padding + selSlice.depth * (sliceHeight + rowSpacing);
+ ctx.strokeStyle = `hsl(${color.h}, ${color.s}%, 30%)`;
+ ctx.beginPath();
+ const THICKNESS = 3;
+ ctx.lineWidth = THICKNESS;
+ ctx.strokeRect(
+ selSlice.x, y - THICKNESS / 2, selSlice.w, sliceHeight + THICKNESS);
+ ctx.closePath();
+ }
+
+ // TODO(hjd): Remove this.
+ // The only thing this does is drawing the sched latency arrow. We should
+ // have some abstraction for that arrow (ideally the same we'd use for
+ // flows).
+ this.drawSchedLatencyArrow(ctx, selSlice);
+
+ // If a slice is hovered, draw the tooltip.
+ const tooltip = this.hoverTooltip;
+ if (this.hoveredSlice !== undefined && tooltip.length > 0 &&
+ this.hoverPos !== undefined) {
+ if (tooltip.length === 1) {
+ this.drawTrackHoverTooltip(ctx, this.hoverPos, tooltip[0]);
+ } else {
+ this.drawTrackHoverTooltip(ctx, this.hoverPos, tooltip[0], tooltip[1]);
+ }
+ } // if (howSlice)
+ }
+
+ // This method figures out if the visible window is outside the bounds of
+ // the cached data and if so issues new queries (i.e. sorta subsumes the
+ // onBoundsChange).
+ async maybeRequestData() {
+ // Important: this method is async and is invoked on every frame. Care
+ // must be taken to avoid piling up queries on every frame, hence the FSM.
+ if (this.sqlState === 'UNINITIALIZED') {
+ this.sqlState = 'INITIALIZING';
+
+ // TODO(hjd): we need an onDestroy. Right now if you contract and expand a
+ // track group this will crash, because the 2nd time we create the track
+ // we end up re-issuing the CREATE VIEW table_name.
+ // Right now this DROP VIEW is a hack, because it: (1) assumes that
+ // tableName is a VIEW and not a TABLE; (2) assume the impl track didn't
+ // create any other TABLE/VIEW (which happens to be true right now but
+ // might now be in future).
+ await this.engine.query(`DROP VIEW IF EXISTS ${this.tableName}`);
+ await this.initSqlTable(this.tableName);
+
+ const queryRes = await this.engine.query(`select
+ ifnull(max(dur), 0) as maxDur, count(1) as rowCount
+ from ${this.tableName}`);
+ const row = queryRes.firstRow({maxDur: NUM, rowCount: NUM});
+ this.maxDurNs = row.maxDur;
+ this.sqlState = 'QUERY_DONE';
+ } else if (
+ this.sqlState === 'INITIALIZING' || this.sqlState === 'QUERY_PENDING') {
+ return;
+ }
+
+ const resolutionNs = toNs(globals.getCurResolution());
+ const vizTime = globals.frontendLocalState.visibleWindowTime;
+
+ const startNs = toNs(vizTime.start);
+ const endNs = toNs(vizTime.end);
+
+ // TODO(hjd): figure out / centralize the resolution steps.
+ // Will handle this at the same time as cacheing.
+ const bucketNs = resolutionNs;
+
+ if (startNs >= this.slicesStartNs && endNs <= this.slicesEndNs &&
+ bucketNs === this.slicesBucketNs) {
+ return; // We have the data already, no need to re-query
+ }
+
+ this.sqlState = 'QUERY_PENDING';
+ const queryTsq = `(ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs}`;
+
+ const extraCols = this.extraSqlColumns.join(',');
+ let depthCol = 'depth';
+ let maybeGroupByDepth = 'depth, ';
+ const layout = this.sliceLayout;
+ const isFlat = (layout.maxDepth - layout.minDepth) <= 1;
+ // maxDepth === minDepth only makes sense if track is empty which on the
+ // one hand isn't very useful (and so maybe should be an error) on the
+ // other hand I can see it happening if someone does:
+ // minDepth = min(slices.depth); maxDepth = max(slices.depth);
+ // and slices is empty, so we treat that as flat.
+ if (isFlat) {
+ depthCol = `${this.sliceLayout.minDepth} as depth`;
+ maybeGroupByDepth = '';
+ }
+
+ // TODO(hjd): Re-reason and improve this query:
+ // - Materialize the unfinished slices one off.
+ // - Avoid the union if we know we don't have any -1 slices.
+ // - Maybe we don't need the union at all and can deal in TS?
+ const queryRes = await this.engine.query(`
+ with q1 as (
+ select
+ ${queryTsq} as tsq,
+ ts,
+ max(dur) as dur,
+ id,
+ ${depthCol}
+ ${extraCols ? ',' + extraCols : ''}
+ from ${this.tableName}
+ where
+ ts >= ${startNs - this.maxDurNs /* - durNs */} and
+ ts <= ${endNs /* + durNs */}
+ group by ${maybeGroupByDepth} tsq
+ order by tsq),
+ q2 as (
+ select
+ ${queryTsq} as tsq,
+ ts,
+ -1 as dur,
+ id,
+ ${depthCol}
+ ${extraCols ? ',' + extraCols : ''}
+ from ${this.tableName}
+ where dur = -1
+ group by ${maybeGroupByDepth} tsq
+ )
+ select min(dur) as _unused, * from
+ (select * from q1 union all select * from q2)
+ group by ${maybeGroupByDepth} tsq
+ order by tsq
+ `);
+ this.convertQueryResultToSlices(queryRes, startNs, endNs, bucketNs);
+ this.sqlState = 'QUERY_DONE';
+ globals.rafScheduler.scheduleRedraw();
+ }
+
+ // Here convert each row to a Slice. We do what we can do generically
+ // in the base class, and delegate the rest to the impl via that rowToSlice()
+ // abstract call.
+ convertQueryResultToSlices(
+ queryRes: QueryResult, startNs: number, endNs: number, bucketNs: number) {
+ const slices = new Array<T['slice']>(queryRes.numRows());
+ const it = queryRes.iter(this.getRowSpec());
+
+ let maxDataDepth = this.maxDataDepth;
+ this.slicesStartNs = startNs;
+ this.slicesEndNs = endNs;
+ this.slicesBucketNs = bucketNs;
+ for (let i = 0; it.valid(); it.next(), ++i) {
+ maxDataDepth = Math.max(maxDataDepth, it.depth);
+
+ // Construct the base slice. The Impl will construct and return the full
+ // derived T["slice"] (e.g. CpuSlice) in the rowToSlice() method.
+ slices[i] = this.rowToSlice(it);
+ }
+ this.maxDataDepth = maxDataDepth;
+ this.slices = slices;
+ }
+
+ rowToSlice(row: T['row']): T['slice'] {
+ const startNsQ = row.tsq;
+ const startNs = row.ts;
+ let flags = 0;
+ let durNs: number;
+ if (row.dur === -1) {
+ durNs = toNs(globals.state.traceTime.endSec) - startNs;
+ flags |= SLICE_FLAGS_INCOMPLETE;
+ } else {
+ flags |= (row.dur === 0) ? SLICE_FLAGS_INSTANT : 0;
+ durNs = row.dur;
+ }
+ const endNs = startNs + durNs;
+ const bucketNs = this.slicesBucketNs;
+ let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
+ endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
+
+ return {
+ id: row.id,
+ startS: fromNs(startNsQ),
+ durationS: fromNs(endNsQ - startNsQ),
+ flags,
+ depth: row.depth,
+ title: '',
+ subTitle: '',
+
+ // The derived class doesn't need to initialize these. They are
+ // rewritten on every renderCanvas() call. We just need to initialize
+ // them to something.
+ baseColor: DEFAULT_SLICE_COLOR,
+ color: DEFAULT_SLICE_COLOR,
+ x: -1,
+ w: -1,
+ };
+ }
+
+ private findSlice({x, y}: {x: number, y: number}): undefined|Slice {
+ const trackHeight = this.computedTrackHeight;
+ const sliceHeight = this.computedSliceHeight;
+ const padding = this.sliceLayout.padding;
+ const rowSpacing = this.computedRowSpacing;
+
+ // Need at least a draw pass to resolve the slice layout.
+ if (sliceHeight === 0) {
+ return undefined;
+ }
+
+ if (y >= padding && y <= trackHeight - padding) {
+ const depth = Math.floor((y - padding) / (sliceHeight + rowSpacing));
+ for (const slice of this.slices) {
+ if (slice.depth === depth && slice.x <= x && x <= slice.x + slice.w) {
+ return slice;
+ }
+ }
+ }
+
+ return undefined;
+ }
+
+ onMouseMove(position: {x: number, y: number}): void {
+ this.hoverPos = position;
+ this.updateHoveredSlice(this.findSlice(position));
+ }
+
+ onMouseOut(): void {
+ this.updateHoveredSlice(undefined);
+ }
+
+ private updateHoveredSlice(slice?: T['slice']): void {
+ const lastHoveredSlice = this.hoveredSlice;
+ this.hoveredSlice = slice;
+
+ // Only notify the Impl if the hovered slice changes:
+ if (slice === lastHoveredSlice) return;
+
+ if (this.hoveredSlice === undefined) {
+ globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1}));
+ this.onSliceOut({slice: assertExists(lastHoveredSlice)});
+ this.hoverTooltip = [];
+ this.hoverPos = undefined;
+ } else {
+ const args: OnSliceOverArgs<T['slice']> = {slice: this.hoveredSlice};
+ globals.dispatch(
+ Actions.setHighlightedSliceId({sliceId: this.hoveredSlice.id}));
+ this.onSliceOver(args);
+ this.hoverTooltip = args.tooltip || [];
+ }
+ }
+
+ onMouseClick(position: {x: number, y: number}): boolean {
+ const slice = this.findSlice(position);
+ if (slice === undefined) {
+ return false;
+ }
+ const args: OnSliceClickArgs<T['slice']> = {slice};
+ this.onSliceClick(args);
+ return true;
+ }
+
+ getVisibleSlices(startS: number, endS: number): Array<T['slice']> {
+ let startIdx = -1;
+ let endIdx = -1;
+ let i = 0;
+
+ // TODO(hjd): binary search.
+ for (const slice of this.slices) {
+ if (startIdx < 0 && slice.startS + slice.durationS >= startS) {
+ startIdx = i;
+ }
+ if (slice.startS <= endS) {
+ endIdx = i + 1;
+ } else if (slice.startS > endS) {
+ endIdx = i;
+ break;
+ }
+ i++;
+ }
+ return this.slices.slice(startIdx, endIdx);
+ }
+
+ private updateSliceAndTrackHeight() {
+ const lay = this.sliceLayout;
+
+ const rows =
+ Math.min(Math.max(this.maxDataDepth + 1, lay.minDepth), lay.maxDepth);
+
+ // Compute the track height.
+ let trackHeight;
+ if (lay.heightMode === 'FIXED') {
+ trackHeight = lay.fixedHeight;
+ } else {
+ trackHeight = 2 * lay.padding + rows * (lay.sliceHeight + lay.rowSpacing);
+ }
+
+ // Compute the slice height.
+ let sliceHeight: number;
+ let rowSpacing: number = lay.rowSpacing;
+ if (lay.heightMode === 'FIXED') {
+ const rowHeight = (trackHeight - 2 * lay.padding) / rows;
+ sliceHeight = Math.floor(Math.max(rowHeight - lay.rowSpacing, 0.5));
+ rowSpacing = Math.max(lay.rowSpacing, rowHeight - sliceHeight);
+ rowSpacing = Math.floor(rowSpacing * 2) / 2;
+ } else {
+ sliceHeight = lay.sliceHeight;
+ }
+ this.computedSliceHeight = sliceHeight;
+ this.computedTrackHeight = trackHeight;
+ this.computedRowSpacing = rowSpacing;
+ }
+
+ private drawChevron(
+ ctx: CanvasRenderingContext2D, x: number, y: number, h: number) {
+ // Draw an upward facing chevrons, in order: A, B, C, D, and back to A.
+ // . (x, y)
+ // A
+ // ###
+ // ##C##
+ // ## ##
+ // D B
+ // . (x + CHEVRON_WIDTH_PX, y + h)
+ const HALF_CHEVRON_WIDTH_PX = CHEVRON_WIDTH_PX / 2;
+ const midX = x + HALF_CHEVRON_WIDTH_PX;
+ ctx.beginPath();
+ ctx.moveTo(midX, y); // A.
+ ctx.lineTo(x + CHEVRON_WIDTH_PX, y + h); // B.
+ ctx.lineTo(midX, y + h - HALF_CHEVRON_WIDTH_PX); // C.
+ ctx.lineTo(x, y + h); // D.
+ ctx.lineTo(midX, y); // Back to A.
+ ctx.closePath();
+ ctx.fill();
+ }
+
+ // This is a good default implemenation for highlighting slices. By default
+ // prepareSlices() calls this. However, if the XxxSliceTrack impl overrides
+ // prepareSlices() this gives them a chance to call the highlighting witout
+ // having to reimplement it.
+ protected highlightHovererdAndSameTitle(slices: Slice[]) {
+ for (const slice of slices) {
+ const isHovering = globals.state.highlightedSliceId === slice.id ||
+ (this.hoveredSlice && this.hoveredSlice.title === slice.title);
+ if (isHovering) {
+ slice.color = {
+ c: slice.baseColor.c,
+ h: slice.baseColor.h,
+ s: slice.baseColor.s,
+ l: 30
+ };
+ } else {
+ slice.color = slice.baseColor;
+ }
+ }
+ }
+
+ getHeight(): number {
+ this.updateSliceAndTrackHeight();
+ return this.computedTrackHeight;
+ }
+
+ getSliceRect(_tStart: number, _tEnd: number, _depth: number): SliceRect
+ |undefined {
+ // TODO(hjd): Implement this as part of updating flow events.
+ return undefined;
+ }
+}
+
+// This is the argument passed to onSliceOver(args).
+// This is really a workaround for the fact that TypeScript doesn't allow
+// inner types within a class (whether the class is templated or not).
+export interface OnSliceOverArgs<S extends Slice> {
+ // Input args (BaseSliceTrack -> Impl):
+ slice: S; // The slice being hovered.
+
+ // Output args (Impl -> BaseSliceTrack):
+ tooltip?: string[]; // One entry per row, up to a max of 2.
+}
+
+export interface OnSliceOutArgs<S extends Slice> {
+ // Input args (BaseSliceTrack -> Impl):
+ slice: S; // The slice which is not hovered anymore.
+}
+
+export interface OnSliceClickArgs<S extends Slice> {
+ // Input args (BaseSliceTrack -> Impl):
+ slice: S; // The slice which is clicked.
+}
diff --git a/ui/src/frontend/flamegraph_panel.ts b/ui/src/frontend/flamegraph_panel.ts
index ea3d5a5..35a4d9f 100644
--- a/ui/src/frontend/flamegraph_panel.ts
+++ b/ui/src/frontend/flamegraph_panel.ts
@@ -264,13 +264,13 @@
switch (profileType) {
case ProfileType.PERF_SAMPLE:
return [this.buildButtonComponent(PERF_SAMPLES_KEY, 'samples')];
- case ProfileType.NATIVE_HEAP_PROFILE:
+ case ProfileType.JAVA_HEAP_GRAPH:
return [
this.buildButtonComponent(
SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'space'),
this.buildButtonComponent(OBJECTS_ALLOCATED_NOT_FREED_KEY, 'objects')
];
- case ProfileType.JAVA_HEAP_GRAPH:
+ case ProfileType.NATIVE_HEAP_PROFILE:
return [
this.buildButtonComponent(
SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'space'),
diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
index 3a989cd..4cde97b 100644
--- a/ui/src/frontend/flow_events_renderer.ts
+++ b/ui/src/frontend/flow_events_renderer.ts
@@ -263,8 +263,11 @@
begin: {x: number, y: number, dir: LineDirection},
end: {x: number, y: number, dir: LineDirection}, hue: number,
intensity: number, width: number) {
+ const hasArrowHead = Math.abs(begin.x - end.x) > 3 * TRIANGLE_SIZE;
const END_OFFSET =
- (end.dir === 'RIGHT' || end.dir === 'LEFT' ? TRIANGLE_SIZE : 0);
+ (((end.dir === 'RIGHT' || end.dir === 'LEFT') && hasArrowHead) ?
+ TRIANGLE_SIZE :
+ 0);
const color = `hsl(${hue}, 50%, ${intensity}%)`;
// draw curved line from begin to end (bezier curve)
ctx.strokeStyle = color;
@@ -300,17 +303,23 @@
ctx.arc(end.x, end.y, CIRCLE_RADIUS, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
- } else {
- const dx = this.getDeltaX(end.dir, TRIANGLE_SIZE);
- const dy = this.getDeltaY(end.dir, TRIANGLE_SIZE);
- // draw small triangle
- ctx.fillStyle = color;
- ctx.beginPath();
- ctx.moveTo(end.x, end.y);
- ctx.lineTo(end.x - dx - dy, end.y + dx - dy);
- ctx.lineTo(end.x - dx + dy, end.y - dx - dy);
- ctx.closePath();
- ctx.fill();
+ } else if (hasArrowHead) {
+ this.drawArrowHead(end, ctx, color);
}
}
+
+ private drawArrowHead(
+ end: {x: number; y: number; dir: LineDirection},
+ ctx: CanvasRenderingContext2D, color: string) {
+ const dx = this.getDeltaX(end.dir, TRIANGLE_SIZE);
+ const dy = this.getDeltaY(end.dir, TRIANGLE_SIZE);
+ // draw small triangle
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.moveTo(end.x, end.y);
+ ctx.lineTo(end.x - dx - dy, end.y + dx - dy);
+ ctx.lineTo(end.x - dx + dy, end.y - dx - dy);
+ ctx.closePath();
+ ctx.fill();
+ }
}
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 3c2b847..f730b1d 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -20,10 +20,11 @@
ConversionJobName,
ConversionJobStatus
} from '../common/conversion_jobs';
+import {createEmptyState} from '../common/empty_state';
import {Engine} from '../common/engine';
import {MetricResult} from '../common/metric_data';
import {CurrentSearchResults, SearchSummary} from '../common/search_data';
-import {CallsiteInfo, createEmptyState, State} from '../common/state';
+import {CallsiteInfo, State} from '../common/state';
import {fromNs, toNs} from '../common/time';
import {Analytics, initAnalytics} from './analytics';
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index b264c3c..9265090 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -18,8 +18,9 @@
import {defer} from '../base/deferred';
import {assertExists, reportError, setErrorHandler} from '../base/logging';
import {Actions, DeferredAction, StateActions} from '../common/actions';
+import {createEmptyState} from '../common/empty_state';
import {initializeImmerJs} from '../common/immer_init';
-import {createEmptyState, State} from '../common/state';
+import {State} from '../common/state';
import {initWasm} from '../common/wasm_engine_proxy';
import {ControllerWorkerInitMessage} from '../common/worker_messages';
import {
diff --git a/ui/src/frontend/named_slice_track.ts b/ui/src/frontend/named_slice_track.ts
new file mode 100644
index 0000000..90534ad
--- /dev/null
+++ b/ui/src/frontend/named_slice_track.ts
@@ -0,0 +1,85 @@
+// 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 {Actions} from '../common/actions';
+import {
+ Color,
+ hslForSlice,
+} from '../common/colorizer';
+import {STR_NULL} from '../common/query_result';
+
+import {
+ BASE_SLICE_ROW,
+ BaseSliceTrack,
+ BaseSliceTrackTypes,
+ OnSliceClickArgs,
+ OnSliceOverArgs,
+} from './base_slice_track';
+import {globals} from './globals';
+import {NewTrackArgs} from './track';
+
+export const NAMED_SLICE_ROW = {
+ // Base columns (tsq, ts, dur, id, depth).
+ ...BASE_SLICE_ROW,
+
+ // Impl-specific columns.
+ name: STR_NULL,
+};
+export type NamedSliceRow = typeof NAMED_SLICE_ROW;
+
+export interface NamedSliceTrackTypes extends BaseSliceTrackTypes {
+ row: NamedSliceRow;
+}
+
+export abstract class NamedSliceTrack<
+ T extends NamedSliceTrackTypes = NamedSliceTrackTypes> extends
+ BaseSliceTrack<T> {
+ constructor(args: NewTrackArgs) {
+ super(args);
+ }
+
+ // This is used by the base class to call iter().
+ getRowSpec(): T['row'] {
+ return NAMED_SLICE_ROW;
+ }
+
+ // Converts a SQL result row to an "Impl" Slice.
+ rowToSlice(row: T['row']): T['slice'] {
+ const baseSlice = super.rowToSlice(row);
+ // Ignore PIDs or numeric arguments when hashing.
+ const name = row.name || '';
+ const nameForHashing = name.replace(/\s?\d+/g, '');
+ const hsl = hslForSlice(nameForHashing, /*isSelected=*/ false);
+ // We cache the color so we hash only once per query.
+ const baseColor: Color = {c: '', h: hsl[0], s: hsl[1], l: hsl[2]};
+ return {...baseSlice, title: name, baseColor};
+ }
+
+ onSliceOver(args: OnSliceOverArgs<T['slice']>) {
+ const name = args.slice.title;
+ args.tooltip = [name];
+ }
+
+ onSliceClick(args: OnSliceClickArgs<T['slice']>) {
+ globals.makeSelection(Actions.selectChromeSlice({
+ id: args.slice.id,
+ trackId: this.trackId,
+
+ // |table| here can be either 'slice' or 'annotation'. The
+ // AnnotationSliceTrack overrides the onSliceClick and sets this to
+ // 'annotation'
+ table: 'slice',
+ }));
+ }
+}
diff --git a/ui/src/frontend/pivot_table.ts b/ui/src/frontend/pivot_table.ts
index 5a6b88f..9e75844 100644
--- a/ui/src/frontend/pivot_table.ts
+++ b/ui/src/frontend/pivot_table.ts
@@ -213,21 +213,17 @@
{pivotTableId, row, column, rowIndices, expandedRowColumns}));
continue;
}
- let indentationLevel = 0;
- let expandIconSpace = 0;
- if (column.aggregation !== undefined) {
- indentationLevel = rowIndices.length - 1;
- } else {
- indentationLevel = row.depth;
- if (row.depth > 0 && column.isStackColumn) {
- expandIconSpace = 3;
- }
- }
- // For each indentation level add 2 spaces, if we have an expansion button
- // add 3 spaces to cover the icon size.
+
let value = row.row[column.name]!.toString();
- value = value.padStart(
- (indentationLevel * 2) + expandIconSpace + value.length, ' ');
+ if (column.aggregation === undefined) {
+ // For each indentation level add 2 spaces, if we have an expansion
+ // button add 3 spaces to cover the icon size.
+ let padding = 2 * row.depth;
+ if (row.depth > 0 && column.isStackColumn) {
+ padding += 3;
+ }
+ value = value.padStart(padding + value.length, ' ');
+ }
cells.push(m('td.allow-white-space', value));
}
return m('tr', cells);
diff --git a/ui/src/frontend/post_message_handler.ts b/ui/src/frontend/post_message_handler.ts
index 68d010f..5309b6b 100644
--- a/ui/src/frontend/post_message_handler.ts
+++ b/ui/src/frontend/post_message_handler.ts
@@ -91,12 +91,11 @@
throw new Error('Incoming message trace buffer is empty');
}
- /* Removing this event listener because it is fired multiple times
- * with the same payload, causing issues such as b/182502595. This can be
- * reproduced by taking https://jsfiddle.net/primiano/1hd0a4wj/68/ and
- * replacing 'ui.perfetto.dev' -> 'localhost:10000'. If you open multiple
- * traces or the same trace multiple times, every tab will receive a message
- * for every window.open() call.
+ /* Removing this event listener to avoid callers posting the trace multiple
+ * times. If the callers add an event listener which upon receiving 'PONG'
+ * posts the trace to ui.perfetto.dev, the callers can receive multiple 'PONG'
+ * messages and accidentally post the trace multiple times. This was part of
+ * the cause of b/182502595.
*/
window.removeEventListener('message', postMessageHandler);
diff --git a/ui/src/frontend/record_config.ts b/ui/src/frontend/record_config.ts
index 77da241..847a270 100644
--- a/ui/src/frontend/record_config.ts
+++ b/ui/src/frontend/record_config.ts
@@ -12,20 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {
- getDefaultRecordingTargets,
- NamedRecordConfig,
- RecordConfig,
- RecordingTarget
-} from '../common/state';
+import {getDefaultRecordingTargets, RecordingTarget} from '../common/state';
import {
createEmptyRecordConfig,
- JsonObject,
- runParser,
- validateNamedRecordConfig,
- validateRecordConfig,
- ValidationResult
-} from '../controller/validate_config';
+ NamedRecordConfig,
+ namedRecordConfigValidator,
+ RecordConfig,
+ recordConfigValidator
+} from '../controller/record_config_types';
+import {runValidator, ValidationResult} from '../controller/validators';
const LOCAL_STORAGE_RECORD_CONFIGS_KEY = 'recordConfigs';
const LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY = 'autosaveConfig';
@@ -129,8 +124,8 @@
for (let i = 0; i < parsedConfigsLocalStorage.length; ++i) {
try {
- validConfigLocalStorage.push(runParser(
- validateNamedRecordConfig, parsedConfigsLocalStorage[i]));
+ validConfigLocalStorage.push(runValidator(
+ namedRecordConfigValidator, parsedConfigsLocalStorage[i]));
} catch {
// Parsing failed with unrecoverable error (e.g. title or key are
// missing), ignore the result.
@@ -177,8 +172,7 @@
}
const parsed = JSON.parse(savedItem);
if (parsed !== null && typeof parsed === 'object') {
- this.config =
- runParser(validateRecordConfig, parsed as JsonObject).result;
+ this.config = runValidator(recordConfigValidator, parsed).result;
this.hasSavedConfig = true;
}
}
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
index dd1150c..e1b853e 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/frontend/record_page.ts
@@ -32,12 +32,14 @@
isLinuxTarget,
LoadedConfig,
MAX_TIME,
- RecordConfig,
RecordingTarget,
RecordMode
} from '../common/state';
import {AdbOverWebUsb} from '../controller/adb';
-import {createEmptyRecordConfig} from '../controller/validate_config';
+import {
+ createEmptyRecordConfig,
+ RecordConfig
+} from '../controller/record_config_types';
import {globals} from './globals';
import {createPage, PageAttrs} from './pages';
@@ -66,7 +68,7 @@
id: 'persistConfigsUI',
name: 'Config persistence UI',
description: 'Show experimental config persistence UI on the record page.',
- defaultValue: false,
+ defaultValue: true,
});
const POLL_INTERVAL_MS = [250, 500, 1000, 2500, 5000, 30000, 60000];
@@ -754,7 +756,7 @@
m(Toggle, {
title: 'Resolve kernel symbols',
cssClass: '.thin',
- descr: `Enables lookup via /proc/kallsyms for workqueue,
+ descr: `Enables lookup via /proc/kallsyms for workqueue,
sched_blocked_reason and other events (userdebug/eng builds only).`,
setEnabled: (cfg, val) => cfg.symbolizeKsyms = val,
isEnabled: (cfg) => cfg.symbolizeKsyms
@@ -1434,7 +1436,7 @@
}
},
m(`li${routePage === 'config' ? '.active' : ''}`,
- m('i.material-icons', 'tune'),
+ m('i.material-icons', 'save'),
m('.title', 'Saved configs'),
m('.sub', 'Manage local configs'))) :
null),
diff --git a/ui/src/frontend/record_widgets.ts b/ui/src/frontend/record_widgets.ts
index 082bbd4..11a36eb 100644
--- a/ui/src/frontend/record_widgets.ts
+++ b/ui/src/frontend/record_widgets.ts
@@ -15,12 +15,12 @@
import {Draft, produce} from 'immer';
import * as m from 'mithril';
+import {assertExists} from '../base/logging';
import {Actions} from '../common/actions';
-import {RecordConfig} from '../common/state';
+import {RecordConfig} from '../controller/record_config_types';
import {copyToClipboard} from './clipboard';
import {globals} from './globals';
-import {assertExists} from '../base/logging';
declare type Setter<T> = (draft: Draft<RecordConfig>, val: T) => void;
declare type Getter<T> = (cfg: RecordConfig) => T;
diff --git a/ui/src/frontend/slice.ts b/ui/src/frontend/slice.ts
new file mode 100644
index 0000000..ddbbb98
--- /dev/null
+++ b/ui/src/frontend/slice.ts
@@ -0,0 +1,40 @@
+// 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 {Color} from '../common/colorizer';
+
+export interface Slice {
+ // These properties are updated only once per query result when the Slice
+ // object is created and don't change afterwards.
+ readonly id: number;
+ readonly startS: number;
+ readonly durationS: number;
+ readonly depth: number;
+ readonly flags: number;
+
+ // These can be changed by the Impl.
+ title: string;
+ subTitle: string;
+ baseColor: Color;
+ color: Color;
+
+ // These properties change @ 60FPS and shouldn't be touched by the Impl.
+ // to the Impl. These are really ephemeral and change on every frame. But
+ // the Impl doesn't see every frame. Somebody might be tempted to reason on
+ // those but then fail.
+ // TODO(hjd): Would be nice to find some clever typing hack to avoid exposing
+ // these.
+ x: number;
+ w: number;
+}
diff --git a/ui/src/frontend/slice_layout.ts b/ui/src/frontend/slice_layout.ts
new file mode 100644
index 0000000..1f28617
--- /dev/null
+++ b/ui/src/frontend/slice_layout.ts
@@ -0,0 +1,76 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export interface SliceLayoutBase {
+ padding: number; // top/bottom pixel padding between slices and track.
+ rowSpacing: number; // Spacing between rows.
+ minDepth: number; // Minimum depth a slice can be (normally zero)
+ // Maximum depth a slice can be plus 1 (a half open range with minDepth).
+ // We have a optimization for when maxDepth - minDepth == 1 so it is useful
+ // to set this correctly:
+ maxDepth: number;
+}
+
+export const SLICE_LAYOUT_BASE_DEFAULTS: SliceLayoutBase = Object.freeze({
+ padding: 3,
+ rowSpacing: 0,
+ minDepth: 0,
+ // A realistic bound to avoid tracks with unlimited height. If somebody wants
+ // extremely deep tracks they need to change this explicitly.
+ maxDepth: 128,
+});
+
+export interface SliceLayoutFixed extends SliceLayoutBase {
+ heightMode: 'FIXED';
+ fixedHeight: number; // Outer height of the track.
+}
+
+export const SLICE_LAYOUT_FIXED_DEFAULTS: SliceLayoutFixed = Object.freeze({
+ ...SLICE_LAYOUT_BASE_DEFAULTS,
+ heightMode: 'FIXED',
+ fixedHeight: 30,
+});
+
+export interface SliceLayoutFitContent extends SliceLayoutBase {
+ heightMode: 'FIT_CONTENT';
+ sliceHeight: number; // Only when heightMode = 'FIT_CONTENT'.
+}
+
+export const SLICE_LAYOUT_FIT_CONTENT_DEFAULTS: SliceLayoutFitContent =
+ Object.freeze({
+ ...SLICE_LAYOUT_BASE_DEFAULTS,
+ heightMode: 'FIT_CONTENT',
+ sliceHeight: 18,
+ });
+
+export interface SliceLayoutFlat extends SliceLayoutBase {
+ heightMode: 'FIXED';
+ fixedHeight: number; // Outer height of the track.
+ minDepth: 0;
+ maxDepth: 1;
+}
+
+export const SLICE_LAYOUT_FLAT_DEFAULTS: SliceLayoutFlat = Object.freeze({
+ ...SLICE_LAYOUT_BASE_DEFAULTS,
+ minDepth: 0,
+ maxDepth: 1,
+ heightMode: 'FIXED',
+ fixedHeight: 30,
+});
+
+export type SliceLayout =
+ SliceLayoutFixed|SliceLayoutFitContent|SliceLayoutFlat;
+
+export const DEFAULT_SLICE_LAYOUT: SliceLayout =
+ SLICE_LAYOUT_FIT_CONTENT_DEFAULTS;
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index fed8355..f319614 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -59,9 +59,14 @@
*/
export abstract class Track<Config = {}, Data extends TrackData = TrackData> {
// The UI-generated track ID (not to be confused with the SQL track.id).
- private trackId: string;
+ protected readonly trackId: string;
protected readonly engine: Engine;
+ // When true this is a new controller-less track type.
+ // TODO(hjd): eventually all tracks will be controller-less and this
+ // should be removed then.
+ protected frontendOnly = false;
+
// Caches the last state.track[this.trackId]. This is to deal with track
// deletion, see comments in trackState() below.
private lastTrackState: TrackState;
@@ -94,6 +99,9 @@
}
data(): Data|undefined {
+ if (this.frontendOnly) {
+ return undefined;
+ }
return globals.trackDataStore.get(this.trackId) as Data;
}
@@ -115,11 +123,13 @@
return false;
}
- onMouseOut() {}
+ onMouseOut(): void {}
+
+ onFullRedraw(): void {}
render(ctx: CanvasRenderingContext2D) {
globals.frontendLocalState.addVisibleTrack(this.trackState.id);
- if (this.data() === undefined) {
+ if (this.data() === undefined && !this.frontendOnly) {
const {visibleWindowTime, timeScale} = globals.frontendLocalState;
const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start));
const endPx = Math.ceil(timeScale.timeToPx(visibleWindowTime.end));
@@ -130,37 +140,64 @@
}
drawTrackHoverTooltip(
- ctx: CanvasRenderingContext2D, xPos: number, text: string,
+ ctx: CanvasRenderingContext2D, pos: {x: number, y: number}, text: string,
text2?: string) {
ctx.font = '10px Roboto Condensed';
- const textWidth = ctx.measureText(text).width;
- let width = textWidth;
- let textYPos = this.getHeight() / 2;
-
- if (text2 !== undefined) {
- const text2Width = ctx.measureText(text2).width;
- width = Math.max(textWidth, text2Width);
- textYPos = this.getHeight() / 2 - 6;
- }
-
- // Move tooltip over if it would go off the right edge of the viewport.
- const rectWidth = width + 16;
- const endPx = globals.frontendLocalState.timeScale.endPx;
- if (xPos + rectWidth > endPx) {
- xPos -= (xPos + rectWidth - endPx);
- }
-
- ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
- const rectMargin = this.getHeight() / 12;
- ctx.fillRect(
- xPos, rectMargin, rectWidth, this.getHeight() - rectMargin * 2);
- ctx.fillStyle = 'hsl(200, 50%, 40%)';
- ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
- ctx.fillText(text, xPos + 8, textYPos);
+ ctx.textAlign = 'left';
+ // TODO(hjd): Avoid measuring text all the time (just use monospace?)
+ const textMetrics = ctx.measureText(text);
+ const text2Metrics = ctx.measureText(text2 || '');
+
+ // Padding on each side of the box containing the tooltip:
+ const paddingPx = 4;
+
+ // Figure out the width of the tool tip box:
+ let width = Math.max(textMetrics.width, text2Metrics.width);
+ width += paddingPx * 2;
+
+ // and the height:
+ let height = 0;
+ height += textMetrics.fontBoundingBoxAscent;
+ height += textMetrics.fontBoundingBoxDescent;
if (text2 !== undefined) {
- ctx.fillText(text2, xPos + 8, this.getHeight() / 2 + 6);
+ height += text2Metrics.fontBoundingBoxAscent;
+ height += text2Metrics.fontBoundingBoxDescent;
+ }
+ height += paddingPx * 2;
+
+ let x = pos.x;
+ let y = pos.y;
+
+ // Move box to the top right of the mouse:
+ x += 10;
+ y -= 10;
+
+ // Ensure the box is on screen:
+ const endPx = globals.frontendLocalState.timeScale.endPx;
+ if (x + width > endPx) {
+ x -= x + width - endPx;
+ }
+ if (y < 0) {
+ y = 0;
+ }
+ if (y + height > this.getHeight()) {
+ y -= y + height - this.getHeight();
+ }
+
+ // Draw everything:
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
+ ctx.fillRect(x, y, width, height);
+
+ ctx.fillStyle = 'hsl(200, 50%, 40%)';
+ ctx.fillText(
+ text, x + paddingPx, y + paddingPx + textMetrics.fontBoundingBoxAscent);
+ if (text2 !== undefined) {
+ const yOffsetPx = textMetrics.fontBoundingBoxAscent +
+ textMetrics.fontBoundingBoxDescent +
+ text2Metrics.fontBoundingBoxAscent;
+ ctx.fillText(text, x + paddingPx, y + paddingPx + yOffsetPx);
}
}
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index e842d94..cc05cc3 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -106,6 +106,12 @@
}
}
+ let child = '';
+ if (this.summaryTrackState.labels &&
+ this.summaryTrackState.labels.length > 0) {
+ child = this.summaryTrackState.labels.join(', ');
+ }
+
return m(
`.track-group-panel[collapsed=${collapsed}]`,
{id: 'track_' + this.trackGroupId},
@@ -140,7 +146,10 @@
checkBox) :
''),
- this.summaryTrack ? m(TrackContent, {track: this.summaryTrack}) : null);
+ this.summaryTrack ? m(TrackContent,
+ {track: this.summaryTrack},
+ this.trackGroupState.collapsed ? '' : child) :
+ null);
}
oncreate(vnode: m.CVnodeDOM<Attrs>) {
@@ -158,6 +167,9 @@
this.backgroundColor =
getComputedStyle(dom).getPropertyValue('--expanded-background');
}
+ if (this.summaryTrack !== undefined) {
+ this.summaryTrack.onFullRedraw();
+ }
}
highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 084342b..a916e93 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -182,47 +182,53 @@
private mouseDownY?: number;
private selectionOccurred = false;
- view({attrs}: m.CVnode<TrackContentAttrs>) {
- return m('.track-content', {
- onmousemove: (e: PerfettoMouseEvent) => {
- attrs.track.onMouseMove({x: e.layerX - TRACK_SHELL_WIDTH, y: e.layerY});
- globals.rafScheduler.scheduleRedraw();
- },
- onmouseout: () => {
- attrs.track.onMouseOut();
- globals.rafScheduler.scheduleRedraw();
- },
- onmousedown: (e: PerfettoMouseEvent) => {
- this.mouseDownX = e.layerX;
- this.mouseDownY = e.layerY;
- },
- onmouseup: (e: PerfettoMouseEvent) => {
- if (this.mouseDownX === undefined || this.mouseDownY === undefined) {
- return;
- }
- if (Math.abs(e.layerX - this.mouseDownX) > 1 ||
- Math.abs(e.layerY - this.mouseDownY) > 1) {
- this.selectionOccurred = true;
- }
- this.mouseDownX = undefined;
- this.mouseDownY = undefined;
- },
- onclick: (e: PerfettoMouseEvent) => {
- // 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 (attrs.track.onMouseClick(
- {x: e.layerX - TRACK_SHELL_WIDTH, y: e.layerY})) {
- e.stopPropagation();
- }
- globals.rafScheduler.scheduleRedraw();
- }
- });
+ view(node: m.CVnode<TrackContentAttrs>) {
+ const attrs = node.attrs;
+ return m(
+ '.track-content',
+ {
+ onmousemove: (e: PerfettoMouseEvent) => {
+ attrs.track.onMouseMove(
+ {x: e.layerX - TRACK_SHELL_WIDTH, y: e.layerY});
+ globals.rafScheduler.scheduleRedraw();
+ },
+ onmouseout: () => {
+ attrs.track.onMouseOut();
+ globals.rafScheduler.scheduleRedraw();
+ },
+ onmousedown: (e: PerfettoMouseEvent) => {
+ this.mouseDownX = e.layerX;
+ this.mouseDownY = e.layerY;
+ },
+ onmouseup: (e: PerfettoMouseEvent) => {
+ if (this.mouseDownX === undefined ||
+ this.mouseDownY === undefined) {
+ return;
+ }
+ if (Math.abs(e.layerX - this.mouseDownX) > 1 ||
+ Math.abs(e.layerY - this.mouseDownY) > 1) {
+ this.selectionOccurred = true;
+ }
+ this.mouseDownX = undefined;
+ this.mouseDownY = undefined;
+ },
+ onclick: (e: PerfettoMouseEvent) => {
+ // 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 (attrs.track.onMouseClick(
+ {x: e.layerX - TRACK_SHELL_WIDTH, y: e.layerY})) {
+ e.stopPropagation();
+ }
+ globals.rafScheduler.scheduleRedraw();
+ }
+ },
+ node.children);
}
}
@@ -308,6 +314,18 @@
return m(TrackComponent, {trackState: this.trackState, track: this.track});
}
+ oncreate() {
+ if (this.track !== undefined) {
+ this.track.onFullRedraw();
+ }
+ }
+
+ onupdate() {
+ if (this.track !== undefined) {
+ this.track.onFullRedraw();
+ }
+ }
+
highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
const localState = globals.frontendLocalState;
const selection = globals.state.currentSelection;
diff --git a/ui/src/tracks/chrome_slices/frontend.ts b/ui/src/tracks/chrome_slices/frontend.ts
index abc7129..dc286ff 100644
--- a/ui/src/tracks/chrome_slices/frontend.ts
+++ b/ui/src/tracks/chrome_slices/frontend.ts
@@ -153,8 +153,7 @@
}
if (isIncomplete && rect.width > SLICE_HEIGHT / 4) {
- drawIncompleteSlice(
- ctx, rect.left, rect.top, rect.width, SLICE_HEIGHT, color);
+ drawIncompleteSlice(ctx, rect.left, rect.top, rect.width, SLICE_HEIGHT);
} else if (isThreadSlice) {
// We draw two rectangles, representing the ratio between wall time and
// time spent on cpu.
diff --git a/ui/src/tracks/counter/frontend.ts b/ui/src/tracks/counter/frontend.ts
index 2836c43..45908a6 100644
--- a/ui/src/tracks/counter/frontend.ts
+++ b/ui/src/tracks/counter/frontend.ts
@@ -84,7 +84,7 @@
return new CounterTrack(args);
}
- private mouseXpos = 0;
+ private mousePos = {x: 0, y: 0};
private hoveredValue: number|undefined = undefined;
private hoveredTs: number|undefined = undefined;
private hoveredTsEnd: number|undefined = undefined;
@@ -261,7 +261,7 @@
ctx.stroke();
// Draw the tooltip.
- this.drawTrackHoverTooltip(ctx, this.mouseXpos, text);
+ this.drawTrackHoverTooltip(ctx, this.mousePos, text);
}
// Write the Y scale on the top left corner.
@@ -296,12 +296,12 @@
timeScale.timeToPx(data.end));
}
- onMouseMove({x}: {x: number, y: number}) {
+ onMouseMove(pos: {x: number, y: number}) {
const data = this.data();
if (data === undefined) return;
- this.mouseXpos = x;
+ this.mousePos = pos;
const {timeScale} = globals.frontendLocalState;
- const time = timeScale.pxToTime(x);
+ const time = timeScale.pxToTime(pos.x);
const values = this.config.scale === 'DELTA_FROM_PREVIOUS' ?
data.totalDeltas :
diff --git a/ui/src/tracks/cpu_freq/frontend.ts b/ui/src/tracks/cpu_freq/frontend.ts
index 6feed5f..c8b2d74 100644
--- a/ui/src/tracks/cpu_freq/frontend.ts
+++ b/ui/src/tracks/cpu_freq/frontend.ts
@@ -36,7 +36,7 @@
return new CpuFreqTrack(args);
}
- private mouseXpos = 0;
+ private mousePos = {x: 0, y: 0};
private hoveredValue: number|undefined = undefined;
private hoveredTs: number|undefined = undefined;
private hoveredTsEnd: number|undefined = undefined;
@@ -192,7 +192,7 @@
}
// Draw the tooltip.
- this.drawTrackHoverTooltip(ctx, this.mouseXpos, text);
+ this.drawTrackHoverTooltip(ctx, this.mousePos, text);
}
// Write the Y scale on the top left corner.
@@ -214,12 +214,12 @@
timeScale.timeToPx(data.end));
}
- onMouseMove({x}: {x: number, y: number}) {
+ onMouseMove(pos: {x: number, y: number}) {
const data = this.data();
if (data === undefined) return;
- this.mouseXpos = x;
+ this.mousePos = pos;
const {timeScale} = globals.frontendLocalState;
- const time = timeScale.pxToTime(x);
+ const time = timeScale.pxToTime(pos.x);
const [left, right] = searchSegment(data.timestamps, time);
this.hoveredTs = left === -1 ? undefined : data.timestamps[left];
diff --git a/ui/src/tracks/cpu_slices/frontend.ts b/ui/src/tracks/cpu_slices/frontend.ts
index e81c55f..16f140c 100644
--- a/ui/src/tracks/cpu_slices/frontend.ts
+++ b/ui/src/tracks/cpu_slices/frontend.ts
@@ -39,7 +39,7 @@
return new CpuSliceTrack(args);
}
- private mouseXpos?: number;
+ private mousePos?: {x: number, y: number};
private utidHoveredInThisTrack = -1;
constructor(args: NewTrackArgs) {
@@ -211,28 +211,28 @@
}
const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
- if (hoveredThread !== undefined && this.mouseXpos !== undefined) {
+ if (hoveredThread !== undefined && this.mousePos !== undefined) {
const tidText = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`;
if (hoveredThread.pid) {
const pidText = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`;
- this.drawTrackHoverTooltip(ctx, this.mouseXpos, pidText, tidText);
+ this.drawTrackHoverTooltip(ctx, this.mousePos, pidText, tidText);
} else {
- this.drawTrackHoverTooltip(ctx, this.mouseXpos, tidText);
+ this.drawTrackHoverTooltip(ctx, this.mousePos, tidText);
}
}
}
- onMouseMove({x, y}: {x: number, y: number}) {
+ onMouseMove(pos: {x: number, y: number}) {
const data = this.data();
- this.mouseXpos = x;
+ this.mousePos = pos;
if (data === undefined) return;
const {timeScale} = globals.frontendLocalState;
- if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) {
+ if (pos.y < MARGIN_TOP || pos.y > MARGIN_TOP + RECT_HEIGHT) {
this.utidHoveredInThisTrack = -1;
globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
return;
}
- const t = timeScale.pxToTime(x);
+ const t = timeScale.pxToTime(pos.x);
let hoveredUtid = -1;
for (let i = 0; i < data.starts.length; i++) {
@@ -254,7 +254,7 @@
onMouseOut() {
this.utidHoveredInThisTrack = -1;
globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
- this.mouseXpos = 0;
+ this.mousePos = undefined;
}
onMouseClick({x}: {x: number}) {
diff --git a/ui/src/tracks/generic_slice_track/index.ts b/ui/src/tracks/generic_slice_track/index.ts
new file mode 100644
index 0000000..aaff6ff
--- /dev/null
+++ b/ui/src/tracks/generic_slice_track/index.ts
@@ -0,0 +1,48 @@
+// 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 {
+ NamedSliceTrack,
+ NamedSliceTrackTypes
+} from '../../frontend/named_slice_track';
+import {NewTrackArgs} from '../../frontend/track';
+import {trackRegistry} from '../../frontend/track_registry';
+
+export interface GenericSliceTrackConfig {
+ sqlTrackId: number;
+}
+
+export interface GenericSliceTrackTypes extends NamedSliceTrackTypes {
+ config: GenericSliceTrackConfig;
+}
+
+export class GenericSliceTrack extends NamedSliceTrack<GenericSliceTrackTypes> {
+ static readonly kind = 'GenericSliceTrack';
+ static create(args: NewTrackArgs) {
+ return new GenericSliceTrack(args);
+ }
+
+ constructor(args: NewTrackArgs) {
+ super(args);
+ }
+
+ async initSqlTable(tableName: string): Promise<void> {
+ const sql = `create view ${tableName} as
+ select ts, dur, id, depth, ifnull(name, '') as name
+ from slice where track_id = ${this.config.sqlTrackId}`;
+ await this.engine.query(sql);
+ }
+}
+
+trackRegistry.register(GenericSliceTrack);
diff --git a/ui/src/tracks/process_scheduling/frontend.ts b/ui/src/tracks/process_scheduling/frontend.ts
index dc5f9c6..b83b1289 100644
--- a/ui/src/tracks/process_scheduling/frontend.ts
+++ b/ui/src/tracks/process_scheduling/frontend.ts
@@ -37,7 +37,7 @@
return new ProcessSchedulingTrack(args);
}
- private mouseXpos?: number;
+ private mousePos?: {x: number, y: number};
private utidHoveredInThisTrack = -1;
constructor(args: NewTrackArgs) {
@@ -113,31 +113,31 @@
}
const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
- if (hoveredThread !== undefined && this.mouseXpos !== undefined) {
+ if (hoveredThread !== undefined && this.mousePos !== undefined) {
const tidText = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`;
if (hoveredThread.pid) {
const pidText = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`;
- this.drawTrackHoverTooltip(ctx, this.mouseXpos, pidText, tidText);
+ this.drawTrackHoverTooltip(ctx, this.mousePos, pidText, tidText);
} else {
- this.drawTrackHoverTooltip(ctx, this.mouseXpos, tidText);
+ this.drawTrackHoverTooltip(ctx, this.mousePos, tidText);
}
}
}
- onMouseMove({x, y}: {x: number, y: number}) {
+ onMouseMove(pos: {x: number, y: number}) {
const data = this.data();
- this.mouseXpos = x;
+ this.mousePos = pos;
if (data === undefined) return;
- if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) {
+ if (pos.y < MARGIN_TOP || pos.y > MARGIN_TOP + RECT_HEIGHT) {
this.utidHoveredInThisTrack = -1;
globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
return;
}
const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.maxCpu);
- const cpu = Math.floor((y - MARGIN_TOP) / (cpuTrackHeight + 1));
+ const cpu = Math.floor((pos.y - MARGIN_TOP) / (cpuTrackHeight + 1));
const {timeScale} = globals.frontendLocalState;
- const t = timeScale.pxToTime(x);
+ const t = timeScale.pxToTime(pos.x);
const [i, j] = searchRange(data.starts, t, searchEq(data.cpus, cpu));
if (i === j || i >= data.starts.length || t > data.ends[i]) {
@@ -156,7 +156,7 @@
onMouseOut() {
this.utidHoveredInThisTrack = -1;
globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
- this.mouseXpos = 0;
+ this.mousePos = undefined;
}
}