Merge "stdlib: Add CPU cycles calculation for specified slices" into main
diff --git a/Android.bp b/Android.bp
index a2bc070..273b389 100644
--- a/Android.bp
+++ b/Android.bp
@@ -13595,6 +13595,7 @@
"src/trace_processor/perfetto_sql/stdlib/slices/slices.sql",
"src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql",
"src/trace_processor/perfetto_sql/stdlib/stack_trace/jit.sql",
+ "src/trace_processor/perfetto_sql/stdlib/stacks/cpu_profiling.sql",
"src/trace_processor/perfetto_sql/stdlib/time/conversion.sql",
"src/trace_processor/perfetto_sql/stdlib/v8/jit.sql",
"src/trace_processor/perfetto_sql/stdlib/viz/flamegraph.sql",
diff --git a/BUILD b/BUILD
index 918da3d..787ebb5 100644
--- a/BUILD
+++ b/BUILD
@@ -2936,6 +2936,14 @@
],
)
+# GN target: //src/trace_processor/perfetto_sql/stdlib/stacks:stacks
+perfetto_filegroup(
+ name = "src_trace_processor_perfetto_sql_stdlib_stacks_stacks",
+ srcs = [
+ "src/trace_processor/perfetto_sql/stdlib/stacks/cpu_profiling.sql",
+ ],
+)
+
# GN target: //src/trace_processor/perfetto_sql/stdlib/time:time
perfetto_filegroup(
name = "src_trace_processor_perfetto_sql_stdlib_time_time",
@@ -3025,6 +3033,7 @@
":src_trace_processor_perfetto_sql_stdlib_sched_sched",
":src_trace_processor_perfetto_sql_stdlib_slices_slices",
":src_trace_processor_perfetto_sql_stdlib_stack_trace_stack_trace",
+ ":src_trace_processor_perfetto_sql_stdlib_stacks_stacks",
":src_trace_processor_perfetto_sql_stdlib_time_time",
":src_trace_processor_perfetto_sql_stdlib_v8_v8",
":src_trace_processor_perfetto_sql_stdlib_viz_summary_summary",
diff --git a/CHANGELOG b/CHANGELOG
index 6fe50c9..03fa600 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -10,7 +10,12 @@
Trace Processor:
*
UI:
- *
+ * Scheduling wakeup information now reflects whether the wakeup came
+ from an interrupt context. The per-cpu scheduling tracks now show only
+ non-interrupt wakeups, while the per-thread thread state tracks either
+ link to an exact waker slice or state that the wakeup is from an
+ interrupt. Older traces that are recorded without interrupt context
+ information treat all wakeups as non-interrupt.
SDK:
*
diff --git a/include/perfetto/ext/base/small_set.h b/include/perfetto/ext/base/small_set.h
index 5d8d8bc..8ad41c7 100644
--- a/include/perfetto/ext/base/small_set.h
+++ b/include/perfetto/ext/base/small_set.h
@@ -18,6 +18,7 @@
#define INCLUDE_PERFETTO_EXT_BASE_SMALL_SET_H_
#include <array>
+#include <cstdlib>
namespace perfetto {
diff --git a/infra/perfetto.dev/cloudbuild.yaml b/infra/perfetto.dev/cloudbuild.yaml
index f8e0889..be05455 100644
--- a/infra/perfetto.dev/cloudbuild.yaml
+++ b/infra/perfetto.dev/cloudbuild.yaml
@@ -1,7 +1,10 @@
# See go/perfetto-ui-autopush for docs on how this works end-to-end.
# Reuse the same Docker container of the UI autopusher.
+# If this file is modified, the inline YAML must be copy-pasted into the
+# trigger configs inline YAML in Google Cloud Console > Cloud Build for the
+# "perfetto-site" project (zone: global)
steps:
-- name: gcr.io/perfetto-ui/perfetto-ui-builder
+- name: europe-docker.pkg.dev/perfetto-ui/builder/perfetto-ui-builder
args:
- 'infra/perfetto.dev/cloud_build_entrypoint.sh'
# Timeout: 15 min (last measured time in Feb 2021: 2 min)
diff --git a/infra/ui.perfetto.dev/cloudbuild_release.yaml b/infra/ui.perfetto.dev/cloudbuild_release.yaml
index 1dfe79a..8ad05f3 100644
--- a/infra/ui.perfetto.dev/cloudbuild_release.yaml
+++ b/infra/ui.perfetto.dev/cloudbuild_release.yaml
@@ -1,7 +1,8 @@
# See go/perfetto-ui-autopush for docs on how this works end-to-end.
# If this file is modified, the inline YAML must be copy-pasted
# FROM: infra/ui.perfetto.dev/cloudbuild.yaml
-# TO: TWO trigger configs inline YAML in Google Cloud Console > Cloud Build.
+# TO: TWO trigger configs inline YAML in Google Cloud Console > Cloud Build
+# for the project "perfetto-ui" (zone: europe-west2).
steps:
- name: europe-docker.pkg.dev/perfetto-ui/builder/perfetto-ui-builder
args:
diff --git a/python/generators/sql_processing/utils.py b/python/generators/sql_processing/utils.py
index 1163897..9768619 100644
--- a/python/generators/sql_processing/utils.py
+++ b/python/generators/sql_processing/utils.py
@@ -118,7 +118,8 @@
'intervals': ['interval'],
'graphs': ['graph'],
'slices': ['slice'],
- 'linux': ['cpu', 'memory']
+ 'linux': ['cpu', 'memory'],
+ 'stacks': ['cpu_profiling'],
}
# Allows for nonstandard object names.
diff --git a/python/perfetto/batch_trace_processor/api.py b/python/perfetto/batch_trace_processor/api.py
index 2bfd361..20de918 100644
--- a/python/perfetto/batch_trace_processor/api.py
+++ b/python/perfetto/batch_trace_processor/api.py
@@ -42,6 +42,7 @@
TraceListReference = registry.TraceListReference
Metadata = Dict[str, str]
+MAX_LOAD_WORKERS = 32
# Enum encoding how errors while loading/querying traces in BatchTraceProcessor
# should be handled.
@@ -180,12 +181,14 @@
query_executor = self.platform_delegate.create_query_executor(
len(resolved)) or cf.ThreadPoolExecutor(
max_workers=multiprocessing.cpu_count())
- load_exectuor = self.platform_delegate.create_load_executor(
- len(resolved)) or query_executor
+ # Loading trace involves FS access, so it makes sense to limit parallelism
+ max_load_workers = min(multiprocessing.cpu_count(), MAX_LOAD_WORKERS)
+ load_executor = self.platform_delegate.create_load_executor(
+ len(resolved)) or cf.ThreadPoolExecutor(max_workers=max_load_workers)
self.query_executor = query_executor
self.tps_and_metadata = [
- x for x in load_exectuor.map(self._create_tp, resolved) if x is not None
+ x for x in load_executor.map(self._create_tp, resolved) if x is not None
]
def metric(self, metrics: List[str]):
@@ -368,7 +371,7 @@
try:
return TraceProcessor(
trace=trace.generator, config=self.config.tp_config), trace.metadata
- except PerfettoException as ex:
+ except Exception as ex:
if self.config.load_failure_handling == FailureHandling.RAISE_EXCEPTION:
raise ex
self._stats.load_failures += 1
diff --git a/python/perfetto/trace_processor/shell.py b/python/perfetto/trace_processor/shell.py
index 3561701..41f1655 100644
--- a/python/perfetto/trace_processor/shell.py
+++ b/python/perfetto/trace_processor/shell.py
@@ -68,7 +68,7 @@
_ = request.urlretrieve(f'http://{url}/status')
success = True
break
- except error.URLError:
+ except (error.URLError, ConnectionError):
time.sleep(1)
if not success:
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index 73498c2..de63f54 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -271,29 +271,24 @@
str.compare(0, prefix.size(), prefix) == 0;
}
-// Constructs the display string for device PM callback slices.
+// Constructs the callback phase name for device PM callback slices.
//
-// Format: "<driver name> <device name> <event type>[:<callback phase>]"
-//
-// Note: The optional `<callback phase>` is extracted from the `pm_ops` field
-// of the `device_pm_callback_start` tracepoint.
-std::string ConstructDpmCallbackSliceName(const std::string& driver_name,
- const std::string& device_name,
- const std::string& pm_ops,
- const std::string& event_type) {
- std::string slice_name_base =
- driver_name + " " + device_name + " " + event_type;
+// Format: "<event type>[:<callback phase>]"
+// Examples: suspend, suspend:late, resume:noirq etc.
+std::string ConstructCallbackPhaseName(const std::string& pm_ops,
+ const std::string& event_type) {
+ std::string callback_phase = event_type;
// The Linux kernel has a limitation where the `pm_ops` field in the
// tracepoint is left empty if the phase is either prepare/complete.
if (pm_ops == "") {
if (event_type == "suspend")
- return slice_name_base + ":prepare";
+ return callback_phase + ":prepare";
else if (event_type == "resume")
- return slice_name_base + ":complete";
+ return callback_phase + ":complete";
}
- // Extract callback phase (if present) for slice display name.
+ // Extract phase (if present) for slice details.
//
// The `pm_ops` string may contain both callback phase and callback type, but
// only phase is needed. A prefix match is used due to potential absence of
@@ -301,11 +296,11 @@
const std::vector<std::string> valid_phases = {"early", "late", "noirq"};
for (const std::string& valid_phase : valid_phases) {
if (StrStartsWith(pm_ops, valid_phase)) {
- return slice_name_base + ":" + valid_phase;
+ return callback_phase + ":" + valid_phase;
}
}
- return slice_name_base;
+ return callback_phase;
}
} // namespace
@@ -432,7 +427,20 @@
runtime_status_active_id_(context->storage->InternString("Active")),
runtime_status_suspending_id_(
context->storage->InternString("Suspending")),
- runtime_status_resuming_id_(context->storage->InternString("Resuming")) {
+ runtime_status_resuming_id_(context->storage->InternString("Resuming")),
+ suspend_resume_main_event_id_(
+ context->storage->InternString("Main Kernel Suspend Event")),
+ suspend_resume_device_pm_event_id_(
+ context->storage->InternString("Device PM Suspend Event")),
+ suspend_resume_utid_arg_name_(context->storage->InternString("utid")),
+ suspend_resume_device_arg_name_(
+ context->storage->InternString("device_name")),
+ suspend_resume_driver_arg_name_(
+ context->storage->InternString("driver_name")),
+ suspend_resume_callback_phase_arg_name_(
+ context->storage->InternString("callback_phase")),
+ suspend_resume_event_type_arg_name_(
+ context->storage->InternString("event_type")) {
// Build the lookup table for the strings inside ftrace events (e.g. the
// name of ftrace event fields and the names of their args).
for (size_t i = 0; i < GetDescriptorsSize(); i++) {
@@ -1071,7 +1079,7 @@
break;
}
case FtraceEvent::kSuspendResumeFieldNumber: {
- ParseSuspendResume(ts, fld_bytes);
+ ParseSuspendResume(ts, pid, fld_bytes);
break;
}
case FtraceEvent::kSuspendResumeMinimalFieldNumber: {
@@ -1287,7 +1295,7 @@
break;
}
case FtraceEvent::kDevicePmCallbackStartFieldNumber: {
- ParseDevicePmCallbackStart(ts, fld_bytes);
+ ParseDevicePmCallbackStart(ts, pid, fld_bytes);
break;
}
case FtraceEvent::kDevicePmCallbackEndFieldNumber: {
@@ -3317,6 +3325,7 @@
}
void FtraceParser::ParseSuspendResume(int64_t timestamp,
+ uint32_t tid,
protozero::ConstBytes blob) {
protos::pbzero::SuspendResumeFtraceEvent::Decoder evt(blob.data, blob.size);
@@ -3364,8 +3373,24 @@
TrackId start_id =
context_->async_track_set_tracker->Begin(async_track, cookie);
+
+ auto args_inserter = [&](ArgsTracker::BoundInserter* inserter) {
+ inserter->AddArg(suspend_resume_utid_arg_name_,
+ Variadic::UnsignedInteger(
+ context_->process_tracker->GetOrCreateThread(tid)));
+ inserter->AddArg(suspend_resume_event_type_arg_name_,
+ Variadic::String(suspend_resume_main_event_id_));
+
+ // These fields are set to null as this is not a device PM callback event.
+ inserter->AddArg(suspend_resume_device_arg_name_,
+ Variadic::String(kNullStringId));
+ inserter->AddArg(suspend_resume_driver_arg_name_,
+ Variadic::String(kNullStringId));
+ inserter->AddArg(suspend_resume_callback_phase_arg_name_,
+ Variadic::String(kNullStringId));
+ };
context_->slice_tracker->Begin(timestamp, start_id, suspend_resume_name_id_,
- slice_name_id);
+ slice_name_id, args_inserter);
ongoing_suspend_resume_actions[current_action] = true;
}
@@ -3566,14 +3591,15 @@
// Parses `device_pm_callback_start` events and begins corresponding slices in
// the suspend / resume latency UI track.
void FtraceParser::ParseDevicePmCallbackStart(int64_t ts,
+ uint32_t tid,
protozero::ConstBytes blob) {
protos::pbzero::DevicePmCallbackStartFtraceEvent::Decoder dpm_event(
blob.data, blob.size);
// Device here refers to anything managed by a Linux kernel driver.
std::string device_name = dpm_event.device().ToStdString();
+ std::string driver_name = dpm_event.driver().ToStdString();
int64_t cookie;
-
if (suspend_resume_cookie_map_.Find(device_name) == nullptr) {
cookie = static_cast<int64_t>(suspend_resume_cookie_map_.size());
suspend_resume_cookie_map_[device_name] = cookie;
@@ -3581,18 +3607,37 @@
cookie = suspend_resume_cookie_map_[device_name];
}
- std::string slice_name = ConstructDpmCallbackSliceName(
- dpm_event.driver().ToStdString(), device_name,
- dpm_event.pm_ops().ToStdString(),
- GetDpmCallbackEventString(dpm_event.event()));
- StringId slice_name_id = context_->storage->InternString(slice_name.c_str());
-
auto async_track = context_->async_track_set_tracker->InternGlobalTrackSet(
suspend_resume_name_id_);
TrackId track_id =
context_->async_track_set_tracker->Begin(async_track, cookie);
+
+ std::string slice_name = device_name + " " + driver_name;
+ StringId slice_name_id = context_->storage->InternString(slice_name.c_str());
+
+ std::string callback_phase = ConstructCallbackPhaseName(
+ /*pm_ops=*/dpm_event.pm_ops().ToStdString(),
+ /*event_type=*/GetDpmCallbackEventString(dpm_event.event()));
+
+ auto args_inserter = [&](ArgsTracker::BoundInserter* inserter) {
+ inserter->AddArg(suspend_resume_utid_arg_name_,
+ Variadic::UnsignedInteger(
+ context_->process_tracker->GetOrCreateThread(tid)));
+ inserter->AddArg(suspend_resume_event_type_arg_name_,
+ Variadic::String(suspend_resume_device_pm_event_id_));
+ inserter->AddArg(
+ suspend_resume_device_arg_name_,
+ Variadic::String(context_->storage->InternString(device_name.c_str())));
+ inserter->AddArg(
+ suspend_resume_driver_arg_name_,
+ Variadic::String(context_->storage->InternString(driver_name.c_str())));
+ inserter->AddArg(suspend_resume_callback_phase_arg_name_,
+ Variadic::String(context_->storage->InternString(
+ callback_phase.c_str())));
+ };
+
context_->slice_tracker->Begin(ts, track_id, suspend_resume_name_id_,
- slice_name_id);
+ slice_name_id, args_inserter);
}
// Parses `device_pm_callback_end` events and ends corresponding slices in the
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.h b/src/trace_processor/importers/ftrace/ftrace_parser.h
index a9a7e5e..9469d24 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.h
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.h
@@ -235,7 +235,9 @@
void ParseWakeSourceActivate(int64_t timestamp, protozero::ConstBytes);
void ParseWakeSourceDeactivate(int64_t timestamp, protozero::ConstBytes);
- void ParseSuspendResume(int64_t timestamp, protozero::ConstBytes);
+ void ParseSuspendResume(int64_t timestamp,
+ uint32_t pid,
+ protozero::ConstBytes);
void ParseSuspendResumeMinimal(int64_t timestamp, protozero::ConstBytes);
void ParseSchedCpuUtilCfs(int64_t timestamp, protozero::ConstBytes);
@@ -301,7 +303,9 @@
protozero::ConstBytes);
StringId GetRpmStatusStringId(int32_t rpm_status_val);
void ParseRpmStatus(int64_t ts, protozero::ConstBytes);
- void ParseDevicePmCallbackStart(int64_t ts, protozero::ConstBytes);
+ void ParseDevicePmCallbackStart(int64_t ts,
+ uint32_t pid,
+ protozero::ConstBytes);
void ParseDevicePmCallbackEnd(int64_t ts, protozero::ConstBytes);
void ParsePanelWriteGeneric(int64_t timestamp,
uint32_t pid,
@@ -412,6 +416,14 @@
const StringId runtime_status_active_id_;
const StringId runtime_status_suspending_id_;
const StringId runtime_status_resuming_id_;
+ const StringId suspend_resume_main_event_id_;
+ const StringId suspend_resume_device_pm_event_id_;
+ const StringId suspend_resume_utid_arg_name_;
+ const StringId suspend_resume_device_arg_name_;
+ const StringId suspend_resume_driver_arg_name_;
+ const StringId suspend_resume_callback_phase_arg_name_;
+ const StringId suspend_resume_event_type_arg_name_;
+
std::vector<StringId> syscall_arg_name_ids_;
struct FtraceMessageStrings {
diff --git a/src/trace_processor/importers/perf/spe.h b/src/trace_processor/importers/perf/spe.h
index 156b3db..327e708 100644
--- a/src/trace_processor/importers/perf/spe.h
+++ b/src/trace_processor/importers/perf/spe.h
@@ -22,11 +22,11 @@
#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_SPE_H_
#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_SPE_H_
-#include <strings.h>
#include <cstddef>
#include <cstdint>
#include "perfetto/base/logging.h"
#include "perfetto/public/compiler.h"
+
namespace perfetto::trace_processor::perf_importer::spe {
// Test whether a given bit is set. e.g.
diff --git a/src/trace_processor/perfetto_sql/stdlib/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
index e3df2f4..bf51d9e 100644
--- a/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
@@ -34,6 +34,7 @@
"sched",
"slices",
"stack_trace",
+ "stacks",
"time",
"v8",
"viz",
diff --git a/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql b/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql
index 276f7d7..6ffa588 100644
--- a/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql
@@ -14,6 +14,7 @@
-- limitations under the License.
INCLUDE PERFETTO MODULE graphs.hierarchy;
+INCLUDE PERFETTO MODULE graphs.scan;
CREATE PERFETTO TABLE _callstack_spf_summary AS
SELECT
@@ -137,3 +138,38 @@
FROM _callstacks_for_stack_profile_samples!(metrics) c
LEFT JOIN metrics m USING (callsite_id)
);
+
+CREATE PERFETTO MACRO _callstacks_self_to_cumulative(
+ callstacks TableOrSubquery
+)
+RETURNS TableOrSubquery
+AS
+(
+ SELECT a.*
+ FROM _graph_aggregating_scan!(
+ (
+ SELECT id AS source_node_id, parent_id AS dest_node_id
+ FROM $callstacks
+ WHERE parent_id IS NOT NULL
+ ),
+ (
+ SELECT p.id, p.self_count AS cumulative_count
+ FROM $callstacks p
+ LEFT JOIN $callstacks c ON c.parent_id = p.id
+ WHERE c.id IS NULL
+ ),
+ (cumulative_count),
+ (
+ WITH agg AS (
+ SELECT t.id, SUM(t.cumulative_count) AS child_count
+ FROM $table t
+ GROUP BY t.id
+ )
+ SELECT
+ a.id,
+ a.child_count + r.self_count as cumulative_count
+ FROM agg a
+ JOIN $callstacks r USING (id)
+ )
+ ) a
+)
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql b/src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql
index 1a22bc1..0d58ef1 100644
--- a/src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql
@@ -39,3 +39,48 @@
FROM _callstacks_for_stack_profile_samples!(metrics) c
LEFT JOIN metrics m USING (callsite_id)
);
+
+CREATE PERFETTO TABLE _linux_perf_raw_callstacks AS
+SELECT *
+FROM _linux_perf_callstacks_for_samples!(
+ (SELECT p.callsite_id FROM perf_sample p)
+) c
+ORDER BY c.id;
+
+-- Table summarising the callstacks captured during all
+-- perf samples in the trace.
+--
+-- Specifically, this table returns a tree containing all
+-- the callstacks seen during the trace with `self_count`
+-- equal to the number of samples with that frame as the
+-- leaf and `cumulative_count` equal to the number of
+-- samples with the frame anywhere in the tree.
+CREATE PERFETTO TABLE linux_perf_samples_summary_tree(
+ -- The id of the callstack. A callstack in this context
+ -- is a unique set of frames up to the root.
+ id INT,
+ -- The id of the parent callstack for this callstack.
+ parent_id INT,
+ -- The function name of the frame for this callstack.
+ name STRING,
+ -- The name of the mapping containing the frame. This
+ -- can be a native binary, library, JAR or APK.
+ mapping_name STRING,
+ -- The name of the file containing the function.
+ source_file STRING,
+ -- The line number in the file the function is located at.
+ line_number INT,
+ -- The number of samples with this function as the leaf
+ -- frame.
+ self_count INT,
+ -- The number of samples with this function appearing
+ -- anywhere on the callstack.
+ cumulative_count INT
+) AS
+SELECT r.*, a.cumulative_count
+FROM _callstacks_self_to_cumulative!((
+ SELECT id, parent_id, self_count
+ FROM _linux_perf_raw_callstacks
+)) a
+JOIN _linux_perf_raw_callstacks r USING (id)
+ORDER BY r.id;
diff --git a/src/trace_processor/perfetto_sql/stdlib/stacks/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/stacks/BUILD.gn
new file mode 100644
index 0000000..6f4ad9f
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/stacks/BUILD.gn
@@ -0,0 +1,19 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("../../../../../gn/perfetto_sql.gni")
+
+perfetto_sql_source_set("stacks") {
+ sources = [ "cpu_profiling.sql" ]
+}
diff --git a/src/trace_processor/perfetto_sql/stdlib/stacks/cpu_profiling.sql b/src/trace_processor/perfetto_sql/stdlib/stacks/cpu_profiling.sql
new file mode 100644
index 0000000..a5200e8
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/stacks/cpu_profiling.sql
@@ -0,0 +1,85 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the 'License');
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an 'AS IS' BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+INCLUDE PERFETTO MODULE callstacks.stack_profile;
+INCLUDE PERFETTO MODULE graphs.scan;
+INCLUDE PERFETTO MODULE linux.perf.samples;
+
+CREATE PERFETTO TABLE _cpu_profiling_raw_callstacks AS
+SELECT *
+FROM _callstacks_for_cpu_profile_stack_samples!(
+ (SELECT s.callsite_id FROM cpu_profile_stack_sample s)
+) c
+ORDER BY c.id;
+
+-- Table summarising the callstacks captured during any CPU
+-- profiling which occurred during the trace.
+--
+-- Specifically, this table returns a tree containing all
+-- the callstacks seen during the trace with `self_count`
+-- equal to the number of samples with that frame as the
+-- leaf and `cumulative_count` equal to the number of
+-- samples with the frame anywhere in the tree.
+--
+-- Currently, this table is backed by the following data
+-- sources:
+-- * any perf sampling
+-- * generic CPU profiling (e.g. Chrome, ad-hoc traces)
+CREATE PERFETTO TABLE cpu_profiling_summary_tree(
+ -- The id of the callstack. A callstack in this context
+ -- is a unique set of frames up to the root.
+ id INT,
+ -- The id of the parent callstack for this callstack.
+ parent_id INT,
+ -- The function name of the frame for this callstack.
+ name STRING,
+ -- The name of the mapping containing the frame. This
+ -- can be a native binary, library, JAR or APK.
+ mapping_name STRING,
+ -- The name of the file containing the function.
+ source_file STRING,
+ -- The line number in the file the function is located at.
+ line_number INT,
+ -- The number of samples with this function as the leaf
+ -- frame.
+ self_count INT,
+ -- The number of samples with this function appearing
+ -- anywhere on the callstack.
+ cumulative_count INT
+) AS
+SELECT
+ id,
+ parent_id,
+ name,
+ mapping_name,
+ source_file,
+ line_number,
+ SUM(self_count) AS self_count,
+ SUM(cumulative_count) AS cumulative_count
+FROM (
+ -- Generic CPU profiling.
+ SELECT r.*, a.cumulative_count
+ FROM _callstacks_self_to_cumulative!((
+ SELECT id, parent_id, self_count
+ FROM _cpu_profiling_raw_callstacks
+ )) a
+ JOIN _cpu_profiling_raw_callstacks r USING (id)
+ UNION ALL
+ -- Linux perf sampling.
+ SELECT *
+ FROM linux_perf_samples_summary_tree
+)
+GROUP BY id
+ORDER BY id;
diff --git a/test/ci/ui_tests.sh b/test/ci/ui_tests.sh
index 883245f..00567d4 100755
--- a/test/ci/ui_tests.sh
+++ b/test/ci/ui_tests.sh
@@ -38,12 +38,17 @@
ui/run-integrationtests --out ${OUT_PATH} --no-build
RES=$?
+set +x
+
# Copy the output of screenshots diff testing.
if [ -d ${OUT_PATH}/ui-test-artifacts ]; then
cp -a ${OUT_PATH}/ui-test-artifacts /ci/artifacts/ui-test-artifacts
echo "UI integration test report with screnshots:"
echo "https://storage.googleapis.com/perfetto-ci-artifacts/$PERFETTO_TEST_JOB/ui-test-artifacts/index.html"
echo ""
+ echo "To download locally the changed screenshots run:"
+ echo "tools/download_changed_screenshots.py $PERFETTO_TEST_JOB"
+ echo ""
echo "Perfetto UI build for this CL"
echo "https://storage.googleapis.com/perfetto-ci-artifacts/$PERFETTO_TEST_JOB/ui/index.html"
exit $RES
diff --git a/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/slice-with-flows/slice-with-flows.png.sha256 b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/slice-with-flows/slice-with-flows.png.sha256
index e3ab16b..6c551c6 100644
--- a/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/slice-with-flows/slice-with-flows.png.sha256
+++ b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/slice-with-flows/slice-with-flows.png.sha256
@@ -1 +1 @@
-93a74d2983230b09da323b32f4f51f717b0c64b02ddf1d62c12091409248e5fe
\ No newline at end of file
+098ee0afd6595fa48d2bd70fa0cca88f094f10991dd799b935f4ae123697e5c7
\ No newline at end of file
diff --git a/test/data/ui-screenshots/independent_features.test.ts/trace-error-notification/error-icon.png.sha256 b/test/data/ui-screenshots/independent_features.test.ts/trace-error-notification/error-icon.png.sha256
new file mode 100644
index 0000000..7a64aaf
--- /dev/null
+++ b/test/data/ui-screenshots/independent_features.test.ts/trace-error-notification/error-icon.png.sha256
@@ -0,0 +1 @@
+d11a2d89b1d96ede01644accc98cee5ac6fadb15a13d40baa78977fd5c212670
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-0.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-0.png.sha256
index 82eaca3..46bd74a 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-0.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-0.png.sha256
@@ -1 +1 @@
-46b538d434c9d8835836def8d0bd6be037faaf9e942f8091179981100e72737f
\ No newline at end of file
+e334b59ddfbb12ae351fc3aa2bf514bb2e2f0d574c936c97c04b230294473318
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-1.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-1.png.sha256
index 8e25921..7b0c883 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-1.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-1.png.sha256
@@ -1 +1 @@
-2fbe0d0c22661c01ebe5a1419a4b84ffb98c559e68d4bf9a2fc459ccda3a78af
\ No newline at end of file
+fa851f275f2cb252e576bf05b3e04adac7b4e4a6c3f396542ae84be6d602246e
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-2.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-2.png.sha256
index 5f37b59..9aa430b 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-2.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-2.png.sha256
@@ -1 +1 @@
-3e0e897e1a9a0340b8439e471f4fa9c82dc79b15596ffe49242d8bbcf044271b
\ No newline at end of file
+dad3a33cf20deba74d22f10bfe7ebedd283c9911417f0d90c8453e8b1329eda0
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-3.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-3.png.sha256
index 35f43bc..605e20d 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-3.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-3.png.sha256
@@ -1 +1 @@
-38a5e8f32ffc1ad176c939a1b733918bbca3caf1031de773f0f265a503acdbd7
\ No newline at end of file
+08d737f09abfb1879088ac8767c5842d01f73942b3659645f988db680120a52a
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/one-track-pinned.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/one-track-pinned.png.sha256
index aa57652..2ce1b7b 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/one-track-pinned.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/one-track-pinned.png.sha256
@@ -1 +1 @@
-e362ca800cba3e0ac01f3fd9b612d816a424d1a0e04a566b9fc86d33878c63f1
\ No newline at end of file
+b38f3fd22157b98c4d0f0e82ccac29acffd21ca42ae51bd49c7aa1cfa2d1cd53
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/two-tracks-pinned.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/two-tracks-pinned.png.sha256
index 4832c37..ba5adfd 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/two-tracks-pinned.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/two-tracks-pinned.png.sha256
@@ -1 +1 @@
-b5cb57d21e4227ebae3b3ece5aef8544e5d7c8937e7299b4909ec3689a5bf980
\ No newline at end of file
+799d74d9046bddf2830fd28600f8338359d923c69e430da412365235f6619d6e
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-compressed.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-compressed.png.sha256
index e404c81..b7bdab2 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-compressed.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-compressed.png.sha256
@@ -1 +1 @@
-2788ae1226f4fd68cb8c336271f0d8dee5fa7ef307e09b1ecba1b22cbf31336d
\ No newline at end of file
+9fb445372aa8c9b754de4193d91ea593bd130cab8082f3ea5561ca183a7bc374
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-expanded.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-expanded.png.sha256
index e404c81..b7bdab2 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-expanded.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-expanded.png.sha256
@@ -1 +1 @@
-2788ae1226f4fd68cb8c336271f0d8dee5fa7ef307e09b1ecba1b22cbf31336d
\ No newline at end of file
+9fb445372aa8c9b754de4193d91ea593bd130cab8082f3ea5561ca183a7bc374
\ No newline at end of file
diff --git a/test/data/ui-screenshots/sql_table_tab.test.ts/ShowTable-command/slices-table.png.sha256 b/test/data/ui-screenshots/sql_table_tab.test.ts/ShowTable-command/slices-table.png.sha256
index c088694..0b21fad 100644
--- a/test/data/ui-screenshots/sql_table_tab.test.ts/ShowTable-command/slices-table.png.sha256
+++ b/test/data/ui-screenshots/sql_table_tab.test.ts/ShowTable-command/slices-table.png.sha256
@@ -1 +1 @@
-276cfd8d58f19fdc543e01e79042c021d0f7e338822c106e7063c5118ccf6bb0
\ No newline at end of file
+b3ca41f98356f086d1edfeb67696e272c1f91354103236620cea6432583a6bb2
\ No newline at end of file
diff --git a/test/data/ui-screenshots/sql_table_tab.test.ts/slices-with-same-name/slices-with-same-name.png.sha256 b/test/data/ui-screenshots/sql_table_tab.test.ts/slices-with-same-name/slices-with-same-name.png.sha256
index 93a62f3..ed3df9a 100644
--- a/test/data/ui-screenshots/sql_table_tab.test.ts/slices-with-same-name/slices-with-same-name.png.sha256
+++ b/test/data/ui-screenshots/sql_table_tab.test.ts/slices-with-same-name/slices-with-same-name.png.sha256
@@ -1 +1 @@
-0d1b850b2e4a3fe71efb0be679d732bd397a3ab95145d8894d014e413e17ef14
\ No newline at end of file
+50e835c984c3716426f6e910f0870c98bc21099d376e8400e60539ef797fcbaa
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/parser/simpleperf/tests.py b/test/trace_processor/diff_tests/parser/simpleperf/tests.py
index 18ef4e5..ddadbc3 100644
--- a/test/trace_processor/diff_tests/parser/simpleperf/tests.py
+++ b/test/trace_processor/diff_tests/parser/simpleperf/tests.py
@@ -323,3 +323,27 @@
"0xffffd40999062c","EL1",15
"0xffffd40fb0f124","EL1",2
'''))
+
+ def test_perf_summary_tree(self):
+ return DiffTestBlueprint(
+ trace=DataPath('simpleperf/perf.data'),
+ query='''
+ INCLUDE PERFETTO MODULE linux.perf.samples;
+
+ SELECT *
+ FROM linux_perf_samples_summary_tree
+ LIMIT 10
+ ''',
+ out=Csv('''
+ "id","parent_id","name","mapping_name","source_file","line_number","self_count","cumulative_count"
+ 0,"[NULL]","","/elf","[NULL]","[NULL]",84,84
+ 1,"[NULL]","","/elf","[NULL]","[NULL]",69,69
+ 2,"[NULL]","","/elf","[NULL]","[NULL]",177,177
+ 3,"[NULL]","","/elf","[NULL]","[NULL]",89,89
+ 4,"[NULL]","","/t1","[NULL]","[NULL]",70,70
+ 5,"[NULL]","","/elf","[NULL]","[NULL]",218,218
+ 6,"[NULL]","","/elf","[NULL]","[NULL]",65,65
+ 7,"[NULL]","","/elf","[NULL]","[NULL]",70,70
+ 8,"[NULL]","","/t1","[NULL]","[NULL]",87,87
+ 9,"[NULL]","","/elf","[NULL]","[NULL]",64,64
+ '''))
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 26a03a0..d88c8f1 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -195,6 +195,10 @@
thead {
font-weight: normal;
+
+ td.reorderable-cell {
+ cursor: grab;
+ }
}
tr:hover td {
@@ -215,6 +219,10 @@
// density a little bit.
font-size: 16px;
}
+
+ has-left-border {
+ border-left: 1px solid rgba(60, 76, 92, 0.4);
+ }
}
}
@@ -233,9 +241,6 @@
border-left: 1px solid $table-border-color;
padding-left: 6px;
}
- thead td.reorderable-cell {
- cursor: grab;
- }
.disabled {
cursor: default;
}
diff --git a/ui/src/assets/widgets/anchor.scss b/ui/src/assets/widgets/anchor.scss
index 2894928..60aa5a0 100644
--- a/ui/src/assets/widgets/anchor.scss
+++ b/ui/src/assets/widgets/anchor.scss
@@ -28,9 +28,7 @@
background $pf-anim-timing;
& > .material-icons {
- // For some reason, floating this icon results in the most pleasing vertical
- // alignment.
- float: right;
+ vertical-align: bottom;
margin: 0 0 0 0px;
font-size: inherit;
line-height: inherit;
diff --git a/ui/src/base/semantic_icons.ts b/ui/src/base/semantic_icons.ts
index 96b872a..8025075 100644
--- a/ui/src/base/semantic_icons.ts
+++ b/ui/src/base/semantic_icons.ts
@@ -17,6 +17,7 @@
static readonly UpdateSelection = 'call_made'; // Could be 'open_in_new'
static readonly ChangeViewport = 'query_stats'; // Could be 'search'
static readonly ContextMenu = 'arrow_drop_down'; // Could be 'more_vert'
+ static readonly Menu = 'menu';
static readonly Copy = 'content_copy';
static readonly Delete = 'delete';
static readonly SortedAsc = 'arrow_upward';
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 7a4b1a7..cd9ec83 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -281,10 +281,6 @@
state.highlightedSliceId = args.sliceId;
},
- setHoverCursorTimestamp(state: StateDraft, args: {ts: time}) {
- state.hoverCursorTimestamp = args.ts;
- },
-
setHoveredNoteTimestamp(state: StateDraft, args: {ts: time}) {
state.hoveredNoteTimestamp = args.ts;
},
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
index f39fb0f..50fd8bd 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -16,7 +16,7 @@
import {assertExists} from '../base/logging';
import {StateActions} from './actions';
import {createEmptyState} from './empty_state';
-import {TraceUrlSource} from '../public/trace_info';
+import {TraceUrlSource} from '../public/trace_source';
test('open trace', () => {
const state = createEmptyState();
diff --git a/ui/src/common/cache_manager.ts b/ui/src/common/cache_manager.ts
index 4c56759..78c7567 100644
--- a/ui/src/common/cache_manager.ts
+++ b/ui/src/common/cache_manager.ts
@@ -18,7 +18,7 @@
* containing it is discarded by Chrome (e.g. because the tab was not used for
* a long time) or when the user accidentally hits reload.
*/
-import {TraceArrayBufferSource, TraceSource} from '../public/trace_info';
+import {TraceArrayBufferSource, TraceSource} from '../public/trace_source';
const TRACE_CACHE_NAME = 'cached_traces';
const TRACE_CACHE_SIZE = 10;
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
index 2eccd3e..d31d94e 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/common/empty_state.ts
@@ -63,7 +63,6 @@
sidebarVisible: true,
hoveredUtid: -1,
hoveredPid: -1,
- hoverCursorTimestamp: Time.INVALID,
hoveredNoteTimestamp: Time.INVALID,
highlightedSliceId: -1,
diff --git a/ui/src/common/fake_trace_impl.ts b/ui/src/common/fake_trace_impl.ts
index f97dea5..793df3e 100644
--- a/ui/src/common/fake_trace_impl.ts
+++ b/ui/src/common/fake_trace_impl.ts
@@ -13,9 +13,9 @@
// limitations under the License.
import {Time} from '../base/time';
-import {AppImpl} from '../core/app_trace_impl';
import {TraceInfo} from '../public/trace_info';
import {EngineBase} from '../trace_processor/engine';
+import {TraceImpl} from '../core/trace_impl';
export interface FakeTraceImplArgs {
// If true suppresses exceptions when trying to issue a query. This is to
@@ -40,8 +40,10 @@
traceTzOffset: Time.ZERO,
cpus: [],
gpuCount: 0,
+ importErrors: 0,
+ traceType: 'proto',
};
- return AppImpl.instance.newTraceInstance(
+ return TraceImpl.newInstance(
new FakeEngine(args.allowQueries ?? false),
fakeTraceInfo,
);
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index 55c017c..47e4588 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -21,7 +21,8 @@
import {assertExists, assertTrue} from '../base/logging';
import {raf} from '../core/raf_scheduler';
import {defaultPlugins} from '../core/default_plugins';
-import {AppImpl, CORE_PLUGIN_ID, TraceImpl} from '../core/app_trace_impl';
+import {TraceImpl} from '../core/trace_impl';
+import {AppImpl, CORE_PLUGIN_ID} from '../core/app_impl';
// 'Static' registry of all known plugins.
export class PluginRegistry extends Registry<PluginDescriptor> {
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 6440a48..f8960eb 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -14,7 +14,7 @@
import {time} from '../base/time';
import {RecordConfig} from '../controller/record_config_types';
-import {TraceSource} from '../public/trace_info';
+import {TraceSource} from '../public/trace_source';
/**
* A plain js object, holding objects of type |Class| keyed by string id.
@@ -202,7 +202,6 @@
// Hovered and focused events
hoveredUtid: number;
hoveredPid: number;
- hoverCursorTimestamp: time;
hoveredNoteTimestamp: time;
highlightedSliceId: number;
diff --git a/ui/src/common/state_serialization.ts b/ui/src/common/state_serialization.ts
index 5b35271..ce7e2a1 100644
--- a/ui/src/common/state_serialization.ts
+++ b/ui/src/common/state_serialization.ts
@@ -23,7 +23,8 @@
} from './state_serialization_schema';
import {TimeSpan} from '../base/time';
import {ProfileType} from '../public/selection';
-import {AppImpl, TraceImpl} from '../core/app_trace_impl';
+import {TraceImpl} from '../core/trace_impl';
+import {AppImpl} from '../core/app_impl';
// When it comes to serialization & permalinks there are two different use cases
// 1. Uploading the current trace in a Cloud Storage (GCS) file AND serializing
diff --git a/ui/src/controller/loading_manager.ts b/ui/src/controller/loading_manager.ts
deleted file mode 100644
index cfc1a46..0000000
--- a/ui/src/controller/loading_manager.ts
+++ /dev/null
@@ -1,47 +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.
-
-import {publishLoading} from '../frontend/publish';
-import {LoadingTracker} from '../trace_processor/engine';
-
-// Used to keep track of whether the engine is currently querying.
-export class LoadingManager implements LoadingTracker {
- private static _instance: LoadingManager;
- private numQueuedQueries = 0;
- private numLastUpdate = 0;
-
- static get getInstance(): LoadingManager {
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
- return this._instance || (this._instance = new this());
- }
-
- beginLoading() {
- this.update(1);
- }
-
- endLoading() {
- this.update(-1);
- }
-
- private update(change: number) {
- this.numQueuedQueries += change;
- if (
- this.numQueuedQueries === 0 ||
- Math.abs(this.numLastUpdate - this.numQueuedQueries) > 2
- ) {
- this.numLastUpdate = this.numQueuedQueries;
- publishLoading(this.numQueuedQueries);
- }
- }
-}
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index ebaf733..be5e139 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import m from 'mithril';
import {assertExists, assertTrue} from '../base/logging';
import {Duration, time, Time, TimeSpan} from '../base/time';
import {Actions, DeferredAction} from '../common/actions';
@@ -49,10 +48,7 @@
resetEngineWorker,
WasmEngineProxy,
} from '../trace_processor/wasm_engine_proxy';
-import {showModal} from '../widgets/modal';
-import {Child, Children, Controller} from './controller';
-import {LoadingManager} from './loading_manager';
-import {TraceErrorController} from './trace_error_controller';
+import {Controller} from './controller';
import {
TraceBufferStream,
TraceFileStream,
@@ -64,9 +60,10 @@
deserializeAppStatePhase1,
deserializeAppStatePhase2,
} from '../common/state_serialization';
-import {ProfileType, profileType} from '../public/selection';
import {TraceInfo} from '../public/trace_info';
-import {AppImpl} from '../core/app_trace_impl';
+import {AppImpl} from '../core/app_impl';
+import {raf} from '../core/raf_scheduler';
+import {TraceImpl} from '../core/trace_impl';
type States = 'init' | 'loading_trace' | 'ready';
@@ -137,32 +134,6 @@
defaultValue: true,
});
-// A local storage key where the indication that JSON warning has been shown is
-// stored.
-const SHOWN_JSON_WARNING_KEY = 'shownJsonWarning';
-
-function showJsonWarning() {
- showModal({
- title: 'Warning',
- content: m(
- 'div',
- m(
- 'span',
- 'Perfetto UI features are limited for JSON traces. ',
- 'We recommend recording ',
- m(
- 'a',
- {href: 'https://perfetto.dev/docs/quickstart/chrome-tracing'},
- 'proto-format traces',
- ),
- ' from Chrome.',
- ),
- m('br'),
- ),
- buttons: [],
- });
-}
-
// TODO(stevegolton): Move this into some global "SQL extensions" file and
// ensure it's only run once.
async function defineMaxLayoutDepthSqlFunction(engine: Engine): Promise<void> {
@@ -227,15 +198,7 @@
break;
case 'ready':
- // At this point we are ready to serve queries and handle tracks.
- const engine = assertExists(this.engine);
- const childControllers: Children = [];
-
- childControllers.push(
- Child('traceError', TraceErrorController, {engine}),
- );
-
- return childControllers;
+ return [];
default:
throw new Error(`unknown state ${this.state}`);
@@ -262,7 +225,7 @@
if (useRpc) {
console.log('Opening trace using native accelerator over HTTP+RPC');
engineMode = 'HTTP_RPC';
- engine = new HttpRpcEngine(this.engineId, LoadingManager.getInstance);
+ engine = new HttpRpcEngine(this.engineId);
engine.errorHandler = (err) => {
globals.dispatch(
Actions.setEngineFailed({mode: 'HTTP_RPC', failure: `${err}`}),
@@ -273,11 +236,7 @@
console.log('Opening trace using built-in WASM engine');
engineMode = 'WASM';
const enginePort = resetEngineWorker();
- engine = new WasmEngineProxy(
- this.engineId,
- enginePort,
- LoadingManager.getInstance,
- );
+ engine = new WasmEngineProxy(this.engineId, enginePort);
engine.resetTraceProcessor({
cropTrackEvents: CROP_TRACK_EVENTS_FLAG.get(),
ingestFtraceInRawTable: INGEST_FTRACE_IN_RAW_TABLE_FLAG.get(),
@@ -285,6 +244,7 @@
ftraceDropUntilAllCpusValid: FTRACE_DROP_UNTIL_FLAG.get(),
});
}
+ engine.onResponseReceived = () => raf.scheduleFullRedraw();
this.engine = engine;
if (isMetatracingEnabled()) {
@@ -350,39 +310,20 @@
// traceUuid will be '' if the trace is not cacheable (URL or RPC).
const traceUuid = await this.cacheCurrentTrace();
- const traceDetails = await getTraceTimeDetails(this.engine, engineCfg);
+ const traceDetails = await getTraceInfo(this.engine, engineCfg);
if (traceDetails.traceTitle) {
document.title = `${traceDetails.traceTitle} - Perfetto UI`;
}
- const trace = AppImpl.instance.newTraceInstance(this.engine, traceDetails);
+ const trace = TraceImpl.newInstance(this.engine, traceDetails);
await globals.onTraceLoad(trace);
- const shownJsonWarning =
- window.localStorage.getItem(SHOWN_JSON_WARNING_KEY) !== null;
-
- // Show warning if the trace is in JSON format.
- const query = `select str_value from metadata where name = 'trace_type'`;
- const result = await assertExists(this.engine).query(query);
- const traceType = result.firstRow({str_value: STR}).str_value;
- const isJsonTrace = traceType == 'json';
- if (!shownJsonWarning) {
- // When in embedded mode, the host app will control which trace format
- // it passes to Perfetto, so we don't need to show this warning.
- if (isJsonTrace && !globals.embeddedMode) {
- showJsonWarning();
- // Save that the warning has been shown. Value is irrelevant since only
- // the presence of key is going to be checked.
- window.localStorage.setItem(SHOWN_JSON_WARNING_KEY, 'true');
- }
- }
-
AppImpl.instance.omnibox.reset();
const actions: DeferredAction[] = [Actions.setTraceUuid({traceUuid})];
const visibleTimeSpan = await computeVisibleTime(
traceDetails.start,
traceDetails.end,
- isJsonTrace,
+ trace.traceInfo.traceType === 'json',
this.engine,
);
@@ -426,9 +367,6 @@
publishHasFtrace(res.numRows() > 0);
}
- await this.selectFirstHeapProfile();
- await this.selectPerfSample(traceDetails);
-
const pendingDeeplink = globals.state.pendingDeeplink;
if (pendingDeeplink !== undefined) {
globals.dispatch(Actions.clearPendingDeeplink({}));
@@ -452,7 +390,10 @@
// Trace Processor doesn't support the reliable range feature for JSON
// traces.
- if (!isJsonTrace && ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG.get()) {
+ if (
+ trace.traceInfo.traceType !== 'json' &&
+ ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG.get()
+ ) {
const reliableRangeStart = await computeTraceReliableRangeStart(engine);
if (reliableRangeStart > 0) {
globals.noteManager.addNote({
@@ -477,60 +418,6 @@
return engineMode;
}
- private async selectPerfSample(traceTime: {start: time; end: time}) {
- const profile = await assertExists(this.engine).query(`
- select upid
- from perf_sample
- join thread using (utid)
- where callsite_id is not null
- order by ts desc
- limit 1
- `);
- if (profile.numRows() !== 1) return;
- const row = profile.firstRow({upid: NUM});
- const upid = row.upid;
- const leftTs = traceTime.start;
- const rightTs = traceTime.end;
- globals.selectionManager.selectLegacy({
- kind: 'PERF_SAMPLES',
- id: 0,
- upid,
- leftTs,
- rightTs,
- type: ProfileType.PERF_SAMPLE,
- });
- }
-
- private async selectFirstHeapProfile() {
- const query = `
- select * from (
- select
- min(ts) AS ts,
- 'heap_profile:' || group_concat(distinct heap_name) AS type,
- upid
- from heap_profile_allocation
- group by upid
- union
- select distinct graph_sample_ts as ts, 'graph' as type, upid
- from heap_graph_object
- )
- order by ts
- limit 1
- `;
- const profile = await assertExists(this.engine).query(query);
- if (profile.numRows() !== 1) return;
- const row = profile.firstRow({ts: LONG, type: STR, upid: NUM});
- const ts = Time.fromRaw(row.ts);
- const upid = row.upid;
- globals.selectionManager.selectLegacy({
- kind: 'HEAP_PROFILE',
- id: 0,
- upid,
- ts,
- type: profileType(row.type),
- });
- }
-
private async selectPendingDeeplink(link: PendingDeeplinkState) {
const conditions = [];
const {ts, dur} = link;
@@ -1005,7 +892,7 @@
return new TimeSpan(visibleStart, visibleEnd);
}
-async function getTraceTimeDetails(
+async function getTraceInfo(
engine: Engine,
engineCfg: EngineConfig,
): Promise<TraceInfo> {
@@ -1102,6 +989,12 @@
break;
}
+ const traceType = (
+ await engine.query(
+ `select str_value from metadata where name = 'trace_type'`,
+ )
+ ).firstRow({str_value: STR}).str_value;
+
return {
...traceTime,
traceTitle,
@@ -1111,7 +1004,9 @@
traceTzOffset,
cpus: await getCpus(engine),
gpuCount: await getNumberOfGpus(engine),
+ importErrors: await getTraceErrors(engine),
source: engineCfg.source,
+ traceType,
};
}
@@ -1147,6 +1042,12 @@
return result.firstRow({gpuCount: NUM}).gpuCount;
}
+async function getTraceErrors(engine: Engine): Promise<number> {
+ const sql = `SELECT sum(value) as errs FROM stats WHERE severity != 'info'`;
+ const result = await engine.query(sql);
+ return result.firstRow({errs: NUM}).errs;
+}
+
async function getTracingMetadataTimeBounds(engine: Engine): Promise<TimeSpan> {
const queryRes = await engine.query(`select
name,
diff --git a/ui/src/controller/trace_error_controller.ts b/ui/src/controller/trace_error_controller.ts
deleted file mode 100644
index 0fde7b9..0000000
--- a/ui/src/controller/trace_error_controller.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {publishTraceErrors} from '../frontend/publish';
-import {Engine} from '../trace_processor/engine';
-import {NUM} from '../trace_processor/query_result';
-import {Controller} from './controller';
-
-export interface TraceErrorControllerArgs {
- engine: Engine;
-}
-
-export class TraceErrorController extends Controller<'main'> {
- private hasRun = false;
- constructor(private args: TraceErrorControllerArgs) {
- super('main');
- }
-
- run() {
- if (this.hasRun) {
- return;
- }
- this.hasRun = true;
- const engine = this.args.engine;
- engine
- .query(
- `SELECT sum(value) as sumValue FROM stats WHERE severity != 'info'`,
- )
- .then((result) => {
- const errors = result.firstRow({sumValue: NUM}).sumValue;
- publishTraceErrors(errors);
- });
- }
-}
diff --git a/ui/src/core/app_impl.ts b/ui/src/core/app_impl.ts
new file mode 100644
index 0000000..a2be0f4
--- /dev/null
+++ b/ui/src/core/app_impl.ts
@@ -0,0 +1,127 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {assertTrue} from '../base/logging';
+import {App} from '../public/app';
+import {TraceContext, TraceImpl} from './trace_impl';
+import {CommandManagerImpl} from './command_manager';
+import {OmniboxManagerImpl} from './omnibox_manager';
+import {raf} from './raf_scheduler';
+import {SidebarManagerImpl} from './sidebar_manager';
+
+// The pseudo plugin id used for the core instance of AppImpl.
+
+export const CORE_PLUGIN_ID = '__core__';
+
+/**
+ * Handles the global state of the ui, for anything that is not related to a
+ * specific trace. This is always available even before a trace is loaded (in
+ * contrast to TraceContext, which is bound to the lifetime of a trace).
+ * There is only one instance in total of this class (see instance()).
+ * This class is not exposed to anybody. Both core and plugins should access
+ * this via AppImpl.
+ */
+export class AppContext {
+ readonly commandMgr = new CommandManagerImpl();
+ readonly omniboxMgr = new OmniboxManagerImpl();
+ readonly sidebarMgr = new SidebarManagerImpl();
+
+ // The most recently created trace context. Can be undefined before any trace
+ // is loaded.
+ private traceCtx?: TraceContext;
+
+ // There is only one global instance, lazily initialized on the first call.
+ private static _instance: AppContext;
+ static get instance() {
+ return (AppContext._instance = AppContext._instance ?? new AppContext());
+ }
+
+ private constructor() {}
+
+ get currentTraceCtx(): TraceContext | undefined {
+ return this.traceCtx;
+ }
+
+ // Called by AppImpl.newTraceInstance().
+ setActiveTrace(traceCtx: TraceContext | undefined) {
+ if (this.traceCtx !== undefined) {
+ // This will trigger the unregistration of trace-scoped commands and
+ // sidebar menuitems (and few similar things).
+ this.traceCtx[Symbol.dispose]();
+ }
+ this.traceCtx = traceCtx;
+ }
+}
+/*
+ * Every plugin gets its own instance. This is how we keep track
+ * what each plugin is doing and how we can blame issues on particular
+ * plugins.
+ * The instance exists for the whole duration a plugin is active.
+ */
+
+export class AppImpl implements App {
+ private appCtx: AppContext;
+ readonly pluginId: string;
+ private currentTrace?: TraceImpl;
+
+ private constructor(appCtx: AppContext, pluginId: string) {
+ this.appCtx = appCtx;
+ this.pluginId = pluginId;
+ }
+
+ // Gets access to the one instance that the core can use. Note that this is
+ // NOT the only instance, as other AppImpl instance will be created for each
+ // plugin.
+ private static _instance: AppImpl;
+ static get instance(): AppImpl {
+ AppImpl._instance =
+ AppImpl._instance ?? new AppImpl(AppContext.instance, CORE_PLUGIN_ID);
+ return AppImpl._instance;
+ }
+
+ get commands(): CommandManagerImpl {
+ return this.appCtx.commandMgr;
+ }
+
+ get sidebar(): SidebarManagerImpl {
+ return this.appCtx.sidebarMgr;
+ }
+
+ get omnibox(): OmniboxManagerImpl {
+ return this.appCtx.omniboxMgr;
+ }
+
+ get trace(): TraceImpl | undefined {
+ return this.currentTrace;
+ }
+
+ closeCurrentTrace() {
+ this.currentTrace = undefined;
+ this.appCtx.setActiveTrace(undefined);
+ }
+
+ scheduleRedraw(): void {
+ raf.scheduleFullRedraw();
+ }
+
+ setActiveTrace(traceImpl: TraceImpl, traceCtx: TraceContext) {
+ this.appCtx.setActiveTrace(traceCtx);
+ this.currentTrace = traceImpl;
+ }
+
+ forkForPlugin(pluginId: string): AppImpl {
+ assertTrue(pluginId != CORE_PLUGIN_ID);
+ return new AppImpl(this.appCtx, pluginId);
+ }
+}
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index bba58d5..922ae74 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -38,6 +38,7 @@
'dev.perfetto.TimelineSync',
'dev.perfetto.TraceMetadata',
'org.kernel.LinuxKernelDevices',
+ 'org.kernel.SuspendResumeLatency',
'perfetto.AndroidLog',
'perfetto.Annotation',
'perfetto.AsyncSlices',
diff --git a/ui/src/core/timeline.ts b/ui/src/core/timeline.ts
index 9b0c645..a71cace 100644
--- a/ui/src/core/timeline.ts
+++ b/ui/src/core/timeline.ts
@@ -13,12 +13,14 @@
// limitations under the License.
import {assertTrue} from '../base/logging';
-import {time, TimeSpan} from '../base/time';
+import {Time, time, TimeSpan} from '../base/time';
import {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
import {Area} from '../public/selection';
import {raf} from './raf_scheduler';
import {HighPrecisionTime} from '../base/high_precision_time';
import {Timeline} from '../public/timeline';
+import {timestampFormat, TimestampFormat} from './timestamp_format';
+import {TraceInfo} from '../public/trace_info';
interface Range {
start?: number;
@@ -33,7 +35,7 @@
*/
export class TimelineImpl implements Timeline {
private _visibleWindow: HighPrecisionTimeSpan;
- private readonly traceSpan: TimeSpan;
+ private _hoverCursorTimestamp?: time;
// This is a giant hack. Basically, removing visible window from the state
// means that we no longer update the state periodically while navigating
@@ -48,11 +50,10 @@
areaY: Range = {};
private _selectedArea?: Area;
- constructor(traceSpan: TimeSpan) {
- this.traceSpan = traceSpan;
+ constructor(private readonly traceInfo: TraceInfo) {
this._visibleWindow = HighPrecisionTimeSpan.fromTime(
- traceSpan.start,
- traceSpan.end,
+ traceInfo.start,
+ traceInfo.end,
);
}
@@ -63,7 +64,7 @@
zoomVisibleWindow(ratio: number, centerPoint: number) {
this._visibleWindow = this._visibleWindow
.scale(ratio, centerPoint, MIN_DURATION)
- .fitWithin(this.traceSpan.start, this.traceSpan.end);
+ .fitWithin(this.traceInfo.start, this.traceInfo.end);
raf.scheduleRedraw();
this.retriggerControllersOnChange();
@@ -72,7 +73,7 @@
panVisibleWindow(delta: number) {
this._visibleWindow = this._visibleWindow
.translate(delta)
- .fitWithin(this.traceSpan.start, this.traceSpan.end);
+ .fitWithin(this.traceInfo.start, this.traceInfo.end);
raf.scheduleRedraw();
this.retriggerControllersOnChange();
@@ -130,7 +131,7 @@
updateVisibleTimeHP(ts: HighPrecisionTimeSpan) {
this._visibleWindow = ts
.clampDuration(MIN_DURATION)
- .fitWithin(this.traceSpan.start, this.traceSpan.end);
+ .fitWithin(this.traceInfo.start, this.traceInfo.end);
raf.scheduleRedraw();
this.retriggerControllersOnChange();
@@ -140,4 +141,40 @@
get visibleWindow(): HighPrecisionTimeSpan {
return this._visibleWindow;
}
+
+ get hoverCursorTimestamp(): time | undefined {
+ return this._hoverCursorTimestamp;
+ }
+
+ set hoverCursorTimestamp(t: time | undefined) {
+ this._hoverCursorTimestamp = t;
+ raf.scheduleRedraw();
+ }
+
+ // Offset between t=0 and the configured time domain.
+ timestampOffset(): time {
+ const fmt = timestampFormat();
+ switch (fmt) {
+ case TimestampFormat.Timecode:
+ case TimestampFormat.Seconds:
+ case TimestampFormat.Milliseoncds:
+ case TimestampFormat.Microseconds:
+ return this.traceInfo.start;
+ case TimestampFormat.TraceNs:
+ case TimestampFormat.TraceNsLocale:
+ return Time.ZERO;
+ case TimestampFormat.UTC:
+ return this.traceInfo.utcOffset;
+ case TimestampFormat.TraceTz:
+ return this.traceInfo.traceTzOffset;
+ default:
+ const x: never = fmt;
+ throw new Error(`Unsupported format ${x}`);
+ }
+ }
+
+ // Convert absolute time to domain time.
+ toDomainTime(ts: time): time {
+ return Time.sub(ts, this.timestampOffset());
+ }
}
diff --git a/ui/src/core/app_trace_impl.ts b/ui/src/core/trace_impl.ts
similarity index 74%
rename from ui/src/core/app_trace_impl.ts
rename to ui/src/core/trace_impl.ts
index edd2ac1..641c9d4 100644
--- a/ui/src/core/app_trace_impl.ts
+++ b/ui/src/core/trace_impl.ts
@@ -15,13 +15,10 @@
import {DisposableStack} from '../base/disposable_stack';
import {assertTrue} from '../base/logging';
import {createStore, Migrate, Store} from '../base/store';
-import {TimeSpan} from '../base/time';
-import {TimelineImpl} from '../core/timeline';
-import {App} from '../public/app';
+import {TimelineImpl} from './timeline';
import {Command} from '../public/command';
import {Trace} from '../public/trace';
-import {setScrollToFunction} from '../public/scroll_helper';
-import {ScrollToArgs} from '../public/scroll_helper';
+import {ScrollToArgs, setScrollToFunction} from '../public/scroll_helper';
import {TraceInfo} from '../public/trace_info';
import {TrackDescriptor} from '../public/track';
import {EngineBase, EngineProxy} from '../trace_processor/engine';
@@ -38,126 +35,9 @@
import {ScrollHelper} from './scroll_helper';
import {Selection, SelectionOpts} from '../public/selection';
import {SearchResult} from '../public/search';
-import {raf} from './raf_scheduler';
import {PivotTableManager} from './pivot_table_manager';
import {FlowManager} from './flow_manager';
-
-// The pseudo plugin id used for the core instance of AppImpl.
-export const CORE_PLUGIN_ID = '__core__';
-
-/**
- * Handles the global state of the ui, for anything that is not related to a
- * specific trace. This is always available even before a trace is loaded (in
- * contrast to TraceContext, which is bound to the lifetime of a trace).
- * There is only one instance in total of this class (see instance()).
- * This class is not exposed to anybody. Both core and plugins should access
- * this via AppImpl.
- */
-class AppContext {
- readonly commandMgr = new CommandManagerImpl();
- readonly omniboxMgr = new OmniboxManagerImpl();
- readonly sidebarMgr = new SidebarManagerImpl();
-
- // The most recently created trace context. Can be undefined before any trace
- // is loaded.
- private traceCtx?: TraceContext;
-
- // There is only one global instance, lazily initialized on the first call.
- private static _instance: AppContext;
- static get instance() {
- return (AppContext._instance = AppContext._instance ?? new AppContext());
- }
-
- private constructor() {}
-
- get currentTraceCtx(): TraceContext | undefined {
- return this.traceCtx;
- }
-
- // Called by AppImpl.newTraceInstance().
- setActiveTrace(traceCtx: TraceContext | undefined) {
- if (this.traceCtx !== undefined) {
- // This will trigger the unregistration of trace-scoped commands and
- // sidebar menuitems (and few similar things).
- this.traceCtx[Symbol.dispose]();
- }
- this.traceCtx = traceCtx;
-
- // TODO(primiano): remove this injection once we plumb Trace everywhere.
- setScrollToFunction((args: ScrollToArgs) =>
- traceCtx?.scrollHelper.scrollTo(args),
- );
- }
-}
-
-/*
- * Every plugin gets its own instance. This is how we keep track
- * what each plugin is doing and how we can blame issues on particular
- * plugins.
- * The instance exists for the whole duration a plugin is active.
- */
-export class AppImpl implements App {
- private appCtx: AppContext;
- readonly pluginId: string;
- private currentTrace?: TraceImpl;
-
- private constructor(appCtx: AppContext, pluginId: string) {
- this.appCtx = appCtx;
- this.pluginId = pluginId;
- }
-
- // Gets access to the one instance that the core can use. Note that this is
- // NOT the only instance, as other AppImpl instance will be created for each
- // plugin.
- private static _instance: AppImpl;
- static get instance(): AppImpl {
- AppImpl._instance =
- AppImpl._instance ?? new AppImpl(AppContext.instance, CORE_PLUGIN_ID);
- return AppImpl._instance;
- }
-
- get commands(): CommandManagerImpl {
- return this.appCtx.commandMgr;
- }
-
- get sidebar(): SidebarManagerImpl {
- return this.appCtx.sidebarMgr;
- }
-
- get omnibox(): OmniboxManagerImpl {
- return this.appCtx.omniboxMgr;
- }
-
- get trace(): TraceImpl | undefined {
- return this.currentTrace;
- }
-
- closeCurrentTrace() {
- this.currentTrace = undefined;
- this.appCtx.setActiveTrace(undefined);
- }
-
- scheduleRedraw(): void {
- raf.scheduleFullRedraw();
- }
-
- // This is called by TraceController when loading a new trace, soon after the
- // engine has been set up. It obtains a new TraceImpl for the core. From that
- // we can fork sibling instances (i.e. bound to the same TraceContext) for
- // the various plugins.
- newTraceInstance(engine: EngineBase, traceInfo: TraceInfo): TraceImpl {
- const traceCtx = new TraceContext(this.appCtx, engine, traceInfo);
- this.appCtx.setActiveTrace(traceCtx);
- const newTrace = new TraceImpl(this, traceCtx);
- this.currentTrace = newTrace;
- return this.currentTrace;
- }
-
- forkForPlugin(pluginId: string): AppImpl {
- assertTrue(pluginId != CORE_PLUGIN_ID);
- return new AppImpl(this.appCtx, pluginId);
- }
-}
+import {AppContext, AppImpl, CORE_PLUGIN_ID} from './app_impl';
/**
* Handles the per-trace state of the UI
@@ -167,7 +47,7 @@
* This is the underlying storage for AppImpl, which instead has one instance
* per trace per plugin.
*/
-class TraceContext implements Disposable {
+export class TraceContext implements Disposable {
readonly appCtx: AppContext;
readonly engine: EngineBase;
readonly omniboxMgr = new OmniboxManagerImpl();
@@ -189,8 +69,7 @@
this.appCtx = gctx;
this.engine = engine;
this.traceInfo = traceInfo;
- const traceSpan = new TimeSpan(traceInfo.start, traceInfo.end);
- this.timeline = new TimelineImpl(traceSpan);
+ this.timeline = new TimelineImpl(traceInfo);
this.scrollHelper = new ScrollHelper(
this.traceInfo,
@@ -287,6 +166,22 @@
private commandMgrProxy: CommandManagerImpl;
private sidebarProxy: SidebarManagerImpl;
+ // This is called by TraceController when loading a new trace, soon after the
+ // engine has been set up. It obtains a new TraceImpl for the core. From that
+ // we can fork sibling instances (i.e. bound to the same TraceContext) for
+ // the various plugins.
+ static newInstance(engine: EngineBase, traceInfo: TraceInfo): TraceImpl {
+ const appCtx = AppContext.instance;
+ const appImpl = AppImpl.instance;
+ const traceCtx = new TraceContext(appCtx, engine, traceInfo);
+ const traceImpl = new TraceImpl(appImpl, traceCtx);
+ appImpl.setActiveTrace(traceImpl, traceCtx);
+
+ // TODO(primiano): remove this injection once we plumb Trace everywhere.
+ setScrollToFunction((x: ScrollToArgs) => traceCtx.scrollHelper.scrollTo(x));
+ return traceImpl;
+ }
+
constructor(appImpl: AppImpl, ctx: TraceContext) {
const pluginId = appImpl.pluginId;
this.appImpl = appImpl;
diff --git a/ui/src/core_plugins/android_log/logs_panel.ts b/ui/src/core_plugins/android_log/logs_panel.ts
index af56505..6aa3530 100644
--- a/ui/src/core_plugins/android_log/logs_panel.ts
+++ b/ui/src/core_plugins/android_log/logs_panel.ts
@@ -14,10 +14,8 @@
import m from 'mithril';
import {time, Time, TimeSpan} from '../../base/time';
-import {Actions} from '../../common/actions';
import {raf} from '../../core/raf_scheduler';
import {DetailsShell} from '../../widgets/details_shell';
-import {globals} from '../../frontend/globals';
import {Timestamp} from '../../frontend/widgets/timestamp';
import {Engine} from '../../trace_processor/engine';
import {LONG, NUM, NUM_NULL, STR} from '../../trace_processor/query_result';
@@ -124,11 +122,11 @@
onRowHover: (id) => {
const timestamp = this.entries?.timestamps[id];
if (timestamp !== undefined) {
- globals.dispatch(Actions.setHoverCursorTimestamp({ts: timestamp}));
+ attrs.trace.timeline.hoverCursorTimestamp = timestamp;
}
},
onRowOut: () => {
- globals.dispatch(Actions.setHoverCursorTimestamp({ts: Time.INVALID}));
+ attrs.trace.timeline.hoverCursorTimestamp = undefined;
},
}),
);
diff --git a/ui/src/core_plugins/async_slices/index.ts b/ui/src/core_plugins/async_slices/index.ts
index aebabcc..5f3b199 100644
--- a/ui/src/core_plugins/async_slices/index.ts
+++ b/ui/src/core_plugins/async_slices/index.ts
@@ -36,6 +36,9 @@
async addGlobalAsyncTracks(ctx: Trace): Promise<void> {
const {engine} = ctx;
+ // TODO(stevegolton): The track exclusion logic is currently a hack. This will be replaced
+ // by a mechanism for more specific plugins to override tracks from more generic plugins.
+ const suspendResumeLatencyTrackName = 'Suspend/Resume Latency';
const rawGlobalAsyncTracks = await engine.query(`
with global_tracks_grouped as (
select
@@ -46,6 +49,7 @@
from track t
join _slice_track_summary using (id)
where t.type in ('__intrinsic_track', 'gpu_track', '__intrinsic_cpu_track')
+ and (name != '${suspendResumeLatencyTrackName}' or name is null)
group by parent_id, name
)
select
diff --git a/ui/src/core_plugins/commands/index.ts b/ui/src/core_plugins/commands/index.ts
index 0bb57da..9c87de4 100644
--- a/ui/src/core_plugins/commands/index.ts
+++ b/ui/src/core_plugins/commands/index.ts
@@ -24,7 +24,7 @@
isLegacyTrace,
openFileWithLegacyTraceViewer,
} from '../../frontend/legacy_trace_viewer';
-import {AppImpl} from '../../core/app_trace_impl';
+import {AppImpl} from '../../core/app_impl';
import {addQueryResultsTab} from '../../public/lib/query_table/query_result_tab';
const SQL_STATS = `
diff --git a/ui/src/core_plugins/cpu_slices/sched_details_tab.ts b/ui/src/core_plugins/cpu_slices/sched_details_tab.ts
index 9070451..48f1be1 100644
--- a/ui/src/core_plugins/cpu_slices/sched_details_tab.ts
+++ b/ui/src/core_plugins/cpu_slices/sched_details_tab.ts
@@ -122,7 +122,7 @@
private renderSchedLatencyInfo(data: Data): m.Children {
if (
data.wakeup?.wakeupTs === undefined ||
- data.wakeup?.wakerThread === undefined
+ data.wakeup?.wakerUtid === undefined
) {
return null;
}
@@ -142,12 +142,13 @@
private renderWakeupText(data: Data): m.Children {
if (
- data.wakeup?.wakerThread === undefined ||
- data.wakeup?.wakeupTs === undefined
+ data.wakeup?.wakerUtid === undefined ||
+ data.wakeup?.wakeupTs === undefined ||
+ data.wakeup?.wakerCpu === undefined
) {
return null;
}
- const threadInfo = globals.threads.get(data.wakeup.wakerThread.utid);
+ const threadInfo = globals.threads.get(data.wakeup.wakerUtid);
if (!threadInfo) {
return null;
}
diff --git a/ui/src/core_plugins/ftrace/ftrace_explorer.ts b/ui/src/core_plugins/ftrace/ftrace_explorer.ts
index b581958..1bcd6d6 100644
--- a/ui/src/core_plugins/ftrace/ftrace_explorer.ts
+++ b/ui/src/core_plugins/ftrace/ftrace_explorer.ts
@@ -14,7 +14,6 @@
import m from 'mithril';
import {time, Time} from '../../base/time';
-import {Actions} from '../../common/actions';
import {colorForFtrace} from '../../core/colorizer';
import {DetailsShell} from '../../widgets/details_shell';
import {
@@ -23,7 +22,6 @@
PopupMultiSelect,
} from '../../widgets/multiselect';
import {PopupPosition} from '../../widgets/popup';
-import {globals} from '../../frontend/globals';
import {Timestamp} from '../../frontend/widgets/timestamp';
import {FtraceFilter, FtraceStat} from './common';
import {Engine} from '../../trace_processor/engine';
@@ -156,8 +154,15 @@
this.pagination = {offset, count};
this.reloadData(attrs);
},
- onRowHover: this.onRowOver.bind(this),
- onRowOut: this.onRowOut.bind(this),
+ onRowHover: (id) => {
+ const event = this.data?.events.find((event) => event.id === id);
+ if (event) {
+ attrs.trace.timeline.hoverCursorTimestamp = event.ts;
+ }
+ },
+ onRowOut: () => {
+ attrs.trace.timeline.hoverCursorTimestamp = undefined;
+ },
}),
);
}
@@ -202,17 +207,6 @@
});
}
- private onRowOver(id: number) {
- const event = this.data?.events.find((event) => event.id === id);
- if (event) {
- globals.dispatch(Actions.setHoverCursorTimestamp({ts: event.ts}));
- }
- }
-
- private onRowOut() {
- globals.dispatch(Actions.setHoverCursorTimestamp({ts: Time.INVALID}));
- }
-
private renderTitle() {
if (this.data) {
const {numEvents} = this.data;
diff --git a/ui/src/core_plugins/heap_profile/index.ts b/ui/src/core_plugins/heap_profile/index.ts
index 37c3b92..bf55a11 100644
--- a/ui/src/core_plugins/heap_profile/index.ts
+++ b/ui/src/core_plugins/heap_profile/index.ts
@@ -15,7 +15,7 @@
import m from 'mithril';
import {assertExists, assertFalse} from '../../base/logging';
import {Monitor} from '../../base/monitor';
-import {ProfileType, Selection} from '../../public/selection';
+import {profileType, ProfileType, Selection} from '../../public/selection';
import {HeapProfileSelection} from '../../public/selection';
import {Timestamp} from '../../frontend/widgets/timestamp';
import {Engine} from '../../trace_processor/engine';
@@ -23,7 +23,7 @@
import {DetailsPanel} from '../../public/details_panel';
import {Trace} from '../../public/trace';
import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
-import {NUM} from '../../trace_processor/query_result';
+import {LONG, NUM, STR} from '../../trace_processor/query_result';
import {DetailsShell} from '../../widgets/details_shell';
import {HeapProfileTrack} from './heap_profile_track';
import {
@@ -31,7 +31,7 @@
QueryFlamegraphAttrs,
metricsFromTableOrSubquery,
} from '../../core/query_flamegraph';
-import {time} from '../../base/time';
+import {Time, time} from '../../base/time';
import {Popup} from '../../widgets/popup';
import {Icon} from '../../widgets/icon';
import {Button} from '../../widgets/button';
@@ -84,9 +84,41 @@
ctx.tabs.registerDetailsPanel(
new HeapProfileFlamegraphDetailsPanel(ctx.engine, incomplete),
);
+
+ await selectFirstHeapProfile(ctx);
}
}
+async function selectFirstHeapProfile(ctx: Trace) {
+ const query = `
+ select * from (
+ select
+ min(ts) AS ts,
+ 'heap_profile:' || group_concat(distinct heap_name) AS type,
+ upid
+ from heap_profile_allocation
+ group by upid
+ union
+ select distinct graph_sample_ts as ts, 'graph' as type, upid
+ from heap_graph_object
+ )
+ order by ts
+ limit 1
+ `;
+ const profile = await assertExists(ctx.engine).query(query);
+ if (profile.numRows() !== 1) return;
+ const row = profile.firstRow({ts: LONG, type: STR, upid: NUM});
+ const ts = Time.fromRaw(row.ts);
+ const upid = row.upid;
+ ctx.selection.selectLegacy({
+ kind: 'HEAP_PROFILE',
+ id: 0,
+ upid,
+ ts,
+ type: profileType(row.type),
+ });
+}
+
class HeapProfileFlamegraphDetailsPanel implements DetailsPanel {
private sel?: HeapProfileSelection;
private selMonitor = new Monitor([
diff --git a/ui/src/core_plugins/perf_samples_profile/index.ts b/ui/src/core_plugins/perf_samples_profile/index.ts
index 3a94baa..b47e426 100644
--- a/ui/src/core_plugins/perf_samples_profile/index.ts
+++ b/ui/src/core_plugins/perf_samples_profile/index.ts
@@ -20,7 +20,11 @@
import {Trace} from '../../public/trace';
import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
-import {PerfSamplesSelection, Selection} from '../../public/selection';
+import {
+ PerfSamplesSelection,
+ ProfileType,
+ Selection,
+} from '../../public/selection';
import {
QueryFlamegraph,
QueryFlamegraphAttrs,
@@ -125,9 +129,35 @@
ctx.tabs.registerDetailsPanel(
new PerfSamplesFlamegraphDetailsPanel(ctx.engine),
);
+
+ await selectPerfSample(ctx);
}
}
+async function selectPerfSample(ctx: Trace) {
+ const profile = await assertExists(ctx.engine).query(`
+ select upid
+ from perf_sample
+ join thread using (utid)
+ where callsite_id is not null
+ order by ts desc
+ limit 1
+ `);
+ if (profile.numRows() !== 1) return;
+ const row = profile.firstRow({upid: NUM});
+ const upid = row.upid;
+ const leftTs = ctx.traceInfo.start;
+ const rightTs = ctx.traceInfo.end;
+ ctx.selection.selectLegacy({
+ kind: 'PERF_SAMPLES',
+ id: 0,
+ upid,
+ leftTs,
+ rightTs,
+ type: ProfileType.PERF_SAMPLE,
+ });
+}
+
class PerfSamplesFlamegraphDetailsPanel implements DetailsPanel {
private sel?: PerfSamplesSelection;
private selMonitor = new Monitor([
diff --git a/ui/src/core_plugins/track_utils/index.ts b/ui/src/core_plugins/track_utils/index.ts
index 50d97a5..e671907 100644
--- a/ui/src/core_plugins/track_utils/index.ts
+++ b/ui/src/core_plugins/track_utils/index.ts
@@ -16,7 +16,7 @@
import {Trace} from '../../public/trace';
import {PromptOption} from '../../public/omnibox';
import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
-import {AppImpl} from '../../core/app_trace_impl';
+import {AppImpl} from '../../core/app_impl';
import {getTimeSpanOfSelectionOrVisibleWindow} from '../../public/utils';
class TrackUtilsPlugin implements PerfettoPlugin {
diff --git a/ui/src/frontend/aggregation_panel.ts b/ui/src/frontend/aggregation_panel.ts
index 8f7c4df..01f90b3 100644
--- a/ui/src/frontend/aggregation_panel.ts
+++ b/ui/src/frontend/aggregation_panel.ts
@@ -25,7 +25,7 @@
import {Anchor} from '../widgets/anchor';
import {Icons} from '../base/semantic_icons';
import {translateState} from '../trace_processor/sql_utils/thread_state';
-import {TraceImpl} from '../core/app_trace_impl';
+import {TraceImpl} from '../core/trace_impl';
export interface AggregationPanelAttrs {
data?: AggregateData;
diff --git a/ui/src/frontend/aggregation_tab.ts b/ui/src/frontend/aggregation_tab.ts
index a5a8ba3..3a223e6 100644
--- a/ui/src/frontend/aggregation_tab.ts
+++ b/ui/src/frontend/aggregation_tab.ts
@@ -35,7 +35,7 @@
} from '../core/query_flamegraph';
import {DisposableStack} from '../base/disposable_stack';
import {assertExists} from '../base/logging';
-import {TraceImpl} from '../core/app_trace_impl';
+import {TraceImpl} from '../core/trace_impl';
import {Trace} from '../public/trace';
interface View {
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index 38e8165..b733463 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -22,7 +22,7 @@
import {getCurrentModalKey, showModal} from '../widgets/modal';
import {globals} from './globals';
import {Router} from './router';
-import {AppImpl} from '../core/app_trace_impl';
+import {AppImpl} from '../core/app_impl';
const MODAL_KEY = 'crash_modal';
diff --git a/ui/src/frontend/flow_events_panel.ts b/ui/src/frontend/flow_events_panel.ts
index a4acfef..434d549 100644
--- a/ui/src/frontend/flow_events_panel.ts
+++ b/ui/src/frontend/flow_events_panel.ts
@@ -16,7 +16,7 @@
import {Icons} from '../base/semantic_icons';
import {raf} from '../core/raf_scheduler';
import {Flow} from '../core/flow_types';
-import {TraceImpl} from '../core/app_trace_impl';
+import {TraceImpl} from '../core/trace_impl';
export const ALL_CATEGORIES = '_all_';
diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
index e69de9d..8a184c1 100644
--- a/ui/src/frontend/flow_events_renderer.ts
+++ b/ui/src/frontend/flow_events_renderer.ts
@@ -21,7 +21,7 @@
import {RenderedPanelInfo} from './panel_container';
import {TimeScale} from '../base/time_scale';
import {TrackNode} from '../public/workspace';
-import {TraceImpl} from '../core/app_trace_impl';
+import {TraceImpl} from '../core/trace_impl';
const TRACK_GROUP_CONNECTION_OFFSET = 5;
const TRIANGLE_SIZE = 5;
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index e0b7e7f..ecd716f 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -14,7 +14,7 @@
import {assertExists} from '../base/logging';
import {createStore, Store} from '../base/store';
-import {Time, time} from '../base/time';
+import {time} from '../base/time';
import {Actions, DeferredAction} from '../common/actions';
import {CommandManagerImpl} from '../core/command_manager';
import {
@@ -23,7 +23,6 @@
} from '../common/conversion_jobs';
import {createEmptyState} from '../common/empty_state';
import {EngineConfig, State} from '../common/state';
-import {TimestampFormat, timestampFormat} from '../core/timestamp_format';
import {setPerfHooks} from '../core/perf';
import {raf} from '../core/raf_scheduler';
import {ServiceWorkerController} from './service_worker_controller';
@@ -34,11 +33,8 @@
import {getServingRoot} from '../base/http_utils';
import {Workspace} from '../public/workspace';
import {ratelimit} from './rate_limiters';
-import {
- AppImpl,
- setRerunControllersFunction,
- TraceImpl,
-} from '../core/app_trace_impl';
+import {setRerunControllersFunction, TraceImpl} from '../core/trace_impl';
+import {AppImpl} from '../core/app_impl';
import {createFakeTraceImpl} from '../common/fake_trace_impl';
type DispatchMultiple = (actions: DeferredAction[]) => void;
@@ -89,10 +85,8 @@
private _trackDataStore?: TrackDataStore = undefined;
private _overviewStore?: OverviewStore = undefined;
private _threadMap?: ThreadMap = undefined;
- private _numQueriesQueued = 0;
private _bufferUsage?: number = undefined;
private _recordingLog?: string = undefined;
- private _traceErrors?: number = undefined;
private _metricError?: string = undefined;
private _jobStatus?: Map<ConversionJobName, ConversionJobStatus> = undefined;
private _embeddedMode?: boolean = undefined;
@@ -102,7 +96,6 @@
httpRpcState: HttpRpcState = {connected: false};
showPanningHint = false;
permalinkHash?: string;
- showTraceErrorPopup = true;
extraSqlPackages: SqlPackage[] = [];
get workspace(): Workspace {
@@ -248,14 +241,6 @@
return assertExists(this._threadMap);
}
- get traceErrors() {
- return this._traceErrors;
- }
-
- setTraceErrors(arg: number) {
- this._traceErrors = arg;
- }
-
get metricError() {
return this._metricError;
}
@@ -264,14 +249,6 @@
this._metricError = arg;
}
- set numQueuedQueries(value: number) {
- this._numQueriesQueued = value;
- }
-
- get numQueuedQueries() {
- return this._numQueriesQueued;
- }
-
get bufferUsage() {
return this._bufferUsage;
}
@@ -394,33 +371,6 @@
get noteManager() {
return this._trace.notes;
}
-
- // Offset between t=0 and the configured time domain.
- timestampOffset(): time {
- const fmt = timestampFormat();
- switch (fmt) {
- case TimestampFormat.Timecode:
- case TimestampFormat.Seconds:
- case TimestampFormat.Milliseoncds:
- case TimestampFormat.Microseconds:
- return this._trace.traceInfo.start;
- case TimestampFormat.TraceNs:
- case TimestampFormat.TraceNsLocale:
- return Time.ZERO;
- case TimestampFormat.UTC:
- return this._trace.traceInfo.utcOffset;
- case TimestampFormat.TraceTz:
- return this._trace.traceInfo.traceTzOffset;
- default:
- const x: never = fmt;
- throw new Error(`Unsupported format ${x}`);
- }
- }
-
- // Convert absolute time to domain time.
- toDomainTime(ts: time): time {
- return Time.sub(ts, this.timestampOffset());
- }
}
export const globals = new Globals();
diff --git a/ui/src/frontend/idle_detector.ts b/ui/src/frontend/idle_detector.ts
index 2ecadf1..d4e594f 100644
--- a/ui/src/frontend/idle_detector.ts
+++ b/ui/src/frontend/idle_detector.ts
@@ -13,8 +13,8 @@
// limitations under the License.
import {defer} from '../base/deferred';
-import {globals} from './globals';
import {raf} from '../core/raf_scheduler';
+import {AppImpl} from '../core/app_impl';
/**
* This class is exposed by index.ts as window.waitForPerfettoIdle() and is used
@@ -68,8 +68,9 @@
}
private idleIndicators() {
+ const reqsPending = AppImpl.instance.trace?.engine.numRequestsPending ?? 0;
return [
- globals.numQueuedQueries == 0,
+ reqsPending === 0,
!raf.hasPendingRedraws,
!document.getAnimations().some((a) => a.playState === 'running'),
document.querySelector('.progress.progress-anim') == null,
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 4918954..8cb0d25 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -61,7 +61,7 @@
import {IdleDetector} from './idle_detector';
import {IdleDetectorWindow} from './idle_detector_interface';
import {pageWithTrace} from './pages';
-import {AppImpl} from '../core/app_trace_impl';
+import {AppImpl} from '../core/app_impl';
import {setAddSqlTableTabImplFunction} from './sql_table_tab_interface';
import {addSqlTableTabImpl} from './sql_table_tab';
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index ad90d88..c15476c 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -182,7 +182,7 @@
if (size.width > 0 && timespan.duration > 0n) {
const maxMajorTicks = getMaxMajorTicks(size.width);
- const offset = globals.timestampOffset();
+ const offset = globals.trace.timeline.timestampOffset();
const tickGen = generateTicks(timespan, maxMajorTicks, offset);
for (const {type, time} of tickGen) {
const px = Math.floor(timescale.timeToPx(time));
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index c7d30e2..3545ba1 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -112,7 +112,7 @@
if (size.width > TRACK_SHELL_WIDTH && traceContext.duration > 0n) {
const maxMajorTicks = getMaxMajorTicks(this.width - TRACK_SHELL_WIDTH);
- const offset = globals.timestampOffset();
+ const offset = globals.trace.timeline.timestampOffset();
const tickGen = generateTicks(traceContext, maxMajorTicks, offset);
// Draw time labels
@@ -124,7 +124,7 @@
if (xPos > this.width) break;
if (type === TickType.MAJOR) {
ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
- const domainTime = globals.toDomainTime(time);
+ const domainTime = globals.trace.timeline.toDomainTime(time);
renderTimestamp(ctx, domainTime, xPos + 5, 18, MIN_PX_PER_STEP);
} else if (type == TickType.MEDIUM) {
ctx.fillRect(xPos - 1, 0, 1, 8);
diff --git a/ui/src/frontend/pages.ts b/ui/src/frontend/pages.ts
index 3c966cd..5907dec 100644
--- a/ui/src/frontend/pages.ts
+++ b/ui/src/frontend/pages.ts
@@ -13,7 +13,8 @@
// limitations under the License.
import m from 'mithril';
-import {AppImpl, TraceImpl} from '../core/app_trace_impl';
+import {TraceImpl} from '../core/trace_impl';
+import {AppImpl} from '../core/app_impl';
import {HomePage} from './home_page';
export interface PageAttrs {
diff --git a/ui/src/frontend/permalink.ts b/ui/src/frontend/permalink.ts
index b58a904..a02b455 100644
--- a/ui/src/frontend/permalink.ts
+++ b/ui/src/frontend/permalink.ts
@@ -40,7 +40,7 @@
} from '../common/state_serialization_schema';
import {z} from 'zod';
import {showModal} from '../widgets/modal';
-import {AppImpl} from '../core/app_trace_impl';
+import {AppImpl} from '../core/app_impl';
// Permalink serialization has two layers:
// 1. Serialization of the app state (state_serialization.ts):
diff --git a/ui/src/frontend/pivot_table.ts b/ui/src/frontend/pivot_table.ts
index abd3520..e07614c 100644
--- a/ui/src/frontend/pivot_table.ts
+++ b/ui/src/frontend/pivot_table.ts
@@ -43,7 +43,7 @@
import {assertExists, assertFalse} from '../base/logging';
import {Filter, SqlColumn} from './widgets/sql/table/column';
import {argSqlColumn} from './widgets/sql/table/well_known_columns';
-import {TraceImpl} from '../core/app_trace_impl';
+import {TraceImpl} from '../core/trace_impl';
import {PivotTableManager} from '../core/pivot_table_manager';
interface PathItem {
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
index ba02af6..ab30a19 100644
--- a/ui/src/frontend/publish.ts
+++ b/ui/src/frontend/publish.ts
@@ -60,13 +60,6 @@
globals.publishRedraw();
}
-export function publishLoading(numQueuedQueries: number) {
- globals.numQueuedQueries = numQueuedQueries;
- // TODO(hjd): Clean up loadingAnimation given that this now causes a full
- // redraw anyways. Also this should probably just go via the global state.
- raf.scheduleFullRedraw();
-}
-
export function publishBufferUsage(args: {percentage: number}) {
globals.setBufferUsage(args.percentage);
globals.publishRedraw();
@@ -77,11 +70,6 @@
globals.publishRedraw();
}
-export function publishTraceErrors(numErrors: number) {
- globals.setTraceErrors(numErrors);
- globals.publishRedraw();
-}
-
export function publishMetricError(error: string) {
globals.setMetricError(error);
globals.publishRedraw();
diff --git a/ui/src/frontend/search_overview_track.ts b/ui/src/frontend/search_overview_track.ts
index 427c750..6329d9c 100644
--- a/ui/src/frontend/search_overview_track.ts
+++ b/ui/src/frontend/search_overview_track.ts
@@ -19,7 +19,7 @@
import {TimeScale} from '../base/time_scale';
import {Optional} from '../base/utils';
import {calculateResolution} from '../common/resolution';
-import {TraceImpl} from '../core/app_trace_impl';
+import {TraceImpl} from '../core/trace_impl';
import {LONG, NUM} from '../trace_processor/query_result';
import {escapeSearchQuery} from '../trace_processor/query_utils';
import {createVirtualTable} from '../trace_processor/sql_utils';
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index 3619e94..24dd203 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -48,7 +48,8 @@
import {openInOldUIWithSizeCheck} from './legacy_trace_viewer';
import {formatHotkey} from '../base/hotkeys';
import {SidebarMenuItem} from '../public/sidebar';
-import {AppImpl} from '../core/app_trace_impl';
+import {AppImpl} from '../core/app_impl';
+import {Trace} from '../public/trace';
const GITILES_URL =
'https://android.googlesource.com/platform/external/perfetto';
@@ -96,6 +97,10 @@
defaultValue: true,
});
+export interface OptionalTraceAttrs {
+ trace?: Trace;
+}
+
function shouldShowHiringBanner(): boolean {
return globals.isInternalUser && HIRING_BANNER_FLAG.get();
}
@@ -536,20 +541,20 @@
downloadData('metatrace', result.metatrace, jsEvents);
}
-const EngineRPCWidget: m.Component = {
- view() {
+class EngineRPCWidget implements m.ClassComponent<OptionalTraceAttrs> {
+ view({attrs}: m.CVnode<OptionalTraceAttrs>) {
let cssClass = '';
let title = 'Number of pending SQL queries';
let label: string;
let failed = false;
let mode: EngineMode | undefined;
- const engine = globals.state.engine;
- if (engine !== undefined) {
- mode = engine.mode;
- if (engine.failed !== undefined) {
+ const engineCfg = globals.state.engine;
+ if (engineCfg !== undefined) {
+ mode = engineCfg.mode;
+ if (engineCfg.failed !== undefined) {
cssClass += '.red';
- title = 'Query engine crashed\n' + engine.failed;
+ title = 'Query engine crashed\n' + engineCfg.failed;
failed = true;
}
}
@@ -579,14 +584,15 @@
title += '\n(Query engine: built-in WASM)';
}
+ const numReqs = attrs.trace?.engine.numRequestsPending ?? 0;
return m(
`.dbg-info-square${cssClass}`,
{title},
m('div', label),
- m('div', `${failed ? 'FAIL' : globals.numQueuedQueries}`),
+ m('div', `${failed ? 'FAIL' : numReqs}`),
);
- },
-};
+ }
+}
const ServiceWorkerWidget: m.Component = {
view() {
@@ -668,11 +674,11 @@
},
};
-const SidebarFooter: m.Component = {
- view() {
+class SidebarFooter implements m.ClassComponent<OptionalTraceAttrs> {
+ view({attrs}: m.CVnode<OptionalTraceAttrs>) {
return m(
'.sidebar-footer',
- m(EngineRPCWidget),
+ m(EngineRPCWidget, attrs),
m(ServiceWorkerWidget),
m(
'.version',
@@ -687,8 +693,8 @@
),
),
);
- },
-};
+ }
+}
class HiringBanner implements m.ClassComponent {
view() {
@@ -706,9 +712,9 @@
}
}
-export class Sidebar implements m.ClassComponent {
+export class Sidebar implements m.ClassComponent<OptionalTraceAttrs> {
private _redrawWhileAnimating = new Animation(() => raf.scheduleFullRedraw());
- view() {
+ view({attrs}: m.CVnode<OptionalTraceAttrs>) {
if (globals.hideSidebar) return null;
const vdomSections = [];
for (const section of getSections()) {
@@ -840,7 +846,11 @@
),
m(
'.sidebar-scroll',
- m('.sidebar-scroll-container', ...vdomSections, m(SidebarFooter)),
+ m(
+ '.sidebar-scroll-container',
+ ...vdomSections,
+ m(SidebarFooter, attrs),
+ ),
),
);
}
diff --git a/ui/src/frontend/sql_table_tab.ts b/ui/src/frontend/sql_table_tab.ts
index bca7901..495b4d4 100644
--- a/ui/src/frontend/sql_table_tab.ts
+++ b/ui/src/frontend/sql_table_tab.ts
@@ -28,8 +28,9 @@
import {SqlTable} from './widgets/sql/table/table';
import {SqlTableDescription} from './widgets/sql/table/table_description';
import {Trace} from '../public/trace';
+import {MenuItem, PopupMenu2} from '../widgets/menu';
-export interface SqlTableTabConfig {
+export interface AddSqlTableTabParams {
table: SqlTableDescription;
filters?: Filter[];
imports?: string[];
@@ -37,17 +38,30 @@
export function addSqlTableTabImpl(
trace: Trace,
- config: SqlTableTabConfig,
+ config: AddSqlTableTabParams,
): void {
+ addSqlTableTabWithState(
+ new SqlTableState(trace, config.table, {
+ filters: config.filters,
+ imports: config.imports,
+ }),
+ );
+}
+
+function addSqlTableTabWithState(state: SqlTableState) {
const queryResultsTab = new SqlTableTab({
- config,
- trace,
+ config: {state},
+ trace: state.trace,
uuid: uuidv4(),
});
addBottomTab(queryResultsTab, 'sqlTable');
}
+interface SqlTableTabConfig {
+ state: SqlTableState;
+}
+
class SqlTableTab extends BottomTab<SqlTableTabConfig> {
static readonly kind = 'dev.perfetto.SqlTableTab';
@@ -56,14 +70,7 @@
constructor(args: NewBottomTabArgs<SqlTableTabConfig>) {
super(args);
- this.state = new SqlTableState(this.trace, this.config.table, {
- filters: this.config.filters,
- imports: this.config.imports,
- });
- }
-
- static create(args: NewBottomTabArgs<SqlTableTabConfig>): SqlTableTab {
- return new SqlTableTab(args);
+ this.state = args.config.state;
}
viewTab() {
@@ -111,11 +118,25 @@
buttons: [
...navigation,
addDebugTrack,
- m(Button, {
- label: 'Copy SQL query',
- onclick: () =>
- copyToClipboard(this.state.getNonPaginatedSQLQuery()),
- }),
+ m(
+ PopupMenu2,
+ {
+ trigger: m(Button, {
+ icon: Icons.Menu,
+ }),
+ },
+ m(MenuItem, {
+ label: 'Duplicate',
+ icon: 'tab_duplicate',
+ onclick: () => addSqlTableTabWithState(this.state.clone()),
+ }),
+ m(MenuItem, {
+ label: 'Copy SQL query',
+ icon: Icons.Copy,
+ onclick: () =>
+ copyToClipboard(this.state.getNonPaginatedSQLQuery()),
+ }),
+ ),
],
},
m(SqlTable, {
@@ -131,7 +152,7 @@
}
private getDisplayName(): string {
- return this.config.table.displayName ?? this.config.table.name;
+ return this.state.config.displayName ?? this.state.config.name;
}
isLoading(): boolean {
diff --git a/ui/src/frontend/sql_table_tab_interface.ts b/ui/src/frontend/sql_table_tab_interface.ts
index 346189c..3525a9e 100644
--- a/ui/src/frontend/sql_table_tab_interface.ts
+++ b/ui/src/frontend/sql_table_tab_interface.ts
@@ -13,9 +13,12 @@
// limitations under the License.
import {Trace} from '../public/trace';
-import {type SqlTableTabConfig} from './sql_table_tab';
+import {type AddSqlTableTabParams} from './sql_table_tab';
-type AddSqlTableTabFunction = (trace: Trace, config: SqlTableTabConfig) => void;
+type AddSqlTableTabFunction = (
+ trace: Trace,
+ config: AddSqlTableTabParams,
+) => void;
// TODO(primiano): this injection is to break the circular dependency cycle that
// there is between DebugSliceTrack and SqlTableTab. The problem is:
@@ -26,7 +29,10 @@
let addSqlTableTabFunction: AddSqlTableTabFunction;
-export function addSqlTableTab(trace: Trace, config: SqlTableTabConfig): void {
+export function addSqlTableTab(
+ trace: Trace,
+ config: AddSqlTableTabParams,
+): void {
addSqlTableTabFunction(trace, config);
}
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index 3e2674a..bab2361 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import m from 'mithril';
-import {Time, time} from '../base/time';
+import {time} from '../base/time';
import {raf} from '../core/raf_scheduler';
import {Anchor} from '../widgets/anchor';
import {Button} from '../widgets/button';
@@ -38,7 +38,6 @@
import {globals} from './globals';
import {getProcessName} from '../trace_processor/sql_utils/process';
import {
- ThreadInfo,
getFullThreadName,
getThreadName,
} from '../trace_processor/sql_utils/thread';
@@ -58,6 +57,7 @@
prev?: ThreadState;
next?: ThreadState;
waker?: ThreadState;
+ wakerInterruptCtx?: boolean;
wakee?: ThreadState[];
}
@@ -107,23 +107,21 @@
limit: 1,
})
)[0];
- if (this.state.wakerThread?.utid !== undefined) {
- relatedStates.waker = (
- await getThreadStateFromConstraints(this.engine, {
- filters: [
- `utid = ${this.state.wakerThread?.utid}`,
- `ts <= ${this.state.ts}`,
- `ts + dur >= ${this.state.ts}`,
- ],
- })
- )[0];
+ if (this.state.wakerId !== undefined) {
+ relatedStates.waker = await getThreadState(
+ this.engine,
+ this.state.wakerId,
+ );
}
+ // note: this might be valid even if there is no |waker| slice, in the case
+ // of an interrupt wakeup while in the idle process (which is omitted from
+ // the thread_state table).
+ relatedStates.wakerInterruptCtx = this.state.wakerInterruptCtx;
+
relatedStates.wakee = await getThreadStateFromConstraints(this.engine, {
filters: [
- `waker_utid = ${this.state.thread?.utid}`,
- `state = 'R'`,
- `ts >= ${this.state.ts}`,
- `ts <= ${this.state.ts + this.state.dur}`,
+ `waker_id = ${this.config.id}`,
+ `(irq_context is null or irq_context = 0)`,
],
});
@@ -201,7 +199,6 @@
right: getProcessName(process),
}),
thread && m(TreeNode, {left: 'Thread', right: getThreadName(thread)}),
- state.wakerThread && this.renderWakerThread(state.wakerThread),
m(TreeNode, {
left: 'SQL ID',
right: m(SqlRef, {table: 'thread_state', id: state.threadStateSqlId}),
@@ -232,18 +229,6 @@
);
}
- private renderWakerThread(wakerThread: ThreadInfo) {
- return m(
- TreeNode,
- {left: 'Waker'},
- m(TreeNode, {
- left: 'Process',
- right: getProcessName(wakerThread.process),
- }),
- m(TreeNode, {left: 'Thread', right: getThreadName(wakerThread)}),
- );
- }
-
private renderRelatedThreadStates(): m.Children {
if (this.state === undefined || this.relatedStates === undefined) {
return 'Loading';
@@ -261,17 +246,64 @@
const nameForNextOrPrev = (state: ThreadState) =>
`${state.state} for ${renderDuration(state.dur)}`;
+ const renderWaker = (related: RelatedThreadStates) => {
+ // Could be absent if:
+ // * this thread state wasn't woken up (e.g. it is a running slice).
+ // * the wakeup is from an interrupt during the idle process (which
+ // isn't populated in thread_state).
+ // * at the start of the trace, before all per-cpu scheduling is known.
+ const hasWakerId = related.waker !== undefined;
+ // Interrupt context for the wakeups is absent from older traces.
+ const hasInterruptCtx = related.wakerInterruptCtx !== undefined;
+
+ if (!hasWakerId && !hasInterruptCtx) {
+ return null;
+ }
+ if (related.wakerInterruptCtx) {
+ return m(TreeNode, {
+ left: 'Woken by',
+ right: `Interrupt`,
+ });
+ }
+ return (
+ related.waker &&
+ m(TreeNode, {
+ left: hasInterruptCtx ? 'Woken by' : 'Woken by (maybe interrupt)',
+ right: renderRef(
+ related.waker,
+ getFullThreadName(related.waker.thread),
+ ),
+ })
+ );
+ };
+
+ const renderWakees = (related: RelatedThreadStates) => {
+ if (related.wakee === undefined || related.wakee.length == 0) {
+ return null;
+ }
+ const hasInterruptCtx = related.wakee[0].wakerInterruptCtx !== undefined;
+ return m(
+ TreeNode,
+ {
+ left: hasInterruptCtx
+ ? 'Woken threads'
+ : 'Woken threads (maybe interrupt)',
+ },
+ related.wakee.map((state) =>
+ m(TreeNode, {
+ left: m(Timestamp, {
+ ts: state.ts,
+ display: `+${renderDuration(state.ts - startTs)}`,
+ }),
+ right: renderRef(state, getFullThreadName(state.thread)),
+ }),
+ ),
+ );
+ };
+
return [
m(
Tree,
- this.relatedStates.waker &&
- m(TreeNode, {
- left: 'Waker',
- right: renderRef(
- this.relatedStates.waker,
- getFullThreadName(this.relatedStates.waker.thread),
- ),
- }),
this.relatedStates.prev &&
m(TreeNode, {
left: 'Previous state',
@@ -288,26 +320,8 @@
nameForNextOrPrev(this.relatedStates.next),
),
}),
- this.relatedStates.wakee &&
- this.relatedStates.wakee.length > 0 &&
- m(
- TreeNode,
- {
- left: 'Woken threads',
- },
- this.relatedStates.wakee.map((state) =>
- m(TreeNode, {
- left: m(Timestamp, {
- ts: state.ts,
- display: [
- 'Start+',
- m(DurationWidget, {dur: Time.sub(state.ts, startTs)}),
- ],
- }),
- right: renderRef(state, getFullThreadName(state.thread)),
- }),
- ),
- ),
+ renderWaker(this.relatedStates),
+ renderWakees(this.relatedStates),
),
globals.commandManager.hasCommand(CRITICAL_PATH_LITE_CMD) &&
m(Button, {
diff --git a/ui/src/frontend/tickmark_panel.ts b/ui/src/frontend/tickmark_panel.ts
index 2f4d017..c330d1c 100644
--- a/ui/src/frontend/tickmark_panel.ts
+++ b/ui/src/frontend/tickmark_panel.ts
@@ -24,7 +24,7 @@
createSearchOverviewTrack,
SearchOverviewTrack,
} from './search_overview_track';
-import {TraceImpl} from '../core/app_trace_impl';
+import {TraceImpl} from '../core/trace_impl';
// We want to create the overview track only once per trace, but this
// class can be delete and re-instantiated when switching between pages via
@@ -75,7 +75,7 @@
if (size.width > 0 && timespan.duration > 0n) {
const maxMajorTicks = getMaxMajorTicks(size.width);
- const offset = globals.timestampOffset();
+ const offset = globals.trace.timeline.timestampOffset();
const tickGen = generateTicks(timespan, maxMajorTicks, offset);
for (const {type, time} of tickGen) {
const px = Math.floor(timescale.timeToPx(time));
diff --git a/ui/src/frontend/time_axis_panel.ts b/ui/src/frontend/time_axis_panel.ts
index 8ca13db..9253488 100644
--- a/ui/src/frontend/time_axis_panel.ts
+++ b/ui/src/frontend/time_axis_panel.ts
@@ -55,7 +55,7 @@
}
private renderOffsetTimestamp(ctx: CanvasRenderingContext2D): void {
- const offset = globals.timestampOffset();
+ const offset = globals.trace.timeline.timestampOffset();
switch (timestampFormat()) {
case TimestampFormat.TraceNs:
case TimestampFormat.TraceNsLocale:
@@ -91,7 +91,7 @@
right: size.width,
});
const timespan = visibleWindow.toTimeSpan();
- const offset = globals.timestampOffset();
+ const offset = globals.trace.timeline.timestampOffset();
// Draw time axis.
if (size.width > 0 && timespan.duration > 0n) {
@@ -101,7 +101,7 @@
if (type === TickType.MAJOR) {
const position = Math.floor(timescale.timeToPx(time));
ctx.fillRect(position, 0, 1, size.height);
- const domainTime = globals.toDomainTime(time);
+ const domainTime = globals.trace.timeline.toDomainTime(time);
renderTimestamp(ctx, domainTime, position + 5, 10, MIN_PX_PER_STEP);
}
}
diff --git a/ui/src/frontend/time_selection_panel.ts b/ui/src/frontend/time_selection_panel.ts
index cbfeaae..db808aa 100644
--- a/ui/src/frontend/time_selection_panel.ts
+++ b/ui/src/frontend/time_selection_panel.ts
@@ -162,7 +162,7 @@
if (size.width > 0 && timespan.duration > 0n) {
const maxMajorTicks = getMaxMajorTicks(size.width);
- const offset = globals.timestampOffset();
+ const offset = globals.trace.timeline.timestampOffset();
const tickGen = generateTicks(timespan, maxMajorTicks, offset);
for (const {type, time} of tickGen) {
const px = Math.floor(timescale.timeToPx(time));
@@ -184,19 +184,19 @@
this.renderSpan(ctx, timescale, size, start, end);
}
- if (globals.state.hoverCursorTimestamp !== -1n) {
+ if (globals.trace.timeline.hoverCursorTimestamp !== undefined) {
this.renderHover(
ctx,
timescale,
size,
- globals.state.hoverCursorTimestamp,
+ globals.trace.timeline.hoverCursorTimestamp,
);
}
for (const note of globals.noteManager.notes.values()) {
const noteIsSelected =
selection.kind === 'note' && selection.id === note.id;
- if (note.noteType === 'SPAN' && !noteIsSelected) {
+ if (note.noteType === 'SPAN' && noteIsSelected) {
this.renderSpan(ctx, timescale, size, note.start, note.end);
}
}
@@ -211,7 +211,7 @@
ts: time,
) {
const xPos = Math.floor(timescale.timeToPx(ts));
- const domainTime = globals.toDomainTime(ts);
+ const domainTime = globals.trace.timeline.toDomainTime(ts);
const label = stringifyTimestamp(domainTime);
drawIBar(ctx, xPos, this.getBBoxFromSize(size), label);
}
diff --git a/ui/src/frontend/topbar.ts b/ui/src/frontend/topbar.ts
index 80c3cc6..76f385e 100644
--- a/ui/src/frontend/topbar.ts
+++ b/ui/src/frontend/topbar.ts
@@ -20,24 +20,22 @@
import {Popup, PopupPosition} from '../widgets/popup';
import {assertFalse} from '../base/logging';
import {OmniboxMode} from '../core/omnibox_manager';
-import {AppImpl} from '../core/app_trace_impl';
+import {AppImpl} from '../core/app_impl';
+import {Trace, TraceAttrs} from '../public/trace';
export const DISMISSED_PANNING_HINT_KEY = 'dismissedPanningHint';
-class Progress implements m.ClassComponent {
- view(_vnode: m.Vnode): m.Children {
- const classes = classNames(this.isLoading() && 'progress-anim');
+class Progress implements m.ClassComponent<TraceAttrs> {
+ view({attrs}: m.CVnode<TraceAttrs>): m.Children {
+ const engine = attrs.trace.engine;
+ const engineCfg = globals.getCurrentEngine();
+ const isLoading =
+ (engineCfg && !engineCfg.ready) ||
+ engine.numRequestsPending > 0 ||
+ taskTracker.hasPendingTasks();
+ const classes = classNames(isLoading && 'progress-anim');
return m('.progress', {class: classes});
}
-
- private isLoading(): boolean {
- const engine = globals.getCurrentEngine();
- return (
- (engine && !engine.ready) ||
- globals.numQueuedQueries > 0 ||
- taskTracker.hasPendingTasks()
- );
- }
}
class HelpPanningNotification implements m.ClassComponent {
@@ -75,12 +73,15 @@
}
}
-class TraceErrorIcon implements m.ClassComponent {
- view() {
+class TraceErrorIcon implements m.ClassComponent<TraceAttrs> {
+ private tracePopupErrorDismissed = false;
+
+ view({attrs}: m.CVnode<TraceAttrs>) {
+ const trace = attrs.trace;
if (globals.embeddedMode) return;
const mode = AppImpl.instance.omnibox.mode;
- const errors = globals.traceErrors;
+ const errors = trace.traceInfo.importErrors;
if (
(!Boolean(errors) && !globals.metricError) ||
mode === OmniboxMode.Command
@@ -96,11 +97,11 @@
Popup,
{
trigger: m('.popup-trigger'),
- isOpen: globals.showTraceErrorPopup,
+ isOpen: !this.tracePopupErrorDismissed,
position: PopupPosition.Left,
onChange: (shouldOpen: boolean) => {
assertFalse(shouldOpen);
- globals.showTraceErrorPopup = false;
+ this.tracePopupErrorDismissed = true;
},
},
m('.error-popup', 'Data-loss/import error. Click for more info.'),
@@ -122,6 +123,7 @@
export interface TopbarAttrs {
omnibox: m.Children;
+ trace?: Trace;
}
export class Topbar implements m.ClassComponent<TopbarAttrs> {
@@ -131,9 +133,9 @@
'.topbar',
{class: globals.state.sidebarVisible ? '' : 'hide-sidebar'},
omnibox,
- m(Progress),
+ attrs.trace && m(Progress, {trace: attrs.trace}),
m(HelpPanningNotification),
- m(TraceErrorIcon),
+ attrs.trace && m(TraceErrorIcon, {trace: attrs.trace}),
);
}
}
diff --git a/ui/src/frontend/trace_attrs.ts b/ui/src/frontend/trace_attrs.ts
index 4a34aeb..785b327 100644
--- a/ui/src/frontend/trace_attrs.ts
+++ b/ui/src/frontend/trace_attrs.ts
@@ -14,12 +14,12 @@
import m from 'mithril';
import {assertExists} from '../base/logging';
-import {TraceUrlSource} from '../public/trace_info';
+import {TraceUrlSource} from '../public/trace_source';
import {createPermalink} from './permalink';
import {showModal} from '../widgets/modal';
import {onClickCopy} from './clipboard';
import {globals} from './globals';
-import {AppImpl} from '../core/app_trace_impl';
+import {AppImpl} from '../core/app_impl';
export function isShareable() {
return globals.isInternalUser && isDownloadable();
diff --git a/ui/src/frontend/trace_url_handler.ts b/ui/src/frontend/trace_url_handler.ts
index 43abedb..3dc2d80 100644
--- a/ui/src/frontend/trace_url_handler.ts
+++ b/ui/src/frontend/trace_url_handler.ts
@@ -21,7 +21,7 @@
import {globals} from './globals';
import {Route, Router} from './router';
import {taskTracker} from './task_tracker';
-import {AppImpl} from '../core/app_trace_impl';
+import {AppImpl} from '../core/app_impl';
function getCurrentTraceUrl(): undefined | string {
const source = AppImpl.instance.trace?.traceInfo.source;
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index d9ca823..831242b 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -31,6 +31,7 @@
import {globals} from './globals';
import {Panel} from './panel_container';
import {TrackWidget} from '../widgets/track_widget';
+import {raf} from '../core/raf_scheduler';
const SHOW_TRACK_DETAILS_BUTTON = featureFlags.register({
id: 'showTrackDetailsButton',
@@ -117,12 +118,15 @@
...pos,
timescale,
});
+ raf.scheduleRedraw();
},
onTrackContentMouseOut: () => {
trackRenderer?.track.onMouseOut?.();
+ raf.scheduleRedraw();
},
onTrackContentClick: (pos, bounds) => {
const timescale = getTimescaleForBounds(bounds);
+ raf.scheduleRedraw();
return (
trackRenderer?.track.onMouseClick?.({
...pos,
diff --git a/ui/src/frontend/ui_main.ts b/ui/src/frontend/ui_main.ts
index e11d9f4..5912189 100644
--- a/ui/src/frontend/ui_main.ts
+++ b/ui/src/frontend/ui_main.ts
@@ -29,7 +29,7 @@
import {Command} from '../public/command';
import {HotkeyConfig, HotkeyContext} from '../widgets/hotkey_context';
import {HotkeyGlyphs} from '../widgets/hotkey_glyphs';
-import {maybeRenderFullscreenModalDialog} from '../widgets/modal';
+import {maybeRenderFullscreenModalDialog, showModal} from '../widgets/modal';
import {onClickCopy} from './clipboard';
import {CookieConsent} from './cookie_consent';
import {globals} from './globals';
@@ -45,7 +45,8 @@
import {PromptOption} from '../public/omnibox';
import {DisposableStack} from '../base/disposable_stack';
import {Spinner} from '../widgets/spinner';
-import {AppImpl, TraceImpl} from '../core/app_trace_impl';
+import {TraceImpl} from '../core/trace_impl';
+import {AppImpl} from '../core/app_impl';
import {NotesEditorTab} from './notes_panel';
import {NotesListEditor} from './notes_list_editor';
import {getTimeSpanOfSelectionOrVisibleWindow} from '../public/utils';
@@ -128,6 +129,8 @@
if (trace === undefined) return;
assertTrue(trace instanceof TraceImpl);
this.trace = trace;
+ document.title = `${trace.traceInfo.traceTitle || 'Trace'} - Perfetto UI`;
+ this.maybeShowJsonWarning();
// Register the aggregation tabs.
this.trash.use(new AggregationsTabs(trace));
@@ -674,9 +677,10 @@
{hotkeys},
m(
'main',
- m(Sidebar),
+ m(Sidebar, {trace: this.trace}),
m(Topbar, {
omnibox: this.renderOmnibox(),
+ trace: this.trace,
}),
m(Alerts),
children,
@@ -733,4 +737,44 @@
AppImpl.instance.omnibox.clearFocusFlag();
}
}
+
+ private async maybeShowJsonWarning() {
+ // Show warning if the trace is in JSON format.
+ const isJsonTrace = this.trace?.traceInfo.traceType === 'json';
+ const SHOWN_JSON_WARNING_KEY = 'shownJsonWarning';
+
+ if (
+ !isJsonTrace ||
+ window.localStorage.getItem(SHOWN_JSON_WARNING_KEY) === 'true' ||
+ globals.embeddedMode
+ ) {
+ // When in embedded mode, the host app will control which trace format
+ // it passes to Perfetto, so we don't need to show this warning.
+ return;
+ }
+
+ // Save that the warning has been shown. Value is irrelevant since only
+ // the presence of key is going to be checked.
+ window.localStorage.setItem(SHOWN_JSON_WARNING_KEY, 'true');
+
+ showModal({
+ title: 'Warning',
+ content: m(
+ 'div',
+ m(
+ 'span',
+ 'Perfetto UI features are limited for JSON traces. ',
+ 'We recommend recording ',
+ m(
+ 'a',
+ {href: 'https://perfetto.dev/docs/quickstart/chrome-tracing'},
+ 'proto-format traces',
+ ),
+ ' from Chrome.',
+ ),
+ m('br'),
+ ),
+ buttons: [],
+ });
+ }
}
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 0606877..8a9af07 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -441,7 +441,7 @@
if (size.width > 0 && timespan.duration > 0n) {
const maxMajorTicks = getMaxMajorTicks(size.width);
- const offset = globals.timestampOffset();
+ const offset = globals.trace.timeline.timestampOffset();
for (const {type, time} of generateTicks(timespan, maxMajorTicks, offset)) {
const px = Math.floor(timescale.timeToPx(time));
if (type === TickType.MAJOR) {
@@ -459,11 +459,11 @@
timescale: TimeScale,
size: Size2D,
) {
- if (globals.state.hoverCursorTimestamp !== -1n) {
+ if (globals.trace.timeline.hoverCursorTimestamp !== undefined) {
drawVerticalLineAtTime(
ctx,
timescale,
- globals.state.hoverCursorTimestamp,
+ globals.trace.timeline.hoverCursorTimestamp,
size.height,
`#344596`,
);
diff --git a/ui/src/frontend/widgets/process.ts b/ui/src/frontend/widgets/process.ts
index e38d9b5..0738005 100644
--- a/ui/src/frontend/widgets/process.ts
+++ b/ui/src/frontend/widgets/process.ts
@@ -31,7 +31,7 @@
sqlIdRegistry,
} from './sql/details/sql_ref_renderer_registry';
import {asUpid} from '../../trace_processor/sql_utils/core_types';
-import {AppImpl} from '../../core/app_trace_impl';
+import {AppImpl} from '../../core/app_impl';
export function showProcessDetailsMenuItem(
upid: Upid,
diff --git a/ui/src/frontend/widgets/sql/table/column.ts b/ui/src/frontend/widgets/sql/table/column.ts
index 8bbdf70..9af4cac 100644
--- a/ui/src/frontend/widgets/sql/table/column.ts
+++ b/ui/src/frontend/widgets/sql/table/column.ts
@@ -43,6 +43,15 @@
source: SourceTable;
};
+// List of columns of args, corresponding to arg values, which cause a short-form of the ID to be generated.
+// (e.g. arg_set_id[foo].int instead of args[arg_set_id,key=foo].int_value).
+const ARG_COLUMN_TO_SUFFIX: {[key: string]: string} = {
+ display_value: '',
+ int_value: '.int',
+ string_value: '.str',
+ real_value: '.real',
+};
+
// A unique identifier for the SQL column.
export function sqlColumnId(column: SqlColumn): string {
if (typeof column === 'string') {
@@ -54,13 +63,13 @@
}
// Special case: args lookup. For it, we can use a simpler representation (i.e. `arg_set_id[key]`).
if (
- column.column === 'display_value' &&
+ column.column in ARG_COLUMN_TO_SUFFIX &&
column.source.table === 'args' &&
arrayEquals(Object.keys(column.source.joinOn).sort(), ['arg_set_id', 'key'])
) {
const key = column.source.joinOn['key'];
const argSetId = column.source.joinOn['arg_set_id'];
- return `${sqlColumnId(argSetId)}[${sqlColumnId(key)}]`;
+ return `${sqlColumnId(argSetId)}[${sqlColumnId(key)}]${ARG_COLUMN_TO_SUFFIX[column.column]}`;
}
// Otherwise, we need to list all the join constraints.
const lookup = Object.entries(column.source.joinOn)
@@ -137,6 +146,9 @@
// Sometimes to display an interactive cell more than a single value is needed (e.g. "time range" corresponds to (ts, dur) pair. While we want to show the duration, we would want to highlight the interval on hover, for which both timestamp and duration are needed.
dependentColumns?(): {[key: string]: SqlColumn};
+ // The set of underlying sql columns that should be sorted when this column is sorted.
+ sortColumns?(): SqlColumn[];
+
// Render a table cell. `value` corresponds to the fetched SQL value for the primary column, `dependentColumns` are the fetched values for the dependent columns.
abstract renderCell(
value: SqlValue,
diff --git a/ui/src/frontend/widgets/sql/table/render_cell_utils.ts b/ui/src/frontend/widgets/sql/table/render_cell_utils.ts
index c00044e..459baf2 100644
--- a/ui/src/frontend/widgets/sql/table/render_cell_utils.ts
+++ b/ui/src/frontend/widgets/sql/table/render_cell_utils.ts
@@ -96,7 +96,7 @@
filterOptionMenuItem(
option,
c,
- (cols) => `${cols[0]} ${FILTER_OPTION_TO_OP[option]}`,
+ (cols) => `${cols[0]} ${FILTER_OPTION_TO_OP[option].op}`,
tableManager,
),
);
@@ -107,7 +107,7 @@
option,
c,
(cols) =>
- `${cols[0]} ${FILTER_OPTION_TO_OP[option]} ${sqliteString(value)}`,
+ `${cols[0]} ${FILTER_OPTION_TO_OP[option].op} ${sqliteString(value)}`,
tableManager,
),
);
@@ -117,7 +117,7 @@
filterOptionMenuItem(
option,
c,
- (cols) => `${cols[0]} ${FILTER_OPTION_TO_OP[option]} ${value}`,
+ (cols) => `${cols[0]} ${FILTER_OPTION_TO_OP[option].op} ${value}`,
tableManager,
),
);
@@ -157,7 +157,7 @@
return result;
}
-function displayValue(value: SqlValue): m.Child {
+export function displayValue(value: SqlValue): m.Child {
if (value === null) {
return m('i', 'NULL');
}
diff --git a/ui/src/frontend/widgets/sql/table/state.ts b/ui/src/frontend/widgets/sql/table/state.ts
index cfc85a9..1f513b8 100644
--- a/ui/src/frontend/widgets/sql/table/state.ts
+++ b/ui/src/frontend/widgets/sql/table/state.ts
@@ -21,6 +21,7 @@
SqlColumn,
sqlColumnId,
TableColumn,
+ tableColumnId,
} from './column';
import {buildSqlQuery} from './query_builder';
import {raf} from '../../../../core/raf_scheduler';
@@ -76,7 +77,10 @@
// Columns currently displayed to the user. All potential columns can be found `this.table.columns`.
private columns: TableColumn[];
private filters: Filter[];
- private orderBy: ColumnOrderClause[];
+ private orderBy: {
+ column: TableColumn;
+ direction: SortDirection;
+ }[];
private offset = 0;
private request: Request;
private data?: Data;
@@ -85,12 +89,15 @@
constructor(
readonly trace: Trace,
readonly config: SqlTableDescription,
- args?: {
+ private readonly args?: {
initialColumns?: TableColumn[];
additionalColumns?: TableColumn[];
imports?: string[];
filters?: Filter[];
- orderBy?: ColumnOrderClause[];
+ orderBy?: {
+ column: TableColumn;
+ direction: SortDirection;
+ }[];
},
) {
this.additionalImports = args?.imports || [];
@@ -122,12 +129,21 @@
}
}
- this.orderBy = [];
+ this.orderBy = args?.orderBy ?? [];
this.request = this.buildRequest();
this.reload();
}
+ clone(): SqlTableState {
+ return new SqlTableState(this.trace, this.config, {
+ initialColumns: this.columns,
+ imports: this.args?.imports,
+ filters: this.filters,
+ orderBy: this.orderBy,
+ });
+ }
+
private getSQLImports() {
const tableImports = this.config.imports || [];
return [...tableImports, ...this.additionalImports]
@@ -149,7 +165,7 @@
table: this.config.name,
columns,
filters: this.filters,
- orderBy: this.orderBy,
+ orderBy: this.getOrderedBy(),
});
}
@@ -365,10 +381,10 @@
return this.filters;
}
- sortBy(clause: ColumnOrderClause) {
+ sortBy(clause: {column: TableColumn; direction: SortDirection}) {
// Remove previous sort by the same column.
this.orderBy = this.orderBy.filter(
- (c) => !isSqlColumnEqual(c.column, clause.column),
+ (c) => tableColumnId(c.column) != tableColumnId(clause.column),
);
// Add the new sort clause to the front, so we effectively stable-sort the
// data currently displayed to the user.
@@ -383,14 +399,23 @@
isSortedBy(column: TableColumn): SortDirection | undefined {
if (this.orderBy.length === 0) return undefined;
- if (!isSqlColumnEqual(this.orderBy[0].column, column.primaryColumn())) {
+ if (tableColumnId(this.orderBy[0].column) !== tableColumnId(column)) {
return undefined;
}
return this.orderBy[0].direction;
}
getOrderedBy(): ColumnOrderClause[] {
- return this.orderBy;
+ const result: ColumnOrderClause[] = [];
+ for (const orderBy of this.orderBy) {
+ const sortColumns = orderBy.column.sortColumns?.() ?? [
+ orderBy.column.primaryColumn(),
+ ];
+ for (const column of sortColumns) {
+ result.push({column, direction: orderBy.direction});
+ }
+ }
+ return result;
}
addColumn(column: TableColumn, index: number) {
@@ -404,12 +429,24 @@
// We can only filter by the visibile columns to avoid confusing the user,
// so we remove order by clauses that refer to the hidden column.
this.orderBy = this.orderBy.filter(
- (c) => !isSqlColumnEqual(c.column, column.primaryColumn()),
+ (c) => tableColumnId(c.column) !== tableColumnId(column),
);
// TODO(altimin): we can avoid the fetch here if the orderBy hasn't changed.
this.reload({offset: 'keep'});
}
+ moveColumn(fromIndex: number, toIndex: number) {
+ if (fromIndex === toIndex) return;
+ const column = this.columns[fromIndex];
+ this.columns.splice(fromIndex, 1);
+ if (fromIndex < toIndex) {
+ // We have deleted a column, therefore we need to adjust the target index.
+ --toIndex;
+ }
+ this.columns.splice(toIndex, 0, column);
+ raf.scheduleFullRedraw();
+ }
+
getSelectedColumns(): TableColumn[] {
return this.columns;
}
diff --git a/ui/src/frontend/widgets/sql/table/state_unittest.ts b/ui/src/frontend/widgets/sql/table/state_unittest.ts
index c19d6e6..c4985b0 100644
--- a/ui/src/frontend/widgets/sql/table/state_unittest.ts
+++ b/ui/src/frontend/widgets/sql/table/state_unittest.ts
@@ -76,7 +76,7 @@
// Sort by name column and verify that it is sorted by.
state.sortBy({
- column: nameColumn.primaryColumn(),
+ column: nameColumn,
direction: 'ASC',
});
expect(state.isSortedBy(idColumn)).toBe(undefined);
@@ -84,7 +84,7 @@
// Sort by the same column in the opposite direction.
state.sortBy({
- column: nameColumn.primaryColumn(),
+ column: nameColumn,
direction: 'DESC',
});
expect(state.isSortedBy(idColumn)).toBe(undefined);
@@ -92,7 +92,7 @@
// Sort by the id column.
state.sortBy({
- column: idColumn.primaryColumn(),
+ column: idColumn,
direction: 'ASC',
});
expect(state.isSortedBy(idColumn)).toBe('ASC');
diff --git a/ui/src/frontend/widgets/sql/table/table.ts b/ui/src/frontend/widgets/sql/table/table.ts
index bdbcef6..7aeafd0 100644
--- a/ui/src/frontend/widgets/sql/table/table.ts
+++ b/ui/src/frontend/widgets/sql/table/table.ts
@@ -37,7 +37,7 @@
SqlValue,
} from '../../../../trace_processor/query_result';
import {Anchor} from '../../../../widgets/anchor';
-import {BasicTable} from '../../../../widgets/basic_table';
+import {BasicTable, ReorderableColumns} from '../../../../widgets/basic_table';
import {Spinner} from '../../../../widgets/spinner';
import {ArgumentSelector} from './argument_selector';
@@ -62,8 +62,7 @@
const sqlValue = row[columns[sqlColumnId(column.primaryColumn())]];
const additionalValues: {[key: string]: SqlValue} = {};
- const dependentColumns =
- column.dependentColumns !== undefined ? column.dependentColumns() : {};
+ const dependentColumns = column.dependentColumns?.() ?? {};
for (const [key, col] of Object.entries(dependentColumns)) {
additionalValues[key] = row[columns[sqlColumnId(col)]];
}
@@ -276,7 +275,7 @@
icon: Icons.SortedDesc,
onclick: () => {
this.state.sortBy({
- column: column.primaryColumn(),
+ column: column,
direction: 'DESC',
});
},
@@ -287,7 +286,7 @@
icon: Icons.SortedAsc,
onclick: () => {
this.state.sortBy({
- column: column.primaryColumn(),
+ column: column,
direction: 'ASC',
});
},
@@ -342,18 +341,26 @@
view() {
const rows = this.state.getDisplayedRows();
+ const columns = this.state.getSelectedColumns();
+ const columnDescriptors = columns.map((column, i) => {
+ return {
+ title: this.renderColumnHeader(column, i),
+ render: (row: Row) => renderCell(column, row, this.state),
+ };
+ });
+
return [
m('div', this.renderFilters()),
m(
BasicTable<Row>,
{
data: rows,
- columns: this.state.getSelectedColumns().map((column, i) => {
- return {
- title: this.renderColumnHeader(column, i),
- render: (row: Row) => renderCell(column, row, this.state),
- };
- }),
+ columns: [
+ new ReorderableColumns(
+ columnDescriptors,
+ (from: number, to: number) => this.state.moveColumn(from, to),
+ ),
+ ],
},
this.state.isLoading() && m(Spinner),
this.state.getQueryError() !== undefined &&
diff --git a/ui/src/frontend/widgets/sql/table/well_known_columns.ts b/ui/src/frontend/widgets/sql/table/well_known_columns.ts
index b360824..a4e52bc 100644
--- a/ui/src/frontend/widgets/sql/table/well_known_columns.ts
+++ b/ui/src/frontend/widgets/sql/table/well_known_columns.ts
@@ -46,6 +46,7 @@
TableManager,
} from './column';
import {
+ displayValue,
getStandardContextMenuItems,
getStandardFilters,
renderStandardCell,
@@ -1018,6 +1019,109 @@
}
}
+class ArgColumn extends TableColumn {
+ private displayValue: SqlColumn;
+ private stringValue: SqlColumn;
+ private intValue: SqlColumn;
+ private realValue: SqlColumn;
+
+ constructor(
+ private argSetId: SqlColumn,
+ private key: string,
+ ) {
+ super();
+
+ const argTable: SourceTable = {
+ table: 'args',
+ joinOn: {
+ arg_set_id: argSetId,
+ key: sqliteString(key),
+ },
+ };
+
+ this.displayValue = {
+ column: 'display_value',
+ source: argTable,
+ };
+ this.stringValue = {
+ column: 'string_value',
+ source: argTable,
+ };
+ this.intValue = {
+ column: 'int_value',
+ source: argTable,
+ };
+ this.realValue = {
+ column: 'real_value',
+ source: argTable,
+ };
+ }
+
+ override primaryColumn(): SqlColumn {
+ return this.displayValue;
+ }
+
+ override sortColumns(): SqlColumn[] {
+ return [this.stringValue, this.intValue, this.realValue];
+ }
+
+ override dependentColumns() {
+ return {
+ stringValue: this.stringValue,
+ intValue: this.intValue,
+ realValue: this.realValue,
+ };
+ }
+
+ getTitle() {
+ return `${sqlColumnId(this.argSetId)}[${this.key}]`;
+ }
+
+ renderCell(
+ value: SqlValue,
+ tableManager: TableManager,
+ dependentColumns: {[key: string]: SqlValue},
+ ): m.Children {
+ const strValue = dependentColumns['stringValue'];
+ const intValue = dependentColumns['intValue'];
+ const realValue = dependentColumns['realValue'];
+
+ let contextMenuItems: m.Child[] = [];
+ if (strValue !== null) {
+ contextMenuItems = getStandardContextMenuItems(
+ strValue,
+ this.stringValue,
+ tableManager,
+ );
+ } else if (intValue !== null) {
+ contextMenuItems = getStandardContextMenuItems(
+ intValue,
+ this.intValue,
+ tableManager,
+ );
+ } else if (realValue !== null) {
+ contextMenuItems = getStandardContextMenuItems(
+ realValue,
+ this.realValue,
+ tableManager,
+ );
+ } else {
+ contextMenuItems = getStandardContextMenuItems(
+ value,
+ this.displayValue,
+ tableManager,
+ );
+ }
+ return m(
+ PopupMenu2,
+ {
+ trigger: m(Anchor, displayValue(value)),
+ },
+ ...contextMenuItems,
+ );
+ }
+}
+
export class ArgSetColumnSet extends TableColumnSet {
constructor(
private column: SqlColumn,
@@ -1067,8 +1171,5 @@
}
export function argTableColumn(argSetId: SqlColumn, key: string) {
- return new StandardColumn(argSqlColumn(argSetId, key), {
- title: `${sqlColumnId(argSetId)}[${key}]`,
- alias: `arg_${key.replace(/[^a-zA-Z0-9_]/g, '__')}`,
- });
+ return new ArgColumn(argSetId, key);
}
diff --git a/ui/src/frontend/widgets/thread.ts b/ui/src/frontend/widgets/thread.ts
index ea61cd9..5d8018d 100644
--- a/ui/src/frontend/widgets/thread.ts
+++ b/ui/src/frontend/widgets/thread.ts
@@ -31,7 +31,7 @@
} from './sql/details/sql_ref_renderer_registry';
import {asUtid} from '../../trace_processor/sql_utils/core_types';
import {Utid} from '../../trace_processor/sql_utils/core_types';
-import {AppImpl} from '../../core/app_trace_impl';
+import {AppImpl} from '../../core/app_impl';
export function showThreadDetailsMenuItem(
utid: Utid,
diff --git a/ui/src/frontend/widgets/timestamp.ts b/ui/src/frontend/widgets/timestamp.ts
index a53a166..d27b95b 100644
--- a/ui/src/frontend/widgets/timestamp.ts
+++ b/ui/src/frontend/widgets/timestamp.ts
@@ -16,7 +16,6 @@
import {copyToClipboard} from '../../base/clipboard';
import {Icons} from '../../base/semantic_icons';
import {time, Time} from '../../base/time';
-import {Actions} from '../../common/actions';
import {
setTimestampFormat,
TimestampFormat,
@@ -49,12 +48,10 @@
Anchor,
{
onmouseover: () => {
- globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
+ globals.trace.timeline.hoverCursorTimestamp = ts;
},
onmouseout: () => {
- globals.dispatch(
- Actions.setHoverCursorTimestamp({ts: Time.INVALID}),
- );
+ globals.trace.timeline.hoverCursorTimestamp = undefined;
},
},
attrs.display ?? renderTimestamp(ts),
@@ -105,7 +102,7 @@
function renderTimestamp(time: time): m.Children {
const fmt = timestampFormat();
- const domainTime = globals.toDomainTime(time);
+ const domainTime = globals.trace.timeline.toDomainTime(time);
switch (fmt) {
case TimestampFormat.UTC:
case TimestampFormat.TraceTz:
diff --git a/ui/src/plugins/org.kernel.SuspendResumeLatency/OWNERS b/ui/src/plugins/org.kernel.SuspendResumeLatency/OWNERS
new file mode 100644
index 0000000..3757ab5
--- /dev/null
+++ b/ui/src/plugins/org.kernel.SuspendResumeLatency/OWNERS
@@ -0,0 +1,3 @@
+isaacmanjarres@google.com
+saravanak@google.com
+vilasbhat@google.com
diff --git a/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts b/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts
new file mode 100644
index 0000000..e816465
--- /dev/null
+++ b/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts
@@ -0,0 +1,96 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {NUM, STR_NULL} from '../../trace_processor/query_result';
+import {AsyncSliceTrack} from '../../core_plugins/async_slices/async_slice_track';
+import {NewTrackArgs} from '../../frontend/track';
+import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
+import {Trace} from '../../public/trace';
+import {TrackNode} from '../../public/workspace';
+import {ASYNC_SLICE_TRACK_KIND} from '../../public/track_kinds';
+import {SuspendResumeDetailsPanel} from './suspend_resume_details';
+import {Slice} from '../../public/track';
+import {OnSliceClickArgs} from '../../frontend/base_slice_track';
+import {globals} from '../../frontend/globals';
+
+// SuspendResumeSliceTrack exists so as to override the `onSliceClick` function
+// in AsyncSliceTrack.
+class SuspendResumeSliceTrack extends AsyncSliceTrack {
+ constructor(args: NewTrackArgs, maxDepth: number, trackIds: number[]) {
+ super(args, maxDepth, trackIds);
+ }
+
+ onSliceClick(args: OnSliceClickArgs<Slice>) {
+ globals.selectionManager.selectTrackEvent(this.uri, args.slice.id);
+ }
+}
+
+class SuspendResumeLatency implements PerfettoPlugin {
+ async onTraceLoad(ctx: Trace): Promise<void> {
+ const {engine} = ctx;
+ const rawGlobalAsyncTracks = await engine.query(`
+ with global_tracks_grouped as (
+ select
+ name,
+ group_concat(distinct t.id) as trackIds,
+ count() as trackCount
+ from track t
+ where t.name = "Suspend/Resume Latency"
+ )
+ select
+ t.trackIds as trackIds,
+ case
+ when
+ t.trackCount > 0
+ then
+ __max_layout_depth(t.trackCount, t.trackIds)
+ else 0
+ end as maxDepth
+ from global_tracks_grouped t
+ `);
+ const it = rawGlobalAsyncTracks.iter({
+ trackIds: STR_NULL,
+ maxDepth: NUM,
+ });
+ // If no Suspend/Resume tracks exist, then nothing to do.
+ if (it.trackIds == null) {
+ return;
+ }
+ const rawTrackIds = it.trackIds;
+ const trackIds = rawTrackIds.split(',').map((v) => Number(v));
+ const maxDepth = it.maxDepth;
+
+ const uri = `/suspend_resume_latency`;
+ const displayName = `Suspend/Resume Latency`;
+ ctx.tracks.registerTrack({
+ uri,
+ title: displayName,
+ tags: {
+ trackIds,
+ kind: ASYNC_SLICE_TRACK_KIND,
+ },
+ track: new SuspendResumeSliceTrack({uri, trace: ctx}, maxDepth, trackIds),
+ detailsPanel: new SuspendResumeDetailsPanel(ctx.engine),
+ });
+
+ // Display the track in the UI.
+ const track = new TrackNode({uri, title: displayName});
+ ctx.workspace.addChildInOrder(track);
+ }
+}
+
+export const plugin: PluginDescriptor = {
+ pluginId: 'org.kernel.SuspendResumeLatency',
+ plugin: SuspendResumeLatency,
+};
diff --git a/ui/src/plugins/org.kernel.SuspendResumeLatency/suspend_resume_details.ts b/ui/src/plugins/org.kernel.SuspendResumeLatency/suspend_resume_details.ts
new file mode 100644
index 0000000..4de8404
--- /dev/null
+++ b/ui/src/plugins/org.kernel.SuspendResumeLatency/suspend_resume_details.ts
@@ -0,0 +1,244 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {AsyncLimiter} from '../../base/async_limiter';
+import {Duration, duration, Time, time} from '../../base/time';
+import {raf} from '../../core/raf_scheduler';
+import {LONG, NUM, STR_NULL} from '../../trace_processor/query_result';
+import m from 'mithril';
+import {DetailsShell} from '../../widgets/details_shell';
+import {GridLayout} from '../../widgets/grid_layout';
+import {Section} from '../../widgets/section';
+import {Tree, TreeNode} from '../../widgets/tree';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {DurationWidget} from '../../frontend/widgets/duration';
+import {Anchor} from '../../widgets/anchor';
+import {globals} from '../../frontend/globals';
+import {scrollTo} from '../../public/scroll_helper';
+import {Engine} from '../../trace_processor/engine';
+import {TrackSelectionDetailsPanel} from '../../public/details_panel';
+import {THREAD_STATE_TRACK_KIND} from '../../public/track_kinds';
+
+interface SuspendResumeEventDetails {
+ ts: time;
+ dur: duration;
+ utid: number;
+ event_type: string;
+ device_name: string;
+ driver_name: string;
+ callback_phase: string;
+ thread_state_id: number;
+}
+
+export class SuspendResumeDetailsPanel implements TrackSelectionDetailsPanel {
+ private readonly queryLimiter = new AsyncLimiter();
+ private readonly engine: Engine;
+ private id?: number;
+ private suspendResumeEventDetails?: SuspendResumeEventDetails;
+
+ constructor(engine: Engine) {
+ this.engine = engine;
+ }
+
+ render(id: number): m.Children {
+ if (id !== this.id) {
+ this.id = id;
+ this.queryLimiter.schedule(async () => {
+ this.suspendResumeEventDetails = await loadSuspendResumeEventDetails(
+ this.engine,
+ id,
+ );
+ raf.scheduleFullRedraw();
+ });
+ }
+
+ return this.renderView();
+ }
+
+ private renderView() {
+ const eventDetails = this.suspendResumeEventDetails;
+ if (eventDetails) {
+ const threadInfo = globals.threads.get(eventDetails.utid);
+ if (!threadInfo) {
+ return null;
+ }
+ return m(
+ DetailsShell,
+ {title: 'Suspend / Resume Event'},
+ m(
+ GridLayout,
+ m(
+ Section,
+ {title: 'Properties'},
+ m(
+ Tree,
+ m(TreeNode, {
+ left: 'Device Name',
+ right: eventDetails.device_name,
+ }),
+ m(TreeNode, {
+ left: 'Start time',
+ right: m(Timestamp, {ts: eventDetails.ts}),
+ }),
+ m(TreeNode, {
+ left: 'Duration',
+ right: m(DurationWidget, {dur: eventDetails.dur}),
+ }),
+ m(TreeNode, {
+ left: 'Driver Name',
+ right: eventDetails.driver_name,
+ }),
+ m(TreeNode, {
+ left: 'Callback Phase',
+ right: eventDetails.callback_phase,
+ }),
+ m(TreeNode, {
+ left: 'Thread',
+ right: m(
+ Anchor,
+ {
+ icon: 'call_made',
+ onclick: () => {
+ this.goToThread(
+ eventDetails.utid,
+ eventDetails.ts,
+ eventDetails.thread_state_id,
+ );
+ },
+ },
+ `${threadInfo.threadName} [${threadInfo.tid}]`,
+ ),
+ }),
+ m(TreeNode, {left: 'Event Type', right: eventDetails.event_type}),
+ ),
+ ),
+ ),
+ );
+ } else {
+ return m(DetailsShell, {
+ title: 'Suspend / Resume Event',
+ description: 'Loading...',
+ });
+ }
+ }
+
+ isLoading(): boolean {
+ return this.suspendResumeEventDetails === undefined;
+ }
+
+ goToThread(utid: number, ts: time, threadStateId: number) {
+ const threadInfo = globals.threads.get(utid);
+ if (threadInfo === undefined) {
+ return;
+ }
+
+ const trackDescriptor = globals.trackManager.findTrack(
+ (td) =>
+ td.tags?.kind === THREAD_STATE_TRACK_KIND &&
+ td.tags?.utid === threadInfo.utid,
+ );
+
+ if (trackDescriptor) {
+ globals.selectionManager.selectSqlEvent(
+ 'thread_state',
+ threadStateId,
+ );
+ scrollTo({
+ track: {uri: trackDescriptor.uri, expandGroup: true},
+ time: {start: ts},
+ });
+ }
+ }
+}
+
+async function loadSuspendResumeEventDetails(
+ engine: Engine,
+ id: number,
+): Promise<SuspendResumeEventDetails> {
+ const suspendResumeDetailsQuery = `
+ SELECT ts,
+ dur,
+ EXTRACT_ARG(arg_set_id, 'utid') as utid,
+ EXTRACT_ARG(arg_set_id, 'event_type') as event_type,
+ EXTRACT_ARG(arg_set_id, 'device_name') as device_name,
+ EXTRACT_ARG(arg_set_id, 'driver_name') as driver_name,
+ EXTRACT_ARG(arg_set_id, 'callback_phase') as callback_phase
+ FROM slice
+ WHERE slice_id = ${id};
+ `;
+
+ const suspendResumeDetailsResult = await engine.query(
+ suspendResumeDetailsQuery,
+ );
+ const suspendResumeEventRow = suspendResumeDetailsResult.iter({
+ ts: LONG,
+ dur: LONG,
+ utid: NUM,
+ event_type: STR_NULL,
+ device_name: STR_NULL,
+ driver_name: STR_NULL,
+ callback_phase: STR_NULL,
+ });
+ if (!suspendResumeEventRow.valid()) {
+ return {
+ ts: Time.fromRaw(0n),
+ dur: Duration.fromRaw(0n),
+ utid: 0,
+ event_type: 'Error',
+ device_name: 'Error',
+ driver_name: 'Error',
+ callback_phase: 'Error',
+ thread_state_id: 0,
+ };
+ }
+
+ const threadStateQuery = `
+ SELECT t.id as threadStateId
+ FROM thread_state t
+ WHERE t.utid = ${suspendResumeEventRow.utid}
+ AND t.ts <= ${suspendResumeEventRow.ts}
+ AND t.ts + t.dur > ${suspendResumeEventRow.ts};
+ `;
+ const threadStateResult = await engine.query(threadStateQuery);
+ let threadStateId = 0;
+ if (threadStateResult.numRows() > 0) {
+ const threadStateRow = threadStateResult.firstRow({
+ threadStateId: NUM,
+ });
+ threadStateId = threadStateRow.threadStateId;
+ }
+
+ return {
+ ts: Time.fromRaw(suspendResumeEventRow.ts),
+ dur: Duration.fromRaw(suspendResumeEventRow.dur),
+ utid: suspendResumeEventRow.utid,
+ event_type:
+ suspendResumeEventRow.event_type !== null
+ ? suspendResumeEventRow.event_type
+ : 'N/A',
+ device_name:
+ suspendResumeEventRow.device_name !== null
+ ? suspendResumeEventRow.device_name
+ : 'N/A',
+ driver_name:
+ suspendResumeEventRow.driver_name !== null
+ ? suspendResumeEventRow.driver_name
+ : 'N/A',
+ callback_phase:
+ suspendResumeEventRow.callback_phase !== null
+ ? suspendResumeEventRow.callback_phase
+ : 'N/A',
+ thread_state_id: threadStateId,
+ };
+}
diff --git a/ui/src/public/timeline.ts b/ui/src/public/timeline.ts
index de7e65f..8eeb0f3 100644
--- a/ui/src/public/timeline.ts
+++ b/ui/src/public/timeline.ts
@@ -24,4 +24,13 @@
// A span representing the current viewport location.
readonly visibleWindow: HighPrecisionTimeSpan;
+
+ // Render a vertical line on the timeline at this timestamp.
+ hoverCursorTimestamp: time | undefined;
+
+ // Get the current timestamp offset.
+ timestampOffset(): time;
+
+ // Get a time in the current domain as specified by timestampOffset.
+ toDomainTime(ts: time): time;
}
diff --git a/ui/src/public/trace_info.ts b/ui/src/public/trace_info.ts
index 11d0cf6..1e4ccd5 100644
--- a/ui/src/public/trace_info.ts
+++ b/ui/src/public/trace_info.ts
@@ -13,6 +13,7 @@
// limitations under the License.
import {time} from '../base/time';
+import {TraceSource} from './trace_source';
export interface TraceInfo {
readonly source: TraceSource;
@@ -41,46 +42,12 @@
// The number of gpus in the trace
readonly gpuCount: number;
+
+ // The number of import/analysis errors present in the `stats` table.
+ readonly importErrors: number;
+
+ // The trace type inferred by TraceProcessor (e.g. 'proto', 'json, ...).
+ // See TraceTypeToString() in src/trace_processor/util/trace_type.cc for
+ // all the available types.
+ readonly traceType: string;
}
-
-export interface TraceFileSource {
- type: 'FILE';
- file: File;
-}
-
-export interface TraceArrayBufferSource {
- type: 'ARRAY_BUFFER';
- buffer: ArrayBuffer;
- title: string;
- url?: string;
- fileName?: string;
-
- // |uuid| is set only when loading via ?local_cache_key=1234. When set,
- // this matches global.state.traceUuid, with the exception of the following
- // time window: When a trace T1 is loaded and the user loads another trace T2,
- // this |uuid| will be == T2, but the globals.state.traceUuid will be
- // temporarily == T1 until T2 has been loaded (consistently to what happens
- // with all other state fields).
- uuid?: string;
- // if |localOnly| is true then the trace should not be shared or downloaded.
- localOnly?: boolean;
-
- // The set of extra args, keyed by plugin, that can be passed when opening the
- // trace via postMessge deep-linking. See post_message_handler.ts for details.
- pluginArgs?: {[pluginId: string]: {[key: string]: unknown}};
-}
-
-export interface TraceUrlSource {
- type: 'URL';
- url: string;
-}
-
-export interface TraceHttpRpcSource {
- type: 'HTTP_RPC';
-}
-
-export type TraceSource =
- | TraceFileSource
- | TraceArrayBufferSource
- | TraceUrlSource
- | TraceHttpRpcSource;
diff --git a/ui/src/public/trace_source.ts b/ui/src/public/trace_source.ts
new file mode 100644
index 0000000..152942d
--- /dev/null
+++ b/ui/src/public/trace_source.ts
@@ -0,0 +1,55 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export interface TraceFileSource {
+ type: 'FILE';
+ file: File;
+}
+
+export interface TraceArrayBufferSource {
+ type: 'ARRAY_BUFFER';
+ buffer: ArrayBuffer;
+ title: string;
+ url?: string;
+ fileName?: string;
+
+ // |uuid| is set only when loading via ?local_cache_key=1234. When set,
+ // this matches global.state.traceUuid, with the exception of the following
+ // time window: When a trace T1 is loaded and the user loads another trace T2,
+ // this |uuid| will be == T2, but the globals.state.traceUuid will be
+ // temporarily == T1 until T2 has been loaded (consistently to what happens
+ // with all other state fields).
+ uuid?: string;
+ // if |localOnly| is true then the trace should not be shared or downloaded.
+ localOnly?: boolean;
+
+ // The set of extra args, keyed by plugin, that can be passed when opening the
+ // trace via postMessge deep-linking. See post_message_handler.ts for details.
+ pluginArgs?: {[pluginId: string]: {[key: string]: unknown}};
+}
+
+export interface TraceUrlSource {
+ type: 'URL';
+ url: string;
+}
+
+export interface TraceHttpRpcSource {
+ type: 'HTTP_RPC';
+}
+
+export type TraceSource =
+ | TraceFileSource
+ | TraceArrayBufferSource
+ | TraceUrlSource
+ | TraceHttpRpcSource;
diff --git a/ui/src/test/independent_features.test.ts b/ui/src/test/independent_features.test.ts
index 329493c..c761ded 100644
--- a/ui/src/test/independent_features.test.ts
+++ b/ui/src/test/independent_features.test.ts
@@ -32,3 +32,12 @@
await pth.toggleTrackGroup(trackGroup);
await pth.waitForIdleAndScreenshot('track_with_debuggable_chip_expanded.png');
});
+
+test('trace error notification', async ({browser}) => {
+ const page = await browser.newPage();
+ const pth = new PerfettoTestHelper(page);
+ await pth.openTraceFile('clusterfuzz_14753');
+ await pth.waitForIdleAndScreenshot('error-icon.png', {
+ clip: {x: 1800, y: 0, width: 150, height: 150},
+ });
+});
diff --git a/ui/src/test/perfetto_ui_test_helper.ts b/ui/src/test/perfetto_ui_test_helper.ts
index 22f1539..a709f1f 100644
--- a/ui/src/test/perfetto_ui_test_helper.ts
+++ b/ui/src/test/perfetto_ui_test_helper.ts
@@ -63,7 +63,6 @@
assertExists(file).setInputFiles(tracePath);
await this.waitForPerfettoIdle();
await this.page.mouse.move(0, 0);
- await this.page.mouse.click(0, 0);
}
waitForPerfettoIdle(idleHysteresisMs?: number): Promise<void> {
diff --git a/ui/src/trace_processor/engine.ts b/ui/src/trace_processor/engine.ts
index 92ee1c5..d4727f3 100644
--- a/ui/src/trace_processor/engine.ts
+++ b/ui/src/trace_processor/engine.ts
@@ -35,17 +35,7 @@
WritableQueryResult,
} from './query_result';
import TPM = TraceProcessorRpc.TraceProcessorMethod;
-import {Result} from '../base/utils';
-
-export interface LoadingTracker {
- beginLoading(): void;
- endLoading(): void;
-}
-
-export class NullLoadingTracker implements LoadingTracker {
- beginLoading(): void {}
- endLoading(): void {}
-}
+import {exists, Result} from '../base/utils';
// This is used to skip the decoding of queryResult from protobufjs and deal
// with it ourselves. See the comment below around `QueryResult.decode = ...`.
@@ -102,6 +92,7 @@
): Promise<string | Uint8Array>;
getProxy(tag: string): EngineProxy;
+ readonly numRequestsPending: number;
}
// Abstract interface of a trace proccessor.
@@ -117,7 +108,6 @@
// 2. Call onRpcResponseBytes() when response data is received.
export abstract class EngineBase implements Engine {
abstract readonly id: string;
- private loadingTracker: LoadingTracker;
private txSeqId = 0;
private rxSeqId = 0;
private rxBuf = new ProtoRingBuffer();
@@ -130,10 +120,10 @@
private pendingReadMetatrace?: Deferred<DisableAndReadMetatraceResult>;
private pendingRegisterSqlModule?: Deferred<void>;
private _isMetatracingEnabled = false;
+ private _numRequestsPending = 0;
- constructor(tracker?: LoadingTracker) {
- this.loadingTracker = tracker ? tracker : new NullLoadingTracker();
- }
+ // TraceController sets this to raf.scheduleFullRedraw().
+ onResponseReceived?: () => void;
// Called to send data to the TraceProcessor instance. This turns into a
// postMessage() or a HTTP request, depending on the Engine implementation.
@@ -210,7 +200,7 @@
case TPM.TPM_APPEND_TRACE_DATA:
const appendResult = assertExists(rpc.appendResult);
const pendingPromise = assertExists(this.pendingParses.shift());
- if (appendResult.error && appendResult.error.length > 0) {
+ if (exists(appendResult.error) && appendResult.error.length > 0) {
pendingPromise.reject(appendResult.error);
} else {
pendingPromise.resolve();
@@ -240,7 +230,7 @@
const pendingComputeMetric = assertExists(
this.pendingComputeMetrics.shift(),
);
- if (metricRes.error && metricRes.error.length > 0) {
+ if (exists(metricRes.error) && metricRes.error.length > 0) {
const error = new QueryError(
`ComputeMetric() error: ${metricRes.error}`,
{
@@ -267,7 +257,7 @@
case TPM.TPM_REGISTER_SQL_MODULE:
const registerResult = assertExists(rpc.registerSqlModuleResult);
const res = assertExists(this.pendingRegisterSqlModule);
- if (registerResult.error && registerResult.error.length > 0) {
+ if (exists(registerResult.error) && registerResult.error.length > 0) {
res.reject(registerResult.error);
} else {
res.resolve();
@@ -282,8 +272,10 @@
} // switch(rpc.response);
if (isFinalResponse) {
- this.loadingTracker.endLoading();
+ --this._numRequestsPending;
}
+
+ this.onResponseReceived?.();
}
// TraceProcessor methods below this point.
@@ -502,7 +494,7 @@
const outerProto = TraceProcessorRpcStream.create();
outerProto.msg.push(rpc);
const buf = TraceProcessorRpcStream.encode(outerProto).finish();
- this.loadingTracker.beginLoading();
+ ++this._numRequestsPending;
this.rpcSendRequestBytes(buf);
}
@@ -510,6 +502,10 @@
return this.id;
}
+ get numRequestsPending(): number {
+ return this._numRequestsPending;
+ }
+
getProxy(tag: string): EngineProxy {
return new EngineProxy(this, tag);
}
@@ -565,6 +561,10 @@
return this.engine.getProxy(`${this.tag}/${tag}`);
}
+ get numRequestsPending() {
+ return this.engine.numRequestsPending;
+ }
+
[Symbol.dispose]() {
this._isAlive = false;
}
diff --git a/ui/src/trace_processor/http_rpc_engine.ts b/ui/src/trace_processor/http_rpc_engine.ts
index 3277e2c..f8d5de8 100644
--- a/ui/src/trace_processor/http_rpc_engine.ts
+++ b/ui/src/trace_processor/http_rpc_engine.ts
@@ -16,7 +16,7 @@
import {assertExists} from '../base/logging';
import {globals} from '../frontend/globals';
import {StatusResult} from '../protos';
-import {EngineBase, LoadingTracker} from '../trace_processor/engine';
+import {EngineBase} from '../trace_processor/engine';
const RPC_CONNECT_TIMEOUT_MS = 2000;
@@ -36,8 +36,8 @@
// Can be changed by frontend/index.ts when passing ?rpc_port=1234 .
static rpcPort = '9001';
- constructor(id: string, loadingTracker?: LoadingTracker) {
- super(loadingTracker);
+ constructor(id: string) {
+ super();
this.id = id;
}
diff --git a/ui/src/trace_processor/sql_utils/sched.ts b/ui/src/trace_processor/sql_utils/sched.ts
index 2979e33..24c6c45 100644
--- a/ui/src/trace_processor/sql_utils/sched.ts
+++ b/ui/src/trace_processor/sql_utils/sched.ts
@@ -23,6 +23,7 @@
asUtid,
SchedSqlId,
ThreadStateSqlId,
+ Utid,
} from './core_types';
import {getThreadInfo, ThreadInfo} from './thread';
import {getThreadState, getThreadStateFromConstraints} from './thread_state';
@@ -46,7 +47,7 @@
export interface SchedWakeupInfo {
wakeupTs?: time;
- wakerThread?: ThreadInfo;
+ wakerUtid?: Utid;
wakerCpu?: number;
}
@@ -116,30 +117,32 @@
return result[0];
}
+// Returns the thread and time of the wakeup that resulted in this running
+// sched slice. Omits wakeups that are known to be from interrupt context,
+// since we cannot always recover the correct waker cpu with the current
+// table layout.
export async function getSchedWakeupInfo(
engine: Engine,
sched: Sched,
): Promise<SchedWakeupInfo | undefined> {
- const running = await getThreadStateFromConstraints(engine, {
+ const prevRunnable = await getThreadStateFromConstraints(engine, {
filters: [
- 'state="R"',
+ 'state = "R"',
`ts + dur = ${sched.ts}`,
`utid = ${sched.thread.utid}`,
+ `(irq_context is null or irq_context = 0)`,
],
});
- if (running.length === 0) {
+ if (prevRunnable.length === 0 || prevRunnable[0].wakerId === undefined) {
return undefined;
}
- if (running[0].wakerId === undefined) {
- return undefined;
- }
- const waker = await getThreadState(engine, running[0].wakerId);
+ const waker = await getThreadState(engine, prevRunnable[0].wakerId);
if (waker === undefined) {
return undefined;
}
return {
wakerCpu: waker?.cpu,
- wakerThread: running[0].wakerThread,
- wakeupTs: running[0].ts,
+ wakerUtid: prevRunnable[0].wakerUtid,
+ wakeupTs: prevRunnable[0].ts,
};
}
diff --git a/ui/src/trace_processor/sql_utils/thread_state.ts b/ui/src/trace_processor/sql_utils/thread_state.ts
index 693e952..92834fe 100644
--- a/ui/src/trace_processor/sql_utils/thread_state.ts
+++ b/ui/src/trace_processor/sql_utils/thread_state.ts
@@ -25,6 +25,7 @@
asUtid,
SchedSqlId,
ThreadStateSqlId,
+ Utid,
} from './core_types';
import {getThreadInfo, ThreadInfo} from './thread';
@@ -81,8 +82,7 @@
return result;
}
-// Representation of a single thread state object, corresponding to
-// a row for the |thread_slice| table.
+// Single thread state slice, corresponding to a row of |thread_slice| table.
export interface ThreadState {
// Id into |thread_state| table.
threadStateSqlId: ThreadStateSqlId;
@@ -96,11 +96,18 @@
cpu?: number;
// Human-readable name of this thread state.
state: string;
+ // Kernel function where the thread has suspended.
blockedFunction?: string;
-
+ // Description of the thread itself.
thread?: ThreadInfo;
- wakerThread?: ThreadInfo;
+ // Thread that was running when this thread was woken up.
+ wakerUtid?: Utid;
+ // Active thread state at the time of the wakeup.
wakerId?: ThreadStateSqlId;
+ // Was the wakeup from an interrupt context? It is possible for this to be
+ // unset even for runnable states, if the trace was recorded without
+ // interrupt information.
+ wakerInterruptCtx?: boolean;
}
// Gets a list of thread state objects from Trace Processor with given
@@ -125,7 +132,8 @@
io_wait as ioWait,
thread_state.utid as utid,
waker_utid as wakerUtid,
- waker_id as wakerId
+ waker_id as wakerId,
+ irq_context as wakerInterruptCtx
FROM thread_state
${constraintsToQuerySuffix(constraints)}`);
const it = query.iter({
@@ -140,13 +148,13 @@
utid: NUM,
wakerUtid: NUM_NULL,
wakerId: NUM_NULL,
+ wakerInterruptCtx: NUM_NULL,
});
const result: ThreadState[] = [];
for (; it.valid(); it.next()) {
const ioWait = it.ioWait === null ? undefined : it.ioWait > 0;
- const wakerUtid = asUtid(it.wakerUtid ?? undefined);
// TODO(altimin): Consider fetcing thread / process info using a single
// query instead of one per row.
@@ -159,10 +167,11 @@
state: translateState(it.state ?? undefined, ioWait),
blockedFunction: it.blockedFunction ?? undefined,
thread: await getThreadInfo(engine, asUtid(it.utid)),
- wakerThread: wakerUtid
- ? await getThreadInfo(engine, wakerUtid)
- : undefined,
+ wakerUtid: asUtid(it.wakerUtid ?? undefined),
wakerId: asThreadStateSqlId(it.wakerId ?? undefined),
+ wakerInterruptCtx: fromNumNull(it.wakerInterruptCtx) as
+ | boolean
+ | undefined,
});
}
return result;
diff --git a/ui/src/trace_processor/wasm_engine_proxy.ts b/ui/src/trace_processor/wasm_engine_proxy.ts
index 163fac5..a8f815d 100644
--- a/ui/src/trace_processor/wasm_engine_proxy.ts
+++ b/ui/src/trace_processor/wasm_engine_proxy.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import {assertExists, assertTrue} from '../base/logging';
-import {EngineBase, LoadingTracker} from '../trace_processor/engine';
+import {EngineBase} from '../trace_processor/engine';
let bundlePath: string;
let idleWasmWorker: Worker;
@@ -51,8 +51,8 @@
readonly id: string;
private port: MessagePort;
- constructor(id: string, port: MessagePort, loadingTracker?: LoadingTracker) {
- super(loadingTracker);
+ constructor(id: string, port: MessagePort) {
+ super();
this.id = id;
this.port = port;
this.port.onmessage = this.onMessage.bind(this);
diff --git a/ui/src/widgets/anchor.ts b/ui/src/widgets/anchor.ts
index e60134c..b1b0eff 100644
--- a/ui/src/widgets/anchor.ts
+++ b/ui/src/widgets/anchor.ts
@@ -27,8 +27,8 @@
return m(
'a.pf-anchor',
htmlAttrs,
- icon && m('i.material-icons', icon),
children,
+ icon && m('i.material-icons', icon),
);
}
}
diff --git a/ui/src/widgets/basic_table.ts b/ui/src/widgets/basic_table.ts
index 5231a9f..64490e5 100644
--- a/ui/src/widgets/basic_table.ts
+++ b/ui/src/widgets/basic_table.ts
@@ -13,27 +13,56 @@
// limitations under the License.
import m from 'mithril';
+import {scheduleFullRedraw} from './raf';
export interface ColumnDescriptor<T> {
readonly title: m.Children;
render: (row: T) => m.Children;
}
+// This is a class to be able to perform runtime checks on `columns` below.
+export class ReorderableColumns<T> {
+ constructor(
+ public columns: ColumnDescriptor<T>[],
+ public reorder?: (from: number, to: number) => void,
+ ) {}
+}
+
export interface TableAttrs<T> {
readonly data: ReadonlyArray<T>;
- readonly columns: ReadonlyArray<ColumnDescriptor<T>>;
+ readonly columns: ReadonlyArray<ColumnDescriptor<T> | ReorderableColumns<T>>;
}
export class BasicTable<T> implements m.ClassComponent<TableAttrs<T>> {
- private renderColumnHeader(
- _vnode: m.Vnode<TableAttrs<T>>,
- column: ColumnDescriptor<T>,
- ): m.Children {
- return m('td', column.title);
- }
-
view(vnode: m.Vnode<TableAttrs<T>>): m.Children {
const attrs = vnode.attrs;
+ const columnBlocks: ColumnBlock<T>[] = getColumns(attrs);
+
+ const columns: {column: ColumnDescriptor<T>; extraClasses: string}[] = [];
+ const headers: m.Children[] = [];
+ for (const [blockIndex, block] of columnBlocks.entries()) {
+ const currentColumns = block.columns.map((column, columnIndex) => ({
+ column,
+ extraClasses:
+ columnIndex === 0 && blockIndex !== 0 ? '.has-left-border' : '',
+ }));
+ if (block.reorder === undefined) {
+ for (const {column, extraClasses} of currentColumns) {
+ headers.push(m(`td${extraClasses}`, column.title));
+ }
+ } else {
+ headers.push(
+ m(ReorderableCellGroup, {
+ cells: currentColumns.map(({column, extraClasses}) => ({
+ content: column.title,
+ extraClasses,
+ })),
+ onReorder: block.reorder,
+ }),
+ );
+ }
+ columns.push(...currentColumns);
+ }
return m(
'table.generic-table',
@@ -45,19 +74,152 @@
'table-layout': 'auto',
},
},
- m(
- 'thead',
- m(
- 'tr.header',
- attrs.columns.map((column) => this.renderColumnHeader(vnode, column)),
- ),
- ),
+ m('thead', m('tr.header', headers)),
attrs.data.map((row) =>
m(
'tr',
- attrs.columns.map((column) => m('td', column.render(row))),
+ columns.map(({column, extraClasses}) =>
+ m(`td${extraClasses}`, column.render(row)),
+ ),
),
),
);
}
}
+
+type ColumnBlock<T> = {
+ columns: ColumnDescriptor<T>[];
+ reorder?: (from: number, to: number) => void;
+};
+
+function getColumns<T>(attrs: TableAttrs<T>): ColumnBlock<T>[] {
+ const result: ColumnBlock<T>[] = [];
+ let current: ColumnBlock<T> = {columns: []};
+ for (const col of attrs.columns) {
+ if (col instanceof ReorderableColumns) {
+ if (current.columns.length > 0) {
+ result.push(current);
+ current = {columns: []};
+ }
+ result.push(col);
+ } else {
+ current.columns.push(col);
+ }
+ }
+ if (current.columns.length > 0) {
+ result.push(current);
+ }
+ return result;
+}
+
+export interface ReorderableCellGroupAttrs {
+ cells: {
+ content: m.Children;
+ extraClasses: string;
+ }[];
+ onReorder: (from: number, to: number) => void;
+}
+
+const placeholderElement = document.createElement('span');
+
+// A component that renders a group of cells on the same row that can be
+// reordered between each other by using drag'n'drop.
+//
+// On completed reorder, a callback is fired.
+class ReorderableCellGroup
+ implements m.ClassComponent<ReorderableCellGroupAttrs>
+{
+ private drag?: {
+ from: number;
+ to?: number;
+ };
+
+ private getClassForIndex(index: number): string {
+ if (this.drag?.from === index) {
+ return 'dragged';
+ }
+ if (this.drag?.to === index) {
+ return 'highlight-left';
+ }
+ if (this.drag?.to === index + 1) {
+ return 'highlight-right';
+ }
+ return '';
+ }
+
+ view(vnode: m.Vnode<ReorderableCellGroupAttrs>): m.Children {
+ return vnode.attrs.cells.map((cell, index) =>
+ m(
+ `td.reorderable-cell${cell.extraClasses}`,
+ {
+ draggable: 'draggable',
+ class: this.getClassForIndex(index),
+ ondragstart: (e: DragEvent) => {
+ this.drag = {
+ from: index,
+ };
+ if (e.dataTransfer !== null) {
+ e.dataTransfer.setDragImage(placeholderElement, 0, 0);
+ }
+
+ scheduleFullRedraw();
+ },
+ ondragover: (e: DragEvent) => {
+ let target = e.target as HTMLElement;
+ if (this.drag === undefined || this.drag?.from === index) {
+ // Don't do anything when hovering on the same cell that's
+ // been dragged, or when dragging something other than the
+ // cell from the same group.
+ return;
+ }
+
+ while (
+ target.tagName.toLowerCase() !== 'td' &&
+ target.parentElement !== null
+ ) {
+ target = target.parentElement;
+ }
+
+ // When hovering over cell on the right half, the cell will be
+ // moved to the right of it, vice versa for the left side. This
+ // is done such that it's possible to put dragged cell to every
+ // possible position.
+ const offset = e.clientX - target.getBoundingClientRect().x;
+ const direction =
+ offset > target.clientWidth / 2 ? 'right' : 'left';
+ const dest = direction === 'left' ? index : index + 1;
+ const adjustedDest =
+ dest === this.drag.from || dest === this.drag.from + 1
+ ? undefined
+ : dest;
+ if (adjustedDest !== this.drag.to) {
+ this.drag.to = adjustedDest;
+ scheduleFullRedraw();
+ }
+ },
+ ondragleave: (e: DragEvent) => {
+ if (this.drag?.to !== index) return;
+ this.drag.to = undefined;
+ scheduleFullRedraw();
+ if (e.dataTransfer !== null) {
+ e.dataTransfer.dropEffect = 'none';
+ }
+ },
+ ondragend: () => {
+ if (
+ this.drag !== undefined &&
+ this.drag.to !== undefined &&
+ this.drag.from !== this.drag.to
+ ) {
+ vnode.attrs.onReorder(this.drag.from, this.drag.to);
+ }
+
+ this.drag = undefined;
+ scheduleFullRedraw();
+ },
+ },
+ cell.content,
+ ),
+ );
+ }
+}