Merge "[ui] optimize sched subquery in process sorting"
diff --git a/Android.bp b/Android.bp
index a02d3ba..d068adb 100644
--- a/Android.bp
+++ b/Android.bp
@@ -3991,7 +3991,6 @@
"protos/perfetto/metrics/android/hwui_metric.proto",
"protos/perfetto/metrics/android/ion_metric.proto",
"protos/perfetto/metrics/android/irq_runtime_metric.proto",
- "protos/perfetto/metrics/android/jank_metric.proto",
"protos/perfetto/metrics/android/java_heap_histogram.proto",
"protos/perfetto/metrics/android/java_heap_stats.proto",
"protos/perfetto/metrics/android/lmk_metric.proto",
@@ -4062,7 +4061,6 @@
"protos/perfetto/metrics/android/hwui_metric.proto",
"protos/perfetto/metrics/android/ion_metric.proto",
"protos/perfetto/metrics/android/irq_runtime_metric.proto",
- "protos/perfetto/metrics/android/jank_metric.proto",
"protos/perfetto/metrics/android/java_heap_histogram.proto",
"protos/perfetto/metrics/android/java_heap_stats.proto",
"protos/perfetto/metrics/android/lmk_metric.proto",
@@ -8870,7 +8868,6 @@
"src/trace_processor/metrics/sql/android/android_hwui_threads.sql",
"src/trace_processor/metrics/sql/android/android_ion.sql",
"src/trace_processor/metrics/sql/android/android_irq_runtime.sql",
- "src/trace_processor/metrics/sql/android/android_jank.sql",
"src/trace_processor/metrics/sql/android/android_lmk.sql",
"src/trace_processor/metrics/sql/android/android_lmk_reason.sql",
"src/trace_processor/metrics/sql/android/android_mem.sql",
diff --git a/BUILD b/BUILD
index db272f1..48b45b6 100644
--- a/BUILD
+++ b/BUILD
@@ -1176,7 +1176,6 @@
"src/trace_processor/metrics/sql/android/android_hwui_threads.sql",
"src/trace_processor/metrics/sql/android/android_ion.sql",
"src/trace_processor/metrics/sql/android/android_irq_runtime.sql",
- "src/trace_processor/metrics/sql/android/android_jank.sql",
"src/trace_processor/metrics/sql/android/android_lmk.sql",
"src/trace_processor/metrics/sql/android/android_lmk_reason.sql",
"src/trace_processor/metrics/sql/android/android_mem.sql",
@@ -2948,7 +2947,6 @@
"protos/perfetto/metrics/android/hwui_metric.proto",
"protos/perfetto/metrics/android/ion_metric.proto",
"protos/perfetto/metrics/android/irq_runtime_metric.proto",
- "protos/perfetto/metrics/android/jank_metric.proto",
"protos/perfetto/metrics/android/java_heap_histogram.proto",
"protos/perfetto/metrics/android/java_heap_stats.proto",
"protos/perfetto/metrics/android/lmk_metric.proto",
diff --git a/CHANGELOG b/CHANGELOG
index 9f94ffc..8407784 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,14 +2,27 @@
Tracing service and probes:
*
Trace Processor:
- * Removed enable_perfetto_x64_cpu_opt by default for x64 MacOS
- since it caused issues for CIs.
+ *
UI:
*
SDK:
*
+v27.0 - 2022-07-01:
+ Tracing service and probes:
+ * Fix rare crash due to watchdog timeout being too short.
+ Trace Processor:
+ * Removed enable_perfetto_x64_cpu_opt by default for x64 MacOS
+ since it caused issues for CIs.
+ * Improved performance of filtering and sorting on most queries.
+ UI:
+ * Changed sorting of process groups to take slice count and presence of
+ perf profiles into account.
+ SDK:
+ *
+
+
v26.1 - 2022-06-13:
Trace Processor:
* Fixed build failures on Windows.
diff --git a/infra/ci/controller/Makefile b/infra/ci/controller/Makefile
index 66a159b..42e6194 100644
--- a/infra/ci/controller/Makefile
+++ b/infra/ci/controller/Makefile
@@ -31,7 +31,8 @@
--project ${PROJECT} -v ${GAE_VERSION} -s default -q
lib/.stamp:
- pip install -t lib/ oauth2client httplib2
+ echo "If this fails run `sudo apt install python-pip`"
+ python2.7 -m pip install -t lib/ rsa==4.0 oauth2client==4.1.3 httplib2==0.20.4
touch $@
config.py: ../config.py
diff --git a/infra/ci/controller/controller.py b/infra/ci/controller/controller.py
index 7647fed..d5d2afe 100644
--- a/infra/ci/controller/controller.py
+++ b/infra/ci/controller/controller.py
@@ -318,6 +318,9 @@
if '-ui-' in job_id:
ui_links.append('https://storage.googleapis.com/%s/%s/ui/index.html' %
(GCS_ARTIFACTS, job_id))
+ ui_links.append(
+ 'https://storage.googleapis.com/%s/%s/ui-test-artifacts/index.html' %
+ (GCS_ARTIFACTS, job_id))
if job_obj['status'] == 'COMPLETED':
passed_jobs.append(job_id)
elif not job_config.get('SKIP_VOTING', False):
diff --git a/protos/perfetto/metrics/android/BUILD.gn b/protos/perfetto/metrics/android/BUILD.gn
index 467fcd7..2520d35 100644
--- a/protos/perfetto/metrics/android/BUILD.gn
+++ b/protos/perfetto/metrics/android/BUILD.gn
@@ -37,7 +37,6 @@
"hwui_metric.proto",
"ion_metric.proto",
"irq_runtime_metric.proto",
- "jank_metric.proto",
"java_heap_histogram.proto",
"java_heap_stats.proto",
"lmk_metric.proto",
diff --git a/protos/perfetto/metrics/android/jank_metric.proto b/protos/perfetto/metrics/android/jank_metric.proto
deleted file mode 100644
index 9708adf..0000000
--- a/protos/perfetto/metrics/android/jank_metric.proto
+++ /dev/null
@@ -1,31 +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.
- */
-
-syntax = "proto2";
-
-package perfetto.protos;
-
-message AndroidJankMetrics {
- repeated Warning warnings = 1;
-
- message Warning {
- optional int64 ts = 1;
- optional int64 dur = 2;
-
- optional string process_name = 3;
- optional string warning_text = 4;
- }
-}
\ No newline at end of file
diff --git a/protos/perfetto/metrics/metrics.proto b/protos/perfetto/metrics/metrics.proto
index b2d248a..5f9fe7d 100644
--- a/protos/perfetto/metrics/metrics.proto
+++ b/protos/perfetto/metrics/metrics.proto
@@ -33,7 +33,6 @@
import "protos/perfetto/metrics/android/hwui_metric.proto";
import "protos/perfetto/metrics/android/ion_metric.proto";
import "protos/perfetto/metrics/android/irq_runtime_metric.proto";
-import "protos/perfetto/metrics/android/jank_metric.proto";
import "protos/perfetto/metrics/android/java_heap_histogram.proto";
import "protos/perfetto/metrics/android/java_heap_stats.proto";
import "protos/perfetto/metrics/android/lmk_metric.proto";
@@ -177,8 +176,8 @@
// Metric associated with hwcomposer.
optional AndroidHwcomposerMetrics android_hwcomposer = 28;
- // Detects common bad patterns that might lead to jank.
- optional AndroidJankMetrics android_jank = 29;
+ // Deprecated was AndroidJankMetrics;
+ reserved 29;
// G2D metrics.
optional G2dMetrics g2d = 30;
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index 36c5dbb..ae9a804 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -628,21 +628,6 @@
// End of protos/perfetto/metrics/android/irq_runtime_metric.proto
-// Begin of protos/perfetto/metrics/android/jank_metric.proto
-
-message AndroidJankMetrics {
- repeated Warning warnings = 1;
-
- message Warning {
- optional int64 ts = 1;
- optional int64 dur = 2;
-
- optional string process_name = 3;
- optional string warning_text = 4;
- }
-}
-// End of protos/perfetto/metrics/android/jank_metric.proto
-
// Begin of protos/perfetto/metrics/android/process_metadata.proto
message AndroidProcessMetadata {
@@ -1718,8 +1703,8 @@
// Metric associated with hwcomposer.
optional AndroidHwcomposerMetrics android_hwcomposer = 28;
- // Detects common bad patterns that might lead to jank.
- optional AndroidJankMetrics android_jank = 29;
+ // Deprecated was AndroidJankMetrics;
+ reserved 29;
// G2D metrics.
optional G2dMetrics g2d = 30;
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 5e99d7b..d4e2db6 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/python/perfetto/trace_processor/metrics.descriptor.sha1 b/python/perfetto/trace_processor/metrics.descriptor.sha1
index 0789b59..41405e0 100644
--- a/python/perfetto/trace_processor/metrics.descriptor.sha1
+++ b/python/perfetto/trace_processor/metrics.descriptor.sha1
@@ -2,5 +2,5 @@
// SHA1(tools/gen_binary_descriptors)
// c4a38769074f8a8c2ffbf514b267919b5f2d47df
// SHA1(protos/perfetto/metrics/metrics.proto)
-// 6a6df998653b26dabf7ae6591e2f8a8677669b77
+// b17e5a2952db164ae07fd3266bef5038be350762
\ No newline at end of file
diff --git a/src/android_internal/empty_file.cc b/src/android_internal/empty_file.cc
deleted file mode 100644
index 22ffa8f..0000000
--- a/src/android_internal/empty_file.cc
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * 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.
- */
-
-// TODO(primiano): this file is here only to have one translation unit for the
-// temporary perfetto_src_tracing_ipc target.
-
-__attribute__((visibility("default"))) void PerfettoNoOp();
-void PerfettoNoOp() {}
diff --git a/src/trace_processor/metrics/sql/BUILD.gn b/src/trace_processor/metrics/sql/BUILD.gn
index f00ad8c..52b7331 100644
--- a/src/trace_processor/metrics/sql/BUILD.gn
+++ b/src/trace_processor/metrics/sql/BUILD.gn
@@ -33,7 +33,6 @@
"android/android_hwui_threads.sql",
"android/android_ion.sql",
"android/android_irq_runtime.sql",
- "android/android_jank.sql",
"android/android_lmk_reason.sql",
"android/android_lmk.sql",
"android/android_mem_unagg.sql",
diff --git a/src/trace_processor/metrics/sql/android/android_batt.sql b/src/trace_processor/metrics/sql/android/android_batt.sql
index 3de117b..2ee4530 100644
--- a/src/trace_processor/metrics/sql/android/android_batt.sql
+++ b/src/trace_processor/metrics/sql/android/android_batt.sql
@@ -85,11 +85,9 @@
DROP TABLE IF EXISTS suspend_slice_;
CREATE TABLE suspend_slice_ AS
--- TODO(simonmacm): remove trustworthy hard coding.
SELECT
ts,
- dur,
- true as trustworthy
+ dur
FROM
slice
JOIN
@@ -98,6 +96,7 @@
WHERE
track.name = 'Suspend/Resume Latency'
AND slice.name = 'syscore_resume(0)'
+ AND dur != -1
;
SELECT RUN_METRIC('android/global_counter_span_view.sql',
@@ -168,6 +167,5 @@
)
)
FROM suspend_slice_
- WHERE trustworthy
)
);
diff --git a/src/trace_processor/metrics/sql/android/android_hwui_threads.sql b/src/trace_processor/metrics/sql/android/android_hwui_threads.sql
index ee6a3ab..f06a44d 100644
--- a/src/trace_processor/metrics/sql/android/android_hwui_threads.sql
+++ b/src/trace_processor/metrics/sql/android/android_hwui_threads.sql
@@ -21,7 +21,8 @@
process.name as process_name,
thread.utid
FROM thread
- JOIN {{process_allowlist_table}} process USING (upid)
+ JOIN {{process_allowlist_table}} process_allowlist USING (upid)
+ JOIN process USING (upid)
WHERE thread.is_main_thread;
DROP VIEW IF EXISTS {{table_name_prefix}}_render_thread;
@@ -30,7 +31,8 @@
process.name as process_name,
thread.utid
FROM thread
- JOIN {{process_allowlist_table}} process USING (upid)
+ JOIN {{process_allowlist_table}} process_allowlist USING (upid)
+ JOIN process USING (upid)
WHERE thread.name = 'RenderThread';
DROP VIEW IF EXISTS {{table_name_prefix}}_gpu_completion_thread;
@@ -39,7 +41,8 @@
process.name as process_name,
thread.utid
FROM thread
- JOIN {{process_allowlist_table}} process USING (upid)
+ JOIN {{process_allowlist_table}} process_allowlist USING (upid)
+ JOIN process USING (upid)
WHERE thread.name = 'GPU completion';
DROP VIEW IF EXISTS {{table_name_prefix}}_hwc_release_thread;
@@ -48,7 +51,8 @@
process.name as process_name,
thread.utid
FROM thread
- JOIN {{process_allowlist_table}} process USING (upid)
+ JOIN {{process_allowlist_table}} process_allowlist USING (upid)
+ JOIN process USING (upid)
WHERE thread.name = 'HWC release';
DROP TABLE IF EXISTS {{table_name_prefix}}_main_thread_slices;
diff --git a/src/trace_processor/metrics/sql/android/android_jank.sql b/src/trace_processor/metrics/sql/android/android_jank.sql
deleted file mode 100644
index 9c2cc22..0000000
--- a/src/trace_processor/metrics/sql/android/android_jank.sql
+++ /dev/null
@@ -1,284 +0,0 @@
---
--- Copyright 2020 The Android Open Source Project
---
--- Licensed under the Apache License, Version 2.0 (the "License");
--- you may not use this file except in compliance with the License.
--- You may obtain a copy of the License at
---
--- https://www.apache.org/licenses/LICENSE-2.0
---
--- Unless required by applicable law or agreed to in writing, software
--- distributed under the License is distributed on an "AS IS" BASIS,
--- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
--- See the License for the specific language governing permissions and
--- limitations under the License.
-
-
-DROP TABLE IF EXISTS android_jank_process_allowlist;
-CREATE TABLE android_jank_process_allowlist AS
-SELECT process.name, process.upid
-FROM process
-WHERE process.name IN (
- 'com.android.systemui',
- 'com.google.android.apps.nexuslauncher',
- 'com.google.android.inputmethod.latin'
-);
-
-SELECT RUN_METRIC(
- 'android/android_hwui_threads.sql',
- 'table_name_prefix', 'android_jank',
- 'process_allowlist_table', 'android_jank_process_allowlist');
-
-DROP TABLE IF EXISTS android_jank_thread_state_running;
-CREATE TABLE android_jank_thread_state_running AS
-SELECT utid, ts, dur, state
-FROM thread_state
-WHERE utid IN (SELECT utid FROM android_jank_main_thread_slices)
-AND state = 'Running'
-AND dur > 0;
-
-DROP TABLE IF EXISTS android_jank_thread_state_scheduled;
-CREATE TABLE android_jank_thread_state_scheduled AS
-SELECT utid, ts, dur, state
-FROM thread_state
-WHERE utid IN (SELECT utid FROM android_jank_main_thread_slices)
-AND (state = 'R' OR state = 'R+')
-AND dur > 0;
-
-DROP TABLE IF EXISTS android_jank_thread_state_io_wait;
-CREATE TABLE android_jank_thread_state_io_wait AS
-SELECT utid, ts, dur, state
-FROM thread_state
-WHERE utid IN (SELECT utid FROM android_jank_main_thread_slices)
-AND (((state = 'D' OR state = 'DK') AND io_wait) OR (state = 'DK' AND io_wait IS NULL))
-AND dur > 0;
-
---
--- Main Thread alerts
---
-
--- Expensive measure/layout
-
-DROP TABLE IF EXISTS android_jank_measure_layout_slices;
-CREATE TABLE android_jank_measure_layout_slices AS
-SELECT
- process_name,
- utid,
- id,
- ts,
- dur
-FROM android_jank_main_thread_slices
-WHERE name in ('measure', 'layout')
-AND dur >= 3000000;
-
-CREATE VIRTUAL TABLE IF NOT EXISTS android_jank_measure_layout_slices_state
-USING span_join(android_jank_measure_layout_slices PARTITIONED utid, android_jank_thread_state_running PARTITIONED utid);
-
-DROP TABLE IF EXISTS android_jank_measure_layout_slices_high_cpu;
-CREATE TABLE android_jank_measure_layout_slices_high_cpu AS
-SELECT id FROM android_jank_measure_layout_slices_state
-GROUP BY id
-HAVING SUM(dur) > 3000000;
-
-DROP TABLE IF EXISTS android_jank_measure_layout_alerts;
-CREATE TABLE android_jank_measure_layout_alerts AS
-SELECT
- process_name,
- ts,
- dur,
- 'Expensive measure/layout pass' as alert_name,
- id
-FROM android_jank_measure_layout_slices
-JOIN android_jank_measure_layout_slices_high_cpu USING (id);
-
--- Inflation during ListView recycling
--- as additional alerts for expensive layout slices
-
-DROP TABLE IF EXISTS android_jank_listview_inflation_alerts;
-CREATE TABLE android_jank_listview_inflation_alerts AS
-SELECT
- process_name,
- ts,
- dur,
- 'Inflation during ListView recycling' as alert_name
-FROM android_jank_main_thread_slices
-WHERE name IN ('obtainView', 'setupListItem')
-AND EXISTS (
- SELECT 1
- FROM descendant_slice(android_jank_main_thread_slices.id)
- WHERE name = 'inflate')
-AND EXISTS(
- SELECT 1
- FROM android_jank_measure_layout_alerts
- JOIN ancestor_slice(android_jank_main_thread_slices.id) USING (id)
-);
-
--- Long View#draw()
-
-DROP TABLE IF EXISTS android_jank_view_draw_slices;
-CREATE TABLE android_jank_view_draw_slices AS
-SELECT
- process_name,
- utid,
- id,
- ts,
- dur
-FROM android_jank_main_thread_slices
-WHERE name in ('getDisplayList', 'Record View#draw()')
-AND dur >= 3000000;
-
-CREATE VIRTUAL TABLE IF NOT EXISTS android_jank_view_draw_slices_state
-USING span_join(android_jank_view_draw_slices PARTITIONED utid, android_jank_thread_state_running PARTITIONED utid);
-
-DROP TABLE IF EXISTS android_jank_view_draw_slices_high_cpu;
-CREATE TABLE android_jank_view_draw_slices_high_cpu AS
-SELECT id FROM android_jank_view_draw_slices_state
-GROUP BY id
-HAVING SUM(dur) > 3000000;
-
-DROP TABLE IF EXISTS android_jank_view_draw_alerts;
-CREATE TABLE android_jank_view_draw_alerts AS
-SELECT
- process_name,
- ts,
- dur,
- 'Long View#draw()' as alert_name
-FROM android_jank_main_thread_slices
-JOIN android_jank_view_draw_slices_high_cpu USING (id);
-
--- Scheduling delay and Blocking I/O delay
-
-DROP TABLE IF EXISTS android_jank_long_do_frame_slices;
-CREATE TABLE android_jank_long_do_frame_slices AS
-SELECT
- process_name,
- utid,
- id,
- ts,
- dur
-FROM android_jank_main_thread_slices
-WHERE name GLOB 'Choreographer#doFrame*'
-AND dur >= 5000000;
-
-CREATE VIRTUAL TABLE IF NOT EXISTS android_jank_do_frame_slices_state_scheduled
-USING span_join(android_jank_long_do_frame_slices PARTITIONED utid, android_jank_thread_state_scheduled PARTITIONED utid);
-
-
-DROP TABLE IF EXISTS android_jank_do_frame_slices_long_scheduled;
-CREATE TABLE android_jank_do_frame_slices_long_scheduled AS
-SELECT id FROM android_jank_do_frame_slices_state_scheduled
-GROUP BY id
-HAVING SUM(dur) > 5000000;
-
-DROP TABLE IF EXISTS android_jank_scheduling_delay_alerts;
-CREATE TABLE android_jank_scheduling_delay_alerts AS
-SELECT
- process_name,
- ts,
- dur,
- 'Scheduling delay' as alert_name
-FROM android_jank_long_do_frame_slices
-JOIN android_jank_do_frame_slices_long_scheduled USING (id);
-
-CREATE VIRTUAL TABLE IF NOT EXISTS android_jank_do_frame_slices_state_io_wait
-USING span_join(android_jank_long_do_frame_slices PARTITIONED utid, android_jank_thread_state_io_wait PARTITIONED utid);
-
-DROP TABLE IF EXISTS android_jank_do_frame_slices_long_io_wait;
-CREATE TABLE android_jank_do_frame_slices_long_io_wait AS
-SELECT id FROM android_jank_do_frame_slices_state_io_wait
-GROUP BY id
-HAVING SUM(dur) > 5000000;
-
-DROP TABLE IF EXISTS android_jank_blocking_delay_alerts;
-CREATE TABLE android_jank_blocking_delay_alerts AS
-SELECT
- process_name,
- ts,
- dur,
- 'Blocking I/O delay' as alert_name
-FROM android_jank_do_frame_slices
-JOIN android_jank_do_frame_slices_long_io_wait USING (id);
-
---
--- Render Thread alerts
---
-
--- Expensive Canvas#saveLayer()
-
-DROP TABLE IF EXISTS android_jank_save_layer_alerts;
-CREATE TABLE android_jank_save_layer_alerts AS
-SELECT
- process_name,
- ts,
- dur,
- 'Expensive rendering with Canvas#saveLayer()' as alert_name
-FROM android_jank_render_thread_slices
-WHERE name GLOB '*alpha caused *saveLayer *'
-AND dur >= 1000000;
-
--- Path texture churn
-
-DROP TABLE IF EXISTS android_jank_generate_path_alerts;
-CREATE TABLE android_jank_generate_path_alerts AS
-SELECT
- process_name,
- ts,
- dur,
- 'Path texture churn' as alert_name
-FROM android_jank_render_thread_slices
-WHERE name = 'Generate Path Texture'
-AND dur >= 3000000;
-
--- Expensive Bitmap uploads
-
-DROP TABLE IF EXISTS android_jank_upload_texture_alerts;
-CREATE TABLE android_jank_upload_texture_alerts AS
-SELECT
- process_name,
- ts,
- dur,
- 'Expensive Bitmap uploads' as alert_name
-FROM android_jank_render_thread_slices
-WHERE name GLOB 'Upload *x* Texture'
-AND dur >= 3000000;
-
--- Merge all alerts tables into one table
-DROP TABLE IF EXISTS android_jank_alerts;
-CREATE TABLE android_jank_alerts AS
-SELECT process_name, ts, dur, alert_name FROM android_jank_measure_layout_alerts
-UNION ALL
-SELECT process_name, ts, dur, alert_name FROM android_jank_listview_inflation_alerts
-UNION ALL
-SELECT process_name, ts, dur, alert_name FROM android_jank_scheduling_delay_alerts
-UNION ALL
-SELECT process_name, ts, dur, alert_name FROM android_jank_blocking_delay_alerts
-UNION ALL
-SELECT process_name, ts, dur, alert_name FROM android_jank_save_layer_alerts
-UNION ALL
-SELECT process_name, ts, dur, alert_name FROM android_jank_generate_path_alerts
-UNION ALL
-SELECT process_name, ts, dur, alert_name FROM android_jank_upload_texture_alerts;
-
-DROP VIEW IF EXISTS android_jank_event;
-CREATE VIEW android_jank_event AS
-SELECT
- 'slice' as track_type,
- process_name || ' warnings' as track_name,
- ts,
- 0 as dur,
- group_concat(alert_name) as slice_name
-FROM android_jank_alerts
-GROUP BY track_type, track_name, ts;
-
-DROP VIEW IF EXISTS android_jank_output;
-CREATE VIEW android_jank_output AS
-SELECT AndroidJankMetrics(
- 'warnings', (
- SELECT RepeatedField(
- AndroidJankMetrics_Warning(
- 'ts', ts,
- 'dur', dur,
- 'process_name', process_name,
- 'warning_text', alert_name))
- FROM android_jank_alerts
- ORDER BY process_name, ts, dur));
diff --git a/src/trace_processor/metrics/sql/android/android_startup.sql b/src/trace_processor/metrics/sql/android/android_startup.sql
index 14d9549..7c1a324 100644
--- a/src/trace_processor/metrics/sql/android/android_startup.sql
+++ b/src/trace_processor/metrics/sql/android/android_startup.sql
@@ -38,9 +38,6 @@
-- Define helper functions for system state.
SELECT RUN_METRIC('android/startup/system_state.sql');
--- Define process metadata functions.
-SELECT RUN_METRIC('android/process_metadata.sql');
-
-- Returns the slices for forked processes. Never present in hot starts.
-- Prefer this over process start_ts, since the process might have
-- been preforked.
@@ -253,10 +250,14 @@
WHERE thread_name = 'Jit thread pool'
),
'other_processes_spawned_count', (
- SELECT COUNT(1) FROM process
+ SELECT COUNT(1)
+ FROM process
WHERE
- (process.name IS NULL OR process.name != launches.package) AND
- process.start_ts BETWEEN launches.ts AND launches.ts + launches.dur
+ process.start_ts BETWEEN launches.ts AND launches.ts + launches.dur AND
+ process.upid NOT IN (
+ SELECT upid FROM launch_processes
+ WHERE launch_processes.launch_id = launches.id
+ )
)
),
'hsc', NULL_IF_EMPTY(AndroidStartupMetric_HscMetrics(
diff --git a/src/trace_processor/metrics/sql/android/android_sysui_cuj.sql b/src/trace_processor/metrics/sql/android/android_sysui_cuj.sql
index 7acfd3e..60a2caf 100644
--- a/src/trace_processor/metrics/sql/android/android_sysui_cuj.sql
+++ b/src/trace_processor/metrics/sql/android/android_sysui_cuj.sql
@@ -15,29 +15,83 @@
SELECT RUN_METRIC('android/process_metadata.sql');
+-- Stores information about the last CUJ (important UI transition) in the trace.
+-- There might be more than 1 CUJ in the trace and in that case we pick the one
+-- that finished last.
+-- This limiting to 1 CUJ is done to simplify the rest of the script.
DROP TABLE IF EXISTS android_sysui_cuj_last_cuj;
CREATE TABLE android_sysui_cuj_last_cuj AS
+-- Finds slices like J<SHADE_EXPAND_COLLAPSE> which mark which frames were
+-- rendered during a specific CUJ.
+ WITH cujs AS (
SELECT
- process.name AS name,
+ ROW_NUMBER() OVER (ORDER BY ts) AS cuj_id,
process.upid AS upid,
+ process.name AS process_name,
process_metadata.metadata AS process_metadata,
+ slice.name AS cuj_slice_name,
+ -- Extracts "CUJ_NAME" from "J<CUJ_NAME>"
SUBSTR(slice.name, 3, LENGTH(slice.name) - 3) AS cuj_name,
ts AS ts_start,
- ts + dur AS ts_end,
- dur AS dur
+ dur,
+ ts + dur AS ts_end
FROM slice
- JOIN process_track ON slice.track_id = process_track.id
+ JOIN process_track
+ ON slice.track_id = process_track.id
JOIN process USING (upid)
JOIN process_metadata USING (upid)
WHERE
slice.name GLOB 'J<*>'
- -- Filter out CUJs that are <4ms long - assuming CUJ was cancelled.
- AND slice.dur > 4e6
AND (
process.name GLOB 'com.google.android*'
OR process.name GLOB 'com.android.*')
- ORDER BY ts desc
- LIMIT 1;
+),
+-- Slices logged from FrameTracker#markEvent that describe when
+-- the instrumentation was started and the reason the CUJ ended.
+cuj_state_markers AS (
+SELECT
+ cujs.cuj_id,
+ cuj_state_marker.ts,
+ cuj_state_marker.dur,
+ cuj_state_marker.name,
+ CASE
+ WHEN cuj_state_marker.name GLOB '*#FT#begin*' THEN 'begin'
+ WHEN cuj_state_marker.name GLOB '*#FT#deferMonitoring*' THEN 'deferMonitoring'
+ WHEN cuj_state_marker.name GLOB '*#FT#end*' THEN 'end'
+ WHEN cuj_state_marker.name GLOB '*#FT#cancel*' THEN 'cancel'
+ ELSE 'other'
+ END AS marker_type
+FROM cujs
+LEFT JOIN slice cuj_state_marker
+ ON cuj_state_marker.ts >= cujs.ts_start
+ AND cuj_state_marker.ts < cujs.ts_end
+ -- e.g. J<CUJ_NAME>#FT#end#0
+ AND cuj_state_marker.name GLOB (cujs.cuj_slice_name || "#FT#*")
+)
+SELECT
+ cujs.*,
+ CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM cuj_state_markers csm
+ WHERE csm.cuj_id = cujs.cuj_id
+ AND csm.marker_type = 'cancel')
+ THEN 'canceled'
+ WHEN EXISTS (
+ SELECT 1
+ FROM cuj_state_markers csm
+ WHERE csm.cuj_id = cujs.cuj_id
+ AND csm.marker_type = 'end')
+ THEN 'completed'
+ ELSE NULL
+ END AS state
+FROM cujs
+WHERE
+ state <> 'canceled'
+ -- Older builds don't have the state markers so we allow NULL but filter out
+ -- CUJs that are <4ms long - assuming CUJ was canceled in that case.
+ OR (state IS NULL AND cujs.dur > 4e6)
+ORDER BY ts_end DESC LIMIT 1;
SELECT RUN_METRIC(
'android/android_hwui_threads.sql',
@@ -124,7 +178,7 @@
DROP VIEW IF EXISTS android_sysui_cuj_thread;
CREATE VIEW android_sysui_cuj_thread AS
SELECT
- process.name as process_name,
+ process_name,
thread.utid,
thread.name
FROM thread
diff --git a/src/trace_processor/metrics/sql/android/android_sysui_cuj_surfaceflinger.sql b/src/trace_processor/metrics/sql/android/android_sysui_cuj_surfaceflinger.sql
index 0fe7e2f..bd72bf1 100644
--- a/src/trace_processor/metrics/sql/android/android_sysui_cuj_surfaceflinger.sql
+++ b/src/trace_processor/metrics/sql/android/android_sysui_cuj_surfaceflinger.sql
@@ -59,7 +59,7 @@
SELECT
app_slice.name AS app_vsync,
app_slice.id AS app_slice_id,
- cuj_process.name AS app_process,
+ cuj_process.process_name AS app_process,
sf_slice.name AS sf_vsync,
sf_slice.id AS sf_slice_id
FROM android_sysui_cuj_sf_actual_frame_timeline_slice sf_slice
diff --git a/src/trace_processor/metrics/sql/android/startup/launches.sql b/src/trace_processor/metrics/sql/android/startup/launches.sql
index 4337376..422042b 100644
--- a/src/trace_processor/metrics/sql/android/startup/launches.sql
+++ b/src/trace_processor/metrics/sql/android/startup/launches.sql
@@ -13,6 +13,9 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
+-- Define process metadata functions.
+SELECT RUN_METRIC('android/process_metadata.sql');
+
-- The start of the launching event corresponds to the end of the AM handling
-- the startActivity intent, whereas the end corresponds to the first frame drawn.
-- Only successful app launches have a launching event.
@@ -104,8 +107,7 @@
STARTUP_SLICE_COUNT(l.ts, l.ts_end, t.utid, 'activityStart') a_start,
STARTUP_SLICE_COUNT(l.ts, l.ts_end, t.utid, 'activityResume') a_resume
FROM launches l
- LEFT JOIN package_list ON (l.package = package_list.package_name)
- JOIN process p ON (l.package = p.name OR p.uid = package_list.uid)
+ JOIN process_metadata_table p ON (l.package = p.package_name)
JOIN thread t ON (p.upid = t.upid AND t.is_main_thread)
)
)
diff --git a/src/trace_processor/metrics/sql/android/startup/mcycles_per_launch.sql b/src/trace_processor/metrics/sql/android/startup/mcycles_per_launch.sql
index 25c6809..44fb0aa 100644
--- a/src/trace_processor/metrics/sql/android/startup/mcycles_per_launch.sql
+++ b/src/trace_processor/metrics/sql/android/startup/mcycles_per_launch.sql
@@ -103,7 +103,7 @@
'
SELECT RepeatedField(process_name)
FROM (
- SELECT process.name AS process_name
+ SELECT IFNULL(process.name, "[NULL]") AS process_name
FROM top_mcyles_process_excluding_started_per_launch
JOIN process USING (upid)
WHERE launch_id = $launch_id
diff --git a/src/trace_processor/tables/macros_internal.h b/src/trace_processor/tables/macros_internal.h
index 8454a55..00b375d 100644
--- a/src/trace_processor/tables/macros_internal.h
+++ b/src/trace_processor/tables/macros_internal.h
@@ -402,9 +402,9 @@
PERFETTO_TP_PARENT_COLUMN_FLAG_NO_FLAG_COL)(__VA_ARGS__))
// Creates the sparse vector with the given flags.
-#define PERFETTO_TP_TABLE_CONSTRUCTOR_SV(type, name, ...) \
- name##_ = ColumnStorage<TypedColumn<type>::stored_type>::Create< \
- (name##_flags() & Column::Flag::kDense) != 0>();
+#define PERFETTO_TP_TABLE_CONSTRUCTOR_SV(type, name, ...) \
+ name##_(ColumnStorage<TypedColumn<type>::stored_type>::Create< \
+ (name##_flags() & Column::Flag::kDense) != 0>()),
// Invokes the chosen column constructor by passing the given args.
#define PERFETTO_TP_TABLE_CONSTRUCTOR_COLUMN(type, name, ...) \
@@ -755,20 +755,15 @@
}; \
\
class_name(StringPool* pool, parent_class_name* parent) \
- : macros_internal::MacroTable(pool, parent), parent_(parent) { \
+ : macros_internal::MacroTable(pool, parent), \
+ PERFETTO_TP_TABLE_COLUMNS(DEF, PERFETTO_TP_TABLE_CONSTRUCTOR_SV) \
+ parent_(parent) { \
PERFETTO_CHECK(kIsRootTable == (parent == nullptr)); \
\
PERFETTO_TP_ALL_COLUMNS(DEF, PERFETTO_TP_TABLE_STATIC_ASSERT_FLAG) \
\
/* \
* Expands to \
- * col1_ = NullableVector<col1_type>(mode) \
- * ... \
- */ \
- PERFETTO_TP_TABLE_COLUMNS(DEF, PERFETTO_TP_TABLE_CONSTRUCTOR_SV); \
- \
- /* \
- * Expands to \
* columns_.emplace_back("col1", col1_, Column::kNoFlag, this, \
* static_cast<uint32_t>(columns_.size()), \
* static_cast<uint32_t>(row_maps_.size()) - 1); \
@@ -934,14 +929,14 @@
PERFETTO_TP_TABLE_COLUMNS(DEF, PERFETTO_TP_TABLE_CONSTRUCTOR_COLUMN); \
} \
\
- parent_class_name* parent_ = nullptr; \
- \
/* \
* Expands to \
* NullableVector<col1_type> col1_; \
* ... \
*/ \
PERFETTO_TP_TABLE_COLUMNS(DEF, PERFETTO_TP_TABLE_MEMBER) \
+ \
+ parent_class_name* parent_ = nullptr; \
}
} // namespace trace_processor
diff --git a/test/trace_processor/graphics/android_jank.out b/test/trace_processor/graphics/android_jank.out
deleted file mode 100644
index 0bed674..0000000
--- a/test/trace_processor/graphics/android_jank.out
+++ /dev/null
@@ -1,38 +0,0 @@
-android_jank: {
- warnings {
- ts: 4000500
- dur: 4999500
- process_name: "com.android.systemui"
- warning_text: "Expensive measure/layout pass"
- }
- warnings {
- ts: 4001000
- dur: 3499500
- process_name: "com.android.systemui"
- warning_text: "Inflation during ListView recycling"
- }
- warnings {
- ts: 8000000
- dur: 900000
- process_name: "com.android.systemui"
- warning_text: "Inflation during ListView recycling"
- }
- warnings {
- ts: 1000000
- dur: 19000000
- process_name: "com.android.systemui"
- warning_text: "Scheduling delay"
- }
- warnings {
- ts: 116000000
- dur: 1300000
- process_name: "com.google.android.inputmethod.latin"
- warning_text: "Expensive rendering with Canvas#saveLayer()"
- }
- warnings {
- ts: 108000000
- dur: 4000000
- process_name: "com.google.android.inputmethod.latin"
- warning_text: "Expensive Bitmap uploads"
- }
-}
diff --git a/test/trace_processor/graphics/android_jank.py b/test/trace_processor/graphics/android_jank.py
deleted file mode 100644
index ba6a628..0000000
--- a/test/trace_processor/graphics/android_jank.py
+++ /dev/null
@@ -1,110 +0,0 @@
-#!/usr/bin/env python3
-# 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.
-
-from os import sys, path
-
-import synth_common
-
-
-def add_main_thread_atrace(trace, ts, ts_end, buf):
- trace.add_atrace_begin(ts=ts, tid=PID, pid=PID, buf=buf)
- trace.add_atrace_end(ts=ts_end, tid=PID, pid=PID)
-
-
-def add_render_thread_atrace(trace, ts, ts_end, buf):
- trace.add_atrace_begin(ts=ts, tid=RTID, pid=PID, buf=buf)
- trace.add_atrace_end(ts=ts_end, tid=RTID, pid=PID)
-
-
-trace = synth_common.create_trace()
-
-trace.add_packet()
-trace.add_package_list(
- ts=0, name="com.android.systemui", uid=10001, version_code=1)
-trace.add_package_list(
- ts=0,
- name="com.google.android.inputmethod.latin",
- uid=10002,
- version_code=1)
-
-trace.add_process(pid=1000, ppid=1, cmdline="com.android.systemui", uid=10001)
-trace.add_thread(
- tid=1001, tgid=1000, cmdline="RenderThread", name="RenderThread")
-trace.add_process(
- pid=2000, ppid=1, cmdline="com.google.android.inputmethod.latin", uid=10002)
-trace.add_thread(
- tid=2001, tgid=2000, cmdline="RenderThread", name="RenderThread")
-
-trace.add_ftrace_packet(cpu=0)
-
-# com.android.systemui
-
-trace.add_atrace_begin(
- ts=1_000_000, tid=1000, pid=1000, buf='Choreographer#doFrame')
-trace.add_atrace_begin(ts=1_000_100, tid=1000, pid=1000, buf='traversal')
-trace.add_atrace_begin(ts=1_000_500, tid=1000, pid=1000, buf='measure')
-trace.add_atrace_end(ts=4_000_000, tid=1000, pid=1000)
-trace.add_atrace_begin(ts=4_000_500, tid=1000, pid=1000, buf='layout')
-trace.add_atrace_begin(ts=4_001_000, tid=1000, pid=1000, buf='setupListItem')
-trace.add_atrace_begin(ts=4_500_000, tid=1000, pid=1000, buf='inflate')
-trace.add_atrace_end(ts=5_500_000, tid=1000, pid=1000)
-trace.add_atrace_begin(ts=6_500_000, tid=1000, pid=1000, buf='inflate')
-trace.add_atrace_end(ts=7_500_000, tid=1000, pid=1000)
-trace.add_atrace_end(ts=7_500_500, tid=1000, pid=1000)
-trace.add_atrace_begin(ts=8_000_000, tid=1000, pid=1000, buf='obtainView')
-trace.add_atrace_begin(ts=8_000_100, tid=1000, pid=1000, buf='inflate')
-trace.add_atrace_end(ts=8_500_000, tid=1000, pid=1000)
-trace.add_atrace_end(ts=8_900_000, tid=1000, pid=1000)
-trace.add_atrace_end(ts=9_000_000, tid=1000, pid=1000)
-trace.add_atrace_end(ts=9_000_000, tid=1000, pid=1000)
-trace.add_atrace_end(ts=20_000_000, tid=1000, pid=1000)
-
-trace.add_sched(ts=1_000_000, prev_pid=0, next_pid=1000)
-trace.add_sched(ts=10_000_000, prev_pid=1000, next_pid=0, prev_state='R')
-trace.add_sched(ts=10_500_000, prev_pid=0, next_pid=0)
-trace.add_sched(ts=19_500_000, prev_pid=0, next_pid=1000)
-trace.add_sched(ts=20_500_000, prev_pid=1000, next_pid=0, prev_state='R')
-
-# com.google.android.inputmethod.latin
-
-trace.add_atrace_begin(
- ts=101_000_000, tid=2000, pid=2000, buf='Choreographer#doFrame')
-trace.add_atrace_begin(ts=101_000_100, tid=2000, pid=2000, buf='traversal')
-trace.add_atrace_begin(ts=101_000_500, tid=2000, pid=2000, buf='measure')
-trace.add_atrace_end(ts=104_000_000, tid=2000, pid=2000)
-trace.add_atrace_begin(ts=104_000_500, tid=2000, pid=2000, buf='layout')
-trace.add_atrace_end(ts=105_000_000, tid=2000, pid=2000)
-trace.add_atrace_end(ts=105_000_000, tid=2000, pid=2000)
-trace.add_atrace_begin(ts=105_000_000, tid=2000, pid=2000, buf='draw')
-trace.add_atrace_end(ts=119_000_000, tid=2000, pid=2000)
-trace.add_atrace_end(ts=120_000_000, tid=2000, pid=2000)
-
-trace.add_atrace_begin(ts=105_000_000, tid=2001, pid=2000, buf='DrawFrames 3')
-trace.add_atrace_begin(
- ts=108_000_000, tid=2001, pid=2000, buf='Upload 300x300 Texture')
-trace.add_atrace_end(ts=112_000_000, tid=2001, pid=2000)
-trace.add_atrace_begin(
- ts=116_000_000,
- tid=2001,
- pid=2000,
- buf='alpha caused unclipped saveLayer 201x319')
-trace.add_atrace_end(ts=117_300_000, tid=2001, pid=2000)
-trace.add_atrace_end(ts=118_000_000, tid=2001, pid=2000)
-
-trace.add_sched(ts=101_000_000, prev_pid=0, next_pid=2000)
-trace.add_sched(ts=120_000_000, prev_pid=2000, next_pid=0, prev_state='R')
-trace.add_sched(ts=120_500_000, prev_pid=0, next_pid=0)
-
-sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/test/trace_processor/graphics/android_sysui_cuj.py b/test/trace_processor/graphics/android_sysui_cuj.py
index 1845aa7..b1f3bd8 100644
--- a/test/trace_processor/graphics/android_sysui_cuj.py
+++ b/test/trace_processor/graphics/android_sysui_cuj.py
@@ -125,9 +125,15 @@
trace.add_thread(
tid=JITID, tgid=PID, cmdline="Jit thread pool", name="Jit thread pool")
trace.add_ftrace_packet(cpu=0)
+trace.add_atrace_async_begin(ts=5, tid=PID, pid=PID, buf="J<SHOULD_BE_IGNORED>")
trace.add_atrace_async_begin(ts=10, tid=PID, pid=PID, buf="J<SHADE_ROW_EXPAND>")
trace.add_atrace_async_end(
+ ts=100_000_000, tid=PID, pid=PID, buf="J<SHOULD_BE_IGNORED>")
+trace.add_atrace_async_begin(
+ ts=100_100_000, tid=PID, pid=PID, buf="J<CANCELED>")
+trace.add_atrace_async_end(
ts=901_000_010, tid=PID, pid=PID, buf="J<SHADE_ROW_EXPAND>")
+trace.add_atrace_async_end(ts=999_000_000, tid=PID, pid=PID, buf="J<CANCELED>")
add_frame(
trace,
@@ -350,6 +356,9 @@
ts_gpu=1_400_000_000,
ts_end_gpu=1_500_000_000)
+add_main_thread_atrace(
+ trace, ts=990_000_000, ts_end=995_000_000, buf="J<CANCELED>#FT#cancel#0")
+
add_expected_frame_events(ts=0, dur=16_000_000, token_start=10)
add_actual_frame_events(ts=0, dur=16_000_000, token_start=10)
diff --git a/test/trace_processor/graphics/index b/test/trace_processor/graphics/index
index 4b396e0..56a4a93 100644
--- a/test/trace_processor/graphics/index
+++ b/test/trace_processor/graphics/index
@@ -38,9 +38,6 @@
# Composition layer
composition_layer.py composition_layer_count_test.sql composition_layer_count.out
-# Android Jank metrics
-android_jank.py android_jank android_jank.out
-
# G2D metrics
# TODO(rsavitski): find a real trace and double-check that the textproto is
# realistic. One kernel's source I checked had tgid=0 for all counter events.
diff --git a/test/trace_processor/startup/android_startup_attribution.out b/test/trace_processor/startup/android_startup_attribution.out
index 87fd0ee..e2135ce 100644
--- a/test/trace_processor/startup/android_startup_attribution.out
+++ b/test/trace_processor/startup/android_startup_attribution.out
@@ -47,6 +47,17 @@
activity_hosting_process_count: 1
process {
name: "com.some.app"
+ uid: 10001
+ package {
+ package_name: "com.some.app"
+ apk_version_code: 123
+ debuggable: false
+ }
+ packages_for_uid {
+ package_name: "com.some.app"
+ apk_version_code: 123
+ debuggable: false
+ }
}
event_timestamps {
intent_received: 100
diff --git a/test/trace_processor/startup/android_startup_attribution.py b/test/trace_processor/startup/android_startup_attribution.py
index 2f8a4be..a12601b 100644
--- a/test/trace_processor/startup/android_startup_attribution.py
+++ b/test/trace_processor/startup/android_startup_attribution.py
@@ -34,7 +34,7 @@
trace.add_packet()
trace.add_process(1, 0, 'init')
trace.add_process(SYSTEM_SERVER_PID, 1, 'system_server')
-trace.add_process(APP_PID, 1, 'com.some.app')
+trace.add_process(APP_PID, 1, 'com.some.app', uid=10001)
trace.add_thread(tid=SECOND_APP_TID, tgid=APP_PID, cmdline='second_thread')
trace.add_thread(
tid=JIT_TID,
@@ -48,6 +48,8 @@
trace.add_thread(tid=BINDER_TID, tgid=APP_PID, cmdline='Binder', name='Binder')
trace.add_thread(tid=FONTS_TID, tgid=APP_PID, cmdline='fonts', name='fonts')
+trace.add_package_list(ts=99, name='com.some.app', uid=10001, version_code=123)
+
trace.add_ftrace_packet(cpu=0)
# Start intent.
trace.add_atrace_begin(
diff --git a/test/trace_processor/startup/android_startup_breakdown.out b/test/trace_processor/startup/android_startup_breakdown.out
index 446c5f1..ff9f062 100644
--- a/test/trace_processor/startup/android_startup_breakdown.out
+++ b/test/trace_processor/startup/android_startup_breakdown.out
@@ -57,6 +57,17 @@
activity_hosting_process_count: 1
process {
name: "com.google.android.calendar"
+ uid: 10001
+ package {
+ package_name: "com.google.android.calendar"
+ apk_version_code: 123
+ debuggable: false
+ }
+ packages_for_uid {
+ package_name: "com.google.android.calendar"
+ apk_version_code: 123
+ debuggable: false
+ }
}
activities {
name: "com.google.android.calendar.MainActivity"
diff --git a/test/trace_processor/startup/android_startup_breakdown.py b/test/trace_processor/startup/android_startup_breakdown.py
index 29fdd2c..7ea4256 100644
--- a/test/trace_processor/startup/android_startup_breakdown.py
+++ b/test/trace_processor/startup/android_startup_breakdown.py
@@ -26,7 +26,10 @@
trace.add_packet()
trace.add_process(1, 0, 'init')
trace.add_process(2, 1, 'system_server')
-trace.add_process(3, 1, 'com.google.android.calendar')
+trace.add_process(3, 1, 'com.google.android.calendar', uid=10001)
+
+trace.add_package_list(
+ ts=100, name='com.google.android.calendar', uid=10001, version_code=123)
trace.add_ftrace_packet(cpu=0)
diff --git a/test/trace_processor/startup/android_startup_process_track.out b/test/trace_processor/startup/android_startup_process_track.out
index 328651a..4076197 100644
--- a/test/trace_processor/startup/android_startup_process_track.out
+++ b/test/trace_processor/startup/android_startup_process_track.out
@@ -2,7 +2,7 @@
startup {
startup_id: 1
package_name: "com.google.android.calendar"
- process_name: "com.google.android.calendar"
+ process_name: "com.google.android.calendar:debug"
zygote_new_process: false
to_first_frame {
dur_ns: 7
@@ -37,7 +37,18 @@
}
activity_hosting_process_count: 1
process {
- name: "com.google.android.calendar"
+ name: "com.google.android.calendar:debug"
+ uid: 10001
+ package {
+ package_name: "com.google.android.calendar"
+ apk_version_code: 123
+ debuggable: false
+ }
+ packages_for_uid {
+ package_name: "com.google.android.calendar"
+ apk_version_code: 123
+ debuggable: false
+ }
}
event_timestamps {
intent_received: 100
@@ -89,6 +100,17 @@
activity_hosting_process_count: 1
process {
name: "com.google.android.calendar"
+ uid: 10001
+ package {
+ package_name: "com.google.android.calendar"
+ apk_version_code: 123
+ debuggable: false
+ }
+ packages_for_uid {
+ package_name: "com.google.android.calendar"
+ apk_version_code: 123
+ debuggable: false
+ }
}
event_timestamps {
intent_received: 200
diff --git a/test/trace_processor/startup/android_startup_process_track.py b/test/trace_processor/startup/android_startup_process_track.py
index f24a429..4653836 100644
--- a/test/trace_processor/startup/android_startup_process_track.py
+++ b/test/trace_processor/startup/android_startup_process_track.py
@@ -55,10 +55,21 @@
# (i.e. process exit is taken into account).
trace = synth_common.create_trace()
trace.add_packet()
-trace.add_process(1, 0, 'init')
-trace.add_process(2, 1, 'system_server')
+trace.add_process(1, 0, 'init', uid=10001)
+trace.add_process(2, 1, 'system_server', uid=1000)
+
+trace.add_package_list(
+ ts=99, name='com.google.android.calendar', uid=10001, version_code=123)
+
add_startup(trace, ts=100, pid=3)
+trace.add_packet(ts=140)
+trace.add_process(3, 1, 'com.google.android.calendar:debug', uid=10001)
+
+trace.add_packet()
trace.add_process_free(ts=150, tid=3, comm='', prio=0)
+
add_startup(trace, ts=200, pid=4)
+trace.add_packet(ts=250)
+trace.add_process(4, 1, 'com.google.android.calendar', uid=10001)
sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/ui/build.js b/ui/build.js
index 758dee6..8a2ad50 100644
--- a/ui/build.js
+++ b/ui/build.js
@@ -100,6 +100,7 @@
outDir: pjoin(ROOT_DIR, 'out/ui'),
version: '', // v1.2.3, derived from the CHANGELOG + git.
outUiDir: '',
+ outUiTestArtifactsDir: '',
outDistRootDir: '',
outTscDir: '',
outGenDir: '',
@@ -115,6 +116,10 @@
{r: /ui\/src\/assets\/.+[.]scss/, f: compileScss},
{r: /ui\/src\/assets\/.+[.]scss/, f: compileScss},
{r: /ui\/src\/chrome_extension\/.*/, f: copyExtensionAssets},
+ {
+ r: /ui\/src\/test\/diff_viewer\/(.+[.](?:html|js))/,
+ f: copyUiTestArtifactsAssets,
+ },
{r: /.*\/dist\/.+\/(?!manifest\.json).*/, f: genServiceWorkerManifestJson},
{r: /.*\/dist\/.*/, f: notifyLiveServer},
];
@@ -150,6 +155,7 @@
const clean = !args.no_build;
cfg.outDir = path.resolve(ensureDir(args.out || cfg.outDir));
cfg.outUiDir = ensureDir(pjoin(cfg.outDir, 'ui'), clean);
+ cfg.outUiTestArtifactsDir = ensureDir(pjoin(cfg.outDir, 'ui-test-artifacts'));
cfg.outExtDir = ensureDir(pjoin(cfg.outUiDir, 'chrome_extension'));
cfg.outDistRootDir = ensureDir(pjoin(cfg.outUiDir, 'dist'));
const proc = exec('python3', [VERSION_SCRIPT, '--stdout'], {stdout: 'pipe'});
@@ -219,6 +225,7 @@
buildWasm(args.no_wasm);
scanDir('ui/src/assets');
scanDir('ui/src/chrome_extension');
+ scanDir('ui/src/test/diff_viewer');
scanDir('buildtools/typefaces');
scanDir('buildtools/catapult_trace_viewer');
generateImports('ui/src/tracks', 'all_tracks.ts');
@@ -306,6 +313,10 @@
addTask(cp, [src, pjoin(cfg.outDistDir, 'assets', dst)]);
}
+function copyUiTestArtifactsAssets(src, dst) {
+ addTask(cp, [src, pjoin(cfg.outUiTestArtifactsDir, dst)]);
+}
+
function compileScss() {
const src = pjoin(ROOT_DIR, 'ui/src/assets/perfetto.scss');
const dst = pjoin(cfg.outDistDir, 'perfetto.css');
diff --git a/ui/src/common/recordingV2/recording_config_utils.ts b/ui/src/common/recordingV2/recording_config_utils.ts
index fc95007..85495e8 100644
--- a/ui/src/common/recordingV2/recording_config_utils.ts
+++ b/ui/src/common/recordingV2/recording_config_utils.ts
@@ -71,7 +71,7 @@
export function genTraceConfig(
uiCfg: RecordConfig, targetInfo: TargetInfo): TraceConfig {
const androidApiLevel = (targetInfo.targetType === 'ANDROID') ?
- targetInfo.dynamicTargetInfo?.androidApiLevel :
+ targetInfo.androidApiLevel :
undefined;
const protoCfg = new TraceConfig();
protoCfg.durationMs = uiCfg.durationMs;
diff --git a/ui/src/common/recordingV2/recording_interfaces_v2.ts b/ui/src/common/recordingV2/recording_interfaces_v2.ts
index 3193a3e..823802e 100644
--- a/ui/src/common/recordingV2/recording_interfaces_v2.ts
+++ b/ui/src/common/recordingV2/recording_interfaces_v2.ts
@@ -39,24 +39,41 @@
connectNewTarget(): Promise<RecordingTargetV2>;
}
-export interface DynamicTargetInfo {
+export interface DataSource {
+ name: string;
+
+ // Contains information that is opaque to the recording code. The caller can
+ // use the DataSource name to type cast the DataSource descriptor.
+ // For targets calling QueryServiceState, 'descriptor' will hold the
+ // datasource descriptor:
+ // https://source.corp.google.com/android/external/perfetto/protos/perfetto/
+ // common/data_source_descriptor.proto;l=28-60
+ // For Chrome, 'descriptor' will contain the answer received from
+ // 'GetCategories':
+ // https://source.corp.google.com/android/external/perfetto/ui/src/
+ // chrome_extension/chrome_tracing_controller.ts;l=220
+ descriptor: unknown;
+}
+
+// Common fields for all types of targetInfo: Chrome, Android, Linux etc.
+interface TargetInfoBase {
+ name: string;
+
+ // The dataSources exposed by a target. They are fetched from the target
+ // (ex: using QSS for Android or GetCategories for Chrome).
+ dataSources: DataSource[];
+}
+
+export interface AndroidTargetInfo extends TargetInfoBase {
+ targetType: 'ANDROID';
+
// This is the Android API level. For instance, it can be 32, 31, 30 etc.
// It is the "API level" column here:
// https://source.android.com/setup/start/build-numbers
- androidApiLevel: number;
+ androidApiLevel?: number;
}
-export interface AndroidTargetInfo {
- name: string;
- targetType: 'ANDROID';
- // dynamicTargetInfo is only available after we have been able to connect to
- // a target. On Android connected via WebUSB, that happens only after the user
- // has authorized ADB, which can take several seconds.
- dynamicTargetInfo?: DynamicTargetInfo;
-}
-
-export interface OtherTargetInfo {
- name: string;
+export interface OtherTargetInfo extends TargetInfoBase {
targetType: 'CHROME'|'CHROME_OS'|'LINUX';
}
diff --git a/ui/src/common/recordingV2/targets/android_websocket_target.ts b/ui/src/common/recordingV2/targets/android_websocket_target.ts
index 06d94df..8799269 100644
--- a/ui/src/common/recordingV2/targets/android_websocket_target.ts
+++ b/ui/src/common/recordingV2/targets/android_websocket_target.ts
@@ -34,7 +34,8 @@
targetType: 'ANDROID',
// TODO(octaviant): fetch the OS from the adb connection
// once aosp/2127460 is in
- dynamicTargetInfo: undefined,
+ androidApiLevel: undefined,
+ dataSources: [],
name: this.serialNumber + ' WebSocket',
};
}
diff --git a/ui/src/common/recordingV2/targets/android_webusb_target.ts b/ui/src/common/recordingV2/targets/android_webusb_target.ts
index e0c2c3b..9f02f4f 100644
--- a/ui/src/common/recordingV2/targets/android_webusb_target.ts
+++ b/ui/src/common/recordingV2/targets/android_webusb_target.ts
@@ -16,7 +16,6 @@
import {AdbConnectionOverWebusb} from '../adb_connection_over_webusb';
import {AdbKeyManager} from '../auth/adb_key_manager';
import {
- DynamicTargetInfo,
RecordingTargetV2,
TargetInfo,
TracingSession,
@@ -29,7 +28,7 @@
export class AndroidWebusbTarget implements RecordingTargetV2 {
private adbConnection: AdbConnectionOverWebusb;
- private dynamicTargetInfo?: DynamicTargetInfo;
+ private androidApiLevel?: number;
constructor(
private factory: AndroidWebusbTargetFactory, private device: USBDevice,
@@ -43,7 +42,8 @@
return {
targetType: 'ANDROID',
// The method 'fetchInfo' will populate this after ADB authorization.
- dynamicTargetInfo: this.dynamicTargetInfo,
+ androidApiLevel: this.androidApiLevel,
+ dataSources: [],
name,
};
}
@@ -62,10 +62,10 @@
const adbStream =
await this.adbConnection.connectSocket('/dev/socket/traced_consumer');
- if (!this.dynamicTargetInfo) {
+ if (!this.androidApiLevel) {
const version = await this.adbConnection.shellAndGetOutput(
'getprop ro.build.version.sdk');
- this.dynamicTargetInfo = {androidApiLevel: Number(version)};
+ this.androidApiLevel = Number(version);
if (this.factory.onTargetChange) {
this.factory.onTargetChange();
}
diff --git a/ui/src/controller/record_controller.ts b/ui/src/controller/record_controller.ts
index 4212122..6bc32e4 100644
--- a/ui/src/controller/record_controller.ts
+++ b/ui/src/controller/record_controller.ts
@@ -99,11 +99,12 @@
if (targetType == 'ANDROID') {
targetInfo = {
targetType,
- dynamicTargetInfo: {androidApiLevel},
+ androidApiLevel,
+ dataSources: [],
name: '',
};
} else {
- targetInfo = {targetType, name: ''};
+ targetInfo = {targetType, dataSources: [], name: ''};
}
return genTraceConfig(uiCfg, targetInfo);
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 6a0f049..923fdb5 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -104,7 +104,6 @@
'android_surfaceflinger',
'android_batt',
'android_sysui_cuj',
- 'android_jank',
'android_camera',
'android_other_traces',
'chrome_dropped_frames',
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
index cef2b2a..10e747a 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/frontend/record_page.ts
@@ -13,15 +13,12 @@
// limitations under the License.
-import {produce} from 'immer';
import * as m from 'mithril';
import {Actions} from '../common/actions';
import {featureFlags} from '../common/feature_flags';
-import {MeminfoCounters, VmstatCounters} from '../common/protos';
import {
AdbRecordingTarget,
- getBuiltinChromeCategoryList,
getDefaultRecordingTargets,
hasActiveProbes,
isAdbTarget,
@@ -33,7 +30,6 @@
LoadedConfig,
MAX_TIME,
RecordingTarget,
- RecordMode,
} from '../common/state';
import {AdbOverWebUsb} from '../controller/adb';
import {
@@ -49,20 +45,17 @@
recordTargetStore,
} from './record_config';
import {
- CategoriesCheckboxList,
CodeSnippet,
- CompactProbe,
- Dropdown,
- DropdownAttrs,
- Probe,
- ProbeAttrs,
- Slider,
- SliderAttrs,
- Textarea,
- TextareaAttrs,
- Toggle,
- ToggleAttrs,
} from './record_widgets';
+import {AdvancedSettings} from './recording/advanced_settings';
+import {AndroidSettings} from './recording/android_settings';
+import {ChromeSettings} from './recording/chrome_settings';
+import {CpuSettings} from './recording/cpu_settings';
+import {GpuSettings} from './recording/gpu_settings';
+import {MemorySettings} from './recording/memory_settings';
+import {PowerSettings} from './recording/power_settings';
+import {RecordingSectionAttrs} from './recording/recording_sections';
+import {RecordingSettings} from './recording/recording_settings';
export const PERSIST_CONFIG_FLAG = featureFlags.register({
id: 'persistConfigsUI',
@@ -71,746 +64,18 @@
defaultValue: true,
});
-export const POLL_INTERVAL_MS = [250, 500, 1000, 2500, 5000, 30000, 60000];
-
-export const ATRACE_CATEGORIES = new Map<string, string>();
-ATRACE_CATEGORIES.set('adb', 'ADB');
-ATRACE_CATEGORIES.set('aidl', 'AIDL calls');
-ATRACE_CATEGORIES.set('am', 'Activity Manager');
-ATRACE_CATEGORIES.set('audio', 'Audio');
-ATRACE_CATEGORIES.set('binder_driver', 'Binder Kernel driver');
-ATRACE_CATEGORIES.set('binder_lock', 'Binder global lock trace');
-ATRACE_CATEGORIES.set('bionic', 'Bionic C library');
-ATRACE_CATEGORIES.set('camera', 'Camera');
-ATRACE_CATEGORIES.set('dalvik', 'ART & Dalvik');
-ATRACE_CATEGORIES.set('database', 'Database');
-ATRACE_CATEGORIES.set('gfx', 'Graphics');
-ATRACE_CATEGORIES.set('hal', 'Hardware Modules');
-ATRACE_CATEGORIES.set('input', 'Input');
-ATRACE_CATEGORIES.set('network', 'Network');
-ATRACE_CATEGORIES.set('nnapi', 'Neural Network API');
-ATRACE_CATEGORIES.set('pm', 'Package Manager');
-ATRACE_CATEGORIES.set('power', 'Power Management');
-ATRACE_CATEGORIES.set('res', 'Resource Loading');
-ATRACE_CATEGORIES.set('rro', 'Resource Overlay');
-ATRACE_CATEGORIES.set('rs', 'RenderScript');
-ATRACE_CATEGORIES.set('sm', 'Sync Manager');
-ATRACE_CATEGORIES.set('ss', 'System Server');
-ATRACE_CATEGORIES.set('vibrator', 'Vibrator');
-ATRACE_CATEGORIES.set('video', 'Video');
-ATRACE_CATEGORIES.set('view', 'View System');
-ATRACE_CATEGORIES.set('webview', 'WebView');
-ATRACE_CATEGORIES.set('wm', 'Window Manager');
-
-export const LOG_BUFFERS = new Map<string, string>();
-LOG_BUFFERS.set('LID_CRASH', 'Crash');
-LOG_BUFFERS.set('LID_DEFAULT', 'Main');
-LOG_BUFFERS.set('LID_EVENTS', 'Binary events');
-LOG_BUFFERS.set('LID_KERNEL', 'Kernel');
-LOG_BUFFERS.set('LID_RADIO', 'Radio');
-LOG_BUFFERS.set('LID_SECURITY', 'Security');
-LOG_BUFFERS.set('LID_STATS', 'Stats');
-LOG_BUFFERS.set('LID_SYSTEM', 'System');
-
-export const FTRACE_CATEGORIES = new Map<string, string>();
-FTRACE_CATEGORIES.set('binder/*', 'binder');
-FTRACE_CATEGORIES.set('block/*', 'block');
-FTRACE_CATEGORIES.set('clk/*', 'clk');
-FTRACE_CATEGORIES.set('ext4/*', 'ext4');
-FTRACE_CATEGORIES.set('f2fs/*', 'f2fs');
-FTRACE_CATEGORIES.set('i2c/*', 'i2c');
-FTRACE_CATEGORIES.set('irq/*', 'irq');
-FTRACE_CATEGORIES.set('kmem/*', 'kmem');
-FTRACE_CATEGORIES.set('memory_bus/*', 'memory_bus');
-FTRACE_CATEGORIES.set('mmc/*', 'mmc');
-FTRACE_CATEGORIES.set('oom/*', 'oom');
-FTRACE_CATEGORIES.set('power/*', 'power');
-FTRACE_CATEGORIES.set('regulator/*', 'regulator');
-FTRACE_CATEGORIES.set('sched/*', 'sched');
-FTRACE_CATEGORIES.set('sync/*', 'sync');
-FTRACE_CATEGORIES.set('task/*', 'task');
-FTRACE_CATEGORIES.set('task/*', 'task');
-FTRACE_CATEGORIES.set('vmscan/*', 'vmscan');
-FTRACE_CATEGORIES.set('fastrpc/*', 'fastrpc');
-
-export function RecSettings(cssClass: string) {
- const S = (x: number) => x * 1000;
- const M = (x: number) => x * 1000 * 60;
- const H = (x: number) => x * 1000 * 60 * 60;
-
- const cfg = globals.state.recordConfig;
-
- const recButton = (mode: RecordMode, title: string, img: string) => {
- const checkboxArgs = {
- checked: cfg.mode === mode,
- onchange: (e: InputEvent) => {
- const checked = (e.target as HTMLInputElement).checked;
- if (!checked) return;
- const traceCfg = produce(globals.state.recordConfig, (draft) => {
- draft.mode = mode;
- });
- globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
- },
- };
- return m(
- `label${cfg.mode === mode ? '.selected' : ''}`,
- m(`input[type=radio][name=rec_mode]`, checkboxArgs),
- m(`img[src=${globals.root}assets/${img}]`),
- m('span', title));
- };
-
- return m(
- `.record-section${cssClass}`,
- m('header', 'Recording mode'),
- m('.record-mode',
- recButton('STOP_WHEN_FULL', 'Stop when full', 'rec_one_shot.png'),
- recButton('RING_BUFFER', 'Ring buffer', 'rec_ring_buf.png'),
- recButton('LONG_TRACE', 'Long trace', 'rec_long_trace.png')),
-
- m(Slider, {
- title: 'In-memory buffer size',
- icon: '360',
- values: [4, 8, 16, 32, 64, 128, 256, 512],
- unit: 'MB',
- set: (cfg, val) => cfg.bufferSizeMb = val,
- get: (cfg) => cfg.bufferSizeMb,
- } as SliderAttrs),
-
- m(Slider, {
- title: 'Max duration',
- icon: 'timer',
- values: [S(10), S(15), S(30), S(60), M(5), M(30), H(1), H(6), H(12)],
- isTime: true,
- unit: 'h:m:s',
- set: (cfg, val) => cfg.durationMs = val,
- get: (cfg) => cfg.durationMs,
- } as SliderAttrs),
- m(Slider, {
- title: 'Max file size',
- icon: 'save',
- cssClass: cfg.mode !== 'LONG_TRACE' ? '.hide' : '',
- values: [5, 25, 50, 100, 500, 1000, 1000 * 5, 1000 * 10],
- unit: 'MB',
- set: (cfg, val) => cfg.maxFileSizeMb = val,
- get: (cfg) => cfg.maxFileSizeMb,
- } as SliderAttrs),
- m(Slider, {
- title: 'Flush on disk every',
- cssClass: cfg.mode !== 'LONG_TRACE' ? '.hide' : '',
- icon: 'av_timer',
- values: [100, 250, 500, 1000, 2500, 5000],
- unit: 'ms',
- set: (cfg, val) => cfg.fileWritePeriodMs = val,
- get: (cfg) => cfg.fileWritePeriodMs || 0,
- } as SliderAttrs));
-}
-
-export function PowerSettings(cssClass: string) {
- const DOC_URL = 'https://perfetto.dev/docs/data-sources/battery-counters';
- const descr =
- [m('div',
- m('span', `Polls charge counters and instantaneous power draw from
- the battery power management IC and the power rails from
- the PowerStats HAL (`),
- m('a', {href: DOC_URL, target: '_blank'}, 'see docs for more'),
- m('span', ')'))];
- if (globals.isInternalUser) {
- descr.push(m(
- 'div',
- m('span', 'Googlers: See '),
- m('a',
- {href: 'http://go/power-rails-internal-doc', target: '_blank'},
- 'this doc'),
- m('span', ` for instructions on how to change the refault rail selection
- on internal devices.`),
- ));
- }
- return m(
- `.record-section${cssClass}`,
- m(Probe,
- {
- title: 'Battery drain & power rails',
- img: 'rec_battery_counters.png',
- descr,
- setEnabled: (cfg, val) => cfg.batteryDrain = val,
- isEnabled: (cfg) => cfg.batteryDrain,
- } as ProbeAttrs,
- m(Slider, {
- title: 'Poll interval',
- cssClass: '.thin',
- values: POLL_INTERVAL_MS,
- unit: 'ms',
- set: (cfg, val) => cfg.batteryDrainPollMs = val,
- get: (cfg) => cfg.batteryDrainPollMs,
- } as SliderAttrs)),
- m(Probe, {
- title: 'Board voltages & frequencies',
- img: 'rec_board_voltage.png',
- descr: 'Tracks voltage and frequency changes from board sensors',
- setEnabled: (cfg, val) => cfg.boardSensors = val,
- isEnabled: (cfg) => cfg.boardSensors,
- } as ProbeAttrs));
-}
-
-export function GpuSettings(cssClass: string) {
- return m(
- `.record-section${cssClass}`,
- m(Probe, {
- title: 'GPU frequency',
- img: 'rec_cpu_freq.png',
- descr: 'Records gpu frequency via ftrace',
- setEnabled: (cfg, val) => cfg.gpuFreq = val,
- isEnabled: (cfg) => cfg.gpuFreq,
- } as ProbeAttrs),
- m(Probe, {
- title: 'GPU memory',
- img: 'rec_gpu_mem_total.png',
- descr: `Allows to track per process and global total GPU memory usages.
- (Available on recent Android 12+ kernels)`,
- setEnabled: (cfg, val) => cfg.gpuMemTotal = val,
- isEnabled: (cfg) => cfg.gpuMemTotal,
- } as ProbeAttrs));
-}
-
-export function CpuSettings(cssClass: string) {
- return m(
- `.record-section${cssClass}`,
- m(Probe,
- {
- title: 'Coarse CPU usage counter',
- img: 'rec_cpu_coarse.png',
- descr: `Lightweight polling of CPU usage counters via /proc/stat.
- Allows to periodically monitor CPU usage.`,
- setEnabled: (cfg, val) => cfg.cpuCoarse = val,
- isEnabled: (cfg) => cfg.cpuCoarse,
- } as ProbeAttrs,
- m(Slider, {
- title: 'Poll interval',
- cssClass: '.thin',
- values: POLL_INTERVAL_MS,
- unit: 'ms',
- set: (cfg, val) => cfg.cpuCoarsePollMs = val,
- get: (cfg) => cfg.cpuCoarsePollMs,
- } as SliderAttrs)),
- m(Probe, {
- title: 'Scheduling details',
- img: 'rec_cpu_fine.png',
- descr: 'Enables high-detailed tracking of scheduling events',
- setEnabled: (cfg, val) => cfg.cpuSched = val,
- isEnabled: (cfg) => cfg.cpuSched,
- } as ProbeAttrs),
- m(Probe, {
- title: 'CPU frequency and idle states',
- img: 'rec_cpu_freq.png',
- descr: 'Records cpu frequency and idle state changes via ftrace',
- setEnabled: (cfg, val) => cfg.cpuFreq = val,
- isEnabled: (cfg) => cfg.cpuFreq,
- } as ProbeAttrs),
- m(Probe, {
- title: 'Syscalls',
- img: 'rec_syscalls.png',
- descr: `Tracks the enter and exit of all syscalls. On Android
- requires a userdebug or eng build.`,
- setEnabled: (cfg, val) => cfg.cpuSyscall = val,
- isEnabled: (cfg) => cfg.cpuSyscall,
- } as ProbeAttrs));
-}
-
-export function HeapSettings(cssClass: string) {
- const valuesForMS = [
- 0,
- 1000,
- 10 * 1000,
- 30 * 1000,
- 60 * 1000,
- 5 * 60 * 1000,
- 10 * 60 * 1000,
- 30 * 60 * 1000,
- 60 * 60 * 1000,
- ];
- const valuesForShMemBuff = [
- 0,
- 512,
- 1024,
- 2 * 1024,
- 4 * 1024,
- 8 * 1024,
- 16 * 1024,
- 32 * 1024,
- 64 * 1024,
- 128 * 1024,
- 256 * 1024,
- 512 * 1024,
- 1024 * 1024,
- 64 * 1024 * 1024,
- 128 * 1024 * 1024,
- 256 * 1024 * 1024,
- 512 * 1024 * 1024,
- ];
-
- return m(
- `.${cssClass}`,
- m(Textarea, {
- title: 'Names or pids of the processes to track',
- docsLink:
- 'https://perfetto.dev/docs/data-sources/native-heap-profiler#heapprofd-targets',
- placeholder: 'One per line, e.g.:\n' +
- 'system_server\n' +
- 'com.google.android.apps.photos\n' +
- '1503',
- set: (cfg, val) => cfg.hpProcesses = val,
- get: (cfg) => cfg.hpProcesses,
- } as TextareaAttrs),
- m(Slider, {
- title: 'Sampling interval',
- cssClass: '.thin',
- values: [
- /* eslint-disable no-multi-spaces */
- 0, 1, 2, 4, 8, 16, 32, 64,
- 128, 256, 512, 1024, 2048, 4096, 8192, 16384,
- 32768, 65536, 131072, 262144, 524288, 1048576,
- /* eslint-enable no-multi-spaces */
- ],
- unit: 'B',
- min: 0,
- set: (cfg, val) => cfg.hpSamplingIntervalBytes = val,
- get: (cfg) => cfg.hpSamplingIntervalBytes,
- } as SliderAttrs),
- m(Slider, {
- title: 'Continuous dumps interval ',
- description: 'Time between following dumps (0 = disabled)',
- cssClass: '.thin',
- values: valuesForMS,
- unit: 'ms',
- min: 0,
- set: (cfg, val) => {
- cfg.hpContinuousDumpsInterval = val;
- },
- get: (cfg) => cfg.hpContinuousDumpsInterval,
- } as SliderAttrs),
- m(Slider, {
- title: 'Continuous dumps phase',
- description: 'Time before first dump',
- cssClass: `.thin${
- globals.state.recordConfig.hpContinuousDumpsInterval === 0 ?
- '.greyed-out' :
- ''}`,
- values: valuesForMS,
- unit: 'ms',
- min: 0,
- disabled: globals.state.recordConfig.hpContinuousDumpsInterval === 0,
- set: (cfg, val) => cfg.hpContinuousDumpsPhase = val,
- get: (cfg) => cfg.hpContinuousDumpsPhase,
- } as SliderAttrs),
- m(Slider, {
- title: `Shared memory buffer`,
- cssClass: '.thin',
- values: valuesForShMemBuff.filter(
- (value) => value === 0 || value >= 8192 && value % 4096 === 0),
- unit: 'B',
- min: 0,
- set: (cfg, val) => cfg.hpSharedMemoryBuffer = val,
- get: (cfg) => cfg.hpSharedMemoryBuffer,
- } as SliderAttrs),
- m(Toggle, {
- title: 'Block client',
- cssClass: '.thin',
- descr: `Slow down target application if profiler cannot keep up.`,
- setEnabled: (cfg, val) => cfg.hpBlockClient = val,
- isEnabled: (cfg) => cfg.hpBlockClient,
- } as ToggleAttrs),
- m(Toggle, {
- title: 'All custom allocators (Q+)',
- cssClass: '.thin',
- descr: `If the target application exposes custom allocators, also
-sample from those.`,
- setEnabled: (cfg, val) => cfg.hpAllHeaps = val,
- isEnabled: (cfg) => cfg.hpAllHeaps,
- } as ToggleAttrs),
- // TODO(hjd): Add advanced options.
- );
-}
-
-export function JavaHeapDumpSettings(cssClass: string) {
- const valuesForMS = [
- 0,
- 1000,
- 10 * 1000,
- 30 * 1000,
- 60 * 1000,
- 5 * 60 * 1000,
- 10 * 60 * 1000,
- 30 * 60 * 1000,
- 60 * 60 * 1000,
- ];
-
- return m(
- `.${cssClass}`,
- m(Textarea, {
- title: 'Names or pids of the processes to track',
- placeholder: 'One per line, e.g.:\n' +
- 'com.android.vending\n' +
- '1503',
- set: (cfg, val) => cfg.jpProcesses = val,
- get: (cfg) => cfg.jpProcesses,
- } as TextareaAttrs),
- m(Slider, {
- title: 'Continuous dumps interval ',
- description: 'Time between following dumps (0 = disabled)',
- cssClass: '.thin',
- values: valuesForMS,
- unit: 'ms',
- min: 0,
- set: (cfg, val) => {
- cfg.jpContinuousDumpsInterval = val;
- },
- get: (cfg) => cfg.jpContinuousDumpsInterval,
- } as SliderAttrs),
- m(Slider, {
- title: 'Continuous dumps phase',
- description: 'Time before first dump',
- cssClass: `.thin${
- globals.state.recordConfig.jpContinuousDumpsInterval === 0 ?
- '.greyed-out' :
- ''}`,
- values: valuesForMS,
- unit: 'ms',
- min: 0,
- disabled: globals.state.recordConfig.jpContinuousDumpsInterval === 0,
- set: (cfg, val) => cfg.jpContinuousDumpsPhase = val,
- get: (cfg) => cfg.jpContinuousDumpsPhase,
- } as SliderAttrs),
- );
-}
-
-export function MemorySettings(cssClass: string) {
- const meminfoOpts = new Map<string, string>();
- for (const x in MeminfoCounters) {
- if (typeof MeminfoCounters[x] === 'number' &&
- !`${x}`.endsWith('_UNSPECIFIED')) {
- meminfoOpts.set(x, x.replace('MEMINFO_', '').toLowerCase());
- }
- }
- const vmstatOpts = new Map<string, string>();
- for (const x in VmstatCounters) {
- if (typeof VmstatCounters[x] === 'number' &&
- !`${x}`.endsWith('_UNSPECIFIED')) {
- vmstatOpts.set(x, x.replace('VMSTAT_', '').toLowerCase());
- }
- }
- return m(
- `.record-section${cssClass}`,
- m(Probe,
- {
- title: 'Native heap profiling',
- img: 'rec_native_heap_profiler.png',
- descr: `Track native heap allocations & deallocations of an Android
- process. (Available on Android 10+)`,
- setEnabled: (cfg, val) => cfg.heapProfiling = val,
- isEnabled: (cfg) => cfg.heapProfiling,
- } as ProbeAttrs,
- HeapSettings(cssClass)),
- m(Probe,
- {
- title: 'Java heap dumps',
- img: 'rec_java_heap_dump.png',
- descr: `Dump information about the Java object graph of an
- Android app. (Available on Android 11+)`,
- setEnabled: (cfg, val) => cfg.javaHeapDump = val,
- isEnabled: (cfg) => cfg.javaHeapDump,
- } as ProbeAttrs,
- JavaHeapDumpSettings(cssClass)),
- m(Probe,
- {
- title: 'Kernel meminfo',
- img: 'rec_meminfo.png',
- descr: 'Polling of /proc/meminfo',
- setEnabled: (cfg, val) => cfg.meminfo = val,
- isEnabled: (cfg) => cfg.meminfo,
- } as ProbeAttrs,
- m(Slider, {
- title: 'Poll interval',
- cssClass: '.thin',
- values: POLL_INTERVAL_MS,
- unit: 'ms',
- set: (cfg, val) => cfg.meminfoPeriodMs = val,
- get: (cfg) => cfg.meminfoPeriodMs,
- } as SliderAttrs),
- m(Dropdown, {
- title: 'Select counters',
- cssClass: '.multicolumn',
- options: meminfoOpts,
- set: (cfg, val) => cfg.meminfoCounters = val,
- get: (cfg) => cfg.meminfoCounters,
- } as DropdownAttrs)),
- m(Probe, {
- title: 'High-frequency memory events',
- img: 'rec_mem_hifreq.png',
- descr: `Allows to track short memory spikes and transitories through
- ftrace's mm_event, rss_stat and ion events. Available only
- on recent Android Q+ kernels`,
- setEnabled: (cfg, val) => cfg.memHiFreq = val,
- isEnabled: (cfg) => cfg.memHiFreq,
- } as ProbeAttrs),
- m(Probe, {
- title: 'Low memory killer',
- img: 'rec_lmk.png',
- descr: `Record LMK events. Works both with the old in-kernel LMK
- and the newer userspace lmkd. It also tracks OOM score
- adjustments.`,
- setEnabled: (cfg, val) => cfg.memLmk = val,
- isEnabled: (cfg) => cfg.memLmk,
- } as ProbeAttrs),
- m(Probe,
- {
- title: 'Per process stats',
- img: 'rec_ps_stats.png',
- descr: `Periodically samples all processes in the system tracking:
- their thread list, memory counters (RSS, swap and other
- /proc/status counters) and oom_score_adj.`,
- setEnabled: (cfg, val) => cfg.procStats = val,
- isEnabled: (cfg) => cfg.procStats,
- } as ProbeAttrs,
- m(Slider, {
- title: 'Poll interval',
- cssClass: '.thin',
- values: POLL_INTERVAL_MS,
- unit: 'ms',
- set: (cfg, val) => cfg.procStatsPeriodMs = val,
- get: (cfg) => cfg.procStatsPeriodMs,
- } as SliderAttrs)),
- m(Probe,
- {
- title: 'Virtual memory stats',
- img: 'rec_vmstat.png',
- descr: `Periodically polls virtual memory stats from /proc/vmstat.
- Allows to gather statistics about swap, eviction,
- compression and pagecache efficiency`,
- setEnabled: (cfg, val) => cfg.vmstat = val,
- isEnabled: (cfg) => cfg.vmstat,
- } as ProbeAttrs,
- m(Slider, {
- title: 'Poll interval',
- cssClass: '.thin',
- values: POLL_INTERVAL_MS,
- unit: 'ms',
- set: (cfg, val) => cfg.vmstatPeriodMs = val,
- get: (cfg) => cfg.vmstatPeriodMs,
- } as SliderAttrs),
- m(Dropdown, {
- title: 'Select counters',
- cssClass: '.multicolumn',
- options: vmstatOpts,
- set: (cfg, val) => cfg.vmstatCounters = val,
- get: (cfg) => cfg.vmstatCounters,
- } as DropdownAttrs)));
-}
-
-function AtraceAppsList() {
- if (globals.state.recordConfig.allAtraceApps) {
- return m('div');
- }
-
- return m(Textarea, {
- placeholder: 'Apps to profile, one per line, e.g.:\n' +
- 'com.android.phone\n' +
- 'lmkd\n' +
- 'com.android.nfc',
- cssClass: '.atrace-apps-list',
- set: (cfg, val) => cfg.atraceApps = val,
- get: (cfg) => cfg.atraceApps,
- } as TextareaAttrs);
-}
-
-export function AndroidSettings(cssClass: string) {
- return m(
- `.record-section${cssClass}`,
- m(Probe,
- {
- title: 'Atrace userspace annotations',
- img: 'rec_atrace.png',
- descr: `Enables C++ / Java codebase annotations (ATRACE_BEGIN() /
- os.Trace())`,
- setEnabled: (cfg, val) => cfg.atrace = val,
- isEnabled: (cfg) => cfg.atrace,
- } as ProbeAttrs,
- m(Dropdown, {
- title: 'Categories',
- cssClass: '.multicolumn.atrace-categories',
- options: ATRACE_CATEGORIES,
- set: (cfg, val) => cfg.atraceCats = val,
- get: (cfg) => cfg.atraceCats,
- } as DropdownAttrs),
- m(Toggle, {
- title: 'Record events from all Android apps and services',
- descr: '',
- setEnabled: (cfg, val) => cfg.allAtraceApps = val,
- isEnabled: (cfg) => cfg.allAtraceApps,
- } as ToggleAttrs),
- AtraceAppsList()),
- m(Probe,
- {
- title: 'Event log (logcat)',
- img: 'rec_logcat.png',
- descr: `Streams the event log into the trace. If no buffer filter is
- specified, all buffers are selected.`,
- setEnabled: (cfg, val) => cfg.androidLogs = val,
- isEnabled: (cfg) => cfg.androidLogs,
- } as ProbeAttrs,
- m(Dropdown, {
- title: 'Buffers',
- cssClass: '.multicolumn',
- options: LOG_BUFFERS,
- set: (cfg, val) => cfg.androidLogBuffers = val,
- get: (cfg) => cfg.androidLogBuffers,
- } as DropdownAttrs)),
- m(Probe, {
- title: 'Frame timeline',
- img: 'rec_frame_timeline.png',
- descr: `Records expected/actual frame timings from surface_flinger.
- Requires Android 12 (S) or above.`,
- setEnabled: (cfg, val) => cfg.androidFrameTimeline = val,
- isEnabled: (cfg) => cfg.androidFrameTimeline,
- } as ProbeAttrs));
-}
-
-
-export function ChromeSettings(cssClass: string) {
- return m(
- `.record-section${cssClass}`,
- CompactProbe({
- title: 'Task scheduling',
- setEnabled: (cfg, val) => cfg.taskScheduling = val,
- isEnabled: (cfg) => cfg.taskScheduling,
- }),
- CompactProbe({
- title: 'IPC flows',
- setEnabled: (cfg, val) => cfg.ipcFlows = val,
- isEnabled: (cfg) => cfg.ipcFlows,
- }),
- CompactProbe({
- title: 'Javascript execution',
- setEnabled: (cfg, val) => cfg.jsExecution = val,
- isEnabled: (cfg) => cfg.jsExecution,
- }),
- CompactProbe({
- title: 'Web content rendering, layout and compositing',
- setEnabled: (cfg, val) => cfg.webContentRendering = val,
- isEnabled: (cfg) => cfg.webContentRendering,
- }),
- CompactProbe({
- title: 'UI rendering & surface compositing',
- setEnabled: (cfg, val) => cfg.uiRendering = val,
- isEnabled: (cfg) => cfg.uiRendering,
- }),
- CompactProbe({
- title: 'Input events',
- setEnabled: (cfg, val) => cfg.inputEvents = val,
- isEnabled: (cfg) => cfg.inputEvents,
- }),
- CompactProbe({
- title: 'Navigation & Loading',
- setEnabled: (cfg, val) => cfg.navigationAndLoading = val,
- isEnabled: (cfg) => cfg.navigationAndLoading,
- }),
- CompactProbe({
- title: 'Chrome Logs',
- setEnabled: (cfg, val) => cfg.chromeLogs = val,
- isEnabled: (cfg) => cfg.chromeLogs,
- }),
- ChromeCategoriesSelection());
-}
-
-function ChromeCategoriesSelection() {
- // If we are attempting to record via the Chrome extension, we receive the
- // list of actually supported categories via DevTools. Otherwise, we fall back
- // to an integrated list of categories from a recent version of Chrome.
- let categories = globals.state.chromeCategories;
- if (!categories || !isChromeTarget(globals.state.recordingTarget)) {
- categories = getBuiltinChromeCategoryList();
- }
-
- const defaultCategories = new Map<string, string>();
- const disabledByDefaultCategories = new Map<string, string>();
- const disabledPrefix = 'disabled-by-default-';
- categories.forEach((cat) => {
- if (cat.startsWith(disabledPrefix)) {
- disabledByDefaultCategories.set(cat, cat.replace(disabledPrefix, ''));
- } else {
- defaultCategories.set(cat, cat);
- }
- });
-
- return m(
- '.chrome-categories',
- m(CategoriesCheckboxList, {
- categories: defaultCategories,
- title: 'Additional categories',
- get: (cfg) => cfg.chromeCategoriesSelected,
- set: (cfg, val) => cfg.chromeCategoriesSelected = val,
- }),
- m(CategoriesCheckboxList, {
- categories: disabledByDefaultCategories,
- title: 'High overhead categories',
- get: (cfg) => cfg.chromeHighOverheadCategoriesSelected,
- set: (cfg, val) => cfg.chromeHighOverheadCategoriesSelected = val,
- }));
-}
-
-export function AdvancedSettings(cssClass: string) {
- return m(
- `.record-section${cssClass}`,
- m(Probe,
- {
- title: 'Advanced ftrace config',
- img: 'rec_ftrace.png',
- descr: `Enable individual events and tune the kernel-tracing (ftrace)
- module. The events enabled here are in addition to those from
- enabled by other probes.`,
- setEnabled: (cfg, val) => cfg.ftrace = val,
- isEnabled: (cfg) => cfg.ftrace,
- } as ProbeAttrs,
- m(Toggle, {
- title: 'Resolve kernel symbols',
- cssClass: '.thin',
- 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,
- } as ToggleAttrs),
- m(Slider, {
- title: 'Buf size',
- cssClass: '.thin',
- values: [0, 512, 1024, 2 * 1024, 4 * 1024, 16 * 1024, 32 * 1024],
- unit: 'KB',
- zeroIsDefault: true,
- set: (cfg, val) => cfg.ftraceBufferSizeKb = val,
- get: (cfg) => cfg.ftraceBufferSizeKb,
- } as SliderAttrs),
- m(Slider, {
- title: 'Drain rate',
- cssClass: '.thin',
- values: [0, 100, 250, 500, 1000, 2500, 5000],
- unit: 'ms',
- zeroIsDefault: true,
- set: (cfg, val) => cfg.ftraceDrainPeriodMs = val,
- get: (cfg) => cfg.ftraceDrainPeriodMs,
- } as SliderAttrs),
- m(Dropdown, {
- title: 'Event groups',
- cssClass: '.multicolumn.ftrace-events',
- options: FTRACE_CATEGORIES,
- set: (cfg, val) => cfg.ftraceEvents = val,
- get: (cfg) => cfg.ftraceEvents,
- } as DropdownAttrs),
- m(Textarea, {
- placeholder: 'Add extra events, one per line, e.g.:\n' +
- 'sched/sched_switch\n' +
- 'kmem/*',
- set: (cfg, val) => cfg.ftraceExtraEvents = val,
- get: (cfg) => cfg.ftraceExtraEvents,
- } as TextareaAttrs)));
-}
+export const RECORDING_SECTIONS = [
+ 'buffers',
+ 'instructions',
+ 'config',
+ 'cpu',
+ 'gpu',
+ 'power',
+ 'memory',
+ 'android',
+ 'chrome',
+ 'advanced',
+];
function RecordHeader() {
return m(
@@ -1464,31 +729,41 @@
m('ul', probes));
}
+export function maybeGetActiveCss(routePage: string, section: string): string {
+ return routePage === section ? '.active' : '';
+}
export const RecordPage = createPage({
view({attrs}: m.Vnode<PageAttrs>) {
- const SECTIONS: {[property: string]: (cssClass: string) => m.Child} = {
- buffers: RecSettings,
- instructions: Instructions,
- config: Configurations,
- cpu: CpuSettings,
- gpu: GpuSettings,
- power: PowerSettings,
- memory: MemorySettings,
- android: AndroidSettings,
- chrome: ChromeSettings,
- advanced: AdvancedSettings,
- };
-
const pages: m.Children = [];
// we need to remove the `/` character from the route
let routePage = attrs.subpage ? attrs.subpage.substr(1) : '';
- if (!Object.keys(SECTIONS).includes(routePage)) {
+ if (!RECORDING_SECTIONS.includes(routePage)) {
routePage = 'buffers';
}
- for (const key of Object.keys(SECTIONS)) {
- const cssClass = routePage === key ? '.active' : '';
- pages.push(SECTIONS[key](cssClass));
+ pages.push(recordMenu(routePage));
+
+ pages.push(m(RecordingSettings, {
+ dataSources: [],
+ cssClass: maybeGetActiveCss(routePage, 'buffers'),
+ } as RecordingSectionAttrs));
+ pages.push(Instructions(maybeGetActiveCss(routePage, 'instructions')));
+ pages.push(Configurations(maybeGetActiveCss(routePage, 'config')));
+
+ const settingsSections = new Map([
+ ['cpu', CpuSettings],
+ ['gpu', GpuSettings],
+ ['power', PowerSettings],
+ ['memory', MemorySettings],
+ ['android', AndroidSettings],
+ ['chrome', ChromeSettings],
+ ['advanced', AdvancedSettings],
+ ]);
+ for (const [section, component] of settingsSections.entries()) {
+ pages.push(m(component, {
+ dataSources: [],
+ cssClass: maybeGetActiveCss(routePage, section),
+ } as RecordingSectionAttrs));
}
return m(
diff --git a/ui/src/frontend/record_page_v2.ts b/ui/src/frontend/record_page_v2.ts
index 3a02053..f9fde2d 100644
--- a/ui/src/frontend/record_page_v2.ts
+++ b/ui/src/frontend/record_page_v2.ts
@@ -56,17 +56,20 @@
import {publishBufferUsage} from './publish';
import {autosaveConfigStore, recordConfigStore} from './record_config';
import {
- AdvancedSettings,
- AndroidSettings,
Configurations,
- CpuSettings,
- GpuSettings,
- MemorySettings,
+ maybeGetActiveCss,
PERSIST_CONFIG_FLAG,
- PowerSettings,
- RecSettings,
+ RECORDING_SECTIONS,
} from './record_page';
import {CodeSnippet} from './record_widgets';
+import {AdvancedSettings} from './recording/advanced_settings';
+import {AndroidSettings} from './recording/android_settings';
+import {CpuSettings} from './recording/cpu_settings';
+import {GpuSettings} from './recording/gpu_settings';
+import {MemorySettings} from './recording/memory_settings';
+import {PowerSettings} from './recording/power_settings';
+import {RecordingSectionAttrs} from './recording/recording_sections';
+import {RecordingSettings} from './recording/recording_settings';
// Wraps a tracing session promise while the promise is being resolved (e.g.
// while we are awaiting for ADB auth).
@@ -366,8 +369,8 @@
notes.push(msgLinux);
break;
case 'ANDROID': {
- const androidApiLevel = targetInfo.dynamicTargetInfo?.androidApiLevel;
- if (androidApiLevel && androidApiLevel == 28) {
+ const androidApiLevel = targetInfo.androidApiLevel;
+ if (androidApiLevel === 28) {
notes.push(m('.note', msgFeatNotSupported, msgSideload));
} else if (androidApiLevel && androidApiLevel <= 27) {
notes.push(m('.note', msgPerfettoNotSupported, msgSideload));
@@ -406,8 +409,7 @@
const pbtx = data ? data.configProtoText : '';
let cmd = '';
if (targetInfo.targetType === 'ANDROID' &&
- targetInfo.dynamicTargetInfo?.androidApiLevel &&
- targetInfo.dynamicTargetInfo.androidApiLevel === 28) {
+ targetInfo.androidApiLevel === 28) {
cmd += `echo '${pbBase64}' | \n`;
cmd += 'base64 --decode | \n';
cmd += 'adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace"\n';
@@ -441,7 +443,11 @@
}
const targetInfo = recordingTargetV2.getInfo();
- if (targetInfo.targetType === 'ANDROID' && !targetInfo.dynamicTargetInfo) {
+ // The absence of androidApiLevel shows that we have not connected to the
+ // device, therefore we can not start recording.
+ // TODO(octaviant): encapsulation should be stricter here, look into making
+ // this a method
+ if (targetInfo.targetType === 'ANDROID' && !targetInfo.androidApiLevel) {
return undefined;
}
@@ -648,35 +654,45 @@
}
const targetInfo = recordingTargetV2.getInfo();
- if (targetInfo.targetType === 'ANDROID' && !targetInfo.dynamicTargetInfo) {
+ // The absence of androidApiLevel shows that we have not connected to the
+ // device because we do not have user authorization.
+ if (targetInfo.targetType === 'ANDROID' && !targetInfo.androidApiLevel) {
components.push(
m('.full-centered', 'Please allow USB debugging on the device.'));
return m('.record-container', components);
}
- const SECTIONS: {[property: string]: (cssClass: string) => m.Child} = {
- buffers: RecSettings,
- instructions: Instructions,
- config: Configurations,
- cpu: CpuSettings,
- gpu: GpuSettings,
- power: PowerSettings,
- memory: MemorySettings,
- android: AndroidSettings,
- advanced: AdvancedSettings,
- };
-
const pages: m.Children = [];
// we need to remove the `/` character from the route
let routePage = subpage ? subpage.substr(1) : '';
- if (!Object.keys(SECTIONS).includes(routePage)) {
+ if (!RECORDING_SECTIONS.includes(routePage)) {
routePage = 'buffers';
}
pages.push(recordMenu(routePage));
- for (const key of Object.keys(SECTIONS)) {
- const cssClass = routePage === key ? '.active' : '';
- pages.push(SECTIONS[key](cssClass));
+
+ pages.push(m(RecordingSettings, {
+ dataSources: [],
+ cssClass: maybeGetActiveCss(routePage, 'buffers'),
+ } as RecordingSectionAttrs));
+ pages.push(Instructions(maybeGetActiveCss(routePage, 'instructions')));
+ pages.push(Configurations(maybeGetActiveCss(routePage, 'config')));
+
+ const settingsSections = new Map([
+ ['cpu', CpuSettings],
+ ['gpu', GpuSettings],
+ ['power', PowerSettings],
+ ['memory', MemorySettings],
+ ['android', AndroidSettings],
+ ['advanced', AdvancedSettings],
+ // TODO(octaviant): Add Chrome settings.
+ ]);
+ for (const [section, component] of settingsSections.entries()) {
+ pages.push(m(component, {
+ dataSources: [],
+ cssClass: maybeGetActiveCss(routePage, section),
+ } as RecordingSectionAttrs));
}
+
components.push(m('.record-container-content', pages));
return m('.record-container', components);
}
diff --git a/ui/src/frontend/recording/advanced_settings.ts b/ui/src/frontend/recording/advanced_settings.ts
new file mode 100644
index 0000000..f04ec38
--- /dev/null
+++ b/ui/src/frontend/recording/advanced_settings.ts
@@ -0,0 +1,110 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as m from 'mithril';
+
+import {
+ Dropdown,
+ DropdownAttrs,
+ Probe,
+ ProbeAttrs,
+ Slider,
+ SliderAttrs,
+ Textarea,
+ TextareaAttrs,
+ Toggle,
+ ToggleAttrs,
+} from '../record_widgets';
+import {RecordingSectionAttrs} from './recording_sections';
+
+const FTRACE_CATEGORIES = new Map<string, string>();
+FTRACE_CATEGORIES.set('binder/*', 'binder');
+FTRACE_CATEGORIES.set('block/*', 'block');
+FTRACE_CATEGORIES.set('clk/*', 'clk');
+FTRACE_CATEGORIES.set('ext4/*', 'ext4');
+FTRACE_CATEGORIES.set('f2fs/*', 'f2fs');
+FTRACE_CATEGORIES.set('i2c/*', 'i2c');
+FTRACE_CATEGORIES.set('irq/*', 'irq');
+FTRACE_CATEGORIES.set('kmem/*', 'kmem');
+FTRACE_CATEGORIES.set('memory_bus/*', 'memory_bus');
+FTRACE_CATEGORIES.set('mmc/*', 'mmc');
+FTRACE_CATEGORIES.set('oom/*', 'oom');
+FTRACE_CATEGORIES.set('power/*', 'power');
+FTRACE_CATEGORIES.set('regulator/*', 'regulator');
+FTRACE_CATEGORIES.set('sched/*', 'sched');
+FTRACE_CATEGORIES.set('sync/*', 'sync');
+FTRACE_CATEGORIES.set('task/*', 'task');
+FTRACE_CATEGORIES.set('task/*', 'task');
+FTRACE_CATEGORIES.set('vmscan/*', 'vmscan');
+FTRACE_CATEGORIES.set('fastrpc/*', 'fastrpc');
+
+export class AdvancedSettings implements
+ m.ClassComponent<RecordingSectionAttrs> {
+ view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+ return m(
+ `.record-section${attrs.cssClass}`,
+ m(Probe,
+ {
+ title: 'Advanced ftrace config',
+ img: 'rec_ftrace.png',
+ descr:
+ `Enable individual events and tune the kernel-tracing (ftrace)
+ module. The events enabled here are in addition to those from
+ enabled by other probes.`,
+ setEnabled: (cfg, val) => cfg.ftrace = val,
+ isEnabled: (cfg) => cfg.ftrace,
+ } as ProbeAttrs,
+ m(Toggle, {
+ title: 'Resolve kernel symbols',
+ cssClass: '.thin',
+ 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,
+ } as ToggleAttrs),
+ m(Slider, {
+ title: 'Buf size',
+ cssClass: '.thin',
+ values: [0, 512, 1024, 2 * 1024, 4 * 1024, 16 * 1024, 32 * 1024],
+ unit: 'KB',
+ zeroIsDefault: true,
+ set: (cfg, val) => cfg.ftraceBufferSizeKb = val,
+ get: (cfg) => cfg.ftraceBufferSizeKb,
+ } as SliderAttrs),
+ m(Slider, {
+ title: 'Drain rate',
+ cssClass: '.thin',
+ values: [0, 100, 250, 500, 1000, 2500, 5000],
+ unit: 'ms',
+ zeroIsDefault: true,
+ set: (cfg, val) => cfg.ftraceDrainPeriodMs = val,
+ get: (cfg) => cfg.ftraceDrainPeriodMs,
+ } as SliderAttrs),
+ m(Dropdown, {
+ title: 'Event groups',
+ cssClass: '.multicolumn.ftrace-events',
+ options: FTRACE_CATEGORIES,
+ set: (cfg, val) => cfg.ftraceEvents = val,
+ get: (cfg) => cfg.ftraceEvents,
+ } as DropdownAttrs),
+ m(Textarea, {
+ placeholder: 'Add extra events, one per line, e.g.:\n' +
+ 'sched/sched_switch\n' +
+ 'kmem/*',
+ set: (cfg, val) => cfg.ftraceExtraEvents = val,
+ get: (cfg) => cfg.ftraceExtraEvents,
+ } as TextareaAttrs)));
+ }
+}
diff --git a/ui/src/frontend/recording/android_settings.ts b/ui/src/frontend/recording/android_settings.ts
new file mode 100644
index 0000000..f0b01fd
--- /dev/null
+++ b/ui/src/frontend/recording/android_settings.ts
@@ -0,0 +1,139 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as m from 'mithril';
+import {globals} from '../globals';
+import {
+ Dropdown,
+ DropdownAttrs,
+ Probe,
+ ProbeAttrs,
+ Textarea,
+ TextareaAttrs,
+ Toggle,
+ ToggleAttrs,
+} from '../record_widgets';
+import {RecordingSectionAttrs} from './recording_sections';
+
+const LOG_BUFFERS = new Map<string, string>();
+LOG_BUFFERS.set('LID_CRASH', 'Crash');
+LOG_BUFFERS.set('LID_DEFAULT', 'Main');
+LOG_BUFFERS.set('LID_EVENTS', 'Binary events');
+LOG_BUFFERS.set('LID_KERNEL', 'Kernel');
+LOG_BUFFERS.set('LID_RADIO', 'Radio');
+LOG_BUFFERS.set('LID_SECURITY', 'Security');
+LOG_BUFFERS.set('LID_STATS', 'Stats');
+LOG_BUFFERS.set('LID_SYSTEM', 'System');
+
+const ATRACE_CATEGORIES = new Map<string, string>();
+ATRACE_CATEGORIES.set('adb', 'ADB');
+ATRACE_CATEGORIES.set('aidl', 'AIDL calls');
+ATRACE_CATEGORIES.set('am', 'Activity Manager');
+ATRACE_CATEGORIES.set('audio', 'Audio');
+ATRACE_CATEGORIES.set('binder_driver', 'Binder Kernel driver');
+ATRACE_CATEGORIES.set('binder_lock', 'Binder global lock trace');
+ATRACE_CATEGORIES.set('bionic', 'Bionic C library');
+ATRACE_CATEGORIES.set('camera', 'Camera');
+ATRACE_CATEGORIES.set('dalvik', 'ART & Dalvik');
+ATRACE_CATEGORIES.set('database', 'Database');
+ATRACE_CATEGORIES.set('gfx', 'Graphics');
+ATRACE_CATEGORIES.set('hal', 'Hardware Modules');
+ATRACE_CATEGORIES.set('input', 'Input');
+ATRACE_CATEGORIES.set('network', 'Network');
+ATRACE_CATEGORIES.set('nnapi', 'Neural Network API');
+ATRACE_CATEGORIES.set('pm', 'Package Manager');
+ATRACE_CATEGORIES.set('power', 'Power Management');
+ATRACE_CATEGORIES.set('res', 'Resource Loading');
+ATRACE_CATEGORIES.set('rro', 'Resource Overlay');
+ATRACE_CATEGORIES.set('rs', 'RenderScript');
+ATRACE_CATEGORIES.set('sm', 'Sync Manager');
+ATRACE_CATEGORIES.set('ss', 'System Server');
+ATRACE_CATEGORIES.set('vibrator', 'Vibrator');
+ATRACE_CATEGORIES.set('video', 'Video');
+ATRACE_CATEGORIES.set('view', 'View System');
+ATRACE_CATEGORIES.set('webview', 'WebView');
+ATRACE_CATEGORIES.set('wm', 'Window Manager');
+
+class AtraceAppsList implements m.ClassComponent {
+ view() {
+ if (globals.state.recordConfig.allAtraceApps) {
+ return m('div');
+ }
+
+ return m(Textarea, {
+ placeholder: 'Apps to profile, one per line, e.g.:\n' +
+ 'com.android.phone\n' +
+ 'lmkd\n' +
+ 'com.android.nfc',
+ cssClass: '.atrace-apps-list',
+ set: (cfg, val) => cfg.atraceApps = val,
+ get: (cfg) => cfg.atraceApps,
+ } as TextareaAttrs);
+ }
+}
+
+export class AndroidSettings implements
+ m.ClassComponent<RecordingSectionAttrs> {
+ view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+ return m(
+ `.record-section${attrs.cssClass}`,
+ m(Probe,
+ {
+ title: 'Atrace userspace annotations',
+ img: 'rec_atrace.png',
+ descr: `Enables C++ / Java codebase annotations (ATRACE_BEGIN() /
+ os.Trace())`,
+ setEnabled: (cfg, val) => cfg.atrace = val,
+ isEnabled: (cfg) => cfg.atrace,
+ } as ProbeAttrs,
+ m(Dropdown, {
+ title: 'Categories',
+ cssClass: '.multicolumn.atrace-categories',
+ options: ATRACE_CATEGORIES,
+ set: (cfg, val) => cfg.atraceCats = val,
+ get: (cfg) => cfg.atraceCats,
+ } as DropdownAttrs),
+ m(Toggle, {
+ title: 'Record events from all Android apps and services',
+ descr: '',
+ setEnabled: (cfg, val) => cfg.allAtraceApps = val,
+ isEnabled: (cfg) => cfg.allAtraceApps,
+ } as ToggleAttrs),
+ m(AtraceAppsList)),
+ m(Probe,
+ {
+ title: 'Event log (logcat)',
+ img: 'rec_logcat.png',
+ descr: `Streams the event log into the trace. If no buffer filter is
+ specified, all buffers are selected.`,
+ setEnabled: (cfg, val) => cfg.androidLogs = val,
+ isEnabled: (cfg) => cfg.androidLogs,
+ } as ProbeAttrs,
+ m(Dropdown, {
+ title: 'Buffers',
+ cssClass: '.multicolumn',
+ options: LOG_BUFFERS,
+ set: (cfg, val) => cfg.androidLogBuffers = val,
+ get: (cfg) => cfg.androidLogBuffers,
+ } as DropdownAttrs)),
+ m(Probe, {
+ title: 'Frame timeline',
+ img: 'rec_frame_timeline.png',
+ descr: `Records expected/actual frame timings from surface_flinger.
+ Requires Android 12 (S) or above.`,
+ setEnabled: (cfg, val) => cfg.androidFrameTimeline = val,
+ isEnabled: (cfg) => cfg.androidFrameTimeline,
+ } as ProbeAttrs));
+ }
+}
diff --git a/ui/src/frontend/recording/chrome_settings.ts b/ui/src/frontend/recording/chrome_settings.ts
new file mode 100644
index 0000000..3dbd2a2
--- /dev/null
+++ b/ui/src/frontend/recording/chrome_settings.ts
@@ -0,0 +1,107 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as m from 'mithril';
+
+import {getBuiltinChromeCategoryList, isChromeTarget} from '../../common/state';
+import {globals} from '../globals';
+import {CategoriesCheckboxList, CompactProbe} from '../record_widgets';
+
+import {RecordingSectionAttrs} from './recording_sections';
+
+class ChromeCategoriesSelection implements m.ClassComponent {
+ view() {
+ // If we are attempting to record via the Chrome extension, we receive the
+ // list of actually supported categories via DevTools. Otherwise, we fall
+ // back to an integrated list of categories from a recent version of Chrome.
+ let categories = globals.state.chromeCategories;
+ if (!categories || !isChromeTarget(globals.state.recordingTarget)) {
+ categories = getBuiltinChromeCategoryList();
+ }
+
+ const defaultCategories = new Map<string, string>();
+ const disabledByDefaultCategories = new Map<string, string>();
+ const disabledPrefix = 'disabled-by-default-';
+ categories.forEach((cat) => {
+ if (cat.startsWith(disabledPrefix)) {
+ disabledByDefaultCategories.set(cat, cat.replace(disabledPrefix, ''));
+ } else {
+ defaultCategories.set(cat, cat);
+ }
+ });
+
+ return m(
+ '.chrome-categories',
+ m(CategoriesCheckboxList, {
+ categories: defaultCategories,
+ title: 'Additional categories',
+ get: (cfg) => cfg.chromeCategoriesSelected,
+ set: (cfg, val) => cfg.chromeCategoriesSelected = val,
+ }),
+ m(CategoriesCheckboxList, {
+ categories: disabledByDefaultCategories,
+ title: 'High overhead categories',
+ get: (cfg) => cfg.chromeHighOverheadCategoriesSelected,
+ set: (cfg, val) => cfg.chromeHighOverheadCategoriesSelected = val,
+ }));
+ }
+}
+
+export class ChromeSettings implements m.ClassComponent<RecordingSectionAttrs> {
+ view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+ return m(
+ `.record-section${attrs.cssClass}`,
+ CompactProbe({
+ title: 'Task scheduling',
+ setEnabled: (cfg, val) => cfg.taskScheduling = val,
+ isEnabled: (cfg) => cfg.taskScheduling,
+ }),
+ CompactProbe({
+ title: 'IPC flows',
+ setEnabled: (cfg, val) => cfg.ipcFlows = val,
+ isEnabled: (cfg) => cfg.ipcFlows,
+ }),
+ CompactProbe({
+ title: 'Javascript execution',
+ setEnabled: (cfg, val) => cfg.jsExecution = val,
+ isEnabled: (cfg) => cfg.jsExecution,
+ }),
+ CompactProbe({
+ title: 'Web content rendering, layout and compositing',
+ setEnabled: (cfg, val) => cfg.webContentRendering = val,
+ isEnabled: (cfg) => cfg.webContentRendering,
+ }),
+ CompactProbe({
+ title: 'UI rendering & surface compositing',
+ setEnabled: (cfg, val) => cfg.uiRendering = val,
+ isEnabled: (cfg) => cfg.uiRendering,
+ }),
+ CompactProbe({
+ title: 'Input events',
+ setEnabled: (cfg, val) => cfg.inputEvents = val,
+ isEnabled: (cfg) => cfg.inputEvents,
+ }),
+ CompactProbe({
+ title: 'Navigation & Loading',
+ setEnabled: (cfg, val) => cfg.navigationAndLoading = val,
+ isEnabled: (cfg) => cfg.navigationAndLoading,
+ }),
+ CompactProbe({
+ title: 'Chrome Logs',
+ setEnabled: (cfg, val) => cfg.chromeLogs = val,
+ isEnabled: (cfg) => cfg.chromeLogs,
+ }),
+ m(ChromeCategoriesSelection));
+ }
+}
diff --git a/ui/src/frontend/recording/cpu_settings.ts b/ui/src/frontend/recording/cpu_settings.ts
new file mode 100644
index 0000000..9151f4f
--- /dev/null
+++ b/ui/src/frontend/recording/cpu_settings.ts
@@ -0,0 +1,64 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as m from 'mithril';
+
+import {Probe, ProbeAttrs, Slider, SliderAttrs} from '../record_widgets';
+import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
+
+export class CpuSettings implements m.ClassComponent<RecordingSectionAttrs> {
+ view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+ return m(
+ `.record-section${attrs.cssClass}`,
+ m(Probe,
+ {
+ title: 'Coarse CPU usage counter',
+ img: 'rec_cpu_coarse.png',
+ descr: `Lightweight polling of CPU usage counters via /proc/stat.
+ Allows to periodically monitor CPU usage.`,
+ setEnabled: (cfg, val) => cfg.cpuCoarse = val,
+ isEnabled: (cfg) => cfg.cpuCoarse,
+ } as ProbeAttrs,
+ m(Slider, {
+ title: 'Poll interval',
+ cssClass: '.thin',
+ values: POLL_INTERVAL_MS,
+ unit: 'ms',
+ set: (cfg, val) => cfg.cpuCoarsePollMs = val,
+ get: (cfg) => cfg.cpuCoarsePollMs,
+ } as SliderAttrs)),
+ m(Probe, {
+ title: 'Scheduling details',
+ img: 'rec_cpu_fine.png',
+ descr: 'Enables high-detailed tracking of scheduling events',
+ setEnabled: (cfg, val) => cfg.cpuSched = val,
+ isEnabled: (cfg) => cfg.cpuSched,
+ } as ProbeAttrs),
+ m(Probe, {
+ title: 'CPU frequency and idle states',
+ img: 'rec_cpu_freq.png',
+ descr: 'Records cpu frequency and idle state changes via ftrace',
+ setEnabled: (cfg, val) => cfg.cpuFreq = val,
+ isEnabled: (cfg) => cfg.cpuFreq,
+ } as ProbeAttrs),
+ m(Probe, {
+ title: 'Syscalls',
+ img: 'rec_syscalls.png',
+ descr: `Tracks the enter and exit of all syscalls. On Android
+ requires a userdebug or eng build.`,
+ setEnabled: (cfg, val) => cfg.cpuSyscall = val,
+ isEnabled: (cfg) => cfg.cpuSyscall,
+ } as ProbeAttrs));
+ }
+}
diff --git a/ui/src/frontend/recording/gpu_settings.ts b/ui/src/frontend/recording/gpu_settings.ts
new file mode 100644
index 0000000..134c5a1
--- /dev/null
+++ b/ui/src/frontend/recording/gpu_settings.ts
@@ -0,0 +1,41 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as m from 'mithril';
+
+import {Probe, ProbeAttrs} from '../record_widgets';
+import {RecordingSectionAttrs} from './recording_sections';
+
+export class GpuSettings implements m.ClassComponent<RecordingSectionAttrs> {
+ view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+ return m(
+ `.record-section${attrs.cssClass}`,
+ m(Probe, {
+ title: 'GPU frequency',
+ img: 'rec_cpu_freq.png',
+ descr: 'Records gpu frequency via ftrace',
+ setEnabled: (cfg, val) => cfg.gpuFreq = val,
+ isEnabled: (cfg) => cfg.gpuFreq,
+ } as ProbeAttrs),
+ m(Probe, {
+ title: 'GPU memory',
+ img: 'rec_gpu_mem_total.png',
+ descr:
+ `Allows to track per process and global total GPU memory usages.
+ (Available on recent Android 12+ kernels)`,
+ setEnabled: (cfg, val) => cfg.gpuMemTotal = val,
+ isEnabled: (cfg) => cfg.gpuMemTotal,
+ } as ProbeAttrs));
+ }
+}
diff --git a/ui/src/frontend/recording/memory_settings.ts b/ui/src/frontend/recording/memory_settings.ts
new file mode 100644
index 0000000..db03f37
--- /dev/null
+++ b/ui/src/frontend/recording/memory_settings.ts
@@ -0,0 +1,328 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as m from 'mithril';
+
+import {MeminfoCounters, VmstatCounters} from '../../common/protos';
+import {globals} from '../globals';
+import {
+ Dropdown,
+ DropdownAttrs,
+ Probe,
+ ProbeAttrs,
+ Slider,
+ SliderAttrs,
+ Textarea,
+ TextareaAttrs,
+ Toggle,
+ ToggleAttrs,
+} from '../record_widgets';
+
+import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
+
+class HeapSettings implements m.ClassComponent<RecordingSectionAttrs> {
+ view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+ const valuesForMS = [
+ 0,
+ 1000,
+ 10 * 1000,
+ 30 * 1000,
+ 60 * 1000,
+ 5 * 60 * 1000,
+ 10 * 60 * 1000,
+ 30 * 60 * 1000,
+ 60 * 60 * 1000,
+ ];
+ const valuesForShMemBuff = [
+ 0,
+ 512,
+ 1024,
+ 2 * 1024,
+ 4 * 1024,
+ 8 * 1024,
+ 16 * 1024,
+ 32 * 1024,
+ 64 * 1024,
+ 128 * 1024,
+ 256 * 1024,
+ 512 * 1024,
+ 1024 * 1024,
+ 64 * 1024 * 1024,
+ 128 * 1024 * 1024,
+ 256 * 1024 * 1024,
+ 512 * 1024 * 1024,
+ ];
+
+ return m(
+ `.${attrs.cssClass}`,
+ m(Textarea, {
+ title: 'Names or pids of the processes to track',
+ docsLink:
+ 'https://perfetto.dev/docs/data-sources/native-heap-profiler#heapprofd-targets',
+ placeholder: 'One per line, e.g.:\n' +
+ 'system_server\n' +
+ 'com.google.android.apps.photos\n' +
+ '1503',
+ set: (cfg, val) => cfg.hpProcesses = val,
+ get: (cfg) => cfg.hpProcesses,
+ } as TextareaAttrs),
+ m(Slider, {
+ title: 'Sampling interval',
+ cssClass: '.thin',
+ values: [
+ /* eslint-disable no-multi-spaces */
+ 0, 1, 2, 4, 8, 16, 32, 64,
+ 128, 256, 512, 1024, 2048, 4096, 8192, 16384,
+ 32768, 65536, 131072, 262144, 524288, 1048576,
+ /* eslint-enable no-multi-spaces */
+ ],
+ unit: 'B',
+ min: 0,
+ set: (cfg, val) => cfg.hpSamplingIntervalBytes = val,
+ get: (cfg) => cfg.hpSamplingIntervalBytes,
+ } as SliderAttrs),
+ m(Slider, {
+ title: 'Continuous dumps interval ',
+ description: 'Time between following dumps (0 = disabled)',
+ cssClass: '.thin',
+ values: valuesForMS,
+ unit: 'ms',
+ min: 0,
+ set: (cfg, val) => {
+ cfg.hpContinuousDumpsInterval = val;
+ },
+ get: (cfg) => cfg.hpContinuousDumpsInterval,
+ } as SliderAttrs),
+ m(Slider, {
+ title: 'Continuous dumps phase',
+ description: 'Time before first dump',
+ cssClass: `.thin${
+ globals.state.recordConfig.hpContinuousDumpsInterval === 0 ?
+ '.greyed-out' :
+ ''}`,
+ values: valuesForMS,
+ unit: 'ms',
+ min: 0,
+ disabled: globals.state.recordConfig.hpContinuousDumpsInterval === 0,
+ set: (cfg, val) => cfg.hpContinuousDumpsPhase = val,
+ get: (cfg) => cfg.hpContinuousDumpsPhase,
+ } as SliderAttrs),
+ m(Slider, {
+ title: `Shared memory buffer`,
+ cssClass: '.thin',
+ values: valuesForShMemBuff.filter(
+ (value) => value === 0 || value >= 8192 && value % 4096 === 0),
+ unit: 'B',
+ min: 0,
+ set: (cfg, val) => cfg.hpSharedMemoryBuffer = val,
+ get: (cfg) => cfg.hpSharedMemoryBuffer,
+ } as SliderAttrs),
+ m(Toggle, {
+ title: 'Block client',
+ cssClass: '.thin',
+ descr: `Slow down target application if profiler cannot keep up.`,
+ setEnabled: (cfg, val) => cfg.hpBlockClient = val,
+ isEnabled: (cfg) => cfg.hpBlockClient,
+ } as ToggleAttrs),
+ m(Toggle, {
+ title: 'All custom allocators (Q+)',
+ cssClass: '.thin',
+ descr: `If the target application exposes custom allocators, also
+sample from those.`,
+ setEnabled: (cfg, val) => cfg.hpAllHeaps = val,
+ isEnabled: (cfg) => cfg.hpAllHeaps,
+ } as ToggleAttrs),
+ // TODO(hjd): Add advanced options.
+ );
+ }
+}
+
+class JavaHeapDumpSettings implements m.ClassComponent<RecordingSectionAttrs> {
+ view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+ const valuesForMS = [
+ 0,
+ 1000,
+ 10 * 1000,
+ 30 * 1000,
+ 60 * 1000,
+ 5 * 60 * 1000,
+ 10 * 60 * 1000,
+ 30 * 60 * 1000,
+ 60 * 60 * 1000,
+ ];
+
+ return m(
+ `.${attrs.cssClass}`,
+ m(Textarea, {
+ title: 'Names or pids of the processes to track',
+ placeholder: 'One per line, e.g.:\n' +
+ 'com.android.vending\n' +
+ '1503',
+ set: (cfg, val) => cfg.jpProcesses = val,
+ get: (cfg) => cfg.jpProcesses,
+ } as TextareaAttrs),
+ m(Slider, {
+ title: 'Continuous dumps interval ',
+ description: 'Time between following dumps (0 = disabled)',
+ cssClass: '.thin',
+ values: valuesForMS,
+ unit: 'ms',
+ min: 0,
+ set: (cfg, val) => {
+ cfg.jpContinuousDumpsInterval = val;
+ },
+ get: (cfg) => cfg.jpContinuousDumpsInterval,
+ } as SliderAttrs),
+ m(Slider, {
+ title: 'Continuous dumps phase',
+ description: 'Time before first dump',
+ cssClass: `.thin${
+ globals.state.recordConfig.jpContinuousDumpsInterval === 0 ?
+ '.greyed-out' :
+ ''}`,
+ values: valuesForMS,
+ unit: 'ms',
+ min: 0,
+ disabled: globals.state.recordConfig.jpContinuousDumpsInterval === 0,
+ set: (cfg, val) => cfg.jpContinuousDumpsPhase = val,
+ get: (cfg) => cfg.jpContinuousDumpsPhase,
+ } as SliderAttrs),
+ );
+ }
+}
+
+export class MemorySettings implements m.ClassComponent<RecordingSectionAttrs> {
+ view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+ const meminfoOpts = new Map<string, string>();
+ for (const x in MeminfoCounters) {
+ if (typeof MeminfoCounters[x] === 'number' &&
+ !`${x}`.endsWith('_UNSPECIFIED')) {
+ meminfoOpts.set(x, x.replace('MEMINFO_', '').toLowerCase());
+ }
+ }
+ const vmstatOpts = new Map<string, string>();
+ for (const x in VmstatCounters) {
+ if (typeof VmstatCounters[x] === 'number' &&
+ !`${x}`.endsWith('_UNSPECIFIED')) {
+ vmstatOpts.set(x, x.replace('VMSTAT_', '').toLowerCase());
+ }
+ }
+ return m(
+ `.record-section${attrs.cssClass}`,
+ m(Probe,
+ {
+ title: 'Native heap profiling',
+ img: 'rec_native_heap_profiler.png',
+ descr: `Track native heap allocations & deallocations of an Android
+ process. (Available on Android 10+)`,
+ setEnabled: (cfg, val) => cfg.heapProfiling = val,
+ isEnabled: (cfg) => cfg.heapProfiling,
+ } as ProbeAttrs,
+ m(HeapSettings, attrs)),
+ m(Probe,
+ {
+ title: 'Java heap dumps',
+ img: 'rec_java_heap_dump.png',
+ descr: `Dump information about the Java object graph of an
+ Android app. (Available on Android 11+)`,
+ setEnabled: (cfg, val) => cfg.javaHeapDump = val,
+ isEnabled: (cfg) => cfg.javaHeapDump,
+ } as ProbeAttrs,
+ m(JavaHeapDumpSettings, attrs)),
+ m(Probe,
+ {
+ title: 'Kernel meminfo',
+ img: 'rec_meminfo.png',
+ descr: 'Polling of /proc/meminfo',
+ setEnabled: (cfg, val) => cfg.meminfo = val,
+ isEnabled: (cfg) => cfg.meminfo,
+ } as ProbeAttrs,
+ m(Slider, {
+ title: 'Poll interval',
+ cssClass: '.thin',
+ values: POLL_INTERVAL_MS,
+ unit: 'ms',
+ set: (cfg, val) => cfg.meminfoPeriodMs = val,
+ get: (cfg) => cfg.meminfoPeriodMs,
+ } as SliderAttrs),
+ m(Dropdown, {
+ title: 'Select counters',
+ cssClass: '.multicolumn',
+ options: meminfoOpts,
+ set: (cfg, val) => cfg.meminfoCounters = val,
+ get: (cfg) => cfg.meminfoCounters,
+ } as DropdownAttrs)),
+ m(Probe, {
+ title: 'High-frequency memory events',
+ img: 'rec_mem_hifreq.png',
+ descr: `Allows to track short memory spikes and transitories through
+ ftrace's mm_event, rss_stat and ion events. Available only
+ on recent Android Q+ kernels`,
+ setEnabled: (cfg, val) => cfg.memHiFreq = val,
+ isEnabled: (cfg) => cfg.memHiFreq,
+ } as ProbeAttrs),
+ m(Probe, {
+ title: 'Low memory killer',
+ img: 'rec_lmk.png',
+ descr: `Record LMK events. Works both with the old in-kernel LMK
+ and the newer userspace lmkd. It also tracks OOM score
+ adjustments.`,
+ setEnabled: (cfg, val) => cfg.memLmk = val,
+ isEnabled: (cfg) => cfg.memLmk,
+ } as ProbeAttrs),
+ m(Probe,
+ {
+ title: 'Per process stats',
+ img: 'rec_ps_stats.png',
+ descr: `Periodically samples all processes in the system tracking:
+ their thread list, memory counters (RSS, swap and other
+ /proc/status counters) and oom_score_adj.`,
+ setEnabled: (cfg, val) => cfg.procStats = val,
+ isEnabled: (cfg) => cfg.procStats,
+ } as ProbeAttrs,
+ m(Slider, {
+ title: 'Poll interval',
+ cssClass: '.thin',
+ values: POLL_INTERVAL_MS,
+ unit: 'ms',
+ set: (cfg, val) => cfg.procStatsPeriodMs = val,
+ get: (cfg) => cfg.procStatsPeriodMs,
+ } as SliderAttrs)),
+ m(Probe,
+ {
+ title: 'Virtual memory stats',
+ img: 'rec_vmstat.png',
+ descr: `Periodically polls virtual memory stats from /proc/vmstat.
+ Allows to gather statistics about swap, eviction,
+ compression and pagecache efficiency`,
+ setEnabled: (cfg, val) => cfg.vmstat = val,
+ isEnabled: (cfg) => cfg.vmstat,
+ } as ProbeAttrs,
+ m(Slider, {
+ title: 'Poll interval',
+ cssClass: '.thin',
+ values: POLL_INTERVAL_MS,
+ unit: 'ms',
+ set: (cfg, val) => cfg.vmstatPeriodMs = val,
+ get: (cfg) => cfg.vmstatPeriodMs,
+ } as SliderAttrs),
+ m(Dropdown, {
+ title: 'Select counters',
+ cssClass: '.multicolumn',
+ options: vmstatOpts,
+ set: (cfg, val) => cfg.vmstatCounters = val,
+ get: (cfg) => cfg.vmstatCounters,
+ } as DropdownAttrs)));
+ }
+}
diff --git a/ui/src/frontend/recording/power_settings.ts b/ui/src/frontend/recording/power_settings.ts
new file mode 100644
index 0000000..bd8bfe1
--- /dev/null
+++ b/ui/src/frontend/recording/power_settings.ts
@@ -0,0 +1,69 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as m from 'mithril';
+
+import {globals} from '../globals';
+import {Probe, ProbeAttrs, Slider, SliderAttrs} from '../record_widgets';
+import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
+
+export class PowerSettings implements m.ClassComponent<RecordingSectionAttrs> {
+ view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+ const DOC_URL = 'https://perfetto.dev/docs/data-sources/battery-counters';
+ const descr =
+ [m('div',
+ m('span', `Polls charge counters and instantaneous power draw from
+ the battery power management IC and the power rails from
+ the PowerStats HAL (`),
+ m('a', {href: DOC_URL, target: '_blank'}, 'see docs for more'),
+ m('span', ')'))];
+ if (globals.isInternalUser) {
+ descr.push(m(
+ 'div',
+ m('span', 'Googlers: See '),
+ m('a',
+ {href: 'http://go/power-rails-internal-doc', target: '_blank'},
+ 'this doc'),
+ m('span',
+ ` for instructions on how to change the refault rail selection
+ on internal devices.`),
+ ));
+ }
+ return m(
+ `.record-section${attrs.cssClass}`,
+ m(Probe,
+ {
+ title: 'Battery drain & power rails',
+ img: 'rec_battery_counters.png',
+ descr,
+ setEnabled: (cfg, val) => cfg.batteryDrain = val,
+ isEnabled: (cfg) => cfg.batteryDrain,
+ } as ProbeAttrs,
+ m(Slider, {
+ title: 'Poll interval',
+ cssClass: '.thin',
+ values: POLL_INTERVAL_MS,
+ unit: 'ms',
+ set: (cfg, val) => cfg.batteryDrainPollMs = val,
+ get: (cfg) => cfg.batteryDrainPollMs,
+ } as SliderAttrs)),
+ m(Probe, {
+ title: 'Board voltages & frequencies',
+ img: 'rec_board_voltage.png',
+ descr: 'Tracks voltage and frequency changes from board sensors',
+ setEnabled: (cfg, val) => cfg.boardSensors = val,
+ isEnabled: (cfg) => cfg.boardSensors,
+ } as ProbeAttrs));
+ }
+}
diff --git a/ui/src/frontend/recording/recording_sections.ts b/ui/src/frontend/recording/recording_sections.ts
new file mode 100644
index 0000000..f0e3fa1
--- /dev/null
+++ b/ui/src/frontend/recording/recording_sections.ts
@@ -0,0 +1,22 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {DataSource} from '../../common/recordingV2/recording_interfaces_v2';
+
+export interface RecordingSectionAttrs {
+ dataSources: DataSource[];
+ cssClass: string;
+}
+
+export const POLL_INTERVAL_MS = [250, 500, 1000, 2500, 5000, 30000, 60000];
diff --git a/ui/src/frontend/recording/recording_settings.ts b/ui/src/frontend/recording/recording_settings.ts
new file mode 100644
index 0000000..900fe44
--- /dev/null
+++ b/ui/src/frontend/recording/recording_settings.ts
@@ -0,0 +1,98 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {produce} from 'immer';
+import * as m from 'mithril';
+
+import {Actions} from '../../common/actions';
+import {RecordMode} from '../../common/state';
+import {globals} from '../globals';
+import {Slider, SliderAttrs} from '../record_widgets';
+
+import {RecordingSectionAttrs} from './recording_sections';
+
+export class RecordingSettings implements
+ m.ClassComponent<RecordingSectionAttrs> {
+ view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+ const S = (x: number) => x * 1000;
+ const M = (x: number) => x * 1000 * 60;
+ const H = (x: number) => x * 1000 * 60 * 60;
+
+ const cfg = globals.state.recordConfig;
+
+ const recButton = (mode: RecordMode, title: string, img: string) => {
+ const checkboxArgs = {
+ checked: cfg.mode === mode,
+ onchange: (e: InputEvent) => {
+ const checked = (e.target as HTMLInputElement).checked;
+ if (!checked) return;
+ const traceCfg = produce(globals.state.recordConfig, (draft) => {
+ draft.mode = mode;
+ });
+ globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
+ },
+ };
+ return m(
+ `label${cfg.mode === mode ? '.selected' : ''}`,
+ m(`input[type=radio][name=rec_mode]`, checkboxArgs),
+ m(`img[src=${globals.root}assets/${img}]`),
+ m('span', title));
+ };
+
+ return m(
+ `.record-section${attrs.cssClass}`,
+ m('header', 'Recording mode'),
+ m('.record-mode',
+ recButton('STOP_WHEN_FULL', 'Stop when full', 'rec_one_shot.png'),
+ recButton('RING_BUFFER', 'Ring buffer', 'rec_ring_buf.png'),
+ recButton('LONG_TRACE', 'Long trace', 'rec_long_trace.png')),
+
+ m(Slider, {
+ title: 'In-memory buffer size',
+ icon: '360',
+ values: [4, 8, 16, 32, 64, 128, 256, 512],
+ unit: 'MB',
+ set: (cfg, val) => cfg.bufferSizeMb = val,
+ get: (cfg) => cfg.bufferSizeMb,
+ } as SliderAttrs),
+
+ m(Slider, {
+ title: 'Max duration',
+ icon: 'timer',
+ values: [S(10), S(15), S(30), S(60), M(5), M(30), H(1), H(6), H(12)],
+ isTime: true,
+ unit: 'h:m:s',
+ set: (cfg, val) => cfg.durationMs = val,
+ get: (cfg) => cfg.durationMs,
+ } as SliderAttrs),
+ m(Slider, {
+ title: 'Max file size',
+ icon: 'save',
+ cssClass: cfg.mode !== 'LONG_TRACE' ? '.hide' : '',
+ values: [5, 25, 50, 100, 500, 1000, 1000 * 5, 1000 * 10],
+ unit: 'MB',
+ set: (cfg, val) => cfg.maxFileSizeMb = val,
+ get: (cfg) => cfg.maxFileSizeMb,
+ } as SliderAttrs),
+ m(Slider, {
+ title: 'Flush on disk every',
+ cssClass: cfg.mode !== 'LONG_TRACE' ? '.hide' : '',
+ icon: 'av_timer',
+ values: [100, 250, 500, 1000, 2500, 5000],
+ unit: 'ms',
+ set: (cfg, val) => cfg.fileWritePeriodMs = val,
+ get: (cfg) => cfg.fileWritePeriodMs || 0,
+ } as SliderAttrs));
+ }
+}
diff --git a/ui/src/test/diff_viewer/README.md b/ui/src/test/diff_viewer/README.md
new file mode 100644
index 0000000..119dd75
--- /dev/null
+++ b/ui/src/test/diff_viewer/README.md
@@ -0,0 +1,18 @@
+# CI screenshot diff viewer
+
+This directory contains the source of screenshots diff viewer used on Perfetto
+CI. The way it works as follows:
+
+When a screenshot test is failing, the testing code will write a line of the
+form
+
+```
+failed-screenshot.png;failed-screenshot-diff.png
+```
+
+To a file called `report.txt`. Diff viewer is just a static page that uses Fetch
+API to download this file, parse it, and display images in a list of rows.
+
+The page assumes `report.txt` to be present in the same directory, same goes for
+screenshot files. To simplify deployment, the viewer is developed without a
+framework and constructs DOM using `document.createElement` API.
diff --git a/ui/src/test/diff_viewer/index.html b/ui/src/test/diff_viewer/index.html
new file mode 100644
index 0000000..d4742b4
--- /dev/null
+++ b/ui/src/test/diff_viewer/index.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Diff screenshots report</title>
+ <style>
+ .row {
+ display: flex;
+ padding: 1rem;
+ border-radius: .5rem;
+ border: 1px solid black;
+ margin-bottom: 1rem;
+ }
+ .image-wrapper img {
+ max-width: 45vw;
+ }
+ </style>
+</head>
+<body>
+ <div class="container">
+ Loading...
+ </div>
+ <script src="script.js"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/ui/src/test/diff_viewer/script.js b/ui/src/test/diff_viewer/script.js
new file mode 100644
index 0000000..97a6f64
--- /dev/null
+++ b/ui/src/test/diff_viewer/script.js
@@ -0,0 +1,79 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Helper function to create DOM elements faster: takes a Mithril-style
+// "selector" of the form "tag.class1.class2" and a list of child objects that
+// can be either strings or DOM elements.
+function m(selector, ...children) {
+ const parts = selector.split('.');
+ if (parts.length === 0) {
+ throw new Error(
+ 'Selector passed to element should be of a form tag.class1.class2');
+ }
+
+ const result = document.createElement(parts[0]);
+ for (let i = 1; i < parts.length; i++) {
+ result.classList.add(parts[i]);
+ }
+ for (const child of children) {
+ if (typeof child === 'string') {
+ const childNode = document.createTextNode(child);
+ result.appendChild(childNode);
+ } else {
+ result.appendChild(child);
+ }
+ }
+ return result;
+}
+
+async function loadDiffs() {
+ // report.txt is a text file with a pair of file names on each line, separated
+ // by semicolon. E.g. "screenshot.png;screenshot-diff.png"
+ const report = await fetch('report.txt');
+ const response = await report.text();
+ console.log(response);
+
+ const container = document.querySelector('.container');
+ container.innerHTML = '';
+
+ const lines = response.split('\n');
+ for (const line of lines) {
+ const parts = line.split(';');
+ if (parts.length !== 2) {
+ console.warn(
+ `Malformed line (expected two files separated via semicolon) ${
+ line}!`);
+ continue;
+ }
+
+ const [output, diff] = parts;
+ const outputImage = m('img');
+ outputImage.src = output;
+ const diffImage = m('img');
+ diffImage.src = diff;
+
+ container.appendChild(
+ m('div.row',
+ m('div.cell', output, m('div.image-wrapper', outputImage)),
+ m('div.cell', diff, m('div.image-wrapper', diffImage))));
+ }
+
+ if (lines.length === 0) {
+ container.appendChild(m('div', 'All good!'));
+ }
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ loadDiffs();
+});
diff --git a/ui/src/test/perfetto_ui_test_helper.ts b/ui/src/test/perfetto_ui_test_helper.ts
index 44e927c..97c9bd4 100644
--- a/ui/src/test/perfetto_ui_test_helper.ts
+++ b/ui/src/test/perfetto_ui_test_helper.ts
@@ -84,7 +84,7 @@
}
export async function compareScreenshots(
- actualFilename: string, expectedFilename: string) {
+ reportPath: string, actualFilename: string, expectedFilename: string) {
if (!fs.existsSync(expectedFilename)) {
throw new Error(
`Could not find ${expectedFilename}. Run wih REBASELINE=1.`);
@@ -102,6 +102,9 @@
if (diff > DIFF_MAX_PIXELS) {
const diffFilename = actualFilename.replace('.png', '-diff.png');
fs.writeFileSync(diffFilename, PNG.sync.write(diffPng));
+ fs.appendFileSync(
+ reportPath,
+ `${path.basename(actualFilename)};${path.basename(diffFilename)}\n`);
fail(`Diff test failed on ${diffFilename}, delta: ${diff} pixels`);
}
return diff;
diff --git a/ui/src/test/ui_integrationtest.ts b/ui/src/test/ui_integrationtest.ts
index 24a6533..4794d8d 100644
--- a/ui/src/test/ui_integrationtest.ts
+++ b/ui/src/test/ui_integrationtest.ts
@@ -28,6 +28,8 @@
declare let global: {__BROWSER__: puppeteer.Browser;};
const browser = assertExists(global.__BROWSER__);
const expectedScreenshotPath = path.join('test', 'data', 'ui-screenshots');
+const tmpDir = path.resolve('./ui-test-artifacts');
+const reportPath = path.join(tmpDir, 'report.txt');
async function getPage(): Promise<puppeteer.Page> {
const pages = (await browser.pages());
@@ -41,6 +43,9 @@
jest.setTimeout(60000);
const page = await getPage();
await page.setViewport({width: 1920, height: 1080});
+
+ // Empty the file with collected screenshot diffs
+ fs.writeFileSync(reportPath, '');
});
// After each test (regardless of nesting) capture a screenshot named after the
@@ -51,10 +56,6 @@
testName = testName.replace(/[^a-z0-9-]/gmi, '_').toLowerCase();
const page = await getPage();
- // cwd() is set to //out/ui when running tests, just create a subdir in there.
- // The CI picks up this directory and uploads to GCS after every failed run.
- const tmpDir = path.resolve('./ui-test-artifacts');
- if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir);
const screenshotName = `ui-${testName}.png`;
const actualFilename = path.join(tmpDir, screenshotName);
const expectedFilename = path.join(expectedScreenshotPath, screenshotName);
@@ -64,7 +65,7 @@
console.log('Saving reference screenshot into', expectedFilename);
fs.copyFileSync(actualFilename, expectedFilename);
} else {
- await compareScreenshots(actualFilename, expectedFilename);
+ await compareScreenshots(reportPath, actualFilename, expectedFilename);
}
});