Merge "[CUI] Add SQL model and custom track for CUI page loads." into main
diff --git a/Android.bp b/Android.bp
index 1b8103f..8968afe 100644
--- a/Android.bp
+++ b/Android.bp
@@ -11881,6 +11881,7 @@
"src/trace_processor/perfetto_sql/stdlib/common/metadata.sql",
"src/trace_processor/perfetto_sql/stdlib/common/percentiles.sql",
"src/trace_processor/perfetto_sql/stdlib/common/slices.sql",
+ "src/trace_processor/perfetto_sql/stdlib/common/thread_states.sql",
"src/trace_processor/perfetto_sql/stdlib/common/timestamps.sql",
"src/trace_processor/perfetto_sql/stdlib/experimental/android_broadcast.sql",
"src/trace_processor/perfetto_sql/stdlib/experimental/flat_slices.sql",
diff --git a/BUILD b/BUILD
index 1315915..96bf810 100644
--- a/BUILD
+++ b/BUILD
@@ -2261,6 +2261,7 @@
"src/trace_processor/perfetto_sql/stdlib/common/metadata.sql",
"src/trace_processor/perfetto_sql/stdlib/common/percentiles.sql",
"src/trace_processor/perfetto_sql/stdlib/common/slices.sql",
+ "src/trace_processor/perfetto_sql/stdlib/common/thread_states.sql",
"src/trace_processor/perfetto_sql/stdlib/common/timestamps.sql",
],
)
diff --git a/include/perfetto/protozero/scattered_stream_writer.h b/include/perfetto/protozero/scattered_stream_writer.h
index 96329e9..719c490 100644
--- a/include/perfetto/protozero/scattered_stream_writer.h
+++ b/include/perfetto/protozero/scattered_stream_writer.h
@@ -22,6 +22,8 @@
#include <stdint.h>
#include <string.h>
+#include <algorithm>
+
#include "perfetto/base/compiler.h"
#include "perfetto/base/export.h"
#include "perfetto/base/logging.h"
@@ -76,13 +78,13 @@
// Assumes that the caller checked that there is enough headroom.
// TODO(primiano): perf optimization, this is a tracing hot path. The
- // compiler can make strong optimization on memcpy if the size arg is a
+ // compiler can make strong optimization on std::copy if the size arg is a
// constexpr. Make a templated variant of this for fixed-size writes.
// TODO(primiano): restrict / noalias might also help.
inline void WriteBytesUnsafe(const uint8_t* src, size_t size) {
uint8_t* const end = write_ptr_ + size;
assert(end <= cur_range_.end);
- memcpy(write_ptr_, src, size);
+ std::copy(src, src + size, write_ptr_);
write_ptr_ = end;
}
diff --git a/protos/perfetto/metrics/android/android_boot.proto b/protos/perfetto/metrics/android/android_boot.proto
index 820b4e0..53de221 100644
--- a/protos/perfetto/metrics/android/android_boot.proto
+++ b/protos/perfetto/metrics/android/android_boot.proto
@@ -29,4 +29,10 @@
optional ProcessStateDurations systemui_durations = 2;
optional ProcessStateDurations launcher_durations = 3;
optional ProcessStateDurations gms_durations = 4;
+ // Launcher related boot metrics
+ message LauncherBreakdown {
+ // reports cold start time of NexusLauncher
+ optional int64 cold_start_dur = 1;
+ }
+ optional LauncherBreakdown launcher_breakdown = 5;
}
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index 3f270f6..d072a39 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -143,6 +143,12 @@
optional ProcessStateDurations systemui_durations = 2;
optional ProcessStateDurations launcher_durations = 3;
optional ProcessStateDurations gms_durations = 4;
+ // Launcher related boot metrics
+ message LauncherBreakdown {
+ // reports cold start time of NexusLauncher
+ optional int64 cold_start_dur = 1;
+ }
+ optional LauncherBreakdown launcher_breakdown = 5;
}
// End of protos/perfetto/metrics/android/android_boot.proto
diff --git a/protos/perfetto/trace_processor/trace_processor.proto b/protos/perfetto/trace_processor/trace_processor.proto
index fbf1987..8a6f511 100644
--- a/protos/perfetto/trace_processor/trace_processor.proto
+++ b/protos/perfetto/trace_processor/trace_processor.proto
@@ -45,7 +45,8 @@
// Changes:
// 7. Introduce GUESS_CPU_SIZE
// 8. Add 'json' option to ComputeMetricArgs
- TRACE_PROCESSOR_CURRENT_API_VERSION = 8;
+ // 9. Add get_thread_state_summary_for_interval.
+ TRACE_PROCESSOR_CURRENT_API_VERSION = 9;
}
// At lowest level, the wire-format of the RPC procol is a linear sequence of
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 7af1fde..d62f1fd 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/trace_processor.descriptor b/python/perfetto/trace_processor/trace_processor.descriptor
index 33ca764..8b8383f 100644
--- a/python/perfetto/trace_processor/trace_processor.descriptor
+++ b/python/perfetto/trace_processor/trace_processor.descriptor
Binary files differ
diff --git a/python/tools/check_imports.py b/python/tools/check_imports.py
index ef4ff29..dd096fb 100755
--- a/python/tools/check_imports.py
+++ b/python/tools/check_imports.py
@@ -37,6 +37,91 @@
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
UI_SRC_DIR = os.path.join(ROOT_DIR, 'ui', 'src')
+# Current plan for the dependency tree of the UI code (2023-09-21)
+# black = current
+# red = planning to remove
+# green = planning to add
+PLAN_DOT = """
+digraph g {
+ mithril [shape=rectangle, label="mithril"];
+ protos [shape=rectangle, label="//protos/perfetto"];
+
+ _gen [shape=ellipse, label="/gen"];
+ _base [shape=ellipse, label="/base"];
+ _core [shape=ellipse, label="/core"];
+ _engine [shape=ellipse, label="/engine"];
+
+ _frontend [shape=ellipse, label="/frontend" color=red];
+ _common [shape=ellipse, label="/common" color=red];
+ _controller [shape=ellipse, label="/controller" color=red];
+ _tracks [shape=ellipse, label="/tracks" color=red];
+
+ _widgets [shape=ellipse, label="/widgets"];
+
+ _public [shape=ellipse, label="/public"];
+ _plugins [shape=ellipse, label="/plugins"];
+ _chrome_extension [shape=ellipse, label="/chrome_extension"];
+ _trace_processor [shape=ellipse, label="/trace_processor" color="green"];
+ _protos [shape=ellipse, label="/protos" color="green"];
+ engine_worker_bundle [shape=cds, label="Engine worker bundle"];
+ frontend_bundle [shape=cds, label="Frontend bundle"];
+
+ engine_worker_bundle -> _engine;
+ frontend_bundle -> _core [color=green];
+ frontend_bundle -> _frontend [color=red];
+
+ _core -> _public;
+ _plugins -> _public;
+
+ _widgets -> _base;
+ _core -> _base;
+ _core -> _widgets;
+
+
+ _widgets -> mithril;
+ _plugins -> mithril;
+ _core -> mithril
+
+ _plugins -> _widgets;
+
+ _core -> _chrome_extension;
+
+ _frontend -> _widgets [color=red];
+ _common -> _core [color=red];
+ _frontend -> _core [color=red];
+ _controller -> _core [color=red];
+
+ _frontend -> _controller [color=red];
+ _frontend -> _common [color=red];
+ _controller -> _frontend [color=red];
+ _controller -> _common [color=red];
+ _common -> _controller [color=red];
+ _common -> _frontend [color=red];
+ _tracks -> _frontend [color=red];
+ _tracks -> _controller [color=red];
+ _common -> _chrome_extension [color=red];
+
+ _core -> _trace_processor [color=green];
+
+ _engine -> _trace_processor [color=green];
+ _engine -> _common [color=red];
+ _engine -> _base;
+
+ _gen -> protos;
+ _core -> _gen [color=red];
+
+ _core -> _protos [color=green];
+ _protos -> _gen [color=green];
+ _trace_processor -> _protos [color=green];
+
+ _trace_processor -> _public [color=green];
+
+ npm_trace_processor [shape=cds, label="npm trace_processor" color="green"];
+ npm_trace_processor -> engine_worker_bundle [color="green"];
+ npm_trace_processor -> _trace_processor [color="green"];
+}
+"""
+
class Failure(object):
@@ -183,6 +268,23 @@
'chrome_extension must be a leaf',
),
+ # Widgets
+ NoDep(
+ r'/widgets/.*',
+ r'/frontend/.*',
+ 'widgets should only depend on base',
+ ),
+ NoDep(
+ r'/widgets/.*',
+ r'/core/.*',
+ 'widgets should only depend on base',
+ ),
+ NoDep(
+ r'/widgets/.*',
+ r'/plugins/.*',
+ 'widgets should only depend on base',
+ ),
+
# Fails at the moment as we have several circular dependencies. One
# example:
# ui/src/frontend/cookie_consent.ts
@@ -340,6 +442,11 @@
return 0
+def do_plan_dot(options, _):
+ print(PLAN_DOT, file=sys.stdout)
+ return 0
+
+
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.set_defaults(func=do_check)
@@ -371,6 +478,12 @@
help='Don\'t show external dependencies',
)
+ plan_dot_command = subparsers.add_parser(
+ 'plan-dot',
+ help='Output planned dependency graph in dot format suitble for use in graphviz (e.g. ./tools/check_imports plan-dot | dot -Tpng -ograph.png)'
+ )
+ plan_dot_command.set_defaults(func=do_plan_dot)
+
graph = collections.defaultdict(set)
for path in all_source_files():
for src, target in find_imports(path):
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.cc b/src/trace_processor/importers/proto/proto_trace_reader.cc
index 003f06c..8add6f0 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.cc
+++ b/src/trace_processor/importers/proto/proto_trace_reader.cc
@@ -447,6 +447,8 @@
metadata::all_data_source_started_ns, Variadic::Integer(ts));
}
if (tse.all_data_sources_flushed()) {
+ context_->metadata_tracker->AppendMetadata(
+ metadata::all_data_source_flushed_ns, Variadic::Integer(ts));
context_->sorter->NotifyFlushEvent();
}
if (tse.read_tracing_buffers_completed()) {
diff --git a/src/trace_processor/metrics/sql/android/android_boot.sql b/src/trace_processor/metrics/sql/android/android_boot.sql
index 46caea8..4dc8936 100644
--- a/src/trace_processor/metrics/sql/android/android_boot.sql
+++ b/src/trace_processor/metrics/sql/android/android_boot.sql
@@ -47,5 +47,9 @@
SELECT NULL_IF_EMPTY(ProcessStateDurations(
'total_dur', total_dur,
'uninterruptible_sleep_dur', uint_sleep_dur))
- FROM get_durations('com.google.android.gms.persistent'))
+ FROM get_durations('com.google.android.gms.persistent')),
+ 'launcher_breakdown', (
+ SELECT NULL_IF_EMPTY(AndroidBootMetric_LauncherBreakdown(
+ 'cold_start_dur', dur))
+ FROM slice where name="LauncherColdStartup")
);
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/common/BUILD.gn
index 93ffc3d..bb4d758 100644
--- a/src/trace_processor/perfetto_sql/stdlib/common/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/common/BUILD.gn
@@ -22,6 +22,7 @@
"metadata.sql",
"percentiles.sql",
"slices.sql",
+ "thread_states.sql",
"timestamps.sql",
]
}
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/thread_states.sql b/src/trace_processor/perfetto_sql/stdlib/common/thread_states.sql
new file mode 100644
index 0000000..2e9dce1
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/common/thread_states.sql
@@ -0,0 +1,111 @@
+--
+-- Copyright 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
+--
+-- https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+INCLUDE PERFETTO MODULE common.timestamps;
+INCLUDE PERFETTO MODULE common.cpus;
+
+-- TODO(altimin): this doesn't handle some corner cases which thread_state.ts
+-- handles (as complex strings manipulations in SQL are pretty painful),
+-- but they are pretty niche.
+-- Translates the thread state name from a single-letter shorthard to
+-- a human-readable name.
+CREATE PERFETTO FUNCTION internal_translate_thread_state_name(name STRING)
+RETURNS STRING AS
+SELECT CASE $name
+WHEN 'Running' THEN 'Running'
+WHEN 'R' THEN 'Runnable'
+WHEN 'R+' THEN 'Runnable (Preempted)'
+WHEN 'S' THEN 'Sleeping'
+WHEN 'D' THEN 'Uninterruptible Sleep'
+WHEN 'T' THEN 'Stopped'
+WHEN 't' THEN 'Traced'
+WHEN 'X' THEN 'Exit (Dead)'
+WHEN 'Z' THEN 'Exit (Zombie)'
+WHEN 'x' THEN 'Task Dead'
+WHEN 'I' THEN 'Idle'
+WHEN 'K' THEN 'Wakekill'
+WHEN 'W' THEN 'Waking'
+WHEN 'P' THEN 'Parked'
+WHEN 'N' THEN 'No Load'
+ELSE $name
+END;
+
+-- Returns a human-readable name for a thread state.
+-- @arg id INT Thread state id.
+-- @ret STRING Human-readable name for the thread state.
+CREATE PERFETTO FUNCTION human_readable_thread_state_name(id INT)
+RETURNS STRING AS
+WITH data AS (
+ SELECT
+ internal_translate_thread_state_name(state) AS state,
+ (CASE io_wait
+ WHEN 1 THEN ' (IO)'
+ WHEN 0 THEN ' (non-IO)'
+ ELSE ''
+ END) AS io_wait
+ FROM thread_state
+ WHERE id = $id
+)
+SELECT
+ printf('%s%s', state, io_wait)
+FROM data;
+
+-- Returns an aggregation of thread states (by state and cpu) for a given
+-- interval of time for a given thread.
+-- @arg ts INT The start of the interval.
+-- @arg dur INT The duration of the interval.
+-- @arg utid INT The utid of the thread.
+-- @column state Human-readable thread state name.
+-- @column raw_state Raw thread state name, alias of `thread_state.state`.
+-- @column cpu_type The type of CPU if available (e.g. "big" / "mid" / "little").
+-- @column cpu The CPU index.
+-- @column blocked_function The name of the kernel function execution is blocked in.
+-- @column dur The total duration.
+CREATE PERFETTO FUNCTION thread_state_summary_for_interval(
+ ts INT, dur INT, utid INT)
+RETURNS TABLE(
+ state STRING, raw_state STRING, cpu_type STRING, cpu INT, blocked_function STRING, dur INT)
+AS
+WITH
+states_starting_inside AS (
+ SELECT id
+ FROM thread_state
+ WHERE $ts <= ts
+ AND ts <= $ts + $dur
+ AND utid = $utid
+),
+first_state_starting_before AS (
+ SELECT id
+ FROM thread_state
+ WHERE ts < $ts AND utid = $utid
+ ORDER BY ts DESC
+ LIMIT 1
+),
+relevant_states AS (
+ SELECT * FROM states_starting_inside
+ UNION ALL
+ SELECT * FROM first_state_starting_before
+)
+SELECT
+ human_readable_thread_state_name(id) as state,
+ state as raw_state,
+ guess_cpu_size(cpu) as cpu_type,
+ cpu,
+ blocked_function,
+ sum(spans_overlapping_dur($ts, $dur, ts, dur)) as dur
+FROM thread_state
+JOIN relevant_states USING (id)
+GROUP BY state, raw_state, cpu_type, cpu, blocked_function
+ORDER BY dur desc;
\ No newline at end of file
diff --git a/src/trace_processor/storage/metadata.h b/src/trace_processor/storage/metadata.h
index ba2f4c0..1fa85ae 100644
--- a/src/trace_processor/storage/metadata.h
+++ b/src/trace_processor/storage/metadata.h
@@ -29,6 +29,7 @@
// Compile time list of metadata items.
// clang-format off
#define PERFETTO_TP_METADATA(F) \
+ F(all_data_source_flushed_ns, KeyType::kMulti, Variadic::kInt), \
F(all_data_source_started_ns, KeyType::kSingle, Variadic::kInt), \
F(android_build_fingerprint, KeyType::kSingle, Variadic::kString), \
F(android_sdk_version, KeyType::kSingle, Variadic::kInt), \
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
index 60017fd..3af03af 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
@@ -1 +1 @@
-d4047dbc06457be945fc612e629fca6a8ffaf15332fe76c653b1dd7a9a58d06b
\ No newline at end of file
+b51988f52a96b6576e714111c96018b8f13541a5ccd44b8bf9b1989185edd515
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
index 655b0f9..e268e28 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
@@ -1 +1 @@
-fff7ed44c6100f75f93e21bdb9dbd093a8e67274d5eee255bf96e7d26a5614ca
\ No newline at end of file
+c753a17a466814841035cd006716128187d838c6c00fb7c600f810b6b36a7be9
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
index 8febff4..a26ddd5 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
@@ -1 +1 @@
-31029a524c31322ce5c72f246de163b28047b839275a30b07061153a963a386b
\ No newline at end of file
+784304442fea618ab4ba75c16ccbc3d08fcfbe3ba19d7f7b1a470aa520d801a1
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256 b/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256
index 7a976a8..36fadce 100644
--- a/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256
@@ -1 +1 @@
-4486b3cba3f423541c1efd3f8dba5ee157e8215263c3250e8cefac6382905691
\ No newline at end of file
+ac4b7c10fb3ca19724f2f66b3b4b013cec90bac60d06d09b2bf96e30944750d8
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256
index 99e5f07..4afc696 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256
@@ -1 +1 @@
-4b96c2f13ce655e31532c21eac0652244803043ae6c56b5dcaec0e3158552256
\ No newline at end of file
+3b624767a7b2118e71fb4efa948ffe2021f1e59e30ddfadf0a07fa22203fbb67
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index d8669d6..7430548 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -87,6 +87,7 @@
from diff_tests.parser.translated_args.tests import TranslatedArgs
from diff_tests.parser.ufs.tests import Ufs
from diff_tests.stdlib.android.tests import AndroidStdlib
+from diff_tests.stdlib.common.tests import StdlibCommon
from diff_tests.stdlib.chrome.tests import ChromeStdlib
from diff_tests.stdlib.chrome.tests_chrome_interactions import ChromeInteractions
from diff_tests.stdlib.chrome.tests_scroll_jank import ChromeScrollJankStdlib
@@ -161,7 +162,8 @@
*TranslatedArgs(index_path, 'parser/translated_args',
'TranslatedArgs').fetch(),
*Ufs(index_path, 'parser/ufs', 'Ufs').fetch(),
- # TODO(altimin, lalitm): "parsing" should be split into more specific directories.
+ # TODO(altimin, lalitm): "parsing" should be split into more specific
+ # directories.
*Parsing(index_path, 'parser/parsing', 'Parsing').fetch(),
*ParsingDebugAnnotation(index_path, 'parser/parsing',
'ParsingDebugAnnotation').fetch(),
@@ -211,6 +213,7 @@
*DynamicTables(index_path, 'stdlib/dynamic_tables',
'DynamicTables').fetch(),
*Pkvm(index_path, 'stdlib/pkvm', 'Pkvm').fetch(),
+ *StdlibCommon(index_path, 'stdlib/common', 'StdlibCommon').fetch(),
*Slices(index_path, 'stdlib/slices', 'Slices').fetch(),
*SpanJoinLeftJoin(index_path, 'stdlib/span_join',
'SpanJoinLeftJoin').fetch(),
diff --git a/test/trace_processor/diff_tests/parser/parsing/tests.py b/test/trace_processor/diff_tests/parser/parsing/tests.py
index 83a986c..5261b09 100644
--- a/test/trace_processor/diff_tests/parser/parsing/tests.py
+++ b/test/trace_processor/diff_tests/parser/parsing/tests.py
@@ -1213,3 +1213,27 @@
"name","dur","tid"
"test_event",100,584
"""))
+
+ def test_all_data_source_flushed_metadata(self):
+ return DiffTestBlueprint(
+ trace=TextProto(r"""
+ packet {
+ timestamp: 12344
+ service_event {
+ all_data_sources_flushed: true
+ }
+ }
+ packet {
+ timestamp: 12345
+ service_event {
+ all_data_sources_flushed: true
+ }
+ }
+ """),
+ query="""
+ SELECT name, int_value FROM metadata WHERE name = 'all_data_source_flushed_ns'""",
+ out=Csv("""
+ "name","int_value"
+ "all_data_source_flushed_ns",12344
+ "all_data_source_flushed_ns",12345
+ """))
diff --git a/test/trace_processor/diff_tests/stdlib/common/tests.py b/test/trace_processor/diff_tests/stdlib/common/tests.py
new file mode 100644
index 0000000..a902b5b
--- /dev/null
+++ b/test/trace_processor/diff_tests/stdlib/common/tests.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a
+#
+# 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 python.generators.diff_tests.testing import Path, DataPath, Metric
+from python.generators.diff_tests.testing import Csv, Json, TextProto
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class StdlibCommon(TestSuite):
+
+ def test_thread_state_summary(self):
+ return DiffTestBlueprint(
+ trace=Path('../../common/synth_1.py'),
+ query="""
+ INCLUDE PERFETTO MODULE common.thread_states;
+
+ SELECT
+ state,
+ cpu,
+ dur
+ FROM thread_state_summary_for_interval(
+ 25,
+ 75,
+ (
+ SELECT utid
+ FROM thread
+ WHERE name = 'init'
+ )
+ )
+ """,
+ out=Csv("""
+ "state","cpu","dur"
+ "Running",1,50
+ "Runnable","[NULL]",25
+ """))
\ No newline at end of file
diff --git a/ui/src/base/mithril_utils.ts b/ui/src/base/mithril_utils.ts
index 854221f..1f07578 100644
--- a/ui/src/base/mithril_utils.ts
+++ b/ui/src/base/mithril_utils.ts
@@ -14,10 +14,8 @@
import m from 'mithril';
-import {exists} from './utils';
-
// Check if a mithril component vnode has children
export function hasChildren({children}: m.Vnode<any>): boolean {
return Array.isArray(children) && children.length > 0 &&
- children.some(exists);
+ children.some((value) => value);
}
diff --git a/ui/src/base/time.ts b/ui/src/base/time.ts
index 528fdbf..c8e5837 100644
--- a/ui/src/base/time.ts
+++ b/ui/src/base/time.ts
@@ -319,6 +319,10 @@
this.end = end;
}
+ static fromTimeAndDuration(start: time, duration: duration): TimeSpan {
+ return new TimeSpan(start, Time.add(start, duration));
+ }
+
get duration(): duration {
return this.end - this.start;
}
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index a298b36..881cb37 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -15,11 +15,12 @@
import m from 'mithril';
import {Icons} from '../base/semantic_icons';
-import {duration, Time} from '../base/time';
+import {duration, Time, TimeSpan} from '../base/time';
import {exists} from '../base/utils';
import {EngineProxy} from '../common/engine';
import {runQuery} from '../common/queries';
import {LONG, LONG_NULL, NUM, STR_NULL} from '../common/query_result';
+import {raf} from '../core/raf_scheduler';
import {addDebugSliceTrack} from '../tracks/debug/slice_track';
import {Button} from '../widgets/button';
import {DetailsShell} from '../widgets/details_shell';
@@ -39,6 +40,10 @@
import {renderArguments} from './slice_args';
import {renderDetails} from './slice_details';
import {getSlice, SliceDetails, SliceRef} from './sql/slice';
+import {
+ BreakdownByThreadState,
+ breakDownIntervalByThreadState,
+} from './sql/thread_state';
import {asSliceSqlId} from './sql_types';
interface ContextMenuItem {
@@ -226,6 +231,7 @@
static readonly kind = 'dev.perfetto.ChromeSliceDetailsTab';
private sliceDetails?: SliceDetails;
+ private breakdownByThreadState?: BreakdownByThreadState;
static create(args: NewBottomTabArgs): ChromeSliceDetailsTab {
return new ChromeSliceDetailsTab(args);
@@ -233,11 +239,23 @@
constructor(args: NewBottomTabArgs) {
super(args);
+ this.load();
+ }
+ async load() {
// Start loading the slice details
const {id, table} = this.config;
- getSliceDetails(this.engine, id, table)
- .then((sliceDetails) => this.sliceDetails = sliceDetails);
+ const details = await getSliceDetails(this.engine, id, table);
+
+ if (details !== undefined && details.thread !== undefined) {
+ this.breakdownByThreadState = await breakDownIntervalByThreadState(
+ this.engine,
+ TimeSpan.fromTimeAndDuration(details.ts, details.dur),
+ details.thread.utid);
+ }
+
+ this.sliceDetails = details;
+ raf.scheduleFullRedraw();
}
getTitle(): string {
@@ -245,24 +263,23 @@
}
viewTab() {
- if (exists(this.sliceDetails)) {
- const slice = this.sliceDetails;
- return m(
- DetailsShell,
- {
- title: 'Slice',
- description: slice.name,
- buttons: this.renderContextButton(slice),
- },
- m(
- GridLayout,
- renderDetails(slice),
- this.renderRhs(this.engine, slice),
- ),
- );
- } else {
+ if (!exists(this.sliceDetails)) {
return m(DetailsShell, {title: 'Slice', description: 'Loading...'});
}
+ const slice = this.sliceDetails;
+ return m(
+ DetailsShell,
+ {
+ title: 'Slice',
+ description: slice.name,
+ buttons: this.renderContextButton(slice),
+ },
+ m(
+ GridLayout,
+ renderDetails(slice, this.breakdownByThreadState),
+ this.renderRhs(this.engine, slice),
+ ),
+ );
}
isLoading() {
diff --git a/ui/src/frontend/slice_details.ts b/ui/src/frontend/slice_details.ts
index 1f782a7..6056bbc 100644
--- a/ui/src/frontend/slice_details.ts
+++ b/ui/src/frontend/slice_details.ts
@@ -28,6 +28,10 @@
import {addTab} from './bottom_tab';
import {globals} from './globals';
import {SliceDetails} from './sql/slice';
+import {
+ BreakdownByThreadState,
+ BreakdownByThreadStateTreeNode,
+} from './sql/thread_state';
import {SqlTableTab} from './sql_table/tab';
import {SqlTables} from './sql_table/well_known_tables';
import {getProcessName, getThreadName} from './thread_and_process_info';
@@ -44,7 +48,8 @@
// Renders a widget storing all of the generic details for a slice from the
// slice table.
-export function renderDetails(slice: SliceDetails) {
+export function renderDetails(
+ slice: SliceDetails, durationBreakdown?: BreakdownByThreadState) {
return m(
Section,
{title: 'Details'},
@@ -84,10 +89,18 @@
}),
exists(slice.absTime) &&
m(TreeNode, {left: 'Absolute Time', right: slice.absTime}),
- m(TreeNode, {
- left: 'Duration',
- right: computeDuration(slice.ts, slice.dur),
- }),
+ m(
+ TreeNode,
+ {
+ left: 'Duration',
+ right: computeDuration(slice.ts, slice.dur),
+ },
+ exists(durationBreakdown) && slice.dur > 0 &&
+ m(BreakdownByThreadStateTreeNode, {
+ data: durationBreakdown,
+ dur: slice.dur,
+ }),
+ ),
renderThreadDuration(slice),
slice.thread && m(TreeNode, {
left: 'Thread',
diff --git a/ui/src/frontend/sql/thread_state.ts b/ui/src/frontend/sql/thread_state.ts
new file mode 100644
index 0000000..2de948b
--- /dev/null
+++ b/ui/src/frontend/sql/thread_state.ts
@@ -0,0 +1,139 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+
+import {Duration, duration, TimeSpan} from '../../base/time';
+import {LONG, NUM_NULL, STR, STR_NULL} from '../../common/query_result';
+import {EngineProxy} from '../../public';
+import {TreeNode} from '../../widgets/tree';
+import {Utid} from '../sql_types';
+
+// An individual node of the thread state breakdown tree.
+class Node {
+ parent?: Node;
+ children: Map<string, Node>;
+ dur: duration;
+ startsCollapsed: boolean = true;
+
+ constructor(parent?: Node) {
+ this.parent = parent;
+ this.children = new Map();
+ this.dur = 0n;
+ }
+
+ getOrCreateChild(name: string) {
+ let child = this.children.get(name);
+ if (!child) {
+ child = new Node(this);
+ this.children.set(name, child);
+ }
+ return child;
+ }
+
+ addDuration(dur: duration) {
+ let node: Node|undefined = this;
+ while (node !== undefined) {
+ node.dur += dur;
+ node = node.parent;
+ }
+ }
+}
+
+// Thread state breakdown data (tree).
+// Can be passed to ThreadStateBreakdownTreeNode to be rendered as a part of a
+// tree.
+export interface BreakdownByThreadState {
+ root: Node;
+}
+
+// Compute a breakdown of thread states for a given thread for a given time
+// interval.
+export async function breakDownIntervalByThreadState(
+ engine: EngineProxy, range: TimeSpan, utid: Utid):
+ Promise<BreakdownByThreadState> {
+ // TODO(altimin): this probably should share some code with pivot tables when
+ // we actually get some pivot tables we like.
+ const query = await engine.query(`
+ INCLUDE PERFETTO MODULE common.thread_states;
+
+ SELECT
+ state,
+ raw_state as rawState,
+ cpu_type as cpuType,
+ cpu,
+ blocked_function as blockedFunction,
+ dur
+ FROM thread_state_summary_for_interval(${range.start}, ${range.duration}, ${
+ utid});
+ `);
+ const it = query.iter({
+ state: STR,
+ rawState: STR,
+ cpuType: STR_NULL,
+ cpu: NUM_NULL,
+ blockedFunction: STR_NULL,
+ dur: LONG,
+ });
+ const root = new Node();
+ for (; it.valid(); it.next()) {
+ let currentNode = root;
+ currentNode = currentNode.getOrCreateChild(it.state);
+ // If the CPU time is not null, add it to the breakdown.
+ if (it.cpuType !== null) {
+ currentNode = currentNode.getOrCreateChild(it.cpuType);
+ }
+ if (it.cpu !== null) {
+ currentNode = currentNode.getOrCreateChild(`CPU ${it.cpu}`);
+ }
+ if (it.blockedFunction !== null) {
+ currentNode = currentNode.getOrCreateChild(`${it.blockedFunction}`);
+ }
+ currentNode.addDuration(it.dur);
+ }
+ return {
+ root,
+ };
+}
+
+function renderChildren(node: Node, totalDur: duration): m.Child[] {
+ const res = Array.from(node.children.entries())
+ .map(([name, child]) => renderNode(child, name, totalDur));
+ return res;
+}
+
+function renderNode(node: Node, name: string, totalDur: duration): m.Child {
+ const durPercent = 100. * Number(node.dur) / Number(totalDur);
+ return m(
+ TreeNode,
+ {
+ left: name,
+ right: `${Duration.humanise(node.dur)} (${durPercent.toFixed(2)}%)`,
+ startsCollapsed: node.startsCollapsed,
+ },
+ renderChildren(node, totalDur));
+}
+
+interface BreakdownByThreadStateTreeNodeAttrs {
+ dur: duration;
+ data: BreakdownByThreadState;
+}
+
+// A tree node that displays a nested breakdown a time interval by thread state.
+export class BreakdownByThreadStateTreeNode implements
+ m.ClassComponent<BreakdownByThreadStateTreeNodeAttrs> {
+ view({attrs}: m.Vnode<BreakdownByThreadStateTreeNodeAttrs>): m.Child[] {
+ return renderChildren(attrs.data.root, attrs.dur);
+ }
+}
diff --git a/ui/src/tracks/debug/counter_track.ts b/ui/src/tracks/debug/counter_track.ts
index adcc92d..56e3d42 100644
--- a/ui/src/tracks/debug/counter_track.ts
+++ b/ui/src/tracks/debug/counter_track.ts
@@ -13,6 +13,7 @@
// limitations under the License.
import m from 'mithril';
+import {v4 as uuidv4} from 'uuid';
import {Actions, DEBUG_COUNTER_TRACK_KIND} from '../../common/actions';
import {EngineProxy} from '../../common/engine';
@@ -103,14 +104,19 @@
from data
order by ts;`);
- globals.dispatch(Actions.addTrack({
- uri: DEBUG_COUNTER_TRACK_URI,
- name: trackName.trim() || `Debug Track ${debugTrackId}`,
- trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
- trackGroup: SCROLLING_TRACK_GROUP,
- initialState: {
- sqlTableName,
- columns,
- },
- }));
+ const trackInstanceId = uuidv4();
+ globals.dispatchMultiple([
+ Actions.addTrack({
+ id: trackInstanceId,
+ uri: DEBUG_COUNTER_TRACK_URI,
+ name: trackName.trim() || `Debug Track ${debugTrackId}`,
+ trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
+ trackGroup: SCROLLING_TRACK_GROUP,
+ initialState: {
+ sqlTableName,
+ columns,
+ },
+ }),
+ Actions.toggleTrackPinned({trackId: trackInstanceId}),
+ ]);
}
diff --git a/ui/src/tracks/debug/slice_track.ts b/ui/src/tracks/debug/slice_track.ts
index e05fdaf..6a1accb 100644
--- a/ui/src/tracks/debug/slice_track.ts
+++ b/ui/src/tracks/debug/slice_track.ts
@@ -13,6 +13,7 @@
// limitations under the License.
import m from 'mithril';
+import {v4 as uuidv4} from 'uuid';
import {Disposable} from '../../base/disposable';
import {Actions, DEBUG_SLICE_TRACK_KIND} from '../../common/actions';
@@ -145,14 +146,19 @@
from prepared_data
order by ts;`);
- globals.dispatch(Actions.addTrack({
- uri: DEBUG_SLICE_TRACK_URI,
- name: trackName.trim() || `Debug Track ${debugTrackId}`,
- trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
- trackGroup: SCROLLING_TRACK_GROUP,
- initialState: {
- sqlTableName,
- columns: sliceColumns,
- },
- }));
+ const trackInstanceId = uuidv4();
+ globals.dispatchMultiple([
+ Actions.addTrack({
+ id: trackInstanceId,
+ uri: DEBUG_SLICE_TRACK_URI,
+ name: trackName.trim() || `Debug Track ${debugTrackId}`,
+ trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
+ trackGroup: SCROLLING_TRACK_GROUP,
+ initialState: {
+ sqlTableName,
+ columns: sliceColumns,
+ },
+ }),
+ Actions.toggleTrackPinned({trackId: trackInstanceId}),
+ ]);
}
diff --git a/ui/src/widgets/tree.ts b/ui/src/widgets/tree.ts
index e046a35..034bf7c 100644
--- a/ui/src/widgets/tree.ts
+++ b/ui/src/widgets/tree.ts
@@ -59,6 +59,8 @@
// Whether this node is collapsed or not.
// If omitted, collapsed state 'uncontrolled' - i.e. controlled internally.
collapsed?: boolean;
+ // Whether the node should start collapsed or not, default: false.
+ startsCollapsed?: boolean;
loading?: boolean;
showCaret?: boolean;
// Optional icon to show to the left of the text.
@@ -69,7 +71,12 @@
}
export class TreeNode implements m.ClassComponent<TreeNodeAttrs> {
- private collapsed = false;
+ private collapsed;
+
+ constructor({attrs}: m.CVnode<TreeNodeAttrs>) {
+ this.collapsed = attrs.startsCollapsed ?? false;
+ }
+
view(vnode: m.CVnode<TreeNodeAttrs>): m.Children {
const {children, attrs, attrs: {left, onCollapseChanged = () => {}}} =
vnode;