Merge "tp: introduce bump allocator for object allocation in trace sorter"
diff --git a/docs/contributing/getting-started.md b/docs/contributing/getting-started.md
index a101f29..bbac4c2 100644
--- a/docs/contributing/getting-started.md
+++ b/docs/contributing/getting-started.md
@@ -109,6 +109,9 @@
exceptions are UI-only, docs-only or GN-only changes, for which the Android CI
can be bypassed, as those are not built as part of the Android tree.
+You can also
+[test a pending Perfetto CL against Chrome's TryBots](testing.md#chromium).
+
## Community
You can reach us on our [Discord channel](https://discord.gg/35ShE3A).
diff --git a/docs/contributing/testing.md b/docs/contributing/testing.md
index 49c10a9..e09514b 100644
--- a/docs/contributing/testing.md
+++ b/docs/contributing/testing.md
@@ -177,7 +177,7 @@
adb shell /data/local/tmp/CtsPerfettoTestCases64
```
-Chromium waterfall
+{#chromium} Chromium waterfall
------------------
Perfetto is constantly rolled into chromium's //third_party/perfetto via
[this autoroller](https://autoroll.skia.org/r/perfetto-chromium-autoroll).
@@ -185,4 +185,66 @@
The [Chromium CI](https://build.chromium.org) runs the `perfetto_unittests`
target, as defined in the [buildbot config][chromium_buildbot].
+You can also test a pending Perfetto CL against Chromium's CI / TryBots
+before submitting it. This can be useful when making trickier API changes or to
+test on platforms that the Perfetto CI doesn't cover (e.g. Windows, MacOS),
+allowing you to verify the patch before you submit it (and it then eventually
+auto-rolls into Chromium).
+
+To do this, first make sure you have uploaded your Perfetto patch to the
+Android Gerrit. Next, create a new Chromium CL that modifies Chromium's
+`//src/DEPS` file.
+
+If you recently uploaded your change, it may be enough to modify the git commit
+hash in the `DEPS` entry for `src/third_party/perfetto`:
+
+```
+ 'src/third_party/perfetto':
+ Var('android_git') + '/platform/external/perfetto.git' + '@' + '8fe19f55468ee227e99c1a682bd8c0e8f7e5bcdb',
+```
+
+Replace the git hash with the commit hash of your most recent patch set, which
+you can find in gerrit next to the active patch set number.
+
+Alternatively, you can add `hooks` to patch in the pending CL on top of
+Chromium's current third_party/perfetto revision. For this, add the following
+entries to the `hooks` array in Chromium's `//src/DEPS` file, modifying the
+`refs/changes/XX/YYYYYYY/ZZ` to the appropriate values for your gerrit change.
+You can see these values when pressing the "Download" button in gerrit. You can
+also use this method to patch in multiple Perfetto changes at once by
+adding additional `hooks` entries. [Here][chromium_cl]'s an example CL.
+
+```
+ {
+ 'name': 'fetch_custom_patch',
+ 'pattern': '.',
+ 'action': [ 'git', '-C', 'src/third_party/perfetto/',
+ 'fetch', 'https://android.googlesource.com/platform/external/perfetto',
+ 'refs/changes/XX/YYYYYYY/ZZ',
+ ],
+ },
+ {
+ 'name': 'apply_custom_patch',
+ 'pattern': '.',
+ 'action': ['git', '-C', 'src/third_party/perfetto/',
+ '-c', 'user.name=Custom Patch', '-c', 'user.email=custompatch@example.com',
+ 'cherry-pick', 'FETCH_HEAD',
+ ],
+ },
+```
+
+If you'd like to test your change against the SDK build of Chrome, you
+can add `Cq-Include-Trybots:` lines for perfetto SDK trybots to the change
+description in gerrit (this won't be needed once Chrome's migration to the
+SDK is complete, see [tracking bug][sdk_migration_bug]):
+
+```
+Cq-Include-Trybots: luci.chromium.try:linux-perfetto-rel
+Cq-Include-Trybots: luci.chromium.try:android-perfetto-rel
+Cq-Include-Trybots: luci.chromium.try:mac-perfetto-rel
+Cq-Include-Trybots: luci.chromium.try:win-perfetto-rel
+```
+
[chromium_buildbot]: https://cs.chromium.org/search/?q=perfetto_.*tests+f:%5Esrc/testing.*json$&sq=package:chromium&type=cs
+[chromium_cl]: https://chromium-review.googlesource.com/c/chromium/src/+/2030528
+[sdk_migration_bug]: https://crbug.com/1006541
diff --git a/include/perfetto/ext/base/string_view.h b/include/perfetto/ext/base/string_view.h
index 7c48ea0..1fe3696 100644
--- a/include/perfetto/ext/base/string_view.h
+++ b/include/perfetto/ext/base/string_view.h
@@ -127,11 +127,7 @@
return false;
if (other.size() > size())
return false;
- for (uint32_t i = 0; i < other.size(); ++i) {
- if (at(i) != other.at(i))
- return false;
- }
- return true;
+ return memcmp(data(), other.data(), other.size()) == 0;
}
bool EndsWith(const StringView& other) const {
@@ -141,12 +137,8 @@
return false;
if (other.size() > size())
return false;
- const size_t off = size() - other.size();
- for (size_t i = 0; i < other.size(); ++i) {
- if (at(off + i) != other.at(i))
- return false;
- }
- return true;
+ size_t off = size() - other.size();
+ return memcmp(data() + off, other.data(), other.size()) == 0;
}
std::string ToStdString() const {
diff --git a/include/perfetto/tracing/track_event.h b/include/perfetto/tracing/track_event.h
index 2651927..7366b85 100644
--- a/include/perfetto/tracing/track_event.h
+++ b/include/perfetto/tracing/track_event.h
@@ -17,7 +17,6 @@
#ifndef INCLUDE_PERFETTO_TRACING_TRACK_EVENT_H_
#define INCLUDE_PERFETTO_TRACING_TRACK_EVENT_H_
-#include "perfetto/base/time.h"
#include "perfetto/tracing/internal/track_event_data_source.h"
#include "perfetto/tracing/internal/track_event_internal.h"
#include "perfetto/tracing/internal/track_event_macros.h"
diff --git a/protos/perfetto/trace/chrome/chrome_metadata.proto b/protos/perfetto/trace/chrome/chrome_metadata.proto
index 8882593..d2c46e4 100644
--- a/protos/perfetto/trace/chrome/chrome_metadata.proto
+++ b/protos/perfetto/trace/chrome/chrome_metadata.proto
@@ -85,4 +85,7 @@
// List of all active triggers in current session, when trace was triggered.
repeated TriggerRule active_rules = 2;
+
+ // Hash of the scenario name.
+ optional fixed32 scenario_name_hash = 3;
}
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 5f3473a..a215592 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -3823,6 +3823,9 @@
// List of all active triggers in current session, when trace was triggered.
repeated TriggerRule active_rules = 2;
+
+ // Hash of the scenario name.
+ optional fixed32 scenario_name_hash = 3;
}
// End of protos/perfetto/trace/chrome/chrome_metadata.proto
diff --git a/src/trace_processor/dynamic/experimental_annotated_stack_generator.cc b/src/trace_processor/dynamic/experimental_annotated_stack_generator.cc
index b26cb84..1527295 100644
--- a/src/trace_processor/dynamic/experimental_annotated_stack_generator.cc
+++ b/src/trace_processor/dynamic/experimental_annotated_stack_generator.cc
@@ -56,41 +56,56 @@
// /system/lib64/libc.so
// /system/framework/framework.jar
// /memfd:jit-cache (deleted)
+// /data/dalvik-cache/arm64/<snip>.apk@classes.dex
+// /data/app/<snip>/base.apk!libmonochrome_64.so
// [vdso]
// TODO(rsavitski): consider moving this to a hidden column on
-// stack_profile_mapping, once this logic is sufficiently stable.
+// stack_profile_mapping.
MapType ClassifyMap(NullTermStringView map) {
if (map.empty())
return MapType::kOther;
// Primary mapping where modern ART puts jitted code.
- // TODO(rsavitski): look into /memfd:jit-zygote-cache.
- if (!strncmp(map.c_str(), "/memfd:jit-cache", 16))
+ // The Zygote's JIT region is inherited by all descendant apps, so it can
+ // still appear in their callstacks.
+ if (map.StartsWith("/memfd:jit-cache") ||
+ map.StartsWith("/memfd:jit-zygote-cache")) {
return MapType::kArtJit;
+ }
size_t last_slash_pos = map.rfind('/');
if (last_slash_pos != NullTermStringView::npos) {
- if (!strncmp(map.c_str() + last_slash_pos, "/libart.so", 10))
- return MapType::kNativeLibart;
- if (!strncmp(map.c_str() + last_slash_pos, "/libartd.so", 11))
+ base::StringView suffix = map.substr(last_slash_pos);
+ if (suffix.StartsWith("/libart.so") || suffix.StartsWith("/libartd.so"))
return MapType::kNativeLibart;
}
-
size_t extension_pos = map.rfind('.');
if (extension_pos != NullTermStringView::npos) {
- if (!strncmp(map.c_str() + extension_pos, ".so", 3))
+ base::StringView suffix = map.substr(extension_pos);
+ if (suffix.StartsWith(".so"))
return MapType::kNativeOther;
+ // unqualified dex
+ if (suffix.StartsWith(".dex"))
+ return MapType::kArtInterp;
// dex with verification speedup info, produced by dex2oat
- if (!strncmp(map.c_str() + extension_pos, ".vdex", 5))
+ if (suffix.StartsWith(".vdex"))
return MapType::kArtInterp;
// possibly uncompressed dex in a jar archive
- if (!strncmp(map.c_str() + extension_pos, ".jar", 4))
+ if (suffix.StartsWith(".jar"))
+ return MapType::kArtInterp;
+ // android package (zip file), this can contain uncompressed dexes or
+ // native libraries that are mmap'd directly into the process. We rely on
+ // libunwindstack's MapInfo::GetFullName, which suffixes the mapping with
+ // "!lib.so" if it knows that the referenced piece of the archive is an
+ // uncompressed ELF file. So an unadorned ".apk" is assumed to be a dex
+ // file.
+ if (suffix.StartsWith(".apk"))
return MapType::kArtInterp;
// ahead of time compiled ELFs
- if (!strncmp(map.c_str() + extension_pos, ".oat", 4))
+ if (suffix.StartsWith(".oat"))
return MapType::kArtAot;
// older/alternative name for .oat
- if (!strncmp(map.c_str() + extension_pos, ".odex", 5))
+ if (suffix.StartsWith(".odex"))
return MapType::kArtAot;
}
return MapType::kOther;
@@ -167,7 +182,8 @@
// Walk the callsites root-to-leaf, annotating:
// * managed frames with their execution state (interpreted/jit/aot)
- // * common ART frames, which are usually not relevant
+ // * common ART frames, which are usually not relevant to
+ // visualisation/inspection
//
// This is not a per-frame decision, because we do not want to filter out ART
// frames immediately after a JNI transition (such frames are often relevant).
@@ -175,13 +191,12 @@
// As a consequence of the logic being based on a root-to-leaf walk, a given
// callsite will always have the same annotation, as the parent path is always
// the same, and children callsites do not affect their parents' annotations.
- //
- // This could also be implemented as a hidden column on the callsite table
- // (populated at import time), but we want to be more flexible for now.
StringId art_jni_trampoline =
context_->storage->InternString("art_jni_trampoline");
StringId common_frame = context_->storage->InternString("common-frame");
+ StringId common_frame_interp =
+ context_->storage->InternString("common-frame-interp");
StringId art_interp = context_->storage->InternString("interp");
StringId art_jit = context_->storage->InternString("jit");
StringId art_aot = context_->storage->InternString("aot");
@@ -249,12 +264,41 @@
continue;
}
+ // Mixed callstack, tag libart frames as uninteresting (common-frame).
+ // Special case a subset of interpreter implementation frames as
+ // "common-frame-interp" using frame name prefixes. Those functions are
+ // actually executed, whereas the managed "interp" frames are synthesised as
+ // their caller by the unwinding library (based on the dex_pc virtual
+ // register restored using the libart's DWARF info). The heuristic covers
+ // the "nterp" and "switch" interpreter implementations.
+ //
+ // Example:
+ // <towards root>
+ // android.view.WindowLayout.computeFrames [interp]
+ // nterp_op_iget_object_slow_path [common-frame-interp]
+ //
+ // This annotation is helpful when trying to answer "what mode was the
+ // process in?" based on the leaf frame of the callstack. As we want to
+ // classify such cases as interpreted, even though the leaf frame is
+ // libart.so.
+ //
+ // For "switch" interpreter, we match any frame starting with
+ // "art::interpreter::" according to itanium mangling.
if (annotation_state == State::kEraseLibart &&
map_type == MapType::kNativeLibart) {
+ NullTermStringView fname = context_->storage->GetString(fname_id);
+ if (fname.StartsWith("nterp_") || fname.StartsWith("Nterp") ||
+ fname.StartsWith("ExecuteNterp") ||
+ fname.StartsWith("ExecuteSwitchImpl") ||
+ fname.StartsWith("_ZN3art11interpreter")) {
+ annotations_reversed.push_back(common_frame_interp);
+ continue;
+ }
annotations_reversed.push_back(common_frame);
continue;
}
+ // default - no special annotation
annotations_reversed.push_back(kNullStringId);
}
diff --git a/src/trace_processor/metrics/sql/android/android_startup.sql b/src/trace_processor/metrics/sql/android/android_startup.sql
index 634e908..bd80e8a 100644
--- a/src/trace_processor/metrics/sql/android/android_startup.sql
+++ b/src/trace_processor/metrics/sql/android/android_startup.sql
@@ -151,7 +151,7 @@
MAIN_THREAD_TIME_FOR_LAUNCH_AND_STATE(launches.startup_id, 'Running'), 0
),
'runnable_dur_ns', IFNULL(
- MAIN_THREAD_TIME_FOR_LAUNCH_AND_STATE(launches.startup_id, 'R'), 0
+ MAIN_THREAD_TIME_FOR_LAUNCH_AND_STATE(launches.startup_id, 'R*'), 0
),
'uninterruptible_sleep_dur_ns', IFNULL(
MAIN_THREAD_TIME_FOR_LAUNCH_AND_STATE(launches.startup_id, 'D*'), 0
@@ -320,7 +320,7 @@
UNION ALL
SELECT 'Main Thread - Time spent in Runnable state'
AS slow_cause
- WHERE MAIN_THREAD_TIME_FOR_LAUNCH_AND_STATE(launches.startup_id, 'R') > 1e8
+ WHERE MAIN_THREAD_TIME_FOR_LAUNCH_AND_STATE(launches.startup_id, 'R*') > 100e6
UNION ALL
SELECT 'Main Thread - Time spent in interruptible sleep state'
@@ -358,6 +358,13 @@
WHERE ANDROID_SUM_DUR_FOR_STARTUP_AND_SLICE(launches.startup_id, 'VerifyClass*') > 10e6
UNION ALL
+ SELECT 'Potential CPU contention with '
+ || MOST_ACTIVE_PROCESS_FOR_LAUNCH(launches.startup_id)
+ AS slow_cause
+ WHERE MAIN_THREAD_TIME_FOR_LAUNCH_AND_STATE(launches.startup_id, 'R*') > 100e6
+ AND MOST_ACTIVE_PROCESS_FOR_LAUNCH(launches.startup_id) IS NOT NULL
+
+ UNION ALL
SELECT 'JIT Activity'
AS slow_cause
WHERE THREAD_TIME_FOR_LAUNCH_STATE_AND_THREAD(
diff --git a/src/trace_processor/metrics/sql/android/startup/mcycles_per_launch.sql b/src/trace_processor/metrics/sql/android/startup/mcycles_per_launch.sql
index 993d61e..b47381e 100644
--- a/src/trace_processor/metrics/sql/android/startup/mcycles_per_launch.sql
+++ b/src/trace_processor/metrics/sql/android/startup/mcycles_per_launch.sql
@@ -111,3 +111,16 @@
);
'
);
+
+-- Given a launch id, returns the most active process name.
+SELECT CREATE_FUNCTION(
+ 'MOST_ACTIVE_PROCESS_FOR_LAUNCH(startup_id INT)',
+ 'STRING',
+ '
+ SELECT process.name AS process_name
+ FROM top_mcyles_process_excluding_started_per_launch
+ JOIN process USING (upid)
+ WHERE startup_id = $startup_id
+ ORDER BY mcycles DESC LIMIT 1;
+ '
+);
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_long_tasks.sql b/src/trace_processor/metrics/sql/chrome/chrome_long_tasks.sql
index ec05765..3bd052b 100644
--- a/src/trace_processor/metrics/sql/chrome/chrome_long_tasks.sql
+++ b/src/trace_processor/metrics/sql/chrome/chrome_long_tasks.sql
@@ -24,6 +24,12 @@
'function_prefix', ''
);
+SELECT CREATE_FUNCTION(
+ 'IS_LONG_CHOREOGRAPHER_TASK(dur LONG)',
+ 'BOOL',
+ 'SELECT $dur >= 4 * 1e6'
+);
+
-- Note that not all slices will be mojo slices; filter on interface_name IS
-- NOT NULL for mojo slices specifically.
DROP TABLE IF EXISTS long_tasks_extracted_slices;
@@ -72,7 +78,7 @@
SELECT *
FROM SELECT_BEGIN_MAIN_FRAME_JAVA_SLICES('LongTaskTracker')
UNION ALL
- SELECT * FROM chrome_choreographer_tasks
+ SELECT * FROM chrome_choreographer_tasks WHERE IS_LONG_CHOREOGRAPHER_TASK(dur)
),
-- Intermediate step to allow us to sort java view names.
root_slice_and_java_view_not_grouped AS (
@@ -101,6 +107,7 @@
GET_JAVA_VIEWS_TASK_TYPE(kind) AS task_type,
id
FROM long_task_slices_with_java_views
+ WHERE kind = "SingleThreadProxy::BeginMainFrame"
),
scheduler_tasks_with_mojo AS (
SELECT
@@ -129,7 +136,16 @@
FROM long_tasks_internal_tbl s1
LEFT JOIN scheduler_tasks_with_mojo s2 ON s2.id = s1.id
LEFT JOIN java_views_tasks s3 ON s3.id = s1.id
-LEFT JOIN navigation_tasks s4 ON s4.id = s1.id;
+LEFT JOIN navigation_tasks s4 ON s4.id = s1.id
+UNION ALL
+-- Choreographer slices won't necessarily be associated with an overlying
+-- LongTaskTracker slice, so join them separately.
+SELECT
+ printf('%s(java_views=%s)', kind, java_views) as full_name,
+ GET_JAVA_VIEWS_TASK_TYPE(kind) AS task_type,
+ id
+FROM long_task_slices_with_java_views
+WHERE kind = "Choreographer";
DROP VIEW IF EXISTS chrome_long_tasks;
CREATE VIEW chrome_long_tasks AS
diff --git a/src/trace_processor/util/annotated_callsites.cc b/src/trace_processor/util/annotated_callsites.cc
index 3428330..5e23130 100644
--- a/src/trace_processor/util/annotated_callsites.cc
+++ b/src/trace_processor/util/annotated_callsites.cc
@@ -101,8 +101,34 @@
return {state, CallsiteAnnotation::kArtAot};
}
+ // Mixed callstack, tag libart frames as uninteresting (common-frame).
+ // Special case a subset of interpreter implementation frames as
+ // "common-frame-interp" using frame name prefixes. Those functions are
+ // actually executed, whereas the managed "interp" frames are synthesised as
+ // their caller by the unwinding library (based on the dex_pc virtual
+ // register restored using the libart's DWARF info). The heuristic covers
+ // the "nterp" and "switch" interpreter implementations.
+ //
+ // Example:
+ // <towards root>
+ // android.view.WindowLayout.computeFrames [interp]
+ // nterp_op_iget_object_slow_path [common-frame-interp]
+ //
+ // This annotation is helpful when trying to answer "what mode was the
+ // process in?" based on the leaf frame of the callstack. As we want to
+ // classify such cases as interpreted, even though the leaf frame is
+ // libart.so.
+ //
+ // For "switch" interpreter, we match any frame starting with
+ // "art::interpreter::" according to itanium mangling.
if (state == State::kEraseLibart && map_type == MapType::kNativeLibart) {
- states_.emplace(callsite.id(), state);
+ NullTermStringView fname = context_.storage->GetString(frame.name());
+ if (fname.StartsWith("nterp_") || fname.StartsWith("Nterp") ||
+ fname.StartsWith("ExecuteNterp") ||
+ fname.StartsWith("ExecuteSwitchImpl") ||
+ fname.StartsWith("_ZN3art11interpreter")) {
+ return {state, CallsiteAnnotation::kCommonFrameInterp};
+ }
return {state, CallsiteAnnotation::kCommonFrame};
}
@@ -129,33 +155,47 @@
return MapType::kOther;
// Primary mapping where modern ART puts jitted code.
- // TODO(rsavitski): look into /memfd:jit-zygote-cache.
- if (!strncmp(map.c_str(), "/memfd:jit-cache", 16))
+ // The Zygote's JIT region is inherited by all descendant apps, so it can
+ // still appear in their callstacks.
+ if (map.StartsWith("/memfd:jit-cache") ||
+ map.StartsWith("/memfd:jit-zygote-cache")) {
return MapType::kArtJit;
+ }
size_t last_slash_pos = map.rfind('/');
if (last_slash_pos != NullTermStringView::npos) {
- if (!strncmp(map.c_str() + last_slash_pos, "/libart.so", 10))
- return MapType::kNativeLibart;
- if (!strncmp(map.c_str() + last_slash_pos, "/libartd.so", 11))
+ base::StringView suffix = map.substr(last_slash_pos);
+ if (suffix.StartsWith("/libart.so") || suffix.StartsWith("/libartd.so"))
return MapType::kNativeLibart;
}
size_t extension_pos = map.rfind('.');
if (extension_pos != NullTermStringView::npos) {
- if (!strncmp(map.c_str() + extension_pos, ".so", 3))
+ base::StringView suffix = map.substr(extension_pos);
+ if (suffix.StartsWith(".so"))
return MapType::kNativeOther;
+ // unqualified dex
+ if (suffix.StartsWith(".dex"))
+ return MapType::kArtInterp;
// dex with verification speedup info, produced by dex2oat
- if (!strncmp(map.c_str() + extension_pos, ".vdex", 5))
+ if (suffix.StartsWith(".vdex"))
return MapType::kArtInterp;
// possibly uncompressed dex in a jar archive
- if (!strncmp(map.c_str() + extension_pos, ".jar", 4))
+ if (suffix.StartsWith(".jar"))
+ return MapType::kArtInterp;
+ // android package (zip file), this can contain uncompressed dexes or
+ // native libraries that are mmap'd directly into the process. We rely on
+ // libunwindstack's MapInfo::GetFullName, which suffixes the mapping with
+ // "!lib.so" if it knows that the referenced piece of the archive is an
+ // uncompressed ELF file. So an unadorned ".apk" is assumed to be a dex
+ // file.
+ if (suffix.StartsWith(".apk"))
return MapType::kArtInterp;
// ahead of time compiled ELFs
- if (!strncmp(map.c_str() + extension_pos, ".oat", 4))
+ if (suffix.StartsWith(".oat"))
return MapType::kArtAot;
// older/alternative name for .oat
- if (!strncmp(map.c_str() + extension_pos, ".odex", 5))
+ if (suffix.StartsWith(".odex"))
return MapType::kArtAot;
}
return MapType::kOther;
diff --git a/src/trace_processor/util/annotated_callsites.h b/src/trace_processor/util/annotated_callsites.h
index 4e617f6..0fbb555 100644
--- a/src/trace_processor/util/annotated_callsites.h
+++ b/src/trace_processor/util/annotated_callsites.h
@@ -30,6 +30,7 @@
enum class CallsiteAnnotation {
kNone,
kCommonFrame,
+ kCommonFrameInterp,
kArtInterpreted,
kArtJit,
kArtAot,
@@ -38,7 +39,7 @@
// Helper class to augment callsite with (currently Android-specific)
// annotations. A given callsite will always have the same annotation. This
// class will internally cache already computed annotations. An annotation
-// depends only of the current callsite and the annotation sof its parent
+// depends only of the current callsite and the annotations of its parent
// callsites (going to the root).
class AnnotatedCallsites {
public:
diff --git a/src/trace_processor/util/profile_builder.cc b/src/trace_processor/util/profile_builder.cc
index e1e53ac..f859d53 100644
--- a/src/trace_processor/util/profile_builder.cc
+++ b/src/trace_processor/util/profile_builder.cc
@@ -45,6 +45,8 @@
return "jit";
case CallsiteAnnotation::kCommonFrame:
return "common-frame";
+ case CallsiteAnnotation::kCommonFrameInterp:
+ return "common-frame-interp";
}
PERFETTO_FATAL("For GCC");
}
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.cc b/src/traced/probes/ftrace/ftrace_config_muxer.cc
index 5b0ad79..b0d75e5 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.cc
@@ -597,28 +597,41 @@
FtraceConfigId FtraceConfigMuxer::SetupConfig(const FtraceConfig& request,
FtraceSetupErrors* errors) {
EventFilter filter;
- bool is_ftrace_enabled = ftrace_->IsTracingEnabled();
if (ds_configs_.empty()) {
PERFETTO_DCHECK(active_configs_.empty());
- // If someone outside of perfetto is using ftrace give up now.
- if (!request.preserve_ftrace_buffer() && is_ftrace_enabled &&
- !IsOldAtrace()) {
- PERFETTO_ELOG("ftrace in use by non-Perfetto.");
+ // If someone outside of perfetto is using a non-nop tracer, yield. We can't
+ // realistically figure out all notions of "in use" even if we look at
+ // set_event or events/enable, so this is all we check for.
+ if (!request.preserve_ftrace_buffer() && !ftrace_->IsTracingAvailable()) {
+ PERFETTO_ELOG(
+ "ftrace in use by non-Perfetto. Check that %s current_tracer is nop.",
+ ftrace_->GetRootPath().c_str());
return 0;
}
- // Setup ftrace, without starting it. Setting buffers can be quite slow
- // (up to hundreds of ms).
- if (!request.preserve_ftrace_buffer())
- SetupClock(request);
- SetupBufferSize(request);
- } else {
- // Did someone turn ftrace off behind our back? If so give up.
- if (!active_configs_.empty() && !is_ftrace_enabled && !IsOldAtrace()) {
- PERFETTO_ELOG("ftrace disabled by non-Perfetto.");
- return 0;
+ // Clear tracefs state, remembering which value of "tracing_on" to restore
+ // to after we're done, though we won't restore the rest of the tracefs
+ // state.
+ current_state_.saved_tracing_on = ftrace_->GetTracingOn();
+ if (!request.preserve_ftrace_buffer()) {
+ ftrace_->SetTracingOn(false);
+ // This will fail on release ("user") builds due to ACLs, but that's
+ // acceptable since the per-event enabling/disabling should still be
+ // balanced.
+ ftrace_->DisableAllEvents();
+ ftrace_->ClearTrace();
}
+
+ // Set up the rest of the tracefs state, without starting it.
+ // Notes:
+ // * resizing buffers can be quite slow (up to hundreds of ms).
+ // * resizing buffers doesn't clear their existing contents, which matters
+ // to the preserve_ftrace_buffer option.
+ if (!request.preserve_ftrace_buffer()) {
+ SetupClock(request);
+ }
+ SetupBufferSize(request);
}
std::set<GroupAndName> events = GetFtraceEvents(request, table_);
@@ -754,14 +767,10 @@
return false;
}
+ // Enable tracing_on to activate ftrace ring buffer before activate the first
+ // config.
if (active_configs_.empty()) {
- if (!ds_configs_.at(id).preserve_ftrace_buffer &&
- ftrace_->IsTracingEnabled() && !IsOldAtrace()) {
- // If someone outside of perfetto is using ftrace give up now.
- PERFETTO_ELOG("ftrace in use by non-Perfetto.");
- return false;
- }
- if (!ftrace_->EnableTracing()) {
+ if (!ftrace_->SetTracingOn(true)) {
PERFETTO_ELOG("Failed to enable ftrace.");
return false;
}
@@ -816,14 +825,14 @@
current_state_.ftrace_events.DisableEvent(event->ftrace_event_id);
}
- // If there aren't any more active configs, disable ftrace.
auto active_it = active_configs_.find(config_id);
if (active_it != active_configs_.end()) {
active_configs_.erase(active_it);
if (active_configs_.empty()) {
- // This was the last active config, disable ftrace.
- if (!ftrace_->DisableTracing())
- PERFETTO_ELOG("Failed to disable ftrace.");
+ // This was the last active config for now, but potentially more dormant
+ // configs need to be activated. We are not interested in reading while no
+ // active configs so diasble tracing_on here.
+ ftrace_->SetTracingOn(false);
}
}
@@ -835,6 +844,7 @@
current_state_.cpu_buffer_size_pages = 1;
ftrace_->DisableAllEvents();
ftrace_->ClearTrace();
+ ftrace_->SetTracingOn(current_state_.saved_tracing_on);
}
if (current_state_.atrace_on) {
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.h b/src/traced/probes/ftrace/ftrace_config_muxer.h
index 4247af1..fb5850a 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.h
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.h
@@ -185,6 +185,7 @@
bool atrace_on = false;
std::vector<std::string> atrace_apps;
std::vector<std::string> atrace_categories;
+ bool saved_tracing_on; // Backup for the original tracing_on.
};
FtraceConfigMuxer(const FtraceConfigMuxer&) = delete;
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
index 96c8da6..5be1f1f 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
@@ -237,6 +237,14 @@
FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {});
+ ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+ .WillByDefault(Return("[local] global boot"));
+ EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+ .Times(AnyNumber());
+ EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillOnce(Return("nop"));
+ EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
+ .WillOnce(Return('1'));
EXPECT_CALL(ftrace, WriteToFile(_, _)).WillRepeatedly(Return(true));
EXPECT_CALL(ftrace, WriteToFile("/root/events/raw_syscalls/sys_enter/filter",
"id == 0 || id == 1"));
@@ -259,6 +267,15 @@
config.add_syscall_events("sys_open");
config.add_syscall_events("sys_not_a_call");
+ ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+ .WillByDefault(Return("[local] global boot"));
+ EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
+ .Times(AnyNumber());
+ EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillOnce(Return("nop"));
+ EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
+ .WillOnce(Return('1'));
+
// Unknown syscall is ignored.
ASSERT_TRUE(model.SetupConfig(config));
ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre(0));
@@ -280,6 +297,9 @@
FtraceConfig syscall_read_config = syscall_config;
syscall_read_config.add_syscall_events("sys_read");
+ ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillByDefault(Return("nop"));
+
// Expect no filter for non-syscall config.
model.SetupConfig(empty_config);
ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre());
@@ -314,17 +334,20 @@
FtraceConfigMuxer model(&ftrace, mock_table.get(), GetSyscallTable(), {});
+ EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillOnce(Return("nop"));
+ EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
+ .WillOnce(Return('1'));
+ EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
+ EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
+ EXPECT_CALL(ftrace, ClearFile("/root/trace"));
+ EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
.WillByDefault(Return("[local] global boot"));
EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
.Times(AnyNumber());
-
- EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
- .Times(2)
- .WillRepeatedly(Return('0'));
EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
- EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
EXPECT_CALL(ftrace,
WriteToFile("/root/events/power/cpu_frequency/enable", "1"));
EXPECT_CALL(*mock_table, GetEvent(GroupAndName("power", "cpu_frequency")))
@@ -341,6 +364,8 @@
GetOrCreateEvent(GroupAndName("power", "cpu_frequency")));
FtraceConfigId id = model.SetupConfig(config);
+
+ EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
ASSERT_TRUE(model.ActivateConfig(id));
const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
@@ -379,6 +404,11 @@
.WillByDefault(Return(&event2));
EXPECT_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_two", "foo")));
+ ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillByDefault(Return("nop"));
+ ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+ .WillByDefault(Return("0"));
+
FtraceConfigId id = model.SetupConfig(config);
ASSERT_TRUE(model.ActivateConfig(id));
@@ -397,17 +427,20 @@
FtraceConfig config = CreateFtraceConfig({"sched/*"});
+ EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillOnce(Return("nop"));
+ EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
+ .WillOnce(Return('1'));
+ EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
+ EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
+ EXPECT_CALL(ftrace, ClearFile("/root/trace"));
+ EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
.WillByDefault(Return("[local] global boot"));
EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
.Times(AnyNumber());
-
- EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
- .Times(2)
- .WillRepeatedly(Return('0'));
EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
- EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
EXPECT_CALL(ftrace,
WriteToFile("/root/events/sched/sched_switch/enable", "1"));
EXPECT_CALL(ftrace,
@@ -443,6 +476,8 @@
FtraceConfigId id = model.SetupConfig(config);
ASSERT_TRUE(id);
+
+ EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
ASSERT_TRUE(model.ActivateConfig(id));
const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
@@ -492,6 +527,11 @@
.WillByDefault(Return(&event2));
EXPECT_CALL(*mock_table, GetOrCreateEvent(GroupAndName("group_two", "foo")));
+ ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillByDefault(Return("nop"));
+ ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+ .WillByDefault(Return("0"));
+
FtraceConfigId id = model.SetupConfig(config);
ASSERT_TRUE(model.ActivateConfig(id));
@@ -512,22 +552,27 @@
FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
+ EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillOnce(Return("nop"));
+ EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
+ .WillOnce(Return('1'));
+ EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
+ EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
+ EXPECT_CALL(ftrace, ClearFile("/root/trace"));
+ EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
.WillByDefault(Return("[local] global boot"));
EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
.Times(AnyNumber());
-
- EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
- .Times(2)
- .WillRepeatedly(Return('0'));
EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
- EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
EXPECT_CALL(ftrace,
WriteToFile("/root/events/sched/sched_switch/enable", "1"));
FtraceConfigId id = model.SetupConfig(config);
ASSERT_TRUE(id);
+
+ EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
ASSERT_TRUE(model.ActivateConfig(id));
const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
@@ -542,13 +587,14 @@
ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&ftrace));
EXPECT_CALL(ftrace, NumberOfCpus()).Times(AnyNumber());
+ EXPECT_CALL(ftrace,
+ WriteToFile("/root/events/sched/sched_switch/enable", "0"));
EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", "4"));
EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
- EXPECT_CALL(ftrace,
- WriteToFile("/root/events/sched/sched_switch/enable", "0"));
EXPECT_CALL(ftrace, ClearFile("/root/trace"));
EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+ EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
ASSERT_TRUE(model.RemoveConfig(id));
}
@@ -561,8 +607,8 @@
FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
// If someone is using ftrace already don't stomp on what they are doing.
- EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
- .WillOnce(Return('1'));
+ EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillOnce(Return("function"));
FtraceConfigId id = model.SetupConfig(config);
ASSERT_FALSE(id);
}
@@ -576,8 +622,10 @@
FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
- EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
- .WillOnce(Return('0'));
+ ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillByDefault(Return("nop"));
+ ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+ .WillByDefault(Return("0"));
EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
"--only_userspace", "sched"}),
_))
@@ -618,8 +666,10 @@
FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
- EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
- .WillOnce(Return('0'));
+ ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillByDefault(Return("nop"));
+ ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+ .WillByDefault(Return("0"));
EXPECT_CALL(
atrace,
RunAtrace(
@@ -663,6 +713,10 @@
FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
+ ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillByDefault(Return("nop"));
+ ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+ .WillByDefault(Return("0"));
EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
"--only_userspace", "cat_a",
"-a", "app_a"}),
@@ -734,6 +788,10 @@
FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
+ ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillByDefault(Return("nop"));
+ ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+ .WillByDefault(Return("0"));
EXPECT_CALL(
atrace,
RunAtrace(ElementsAreArray({"atrace", "--async_start", "--only_userspace",
@@ -795,6 +853,10 @@
FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
+ ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillByDefault(Return("nop"));
+ ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+ .WillByDefault(Return("0"));
EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
"--only_userspace", "cat_1",
"-a", "app_1"}),
@@ -832,6 +894,10 @@
FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
+ ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillByDefault(Return("nop"));
+ ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+ .WillByDefault(Return("0"));
FtraceConfigId id_a = model.SetupConfig(config_a);
ASSERT_TRUE(id_a);
@@ -879,8 +945,10 @@
*config.add_atrace_categories() = "cat_1";
*config.add_atrace_categories() = "cat_2";
- EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
- .WillRepeatedly(Return('0'));
+ ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillByDefault(Return("nop"));
+ ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+ .WillByDefault(Return("0"));
FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
@@ -990,17 +1058,20 @@
CreateFtraceConfig({"sched/sched_switch", "cgroup/cgroup_mkdir"});
FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
+ EXPECT_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillOnce(Return("nop"));
+ EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
+ .WillOnce(Return('1'));
+ EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
+ EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
+ EXPECT_CALL(ftrace, ClearFile("/root/trace"));
+ EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
.WillByDefault(Return("[local] global boot"));
EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
.Times(AnyNumber());
-
- EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
- .Times(2)
- .WillRepeatedly(Return('0'));
EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", _));
EXPECT_CALL(ftrace, WriteToFile("/root/trace_clock", "boot"));
- EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
EXPECT_CALL(ftrace,
WriteToFile("/root/events/sched/sched_switch/enable", "1"));
EXPECT_CALL(ftrace,
@@ -1010,6 +1081,8 @@
.WillOnce(Return(true));
FtraceConfigId id = model.SetupConfig(config);
ASSERT_TRUE(id);
+
+ EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
ASSERT_TRUE(model.ActivateConfig(id));
const FtraceDataSourceConfig* ds_config = model.GetDataSourceConfig(id);
@@ -1025,9 +1098,6 @@
EXPECT_THAT(central_filter->GetEnabledEvents(),
Contains(kCgroupMkdirEventId));
- EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
- EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", "4"));
- EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
EXPECT_CALL(ftrace,
WriteToFile("/root/events/sched/sched_switch/enable", "0"));
EXPECT_CALL(ftrace,
@@ -1035,8 +1105,12 @@
.WillOnce(Return(false));
EXPECT_CALL(ftrace, AppendToFile("/root/set_event", "!cgroup:cgroup_mkdir"))
.WillOnce(Return(true));
+ EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "0"));
+ EXPECT_CALL(ftrace, WriteToFile("/root/buffer_size_kb", "4"));
+ EXPECT_CALL(ftrace, WriteToFile("/root/events/enable", "0"));
EXPECT_CALL(ftrace, ClearFile("/root/trace"));
EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+ EXPECT_CALL(ftrace, WriteToFile("/root/tracing_on", "1"));
ASSERT_TRUE(model.RemoveConfig(id));
}
@@ -1058,6 +1132,11 @@
// Second data source - no compact encoding (default).
FtraceConfig config_disabled = CreateFtraceConfig({"sched/sched_switch"});
+ ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillByDefault(Return("nop"));
+ ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+ .WillByDefault(Return("0"));
+
{
FtraceConfigId id = model.SetupConfig(config_enabled);
ASSERT_TRUE(id);
@@ -1086,6 +1165,11 @@
FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
config.mutable_compact_sched()->set_enabled(true);
+ ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillByDefault(Return("nop"));
+ ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+ .WillByDefault(Return("0"));
+
FtraceConfigId id = model.SetupConfig(config);
ASSERT_TRUE(id);
@@ -1124,6 +1208,11 @@
CreateFtraceConfig({"sched/sched_switch", "sched/generic"});
config_with_disable.set_disable_generic_events(true);
+ ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillByDefault(Return("nop"));
+ ON_CALL(ftrace, ReadFileIntoString("/root/events/enable"))
+ .WillByDefault(Return("0"));
+
{
FtraceConfigId id = model.SetupConfig(config_default);
ASSERT_TRUE(id);
@@ -1158,8 +1247,14 @@
*config.add_function_graph_roots() = "sched*";
*config.add_function_graph_roots() = "*mm_fault";
+ ON_CALL(ftrace, ReadFileIntoString("/root/current_tracer"))
+ .WillByDefault(Return("nop"));
+
EXPECT_CALL(ftrace, WriteToFile(_, _)).WillRepeatedly(Return(true));
+ EXPECT_CALL(ftrace, ClearFile("/root/trace"));
+ EXPECT_CALL(ftrace, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+
// Set up config, assert that the tracefs writes happened:
EXPECT_CALL(ftrace, ClearFile("/root/set_ftrace_filter"));
EXPECT_CALL(ftrace, ClearFile("/root/set_graph_function"));
diff --git a/src/traced/probes/ftrace/ftrace_controller.cc b/src/traced/probes/ftrace/ftrace_controller.cc
index d7eb5e6..d82d115 100644
--- a/src/traced/probes/ftrace/ftrace_controller.cc
+++ b/src/traced/probes/ftrace/ftrace_controller.cc
@@ -365,8 +365,8 @@
ftrace_procfs_->ClearTrace();
}
-void FtraceController::DisableAllEvents() {
- ftrace_procfs_->DisableAllEvents();
+bool FtraceController::IsTracingAvailable() {
+ return ftrace_procfs_->IsTracingAvailable();
}
void FtraceController::Flush(FlushRequestID flush_id) {
diff --git a/src/traced/probes/ftrace/ftrace_controller.h b/src/traced/probes/ftrace/ftrace_controller.h
index 9a8ec2b..fd8cdee 100644
--- a/src/traced/probes/ftrace/ftrace_controller.h
+++ b/src/traced/probes/ftrace/ftrace_controller.h
@@ -77,8 +77,8 @@
bool preserve_ftrace_buffer);
virtual ~FtraceController();
- void DisableAllEvents();
void ClearTrace();
+ bool IsTracingAvailable();
bool AddDataSource(FtraceDataSource*) PERFETTO_WARN_UNUSED_RESULT;
bool StartDataSource(FtraceDataSource*);
diff --git a/src/traced/probes/ftrace/ftrace_controller_unittest.cc b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
index 01e075f..ba86ca7 100644
--- a/src/traced/probes/ftrace/ftrace_controller_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
@@ -132,6 +132,13 @@
.WillByDefault(Invoke(this, &MockFtraceProcfs::ReadTracingOn));
EXPECT_CALL(*this, ReadOneCharFromFile("/root/tracing_on"))
.Times(AnyNumber());
+
+ ON_CALL(*this, WriteToFile("/root/current_tracer", _))
+ .WillByDefault(Invoke(this, &MockFtraceProcfs::WriteCurrentTracer));
+ ON_CALL(*this, ReadFileIntoString("/root/current_tracer"))
+ .WillByDefault(Invoke(this, &MockFtraceProcfs::ReadCurrentTracer));
+ EXPECT_CALL(*this, ReadFileIntoString("/root/current_tracer"))
+ .Times(AnyNumber());
}
bool WriteTracingOn(const std::string& /*path*/, const std::string& value) {
@@ -144,6 +151,16 @@
return tracing_on_ ? '1' : '0';
}
+ bool WriteCurrentTracer(const std::string& /*path*/,
+ const std::string& value) {
+ current_tracer_ = value;
+ return true;
+ }
+
+ std::string ReadCurrentTracer(const std::string& /*path*/) {
+ return current_tracer_;
+ }
+
base::ScopedFile OpenPipeForCpu(size_t /*cpu*/) override {
return base::ScopedFile(base::OpenFile("/dev/null", O_RDONLY));
}
@@ -159,7 +176,8 @@
bool is_tracing_on() { return tracing_on_; }
private:
- bool tracing_on_ = false;
+ bool tracing_on_ = true;
+ std::string current_tracer_ = "nop";
};
} // namespace
@@ -261,8 +279,16 @@
FtraceConfig config = CreateFtraceConfig({"group/foo"});
- EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "1"));
+ EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
+ EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
+ EXPECT_CALL(*controller->procfs(), ClearFile("/root/trace"))
+ .WillOnce(Return(true));
+ EXPECT_CALL(*controller->procfs(),
+ ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")))
+ .WillRepeatedly(Return(true));
EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", _));
+ EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "1"));
+
auto data_source = controller->AddFakeDataSource(config);
ASSERT_TRUE(data_source);
@@ -279,19 +305,19 @@
Mock::VerifyAndClearExpectations(controller->runner());
// State clearing on tracing teardown.
+ EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "0"));
+ EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", "4"));
+ EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
EXPECT_CALL(*controller->procfs(), ClearFile("/root/trace"))
.WillOnce(Return(true));
EXPECT_CALL(*controller->procfs(),
ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")))
.WillRepeatedly(Return(true));
- EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "0"));
- EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
- EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
- EXPECT_TRUE(controller->procfs()->is_tracing_on());
+ EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "1"));
data_source.reset();
- EXPECT_FALSE(controller->procfs()->is_tracing_on());
+ EXPECT_TRUE(controller->procfs()->is_tracing_on());
}
TEST(FtraceControllerTest, MultipleSinks) {
@@ -303,6 +329,13 @@
// No read tasks posted as part of adding the data sources.
EXPECT_CALL(*controller->runner(), PostDelayedTask(_, _)).Times(0);
+ EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
+ EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
+ EXPECT_CALL(*controller->procfs(), ClearFile("/root/trace"))
+ .WillOnce(Return(true));
+ EXPECT_CALL(*controller->procfs(),
+ ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")))
+ .WillRepeatedly(Return(true));
EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", _));
EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "1"));
auto data_sourceA = controller->AddFakeDataSource(configA);
@@ -323,17 +356,22 @@
Mock::VerifyAndClearExpectations(controller->runner());
data_sourceA.reset();
+ EXPECT_TRUE(controller->procfs()->is_tracing_on());
// State clearing on tracing teardown.
EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "0"));
EXPECT_CALL(*controller->procfs(), WriteToFile(kBarEnablePath, "0"));
- EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", "4"));
EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
+ EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", "4"));
EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
- EXPECT_CALL(*controller->procfs(), ClearFile("/root/trace"));
+ EXPECT_CALL(*controller->procfs(), ClearFile("/root/trace"))
+ .WillOnce(Return(true));
EXPECT_CALL(*controller->procfs(),
- ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+ ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")))
+ .WillRepeatedly(Return(true));
+ EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "1"));
data_sourceB.reset();
+ EXPECT_TRUE(controller->procfs()->is_tracing_on());
}
TEST(FtraceControllerTest, ControllerMayDieFirst) {
@@ -341,6 +379,13 @@
FtraceConfig config = CreateFtraceConfig({"group/foo"});
+ EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
+ EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
+ EXPECT_CALL(*controller->procfs(), ClearFile("/root/trace"))
+ .WillOnce(Return(true));
+ EXPECT_CALL(*controller->procfs(),
+ ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")))
+ .WillRepeatedly(Return(true));
EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", _));
EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "1"));
auto data_source = controller->AddFakeDataSource(config);
@@ -350,14 +395,15 @@
// State clearing on tracing teardown.
EXPECT_CALL(*controller->procfs(), WriteToFile(kFooEnablePath, "0"));
+ EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
+ EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", "4"));
+ EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
EXPECT_CALL(*controller->procfs(), ClearFile("/root/trace"))
.WillOnce(Return(true));
EXPECT_CALL(*controller->procfs(),
ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")))
.WillRepeatedly(Return(true));
- EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "0"));
- EXPECT_CALL(*controller->procfs(), WriteToFile("/root/buffer_size_kb", "4"));
- EXPECT_CALL(*controller->procfs(), WriteToFile("/root/events/enable", "0"));
+ EXPECT_CALL(*controller->procfs(), WriteToFile("/root/tracing_on", "1"));
controller.reset();
data_source.reset();
}
diff --git a/src/traced/probes/ftrace/ftrace_procfs.cc b/src/traced/probes/ftrace/ftrace_procfs.cc
index 9865512..bd1c60c 100644
--- a/src/traced/probes/ftrace/ftrace_procfs.cc
+++ b/src/traced/probes/ftrace/ftrace_procfs.cc
@@ -180,6 +180,12 @@
return ReadFileIntoString(path);
}
+std::string FtraceProcfs::GetCurrentTracer() {
+ std::string path = root_ + "current_tracer";
+ std::string current_tracer = ReadFileIntoString(path);
+ return base::StripSuffix(current_tracer, "\n");
+}
+
bool FtraceProcfs::SetCurrentTracer(const std::string& tracer) {
std::string path = root_ + "current_tracer";
return WriteToFile(path, tracer);
@@ -407,25 +413,7 @@
return WriteNumberToFile(path, pages * (base::kPageSize / 1024ul));
}
-bool FtraceProcfs::EnableTracing() {
- KernelLogWrite("perfetto: enabled ftrace\n");
- PERFETTO_LOG("enabled ftrace in %s", root_.c_str());
- std::string path = root_ + "tracing_on";
- return WriteToFile(path, "1");
-}
-
-bool FtraceProcfs::DisableTracing() {
- KernelLogWrite("perfetto: disabled ftrace\n");
- PERFETTO_LOG("disabled ftrace in %s", root_.c_str());
- std::string path = root_ + "tracing_on";
- return WriteToFile(path, "0");
-}
-
-bool FtraceProcfs::SetTracingOn(bool enable) {
- return enable ? EnableTracing() : DisableTracing();
-}
-
-bool FtraceProcfs::IsTracingEnabled() {
+bool FtraceProcfs::GetTracingOn() {
std::string path = root_ + "tracing_on";
char tracing_on = ReadOneCharFromFile(path);
if (tracing_on == '\0')
@@ -433,6 +421,37 @@
return tracing_on == '1';
}
+bool FtraceProcfs::SetTracingOn(bool on) {
+ std::string path = root_ + "tracing_on";
+ if (!WriteToFile(path, on ? "1" : "0")) {
+ PERFETTO_PLOG("Failed to write %s", path.c_str());
+ return false;
+ }
+ if (on) {
+ KernelLogWrite("perfetto: enabled ftrace\n");
+ PERFETTO_LOG("enabled ftrace in %s", root_.c_str());
+ } else {
+ KernelLogWrite("perfetto: disabled ftrace\n");
+ PERFETTO_LOG("disabled ftrace in %s", root_.c_str());
+ }
+
+ return true;
+}
+
+bool FtraceProcfs::IsTracingAvailable() {
+ std::string current_tracer = GetCurrentTracer();
+
+ // Ftrace tracing is available if current_tracer == "nop".
+ // events/enable could be 0, 1, X or 0*. 0* means events would be
+ // dynamically enabled so we need to treat as event tracing is in use.
+ // However based on the discussion in asop/2328817, on Android events/enable
+ // is "X" after boot up. To avoid causing more problem, the decision is just
+ // look at current_tracer.
+ // As the discussion in asop/2328817, if GetCurrentTracer failed to
+ // read file and return "", we treat it as tracing is available.
+ return current_tracer == "nop" || current_tracer == "";
+}
+
bool FtraceProcfs::SetClock(const std::string& clock_name) {
std::string path = root_ + "trace_clock";
return WriteToFile(path, clock_name);
diff --git a/src/traced/probes/ftrace/ftrace_procfs.h b/src/traced/probes/ftrace/ftrace_procfs.h
index 115d300..40a7da9 100644
--- a/src/traced/probes/ftrace/ftrace_procfs.h
+++ b/src/traced/probes/ftrace/ftrace_procfs.h
@@ -68,6 +68,7 @@
virtual std::string ReadPageHeaderFormat() const;
+ std::string GetCurrentTracer();
// Sets the "current_tracer". Might fail with EBUSY if tracing pipes have
// already been opened for reading.
bool SetCurrentTracer(const std::string& tracer);
@@ -134,19 +135,17 @@
// Writes the string |str| as an event into the trace buffer.
bool WriteTraceMarker(const std::string& str);
- // Enable tracing.
- bool EnableTracing();
+ // Read tracing_on and return true if tracing_on is 1, otherwise return false.
+ bool GetTracingOn();
- // Disables tracing, does not clear the buffer.
- bool DisableTracing();
+ // Write 1 to tracing_on if |on| is true, otherwise write 0.
+ bool SetTracingOn(bool on);
- // Enables/disables tracing, does not clear the buffer.
- bool SetTracingOn(bool enable);
-
- // Returns true iff tracing is enabled.
- // Necessarily racy: another program could enable/disable tracing at any
- // point.
- bool IsTracingEnabled();
+ // Returns true if ftrace tracing is available.
+ // Ftrace tracing is available iff "/current_tracer" is "nop", indicates
+ // function tracing is not in use. Necessarily
+ // racy: another program could enable/disable tracing at any point.
+ bool IsTracingAvailable();
// Set the clock. |clock_name| should be one of the names returned by
// AvailableClocks. Setting the clock clears the buffer.
diff --git a/src/traced/probes/ftrace/ftrace_procfs_integrationtest.cc b/src/traced/probes/ftrace/ftrace_procfs_integrationtest.cc
index 25e1aa7..24ad32a 100644
--- a/src/traced/probes/ftrace/ftrace_procfs_integrationtest.cc
+++ b/src/traced/probes/ftrace/ftrace_procfs_integrationtest.cc
@@ -81,19 +81,18 @@
void FtraceProcfsIntegrationTest::SetUp() {
ftrace_ = FtraceProcfs::Create(GetFtracePath());
ASSERT_TRUE(ftrace_);
- if (ftrace_->IsTracingEnabled()) {
+ if (!ftrace_->IsTracingAvailable()) {
GTEST_SKIP() << "Something else is using ftrace, skipping";
}
- ftrace_->DisableAllEvents();
ftrace_->ClearTrace();
- ftrace_->EnableTracing();
+ ftrace_->SetTracingOn(true);
}
void FtraceProcfsIntegrationTest::TearDown() {
ftrace_->DisableAllEvents();
ftrace_->ClearTrace();
- ftrace_->DisableTracing();
+ ftrace_->SetTracingOn(false);
}
TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(CreateWithBadPath)) {
@@ -123,20 +122,30 @@
EXPECT_THAT(GetTraceOutput(), Not(HasSubstr("sched_switch")));
}
-TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(EnableDisableTracing)) {
- EXPECT_TRUE(ftrace_->IsTracingEnabled());
+TEST_F(FtraceProcfsIntegrationTest,
+ ANDROID_ONLY_TEST(EnableDisableTraceBuffer)) {
ftrace_->WriteTraceMarker("Before");
- ftrace_->DisableTracing();
- EXPECT_FALSE(ftrace_->IsTracingEnabled());
+ ftrace_->SetTracingOn(false);
ftrace_->WriteTraceMarker("During");
- ftrace_->EnableTracing();
- EXPECT_TRUE(ftrace_->IsTracingEnabled());
+ ftrace_->SetTracingOn(true);
ftrace_->WriteTraceMarker("After");
EXPECT_THAT(GetTraceOutput(), HasSubstr("Before"));
EXPECT_THAT(GetTraceOutput(), Not(HasSubstr("During")));
EXPECT_THAT(GetTraceOutput(), HasSubstr("After"));
}
+TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(IsTracingAvailable)) {
+ EXPECT_TRUE(ftrace_->IsTracingAvailable());
+ ftrace_->SetCurrentTracer("function");
+ EXPECT_FALSE(ftrace_->IsTracingAvailable());
+ ftrace_->SetCurrentTracer("nop");
+ EXPECT_TRUE(ftrace_->IsTracingAvailable());
+ ASSERT_TRUE(ftrace_->EnableEvent("sched", "sched_switch"));
+ EXPECT_FALSE(ftrace_->IsTracingAvailable());
+ ftrace_->DisableAllEvents();
+ EXPECT_TRUE(ftrace_->IsTracingAvailable());
+}
+
TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(ReadFormatFile)) {
std::string format = ftrace_->ReadEventFormat("ftrace", "print");
EXPECT_THAT(format, HasSubstr("name: print"));
@@ -168,7 +177,6 @@
TEST_F(FtraceProcfsIntegrationTest,
ANDROID_ONLY_TEST(FtraceControllerHardReset)) {
ftrace_->SetCpuBufferSizeInPages(4ul);
- ftrace_->EnableTracing();
ftrace_->EnableEvent("sched", "sched_switch");
ftrace_->WriteTraceMarker("Hello, World!");
diff --git a/src/traced/probes/kmem_activity_trigger.cc b/src/traced/probes/kmem_activity_trigger.cc
index b30108c..7e29a96 100644
--- a/src/traced/probes/kmem_activity_trigger.cc
+++ b/src/traced/probes/kmem_activity_trigger.cc
@@ -52,7 +52,7 @@
KmemActivityTrigger::WorkerData::~WorkerData() {
PERFETTO_DCHECK_THREAD(thread_checker_);
if (ftrace_procfs_) {
- ftrace_procfs_->DisableTracing();
+ ftrace_procfs_->SetTracingOn(false);
ftrace_procfs_->ClearTrace();
}
DisarmFtraceFDWatches();
@@ -79,7 +79,7 @@
ftrace_procfs_->DisableAllEvents();
ftrace_procfs_->EnableEvent("vmscan", "mm_vmscan_direct_reclaim_begin");
ftrace_procfs_->EnableEvent("compaction", "mm_compaction_begin");
- ftrace_procfs_->EnableTracing();
+ ftrace_procfs_->SetTracingOn(true);
num_cpus_ = ftrace_procfs_->NumberOfCpus();
for (size_t cpu = 0; cpu < num_cpus_; cpu++) {
diff --git a/src/traced/probes/probes_producer.cc b/src/traced/probes/probes_producer.cc
index 04e5248..0d2573a 100644
--- a/src/traced/probes/probes_producer.cc
+++ b/src/traced/probes/probes_producer.cc
@@ -137,11 +137,6 @@
ftrace_creation_failed_ = true;
return nullptr;
}
-
- if (!ftrace_config.preserve_ftrace_buffer()) {
- ftrace_->DisableAllEvents();
- ftrace_->ClearTrace();
- }
}
PERFETTO_LOG("Ftrace setup (target_buf=%" PRIu32 ")", config.target_buffer());
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index 0e131dd..f0f2f5d 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -45,6 +45,7 @@
#include "src/tracing/test/api_test_support.h"
#include "src/tracing/test/tracing_module.h"
+#include "perfetto/base/time.h"
#include "perfetto/protozero/scattered_heap_buffer.h"
#include "perfetto/tracing/core/data_source_descriptor.h"
#include "perfetto/tracing/core/trace_config.h"
diff --git a/test/cts/heapprofd_java_test_cts.cc b/test/cts/heapprofd_java_test_cts.cc
index 10bc985..c2f3f77 100644
--- a/test/cts/heapprofd_java_test_cts.cc
+++ b/test/cts/heapprofd_java_test_cts.cc
@@ -89,7 +89,8 @@
return helper.trace();
}
-std::vector<protos::gen::TracePacket> TriggerOomHeapDump(std::string app_name) {
+std::vector<protos::gen::TracePacket> TriggerOomHeapDump(std::string app_name,
+ std::string heap_dump_target) {
base::TestTaskRunner task_runner;
// (re)start the target app's main activity
@@ -119,6 +120,10 @@
ds_config->set_name("android.java_hprof.oom");
ds_config->set_target_buffer(0);
+ protos::gen::JavaHprofConfig java_hprof_config;
+ java_hprof_config.add_process_cmdline(heap_dump_target.c_str());
+ ds_config->set_java_hprof_config_raw(java_hprof_config.SerializeAsString());
+
// start tracing
helper.StartTracing(trace_config);
StartAppActivity(app_name, "JavaOomActivity", "target.app.running", &task_runner,
@@ -233,9 +238,17 @@
if (IsUserBuild()) return;
std::string app_name = "android.perfetto.cts.app.debuggable";
- const auto& packets = TriggerOomHeapDump(app_name);
+ const auto& packets = TriggerOomHeapDump(app_name, "*");
AssertGraphPresent(packets);
}
+TEST(HeapprofdJavaCtsTest, DebuggableAppOomNotSelected) {
+ if (IsUserBuild()) return;
+
+ std::string app_name = "android.perfetto.cts.app.debuggable";
+ const auto& packets = TriggerOomHeapDump(app_name, "not.this.app");
+ AssertNoProfileContents(packets);
+}
+
} // namespace
} // namespace perfetto
diff --git a/test/data/perf_sample_annotations.pftrace.sha256 b/test/data/perf_sample_annotations.pftrace.sha256
new file mode 100644
index 0000000..4eb6deb
--- /dev/null
+++ b/test/data/perf_sample_annotations.pftrace.sha256
@@ -0,0 +1 @@
+067957102d4f2c4dbb05e6d1948c76ab426b06d0ba07d0e7237e1442218b700e
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
index f0f270b..f48f5b8 100644
--- a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
+++ b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
@@ -1 +1 @@
-9afd1d1f82d762dc2b2e294bdbd68f834f10ca7c7eab4a9787eaf3e75e8f1bbb
\ No newline at end of file
+2642993d0ccbdcc083d863a15d3a60e4e4458c278190bd057491a68727601ac5
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
index de62f0b..8049eda 100644
--- a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
+++ b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
@@ -1 +1 @@
-a7da2d0e1f2f20dffad44e554dc5a6998bf70d0128ca900907157b4a2303c63a
\ No newline at end of file
+604b0837fc54444c0f347a26821892ce502796859daea4634bbd371b0248b4fd
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256 b/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256
index 8528d4c..f5893a1 100644
--- a/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256
@@ -1 +1 @@
-d10161589d43cb6a0bc1df341581db5489f3ac6efcdf254894a5851178165a13
\ No newline at end of file
+9ce961410f075c01643dbee74ad0f57155f799c2af1c0164e9cbebc9681c65a1
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
index e223d72..9950bf8 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
@@ -1 +1 @@
-0f3c280c0b44ed4e9d4a0849512ca3f21914cd86581cde1bb5e6f1dce4ea7843
\ No newline at end of file
+b94937e792f3103cbb504357fb3cf4c7e0e6e0bad788c8eef05fb7954f52abf9
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
index b4eb4f7..bd9d924 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
@@ -1 +1 @@
-77ad6a3f90b414957bd1fd78d39ceb380b28f384b4d4a0a3ed1e11e394f635be
\ No newline at end of file
+fd6a8a39085b3b2d4fd9f719f08c3f529eac99c06f30dc457278ebc4231dc6fe
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
index 98195eb..70a462d 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
@@ -1 +1 @@
-b4e711f0c25cea261c84ec594ac0939887d81687a70a47e291075c8bb24ff1f1
\ No newline at end of file
+72b31cf41144b44831443984cf80abe87fb213a8d73df767268d7c9ba0a63398
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
index fd28c9c..d9fed2d 100644
--- a/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
@@ -1 +1 @@
-21d088add5542b38703452ac16600f878f865bc220baad0d912bc56d982ab207
\ No newline at end of file
+a9bf192426e9a29380ae8dd47d3974e8607bc7fd01ab0498e9b2cb1cdca75acb
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256
index 32cf9b1..4ee2372 100644
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256
@@ -1 +1 @@
-6bb4ec59ccea148ed9ddd2878dad220dcdeb0264a3a61334c9ee66d57cd8094d
\ No newline at end of file
+c4773556fad9c5b83fc6d8cd2f271faef9e088da862b32a0b1ef14f9d56fa162
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
index fd28c9c..d9fed2d 100644
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
@@ -1 +1 @@
-21d088add5542b38703452ac16600f878f865bc220baad0d912bc56d982ab207
\ No newline at end of file
+a9bf192426e9a29380ae8dd47d3974e8607bc7fd01ab0498e9b2cb1cdca75acb
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256
index 32cf9b1..4ee2372 100644
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256
@@ -1 +1 @@
-6bb4ec59ccea148ed9ddd2878dad220dcdeb0264a3a61334c9ee66d57cd8094d
\ No newline at end of file
+c4773556fad9c5b83fc6d8cd2f271faef9e088da862b32a0b1ef14f9d56fa162
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256
index 651681e..a00bc34 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256
@@ -1 +1 @@
-d8c9876668f6f0673d99adf2ad7ee30dad0ffafe60bd0faa9fb709e49addba20
\ No newline at end of file
+c3461778784561480bd02f6202651ffa3736ac19abfefc8ddd3bbed1e5a2fe08
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
index fd28c9c..d9fed2d 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
@@ -1 +1 @@
-21d088add5542b38703452ac16600f878f865bc220baad0d912bc56d982ab207
\ No newline at end of file
+a9bf192426e9a29380ae8dd47d3974e8607bc7fd01ab0498e9b2cb1cdca75acb
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256
index 32cf9b1..4ee2372 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256
@@ -1 +1 @@
-6bb4ec59ccea148ed9ddd2878dad220dcdeb0264a3a61334c9ee66d57cd8094d
\ No newline at end of file
+c4773556fad9c5b83fc6d8cd2f271faef9e088da862b32a0b1ef14f9d56fa162
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256
index 32cf9b1..4ee2372 100644
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256
@@ -1 +1 @@
-6bb4ec59ccea148ed9ddd2878dad220dcdeb0264a3a61334c9ee66d57cd8094d
\ No newline at end of file
+c4773556fad9c5b83fc6d8cd2f271faef9e088da862b32a0b1ef14f9d56fa162
\ No newline at end of file
diff --git a/test/ftrace_integrationtest.cc b/test/ftrace_integrationtest.cc
index 42d251f..068ebf7 100644
--- a/test/ftrace_integrationtest.cc
+++ b/test/ftrace_integrationtest.cc
@@ -173,7 +173,7 @@
helper.StartTracing(trace_config);
// Wait for traced_probes to start.
- helper.WaitFor([&] { return ftrace_procfs_->IsTracingEnabled(); }, "ftrace");
+ helper.WaitFor([&] { return ftrace_procfs_->GetTracingOn(); }, "ftrace");
// Do a first flush just to synchronize with the producer. The problem here
// is that, on a Linux workstation, the producer can take several seconds just
diff --git a/test/stress_test/stress_test.cc b/test/stress_test/stress_test.cc
index 513b4bb..213017e 100644
--- a/test/stress_test/stress_test.cc
+++ b/test/stress_test/stress_test.cc
@@ -27,6 +27,7 @@
#include "perfetto/base/build_config.h"
#include "perfetto/base/compiler.h"
+#include "perfetto/base/time.h"
#include "perfetto/ext/base/ctrl_c_handler.h"
#include "perfetto/ext/base/file_utils.h"
#include "perfetto/ext/base/scoped_file.h"
diff --git a/test/trace_processor/profiling/perf_sample_switch_interp.textproto b/test/trace_processor/profiling/perf_sample_switch_interp.textproto
new file mode 100644
index 0000000..a8ddcbf
--- /dev/null
+++ b/test/trace_processor/profiling/perf_sample_switch_interp.textproto
@@ -0,0 +1,127 @@
+packet {
+ interned_data {
+ build_ids {
+ iid: 0
+ str: ""
+ }
+ mapping_paths {
+ iid: 0
+ str: ""
+ }
+ function_names {
+ iid: 0
+ str: ""
+ }
+ }
+ sequence_flags: 1
+ trusted_uid: 9999
+ trusted_packet_sequence_id: 2
+ trusted_pid: 24388
+}
+packet {
+ timestamp: 214681394021835
+ interned_data {
+ mappings {
+ iid: 1
+ path_string_ids: 1
+ path_string_ids: 2
+ path_string_ids: 3
+ path_string_ids: 4
+ build_id: 1
+ }
+ build_ids {
+ iid: 1
+ str: ""
+ }
+ mapping_paths {
+ iid: 1
+ str: "apex"
+ }
+ mapping_paths {
+ iid: 2
+ str: "com.android.art"
+ }
+ mapping_paths {
+ iid: 3
+ str: "lib64"
+ }
+ mapping_paths {
+ iid: 4
+ str: "libart.so"
+ }
+ mappings {
+ iid: 2
+ path_string_ids: 6
+ build_id: 1
+ }
+ mapping_paths {
+ iid: 6
+ str: "example.vdex"
+ }
+ frames {
+ iid: 1
+ function_name_id: 1
+ mapping_id: 2
+ }
+ function_names {
+ iid: 1
+ str: "com.example.managed.frame"
+ }
+ frames {
+ iid: 2
+ function_name_id: 2
+ mapping_id: 1
+ }
+ function_names {
+ iid: 2
+ str: "ExecuteSwitchImplAsm"
+ }
+ frames {
+ iid: 3
+ function_name_id: 3
+ mapping_id: 1
+ }
+ function_names {
+ iid: 3
+ str: "_ZN3art11interpreter20ExecuteSwitchImplCppILb0EEEvPNS0_17SwitchImplContextE"
+ }
+ frames {
+ iid: 4
+ function_name_id: 4
+ mapping_id: 1
+ }
+ function_names {
+ iid: 4
+ str: "_ZN3art11interpreter6DoCallILb1EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtbPNS_6JValueE"
+ }
+ frames {
+ iid: 5
+ function_name_id: 5
+ mapping_id: 1
+ }
+ function_names {
+ iid: 5
+ str: "_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc"
+ }
+ callstacks {
+ iid: 1
+ frame_ids: 1
+ frame_ids: 2
+ frame_ids: 3
+ frame_ids: 4
+ frame_ids: 5
+ }
+ }
+ perf_sample {
+ cpu: 0
+ pid: 1000
+ tid: 1000
+ cpu_mode: MODE_USER
+ timebase_count: 42
+ callstack_iid: 1
+ }
+ trusted_uid: 9999
+ trusted_packet_sequence_id: 2
+ trusted_pid: 24388
+}
+
diff --git a/test/trace_processor/profiling/tests.py b/test/trace_processor/profiling/tests.py
index 6bcb65d..33ed4fc 100644
--- a/test/trace_processor/profiling/tests.py
+++ b/test/trace_processor/profiling/tests.py
@@ -285,3 +285,74 @@
ORDER BY ps.ts ASC, eac.depth ASC;
""",
out=Path('perf_sample_sc.out'))
+
+ def test_annotations(self):
+ return DiffTestBlueprint(
+ trace=Path('../../data/perf_sample_annotations.pftrace'),
+ query="""
+ select
+ eac.depth, eac.annotation, spm.name as map_name,
+ ifnull(demangle(spf.name), spf.name) as frame_name
+ from experimental_annotated_callstack eac
+ join stack_profile_frame spf on (eac.frame_id = spf.id)
+ join stack_profile_mapping spm on (spf.mapping = spm.id)
+ where eac.start_id = (
+ select spc.id as callsite_id
+ from stack_profile_callsite spc
+ join stack_profile_frame spf on (spc.frame_id = spf.id)
+ where spf.name = "_ZN3art28ResolveFieldWithAccessChecksEPNS_6ThreadEPNS_11ClassLinkerEtPNS_9ArtMethodEbbm")
+ order by depth asc;
+ """,
+ out=Csv("""
+ "depth","annotation","map_name","frame_name"
+ 0,"[NULL]","/apex/com.android.runtime/lib64/bionic/libc.so","__libc_init"
+ 1,"[NULL]","/system/bin/app_process64","main"
+ 2,"[NULL]","/system/lib64/libandroid_runtime.so","android::AndroidRuntime::start(char const*, android::Vector<android::String8> const&, bool)"
+ 3,"[NULL]","/system/lib64/libandroid_runtime.so","_JNIEnv::CallStaticVoidMethod(_jclass*, _jmethodID*, ...)"
+ 4,"[NULL]","/apex/com.android.art/lib64/libart.so","art::JNI<true>::CallStaticVoidMethodV(_JNIEnv*, _jclass*, _jmethodID*, std::__va_list)"
+ 5,"[NULL]","/apex/com.android.art/lib64/libart.so","art::JValue art::InvokeWithVarArgs<_jmethodID*>(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jmethodID*, std::__va_list)"
+ 6,"[NULL]","/apex/com.android.art/lib64/libart.so","art_quick_invoke_static_stub"
+ 7,"aot","/system/framework/arm64/boot-framework.oat","com.android.internal.os.ZygoteInit.main"
+ 8,"aot","/system/framework/arm64/boot-framework.oat","com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run"
+ 9,"common-frame","/system/framework/arm64/boot.oat","art_jni_trampoline"
+ 10,"[NULL]","/apex/com.android.art/lib64/libart.so","art::Method_invoke(_JNIEnv*, _jobject*, _jobject*, _jobjectArray*) (.__uniq.165753521025965369065708152063621506277)"
+ 11,"common-frame","/apex/com.android.art/lib64/libart.so","_jobject* art::InvokeMethod<(art::PointerSize)8>(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jobject*, _jobject*, unsigned long)"
+ 12,"common-frame","/apex/com.android.art/lib64/libart.so","art_quick_invoke_static_stub"
+ 13,"aot","/system/framework/arm64/boot-framework.oat","android.app.ActivityThread.main"
+ 14,"aot","/system/framework/arm64/boot-framework.oat","android.os.Looper.loop"
+ 15,"aot","/system/framework/arm64/boot-framework.oat","android.os.Looper.loopOnce"
+ 16,"aot","/system/framework/arm64/boot-framework.oat","android.os.Handler.dispatchMessage"
+ 17,"aot","/system/framework/arm64/boot-framework.oat","android.view.Choreographer$FrameDisplayEventReceiver.run"
+ 18,"aot","/system/framework/arm64/boot-framework.oat","android.view.Choreographer.doFrame"
+ 19,"aot","/system/framework/arm64/boot-framework.oat","android.view.Choreographer.doCallbacks"
+ 20,"aot","/system/framework/arm64/boot-framework.oat","android.view.ViewRootImpl$TraversalRunnable.run"
+ 21,"aot","/system/framework/arm64/boot-framework.oat","android.view.ViewRootImpl.doTraversal"
+ 22,"aot","/system/framework/arm64/boot-framework.oat","android.view.ViewRootImpl.performTraversals"
+ 23,"interp","/system/framework/framework.jar","android.view.ViewRootImpl.notifyDrawStarted"
+ 24,"common-frame-interp","/apex/com.android.art/lib64/libart.so","nterp_op_iget_object_slow_path"
+ 25,"common-frame-interp","/apex/com.android.art/lib64/libart.so","nterp_get_instance_field_offset"
+ 26,"common-frame-interp","/apex/com.android.art/lib64/libart.so","NterpGetInstanceFieldOffset"
+ 27,"common-frame","/apex/com.android.art/lib64/libart.so","art::ResolveFieldWithAccessChecks(art::Thread*, art::ClassLinker*, unsigned short, art::ArtMethod*, bool, bool, unsigned long)"
+ """))
+
+ def test_annotations_switch_interpreter(self):
+ return DiffTestBlueprint(
+ trace=Path('perf_sample_switch_interp.textproto'),
+ query="""
+ select
+ eac.depth, eac.annotation, spm.name as map_name,
+ ifnull(demangle(spf.name), spf.name) as frame_name
+ from experimental_annotated_callstack eac
+ join stack_profile_frame spf on (eac.frame_id = spf.id)
+ join stack_profile_mapping spm on (spf.mapping = spm.id)
+ where eac.start_id = (select callsite_id from perf_sample)
+ order by depth asc;
+ """,
+ out=Csv("""
+ "depth","annotation","map_name","frame_name"
+ 0,"interp","/example.vdex","com.example.managed.frame"
+ 1,"common-frame-interp","/apex/com.android.art/lib64/libart.so","ExecuteSwitchImplAsm"
+ 2,"common-frame-interp","/apex/com.android.art/lib64/libart.so","void art::interpreter::ExecuteSwitchImplCpp<false>(art::interpreter::SwitchImplContext*)"
+ 3,"common-frame-interp","/apex/com.android.art/lib64/libart.so","bool art::interpreter::DoCall<true>(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, bool, art::JValue*)"
+ 4,"common-frame","/apex/com.android.art/lib64/libart.so","art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)"
+ """))
diff --git a/test/trace_processor/startup/android_startup.out b/test/trace_processor/startup/android_startup.out
index f804a34..752db7b 100644
--- a/test/trace_processor/startup/android_startup.out
+++ b/test/trace_processor/startup/android_startup.out
@@ -8,7 +8,7 @@
dur_ns: 108
main_thread_by_task_state {
running_dur_ns: 10
- runnable_dur_ns: 80
+ runnable_dur_ns: 90
uninterruptible_sleep_dur_ns: 0
interruptible_sleep_dur_ns: 10
uninterruptible_io_sleep_dur_ns: 0
diff --git a/test/trace_processor/startup/android_startup_breakdown.out b/test/trace_processor/startup/android_startup_breakdown.out
index d58a886..8b35bb5 100644
--- a/test/trace_processor/startup/android_startup_breakdown.out
+++ b/test/trace_processor/startup/android_startup_breakdown.out
@@ -8,7 +8,7 @@
dur_ns: 108000000000
main_thread_by_task_state {
running_dur_ns: 25000000000
- runnable_dur_ns: 5000000000
+ runnable_dur_ns: 30000000000
uninterruptible_sleep_dur_ns: 0
interruptible_sleep_dur_ns: 0
uninterruptible_io_sleep_dur_ns: 0
@@ -107,6 +107,7 @@
slow_start_reason: "Time spent in bindApplication"
slow_start_reason: "Time spent in view inflation"
slow_start_reason: "Time spent in ResourcesManager#getResources"
+ slow_start_reason: "Potential CPU contention with init"
slow_start_reason: "No baseline or cloud profiles"
slow_start_reason: "Optimized artifacts missing, run from apk"
startup_type: "cold"
diff --git a/test/trace_processor/startup/android_startup_breakdown_slow.out b/test/trace_processor/startup/android_startup_breakdown_slow.out
index fec1c2e..3ed8ce3 100644
--- a/test/trace_processor/startup/android_startup_breakdown_slow.out
+++ b/test/trace_processor/startup/android_startup_breakdown_slow.out
@@ -8,7 +8,7 @@
dur_ns: 108000000000
main_thread_by_task_state {
running_dur_ns: 25000000000
- runnable_dur_ns: 5000000000
+ runnable_dur_ns: 30000000000
uninterruptible_sleep_dur_ns: 0
interruptible_sleep_dur_ns: 0
uninterruptible_io_sleep_dur_ns: 0
@@ -107,6 +107,7 @@
slow_start_reason: "Time spent in bindApplication"
slow_start_reason: "Time spent in view inflation"
slow_start_reason: "Time spent in ResourcesManager#getResources"
+ slow_start_reason: "Potential CPU contention with init"
slow_start_reason: "Optimized artifacts missing, run from apk"
startup_type: "cold"
}
diff --git a/test/trace_processor/startup/android_startup_slow.out b/test/trace_processor/startup/android_startup_slow.out
index 9ae0024..333883a 100644
--- a/test/trace_processor/startup/android_startup_slow.out
+++ b/test/trace_processor/startup/android_startup_slow.out
@@ -8,7 +8,7 @@
dur_ns: 108000000000
main_thread_by_task_state {
running_dur_ns: 10000000000
- runnable_dur_ns: 80000000000
+ runnable_dur_ns: 90000000000
uninterruptible_sleep_dur_ns: 5000000000
interruptible_sleep_dur_ns: 5000000000
uninterruptible_io_sleep_dur_ns: 5000000000
@@ -67,5 +67,6 @@
slow_start_reason: "Main Thread - Time spent in Runnable state"
slow_start_reason: "Main Thread - Time spent in interruptible sleep state"
slow_start_reason: "Main Thread - Time spent in Blocking I/O"
+ slow_start_reason: "Potential CPU contention with init"
}
}
diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js
index 16553be..b890b89 100644
--- a/ui/.eslintrc.js
+++ b/ui/.eslintrc.js
@@ -47,7 +47,7 @@
// https://github.com/typescript-eslint/typescript-eslint/issues/2621
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars':
- ['error', {'argsIgnorePattern': '^_.*'}],
+ ['error', {'argsIgnorePattern': '^_.*', 'varsIgnorePattern': '^_.*'}],
// new Array() is banned (use [] instead) but new Array<Foo>() is
// allowed since it can be clearer to put the type by the
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 570fa44..5470643 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -11,6 +11,9 @@
// 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 "fonts";
+
:root {
--sidebar-width: 256px;
--topbar-height: 48px;
@@ -199,28 +202,64 @@
width: 100%;
}
+@mixin table-font-size {
+ font-size: 14px;
+ line-height: 18px;
+}
+
+$table-hover-color: hsl(214, 22%, 90%);
+
+$table-border-color: rgba(60, 76, 92, 0.4);
+
+.pivot-table {
+ @include bottom-panel-font;
+ @include table-font-size;
+
+ width: 100%;
+ border-collapse: collapse;
+
+ thead,
+ i {
+ cursor: pointer;
+ }
+ thead {
+ font-weight: normal;
+ }
+ td {
+ padding: 2px 1px;
+ }
+ td.first {
+ border-left: 1px solid $table-border-color;
+ padding-left: 6px;
+ }
+ tr.header {
+ border-bottom: 1px solid $table-border-color;
+ text-align: center;
+ }
+ thead td.reorderable-cell {
+ cursor: grab;
+ }
+ tr:hover td {
+ background-color: $table-hover-color;
+ }
+ .disabled {
+ cursor: default;
+ }
+ .indent {
+ display: inline-block;
+ // 16px is the width of expand_more/expand_less icon to pad out cells
+ // without the button
+ width: 16px;
+ }
+ strong {
+ font-weight: 400;
+ }
+}
+
.query-table {
width: 100%;
font-size: 14px;
border: 0;
- &.pivot-table {
- thead,
- i {
- cursor: pointer;
- }
- thead td.reorderable-cell {
- cursor: grab;
- }
- .disabled {
- cursor: default;
- }
- .indent {
- display: inline-block;
- // 16px is the width of expand_more/expand_less icon to pad out cells
- // without the button
- width: 16px;
- }
- }
thead td {
position: sticky;
top: 0;
@@ -755,6 +794,7 @@
.pivot-table-redux {
user-select: text;
+ padding: 10px;
button.mode-button {
border-radius: 10px;
@@ -763,11 +803,6 @@
background-color: #c7d0db;
}
- &.edit {
- padding: 10px;
- display: flex;
- }
-
&.query-error {
color: red;
}
@@ -817,11 +852,11 @@
}
&.highlight-left {
- border-left-color: red;
+ background: linear-gradient(90deg, $table-border-color, transparent 20%);
}
&.highlight-right {
- border-right-color: red;
+ background: linear-gradient(270deg, $table-border-color, transparent 20%);
}
}
diff --git a/ui/src/assets/details.scss b/ui/src/assets/details.scss
index 11186c2..3faaadd 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -97,9 +97,7 @@
}
.details-panel {
- font-family: "Roboto Condensed", sans-serif;
- font-weight: 300;
- color: #3c4b5d;
+ @include bottom-panel-font;
.material-icons {
@include transition(0.3s);
@@ -234,8 +232,7 @@
table {
@include transition(0.1s);
- font-size: 14px;
- line-height: 18px;
+ @include table-font-size;
width: 100%;
// Aggregation panel uses multiple table elements that need to be aligned,
// which is done by using fixed table layout.
@@ -245,7 +242,7 @@
tr:hover {
td,
th {
- background-color: hsl(214, 22%, 90%);
+ background-color: $table-hover-color;
&.no-highlight {
background-color: white;
@@ -392,7 +389,7 @@
font-weight: bolder;
font-size: 12px;
.sum-data {
- border-bottom: 1px solid rgba(60, 76, 92, 0.4);
+ border-bottom: 1px solid $table-border-color;
}
}
@@ -543,7 +540,7 @@
background-color: hsl(214, 22%, 95%);
}
&:hover {
- background-color: hsl(214, 22%, 90%);
+ background-color: $table-hover-color;
}
.cell {
font-size: 11px;
diff --git a/ui/src/assets/fonts.scss b/ui/src/assets/fonts.scss
new file mode 100644
index 0000000..6f52d19
--- /dev/null
+++ b/ui/src/assets/fonts.scss
@@ -0,0 +1,20 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@mixin bottom-panel-font {
+ font-family: "Roboto Condensed", sans-serif;
+ font-weight: 300;
+ color: #3c4b5d;
+}
+
diff --git a/ui/src/base/math_utils.ts b/ui/src/base/math_utils.ts
new file mode 100644
index 0000000..f1c1816
--- /dev/null
+++ b/ui/src/base/math_utils.ts
@@ -0,0 +1,23 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Round a number up to the nearest stepsize.
+export function roundUpNearest(val: number, stepsize: number): number {
+ return stepsize * Math.ceil(val / stepsize);
+}
+
+// Round a number down to the nearest stepsize.
+export function roundDownNearest(val: number, stepsize: number): number {
+ return stepsize * Math.floor(val / stepsize);
+}
diff --git a/ui/src/base/math_utils_unittest.ts b/ui/src/base/math_utils_unittest.ts
new file mode 100644
index 0000000..169b793
--- /dev/null
+++ b/ui/src/base/math_utils_unittest.ts
@@ -0,0 +1,29 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {roundDownNearest, roundUpNearest} from './math_utils';
+
+describe('roundUpNearest()', () => {
+ it('rounds decimal values up to the right step size', () => {
+ expect(roundUpNearest(0.1, 0.5)).toBeCloseTo(0.5);
+ expect(roundUpNearest(17.2, 0.5)).toBeCloseTo(17.5);
+ });
+});
+
+describe('roundDownNearest()', () => {
+ it('rounds decimal values down to the right step size', () => {
+ expect(roundDownNearest(0.4, 0.5)).toBeCloseTo(0.0);
+ expect(roundDownNearest(17.4, 0.5)).toBeCloseTo(17.0);
+ });
+});
diff --git a/ui/src/common/query_result.ts b/ui/src/common/query_result.ts
index bae847d..9d44c9e 100644
--- a/ui/src/common/query_result.ts
+++ b/ui/src/common/query_result.ts
@@ -57,8 +57,10 @@
export const STR = 'str';
export const NUM_NULL: number|null = 1;
export const STR_NULL: string|null = 'str_null';
+export const BLOB: Uint8Array = new Uint8Array();
+export const BLOB_NULL: Uint8Array|null = new Uint8Array();
-export type ColumnType = string|number|null;
+export type ColumnType = string|number|null|Uint8Array;
// Info that could help debug a query error. For example the query
// in question, the stack where the query was issued, the active
@@ -119,11 +121,32 @@
return 'STR';
case STR_NULL:
return 'STR_NULL';
+ case BLOB:
+ return 'BLOB';
+ case BLOB_NULL:
+ return 'BLOB_NULL';
default:
return `INVALID(${t})`;
}
}
+function isCompatible(actual: CellType, expected: ColumnType): boolean {
+ switch (actual) {
+ case CellType.CELL_NULL:
+ return expected === NUM_NULL || expected === STR_NULL ||
+ expected === BLOB_NULL;
+ case CellType.CELL_VARINT:
+ case CellType.CELL_FLOAT64:
+ return expected === NUM || expected === NUM_NULL;
+ case CellType.CELL_STRING:
+ return expected === STR || expected === STR_NULL;
+ case CellType.CELL_BLOB:
+ return expected === BLOB || expected === BLOB_NULL;
+ default:
+ throw new Error(`Unknown CellType ${actual}`);
+ }
+}
+
// Disable Long.js support in protobuf. This seems to be enabled only in tests
// but not in production code. In any case, for now we want casting to number
// accepting the 2**53 limitation. This is consistent with passing
@@ -652,8 +675,7 @@
case CellType.CELL_BLOB:
const blob = this.blobCells[this.nextBlobCell++];
- throw new Error(`TODO implement BLOB support (${blob})`);
- // outRow[colName] = blob;
+ rowData[colName] = blob;
break;
default:
@@ -721,20 +743,16 @@
if (expType === undefined) continue;
let err = '';
- if (actualType === CellType.CELL_NULL &&
- (expType !== STR_NULL && expType !== NUM_NULL)) {
- err = 'SQL value is NULL but that was not expected' +
- ` (expected type: ${columnTypeToString(expType)}). ` +
- 'Did you intend to use NUM_NULL or STR_NULL?';
- } else if (
- ((actualType === CellType.CELL_VARINT ||
- actualType === CellType.CELL_FLOAT64) &&
- (expType !== NUM && expType !== NUM_NULL)) ||
- ((actualType === CellType.CELL_STRING) &&
- (expType !== STR && expType !== STR_NULL))) {
- err = `Incompatible cell type. Expected: ${
- columnTypeToString(
- expType)} actual: ${CELL_TYPE_NAMES[actualType]}`;
+ if (!isCompatible(actualType, expType)) {
+ if (actualType === CellType.CELL_NULL) {
+ err = 'SQL value is NULL but that was not expected' +
+ ` (expected type: ${columnTypeToString(expType)}). ` +
+ 'Did you intend to use NUM_NULL, STR_NULL or BLOB_NULL?';
+ } else {
+ err = `Incompatible cell type. Expected: ${
+ columnTypeToString(
+ expType)} actual: ${CELL_TYPE_NAMES[actualType]}`;
+ }
}
if (err.length > 0) {
throw new Error(
diff --git a/ui/src/controller/aggregation/aggregation_controller.ts b/ui/src/controller/aggregation/aggregation_controller.ts
index 0510ca0..4fa1ecc 100644
--- a/ui/src/controller/aggregation/aggregation_controller.ts
+++ b/ui/src/controller/aggregation/aggregation_controller.ts
@@ -146,6 +146,8 @@
column.data[i] = isStringColumn(column) ? internString('NULL') : 0;
} else if (typeof item === 'string') {
column.data[i] = internString(item);
+ } else if (item instanceof Uint8Array) {
+ column.data[i] = internString('<Binary blob>');
} else {
column.data[i] = item;
}
diff --git a/ui/src/frontend/gridline_helper.ts b/ui/src/frontend/gridline_helper.ts
index 6c0b74d..1c9dbfe 100644
--- a/ui/src/frontend/gridline_helper.ts
+++ b/ui/src/frontend/gridline_helper.ts
@@ -12,94 +12,172 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {TimeSpan} from '../common/time';
-
+import {assertTrue} from '../base/logging';
+import {roundDownNearest} from '../base/math_utils';
import {TRACK_BORDER_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
+import {globals} from './globals';
import {TimeScale} from './time_scale';
-export const DESIRED_PX_PER_STEP = 80;
-
-// Returns the step size of a grid line in seconds.
-// The returned step size has two properties:
-// (1) It is 1, 2, or 5, multiplied by some integer power of 10.
-// (2) The number steps in |range| produced by |stepSize| is as close as
-// possible to |desiredSteps|.
-export function getGridStepSize(range: number, desiredSteps: number): number {
+// Returns the optimal step size (in seconds) and tick pattern of ticks within
+// the step. The returned step size has two properties: (1) It is 1, 2, or 5,
+// multiplied by some integer power of 10. (2) It is maximised given the
+// constraint: |range| / stepSize <= |maxNumberOfSteps|.
+export function getStepSize(
+ range: number, maxNumberOfSteps: number): [number, string] {
// First, get the largest possible power of 10 that is smaller than the
- // desired step size, and set it to the current step size.
+ // desired step size, and use it as our initial step size.
// For example, if the range is 2345ms and the desired steps is 10, then the
- // desired step size is 234.5 and the step size will be set to 100.
- const desiredStepSize = range / desiredSteps;
- const zeros = Math.floor(Math.log10(desiredStepSize));
+ // minimum step size is 234.5ms so the step size will initialise to 100.
+ const minStepSize = range / maxNumberOfSteps;
+ const zeros = Math.floor(Math.log10(minStepSize));
const initialStepSize = Math.pow(10, zeros);
- // This function first calculates how many steps within the range a certain
- // stepSize will produce, and returns the difference between that and
- // desiredSteps.
- const distToDesired = (evaluatedStepSize: number) =>
- Math.abs(range / evaluatedStepSize - desiredSteps);
-
// We know that |initialStepSize| is a power of 10, and
// initialStepSize <= desiredStepSize <= 10 * initialStepSize. There are four
// possible candidates for final step size: 1, 2, 5 or 10 * initialStepSize.
- // We pick the candidate that minimizes distToDesired(stepSize).
- const stepSizeMultipliers = [2, 5, 10];
+ // For our example above, this would result in a step size of 500ms, as both
+ // 100ms and 200ms are smaller than the minimum step size of 234.5ms.
+ // We pick the candidate that minimizes the step size without letting the
+ // number of steps exceed |maxNumberOfSteps|. The factor we pick to also
+ // determines the pattern of ticks. This pattern is represented using a string
+ // where:
+ // | = Major tick
+ // : = Medium tick
+ // . = Minor tick
+ const stepSizeMultipliers: [number, string][] =
+ [[1, '|....:....'], [2, '|.:.'], [5, '|....'], [10, '|....:....']];
- let minimalDistance = distToDesired(initialStepSize);
- let minimizingStepSize = initialStepSize;
-
- for (const multiplier of stepSizeMultipliers) {
+ for (const [multiplier, pattern] of stepSizeMultipliers) {
const newStepSize = multiplier * initialStepSize;
- const newDistance = distToDesired(newStepSize);
- if (newDistance < minimalDistance) {
- minimalDistance = newDistance;
- minimizingStepSize = newStepSize;
+ const numberOfNewSteps = range / newStepSize;
+ if (numberOfNewSteps <= maxNumberOfSteps) {
+ return [newStepSize, pattern];
}
}
- return minimizingStepSize;
+
+ throw new Error('Something has gone horribly wrong with maths');
}
-// Generator that returns that (given a width im px, span, and scale) returns
-// pairs of [xInPx, timestampInS] pairs describing where gridlines should be
-// drawn.
-export function gridlines(width: number, span: TimeSpan, timescale: TimeScale):
- Array<[number, number]> {
- const desiredSteps = width / DESIRED_PX_PER_STEP;
- const step = getGridStepSize(span.duration, desiredSteps);
- const actualSteps = Math.floor(span.duration / step);
- const start = Math.round(span.start / step) * step;
- const lines: Array<[number, number]> = [];
- let previousTimestamp = Number.NEGATIVE_INFINITY;
- // Iterating over the number of steps instead of
- // for (let s = start; s < span.end; s += step) because if start is very large
- // number and step very small, s will never reach end.
- for (let i = 0; i <= actualSteps; i++) {
- let xPos = TRACK_SHELL_WIDTH;
- const timestamp = start + i * step;
- xPos += Math.floor(timescale.timeToPx(timestamp));
- if (xPos < TRACK_SHELL_WIDTH) continue;
- if (xPos > width) break;
- if (Math.abs(timestamp - previousTimestamp) > Number.EPSILON) {
- previousTimestamp = timestamp;
- lines.push([xPos, timestamp]);
+function tickPatternToArray(pattern: string): TickType[] {
+ const array = Array.from(pattern);
+ return array.map((char) => {
+ switch (char) {
+ case '|':
+ return TickType.MAJOR;
+ case ':':
+ return TickType.MEDIUM;
+ case '.':
+ return TickType.MINOR;
+ default:
+ // This is almost certainly a developer/fat-finger error
+ throw Error(`Invalid char "${char}" in pattern "${pattern}"`);
+ }
+ });
+}
+
+// Assuming a number only has one non-zero decimal digit, find the number of
+// decimal places required to accurately print that number. I.e. the parameter
+// we should pass to number.toFixed(x). To account for floating point
+// innaccuracies when representing numbers in base-10, we only take the first
+// nonzero fractional digit into account. E.g.
+// 1.0 -> 0
+// 0.5 -> 1
+// 0.009 -> 3
+// 0.00007 -> 5
+// 30000 -> 0
+// 0.30000000000000004 -> 1
+export function guessDecimalPlaces(val: number): number {
+ const neglog10 = -Math.floor(Math.log10(val));
+ const clamped = Math.max(0, neglog10);
+ return clamped;
+}
+
+export enum TickType {
+ MAJOR,
+ MEDIUM,
+ MINOR
+}
+
+export interface Tick {
+ type: TickType;
+ time: number;
+ position: number;
+}
+
+const MIN_PX_PER_STEP = 80;
+
+// An iterable which generates a series of ticks for a given timescale.
+export class TickGenerator implements Iterable<Tick> {
+ private _tickPattern: TickType[];
+ private _patternSize: number;
+
+ constructor(private scale: TimeScale, {minLabelPx = MIN_PX_PER_STEP} = {}) {
+ assertTrue(minLabelPx > 0, 'minLabelPx cannot be lte 0');
+ assertTrue(scale.widthPx > 0, 'widthPx cannot be lte 0');
+ assertTrue(
+ scale.timeSpan.duration > 0, 'timeSpan.duration cannot be lte 0');
+
+ const desiredSteps = scale.widthPx / minLabelPx;
+ const [size, pattern] = getStepSize(scale.timeSpan.duration, desiredSteps);
+ this._patternSize = size;
+ this._tickPattern = tickPatternToArray(pattern);
+ }
+
+ // Returns an iterable, so this object can be iterated over directly using the
+ // `for x of y` notation. The use of a generator here is just to make things
+ // more elegant than creating an array of ticks and building an iterator for
+ // it.
+ * [Symbol.iterator](): Generator<Tick> {
+ const span = this.scale.timeSpan;
+ const stepSize = this._patternSize / this._tickPattern.length;
+ const start = roundDownNearest(span.start, this._patternSize);
+ const timeAtStep = (i: number) => start + (i * stepSize);
+
+ // Iterating using steps instead of
+ // for (let s = start; s < span.end; s += stepSize) because if start is much
+ // larger than stepSize we can enter an infinite loop due to floating
+ // point precision errors.
+ for (let i = 0; timeAtStep(i) < span.end; i++) {
+ const time = timeAtStep(i);
+ if (time >= span.start) {
+ const position = Math.floor(this.scale.timeToPx(time));
+ const type = this._tickPattern[i % this._tickPattern.length];
+ yield {type, time, position};
+ }
}
}
- return lines;
+
+ // The number of decimal places labels should be printed with, assuming labels
+ // are only printed on major ticks.
+ get digits(): number {
+ return guessDecimalPlaces(this._patternSize);
+ }
+}
+
+// Gets the timescale associated with the current visible window.
+export function timeScaleForVisibleWindow(
+ startPx: number, endPx: number): TimeScale {
+ const span = globals.frontendLocalState.visibleWindowTime;
+ const spanRelative = span.add(-globals.state.traceTime.startSec);
+ return new TimeScale(spanRelative, [startPx, endPx]);
}
export function drawGridLines(
ctx: CanvasRenderingContext2D,
- x: TimeScale,
- timeSpan: TimeSpan,
width: number,
height: number): void {
ctx.strokeStyle = TRACK_BORDER_COLOR;
ctx.lineWidth = 1;
- for (const xAndTime of gridlines(width, timeSpan, x)) {
- ctx.beginPath();
- ctx.moveTo(xAndTime[0] + 0.5, 0);
- ctx.lineTo(xAndTime[0] + 0.5, height);
- ctx.stroke();
+ const timeScale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, width);
+ if (timeScale.timeSpan.duration > 0 && timeScale.widthPx > 0) {
+ for (const {type, position} of new TickGenerator(timeScale)) {
+ if (type === TickType.MAJOR) {
+ ctx.beginPath();
+ ctx.moveTo(position + 0.5, 0);
+ ctx.lineTo(position + 0.5, height);
+ ctx.stroke();
+ }
+ }
}
}
diff --git a/ui/src/frontend/gridline_helper_unittest.ts b/ui/src/frontend/gridline_helper_unittest.ts
index 98724d5..3b6dcac 100644
--- a/ui/src/frontend/gridline_helper_unittest.ts
+++ b/ui/src/frontend/gridline_helper_unittest.ts
@@ -12,36 +12,303 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {getGridStepSize} from './gridline_helper';
+import {TimeSpan} from '../common/time';
+
+import {getStepSize, Tick, TickGenerator, TickType} from './gridline_helper';
+import {TimeScale} from './time_scale';
+
+const pattern1 = '|....:....';
+const pattern2 = '|.:.';
+const pattern5 = '|....';
+const timeScale = new TimeScale(new TimeSpan(0, 1), [1, 2]);
test('gridline helper to have sensible step sizes', () => {
- expect(getGridStepSize(10, 14)).toEqual(1);
- expect(getGridStepSize(30, 14)).toEqual(2);
- expect(getGridStepSize(60, 14)).toEqual(5);
- expect(getGridStepSize(100, 14)).toEqual(10);
+ expect(getStepSize(10, 14)).toEqual([1, pattern1]);
+ expect(getStepSize(30, 14)).toEqual([5, pattern5]);
+ expect(getStepSize(60, 14)).toEqual([5, pattern5]);
+ expect(getStepSize(100, 14)).toEqual([10, pattern1]);
- expect(getGridStepSize(10, 21)).toEqual(0.5);
- expect(getGridStepSize(30, 21)).toEqual(2);
- expect(getGridStepSize(60, 21)).toEqual(2);
- expect(getGridStepSize(100, 21)).toEqual(5);
+ expect(getStepSize(10, 21)).toEqual([0.5, pattern5]);
+ expect(getStepSize(30, 21)).toEqual([2, pattern2]);
+ expect(getStepSize(60, 21)).toEqual([5, pattern5]);
+ expect(getStepSize(100, 21)).toEqual([5, pattern5]);
- expect(getGridStepSize(10, 3)).toEqual(5);
- expect(getGridStepSize(30, 3)).toEqual(10);
- expect(getGridStepSize(60, 3)).toEqual(20);
- expect(getGridStepSize(100, 3)).toEqual(50);
+ expect(getStepSize(10, 3)).toEqual([5, pattern5]);
+ expect(getStepSize(30, 3)).toEqual([10, pattern1]);
+ expect(getStepSize(60, 3)).toEqual([20, pattern2]);
+ expect(getStepSize(100, 3)).toEqual([50, pattern5]);
- expect(getGridStepSize(800, 4)).toEqual(200);
+ expect(getStepSize(800, 4)).toEqual([200, pattern2]);
});
test('gridline helper to scale to very small and very large values', () => {
- expect(getGridStepSize(.01, 14)).toEqual(.001);
- expect(getGridStepSize(10000, 14)).toEqual(1000);
+ expect(getStepSize(.01, 14)).toEqual([.001, pattern1]);
+ expect(getStepSize(10000, 14)).toEqual([1000, pattern1]);
});
test('gridline helper to always return a reasonable number of steps', () => {
for (let i = 1; i <= 1000; i++) {
- const stepSize = getGridStepSize(i, 14);
- expect(Math.round(i / stepSize)).toBeGreaterThanOrEqual(7);
- expect(Math.round(i / stepSize)).toBeLessThanOrEqual(21);
+ const [stepSize, _] = getStepSize(i, 14);
+ expect(Math.round(i / stepSize)).toBeGreaterThanOrEqual(6);
+ expect(Math.round(i / stepSize)).toBeLessThanOrEqual(14);
}
});
+
+describe('TickGenerator with range 0.0-1.0 and room for 2 labels', () => {
+ let tickGen: TickGenerator|undefined = undefined;
+ beforeAll(() => {
+ const timeSpan = new TimeSpan(0.0, 1.0);
+ const timeScale = new TimeScale(timeSpan, [0, 200]);
+ tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
+ });
+ it('should produce major ticks at 0.5s and minor ticks at 0.1s starting at 0',
+ () => {
+ const expected = [
+ {type: TickType.MAJOR, time: 0.0},
+ {type: TickType.MINOR, time: 0.1},
+ {type: TickType.MINOR, time: 0.2},
+ {type: TickType.MINOR, time: 0.3},
+ {type: TickType.MINOR, time: 0.4},
+ {type: TickType.MAJOR, time: 0.5},
+ {type: TickType.MINOR, time: 0.6},
+ {type: TickType.MINOR, time: 0.7},
+ {type: TickType.MINOR, time: 0.8},
+ {type: TickType.MINOR, time: 0.9},
+ ];
+ const actual = Array.from(tickGen!);
+ expectTicksEqual(actual, expected);
+ });
+ it('should tell us to use 1 decimal place for labels', () => {
+ expect(tickGen!.digits).toEqual(1);
+ });
+});
+
+describe('TickGenerator with range 0.3-1.3 and room for 2 labels', () => {
+ let tickGen: TickGenerator|undefined = undefined;
+ beforeAll(() => {
+ const timeSpan = new TimeSpan(0.3, 1.3);
+ const timeScale = new TimeScale(timeSpan, [0, 200]);
+ tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
+ });
+ it('should produce major ticks at 0.5s and minor ticks at 0.1s starting at 0',
+ () => {
+ const expected = [
+ {type: TickType.MINOR, time: 0.3},
+ {type: TickType.MINOR, time: 0.4},
+ {type: TickType.MAJOR, time: 0.5},
+ {type: TickType.MINOR, time: 0.6},
+ {type: TickType.MINOR, time: 0.7},
+ {type: TickType.MINOR, time: 0.8},
+ {type: TickType.MINOR, time: 0.9},
+ {type: TickType.MAJOR, time: 1.0},
+ {type: TickType.MINOR, time: 1.1},
+ {type: TickType.MINOR, time: 1.2},
+ ];
+ const actual = Array.from(tickGen!);
+ expectTicksEqual(actual, expected);
+ });
+ it('should tell us to use 1 decimal place for labels', () => {
+ expect(tickGen!.digits).toEqual(1);
+ });
+});
+
+describe('TickGenerator with range 0.0-0.2 and room for 1 label', () => {
+ let tickGen: TickGenerator|undefined = undefined;
+ beforeAll(() => {
+ const timeSpan = new TimeSpan(0.0, 0.2);
+ const timeScale = new TimeScale(timeSpan, [0, 100]);
+ tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
+ });
+ it('should produce major ticks at 0.2s and minor ticks at 0.1s starting at 0',
+ () => {
+ const expected = [
+ {type: TickType.MAJOR, time: 0.0},
+ {type: TickType.MINOR, time: 0.05},
+ {type: TickType.MEDIUM, time: 0.1},
+ {type: TickType.MINOR, time: 0.15},
+ ];
+ const actual = Array.from(tickGen!);
+ expectTicksEqual(actual, expected);
+ });
+ it('should tell us to use 1 decimal place for labels', () => {
+ expect(tickGen!.digits).toEqual(1);
+ });
+});
+
+describe('TickGenerator with range 0.0-0.1 and room for 1 label', () => {
+ let tickGen: TickGenerator|undefined = undefined;
+ beforeAll(() => {
+ const timeSpan = new TimeSpan(0.0, 0.1);
+ const timeScale = new TimeScale(timeSpan, [0, 100]);
+ tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
+ });
+ it('should produce major ticks at 0.1s & minor ticks at 0.02s starting at 0',
+ () => {
+ const expected = [
+ {type: TickType.MAJOR, time: 0.0},
+ {type: TickType.MINOR, time: 0.01},
+ {type: TickType.MINOR, time: 0.02},
+ {type: TickType.MINOR, time: 0.03},
+ {type: TickType.MINOR, time: 0.04},
+ {type: TickType.MEDIUM, time: 0.05},
+ {type: TickType.MINOR, time: 0.06},
+ {type: TickType.MINOR, time: 0.07},
+ {type: TickType.MINOR, time: 0.08},
+ {type: TickType.MINOR, time: 0.09},
+ ];
+ const actual = Array.from(tickGen!);
+ expect(tickGen!.digits).toEqual(1);
+ expectTicksEqual(actual, expected);
+ });
+ it('should tell us to use 1 decimal place for labels', () => {
+ expect(tickGen!.digits).toEqual(1);
+ });
+});
+
+describe('TickGenerator with a very small timespan', () => {
+ let tickGen: TickGenerator|undefined = undefined;
+ beforeAll(() => {
+ const timeSpan = new TimeSpan(0.0, 1e-9);
+ const timeScale = new TimeScale(timeSpan, [0, 100]);
+ tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
+ });
+ it('should generate minor ticks at 2e-10s and one major tick at the start',
+ () => {
+ const expected = [
+ {type: TickType.MAJOR, time: 0.0},
+ {type: TickType.MINOR, time: 1e-10},
+ {type: TickType.MINOR, time: 2e-10},
+ {type: TickType.MINOR, time: 3e-10},
+ {type: TickType.MINOR, time: 4e-10},
+ {type: TickType.MEDIUM, time: 5e-10},
+ {type: TickType.MINOR, time: 6e-10},
+ {type: TickType.MINOR, time: 7e-10},
+ {type: TickType.MINOR, time: 8e-10},
+ {type: TickType.MINOR, time: 9e-10},
+ ];
+ const actual = Array.from(tickGen!);
+ expectTicksEqual(actual, expected);
+ });
+ it('should tell us to use 9 decimal places for labels', () => {
+ expect(tickGen!.digits).toEqual(9);
+ });
+});
+
+describe('TickGenerator with a very large timespan', () => {
+ let tickGen: TickGenerator|undefined = undefined;
+ beforeAll(() => {
+ const timeSpan = new TimeSpan(0.0, 1e9);
+ const timeScale = new TimeScale(timeSpan, [0, 100]);
+ tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
+ });
+ it('should generate minor ticks at 2e8 and one major tick at the start',
+ () => {
+ const expected = [
+ {type: TickType.MAJOR, time: 0.0},
+ {type: TickType.MINOR, time: 1e8},
+ {type: TickType.MINOR, time: 2e8},
+ {type: TickType.MINOR, time: 3e8},
+ {type: TickType.MINOR, time: 4e8},
+ {type: TickType.MEDIUM, time: 5e8},
+ {type: TickType.MINOR, time: 6e8},
+ {type: TickType.MINOR, time: 7e8},
+ {type: TickType.MINOR, time: 8e8},
+ {type: TickType.MINOR, time: 9e8},
+ ];
+ const actual = Array.from(tickGen!);
+ expectTicksEqual(actual, expected);
+ });
+ it('should tell us to use 0 decimal places for labels', () => {
+ expect(tickGen!.digits).toEqual(0);
+ });
+});
+
+describe('TickGenerator where the timespan has a dynamic range of 1e12', () => {
+ // This is the equivalent of zooming in to the nanosecond level, 1000 seconds
+ // into a trace Note: this is about the limit of what this generator can
+ // handle.
+ let tickGen: TickGenerator|undefined = undefined;
+ beforeAll(() => {
+ const timeSpan = new TimeSpan(1000, 1000.000000001);
+ const timeScale = new TimeScale(timeSpan, [0, 100]);
+ tickGen = new TickGenerator(timeScale, {minLabelPx: 100});
+ });
+ it('should generate minor ticks at 1e-10s and one major tick at the start',
+ () => {
+ const expected = [
+ {type: TickType.MAJOR, time: 1000.0000000000},
+ {type: TickType.MINOR, time: 1000.0000000001},
+ {type: TickType.MINOR, time: 1000.0000000002},
+ {type: TickType.MINOR, time: 1000.0000000003},
+ {type: TickType.MINOR, time: 1000.0000000004},
+ {type: TickType.MEDIUM, time: 1000.0000000005},
+ {type: TickType.MINOR, time: 1000.0000000006},
+ {type: TickType.MINOR, time: 1000.0000000007},
+ {type: TickType.MINOR, time: 1000.0000000008},
+ {type: TickType.MINOR, time: 1000.0000000009},
+ ];
+ const actual = Array.from(tickGen!);
+ expectTicksEqual(actual, expected);
+ });
+ it('should tell us to use 9 decimal places for labels', () => {
+ expect(tickGen!.digits).toEqual(9);
+ });
+});
+
+describe(
+ 'TickGenerator where the timespan has a ridiculously huge dynamic range',
+ () => {
+ // We don't expect this to work, just wanna make sure it doesn't crash or
+ // get stuck
+ it('should not crash or get stuck in an infinite loop', () => {
+ const timeSpan = new TimeSpan(1000, 1000.000000000001);
+ const timeScale = new TimeScale(timeSpan, [0, 100]);
+ new TickGenerator(timeScale);
+ });
+ });
+
+describe(
+ 'TickGenerator where the timespan has a ridiculously huge dynamic range',
+ () => {
+ // We don't expect this to work, just wanna make sure it doesn't crash or
+ // get stuck
+ it('should not crash or get stuck in an infinite loop', () => {
+ const timeSpan = new TimeSpan(1000, 1000.000000000001);
+ const timeScale = new TimeScale(timeSpan, [0, 100]);
+ new TickGenerator(timeScale);
+ });
+ });
+
+test('TickGenerator constructed with a 0 width throws an error', () => {
+ expect(() => {
+ const timeScale = new TimeScale(new TimeSpan(0.0, 1.0), [0, 0]);
+ new TickGenerator(timeScale);
+ }).toThrow(Error);
+});
+
+test(
+ 'TickGenerator constructed with desiredPxPerStep of 0 throws an error',
+ () => {
+ expect(() => {
+ new TickGenerator(timeScale, {minLabelPx: 0});
+ }).toThrow(Error);
+ });
+
+test('TickGenerator constructed with a 0 duration throws an error', () => {
+ expect(() => {
+ const timeScale = new TimeScale(new TimeSpan(0.0, 0.0), [0, 1]);
+ new TickGenerator(timeScale);
+ }).toThrow(Error);
+});
+
+function expectTicksEqual(actual: Tick[], expected: any[]) {
+ // TODO(stevegolton) We could write a custom matcher for this; this approach
+ // produces cryptic error messages.
+ expect(actual.length).toEqual(expected.length);
+ for (let i = 0; i < actual.length; ++i) {
+ const ex = expected[i];
+ const ac = actual[i];
+ expect(ac.type).toEqual(ex.type);
+ expect(ac.time).toBeCloseTo(ex.time, 9);
+ }
+}
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index 1a91678..9896611 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -27,7 +27,11 @@
import {TRACK_SHELL_WIDTH} from './css_constants';
import {PerfettoMouseEvent} from './events';
import {globals} from './globals';
-import {gridlines} from './gridline_helper';
+import {
+ TickGenerator,
+ TickType,
+ timeScaleForVisibleWindow,
+} from './gridline_helper';
import {Panel, PanelSize} from './panel';
import {isTraceLoaded} from './sidebar';
@@ -95,13 +99,15 @@
renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
const timeScale = globals.frontendLocalState.timeScale;
- const range = globals.frontendLocalState.visibleWindowTime;
let aNoteIsHovered = false;
ctx.fillStyle = '#999';
ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
- for (const xAndTime of gridlines(size.width, range, timeScale)) {
- ctx.fillRect(xAndTime[0], 0, 1, size.height);
+ const relScale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
+ if (relScale.timeSpan.duration > 0 && relScale.widthPx > 0) {
+ for (const {type, position} of new TickGenerator(relScale)) {
+ if (type === TickType.MAJOR) ctx.fillRect(position, 0, 1, size.height);
+ }
}
ctx.textBaseline = 'bottom';
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index 12b5582..578fa7f 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -16,7 +16,7 @@
import {assertExists} from '../base/logging';
import {hueForCpu} from '../common/colorizer';
-import {TimeSpan, timeToString} from '../common/time';
+import {TimeSpan} from '../common/time';
import {
OVERVIEW_TIMELINE_NON_VISIBLE_COLOR,
@@ -29,6 +29,7 @@
import {OuterDragStrategy} from './drag/outer_drag_strategy';
import {DragGestureHandler} from './drag_gesture_handler';
import {globals} from './globals';
+import {TickGenerator, TickType} from './gridline_helper';
import {Panel, PanelSize} from './panel';
import {TimeScale} from './time_scale';
@@ -80,21 +81,28 @@
if (this.timeScale === undefined) return;
const headerHeight = 25;
const tracksHeight = size.height - headerHeight;
+ const timeSpan = new TimeSpan(0, this.totTime.duration);
- // Draw time labels on the top header.
- ctx.font = '10px Roboto Condensed';
- ctx.fillStyle = '#999';
- for (let i = 0; i < 100; i++) {
- const xPos =
- (i * (this.width - TRACK_SHELL_WIDTH) / 100) + TRACK_SHELL_WIDTH;
- const t = this.timeScale.pxToTime(xPos);
- if (xPos <= 0) continue;
- if (xPos > this.width) break;
- if (i % 10 === 0) {
- ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
- ctx.fillText(timeToString(t - this.totTime.start), xPos + 5, 18);
- } else {
- ctx.fillRect(xPos - 1, 0, 1, 5);
+ const timeScale = new TimeScale(timeSpan, [TRACK_SHELL_WIDTH, this.width]);
+
+ if (timeScale.widthPx > 0) {
+ const tickGen = new TickGenerator(timeScale);
+
+ // Draw time labels on the top header.
+ ctx.font = '10px Roboto Condensed';
+ ctx.fillStyle = '#999';
+ for (const {type, time, position} of tickGen) {
+ const xPos = Math.round(position);
+ if (xPos <= 0) continue;
+ if (xPos > this.width) break;
+ if (type === TickType.MAJOR) {
+ ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
+ ctx.fillText(time.toFixed(tickGen.digits) + ' s', xPos + 5, 18);
+ } else if (type == TickType.MEDIUM) {
+ ctx.fillRect(xPos - 1, 0, 1, 8);
+ } else if (type == TickType.MINOR) {
+ ctx.fillRect(xPos - 1, 0, 1, 5);
+ }
}
}
diff --git a/ui/src/frontend/pivot_table_redux.ts b/ui/src/frontend/pivot_table_redux.ts
index e1c880c..f17bb24 100644
--- a/ui/src/frontend/pivot_table_redux.ts
+++ b/ui/src/frontend/pivot_table_redux.ts
@@ -52,7 +52,7 @@
TableColumn,
} from './pivot_table_redux_types';
import {PopupMenuButton, PopupMenuItem} from './popup_menu';
-import {ReorderableCellGroup} from './reorderable_cells';
+import {ReorderableCell, ReorderableCellGroup} from './reorderable_cells';
interface PathItem {
@@ -85,6 +85,8 @@
return `${column} IS NULL`;
} else if (typeof filter.value === 'number') {
return `${column} = ${filter.value}`;
+ } else if (filter.value instanceof Uint8Array) {
+ throw new Error(`BLOB as DrillFilter not implemented`);
}
return `${column} = ${sqliteString(filter.value)}`;
}
@@ -98,6 +100,13 @@
}
}
+export function markFirst(index: number) {
+ if (index === 0) {
+ return '.first';
+ }
+ return '';
+}
+
export class PivotTableRedux extends Panel<PivotTableReduxAttrs> {
get pivotState() {
return globals.state.nonSerializableState.pivotTableRedux;
@@ -165,7 +174,7 @@
for (let i = 0; i < tree.aggregates.length; i++) {
const renderedValue = this.renderCell(
result.metadata.aggregationColumns[i].column, tree.aggregates[i]);
- renderedCells.push(m('td', renderedValue));
+ renderedCells.push(m('td' + markFirst(i), renderedValue));
}
const drillFilters: DrillFilter[] = [];
@@ -234,7 +243,7 @@
const value = row[aggregationIndex(treeDepth, j)];
const renderedValue = this.renderCell(
result.metadata.aggregationColumns[j].column, value);
- renderedCells.push(m('td', renderedValue));
+ renderedCells.push(m('td.aggregation' + markFirst(j), renderedValue));
}
renderedCells.push(this.renderDrillDownCell(area, drillFilters));
@@ -249,7 +258,7 @@
m('strong', 'Total values:'))];
for (let i = 0; i < queryResult.tree.aggregates.length; i++) {
overallValuesRow.push(
- m('td',
+ m('td' + markFirst(i),
this.renderCell(
queryResult.metadata.aggregationColumns[i].column,
queryResult.tree.aggregates[i])));
@@ -317,7 +326,7 @@
renderAggregationHeaderCell(
aggregation: Aggregation, index: number,
- removeItem: boolean): m.Children {
+ removeItem: boolean): ReorderableCell {
const popupItems: PopupMenuItem[] = [];
const state = globals.state.nonSerializableState.pivotTableRedux;
let icon = 'more_horiz';
@@ -382,13 +391,16 @@
popupItems.push(sliceAggregationsItem);
}
- return [
- this.readableAggregationName(aggregation),
- m(PopupMenuButton, {
- icon,
- items: popupItems,
- }),
- ];
+ return {
+ extraClass: '.aggregation' + markFirst(index),
+ content: [
+ this.readableAggregationName(aggregation),
+ m(PopupMenuButton, {
+ icon,
+ items: popupItems,
+ }),
+ ],
+ };
}
showModal = false;
@@ -422,7 +434,7 @@
renderPivotColumnHeader(
queryResult: PivotTableReduxResult, pivot: TableColumn,
- selectedPivots: Set<string>): m.Children {
+ selectedPivots: Set<string>): ReorderableCell {
const items: PopupMenuItem[] = [{
itemType: 'regular',
text: 'Add argument pivot',
@@ -476,10 +488,12 @@
});
}
- return [
- readableColumnName(pivot),
- m(PopupMenuButton, {icon: 'more_horiz', items}),
- ];
+ return {
+ content: [
+ readableColumnName(pivot),
+ m(PopupMenuButton, {icon: 'more_horiz', items}),
+ ],
+ };
}
renderResultsTable(attrs: PivotTableReduxAttrs) {
@@ -517,13 +531,13 @@
aggregation, index, removeItem));
return m(
- 'table.query-table.pivot-table',
+ 'table.pivot-table',
m('thead',
// First row of the table, containing names of pivot and aggregation
// columns, as well as popup menus to modify the columns. Last cell
// is empty because of an extra column with "drill down" button for
// each pivot table row.
- m('tr',
+ m('tr.header',
m(ReorderableCellGroup, {
cells: pivotTableHeaders,
onReorder: (
diff --git a/ui/src/frontend/query_table.ts b/ui/src/frontend/query_table.ts
index ff240d9..2469b24 100644
--- a/ui/src/frontend/query_table.ts
+++ b/ui/src/frontend/query_table.ts
@@ -79,7 +79,12 @@
const cells = [];
const {row, columns} = vnode.attrs;
for (const col of columns) {
- cells.push(m('td', row[col]));
+ const value = row[col];
+ if (value instanceof Uint8Array) {
+ cells.push(m('td', `<BLOB sz=${value.length}>`));
+ } else {
+ cells.push(m('td', value));
+ }
}
const containsSliceLocation =
QueryTableRow.columnsContainsSliceLocation(columns);
diff --git a/ui/src/frontend/reorderable_cells.ts b/ui/src/frontend/reorderable_cells.ts
index dd5992b..e3977a2 100644
--- a/ui/src/frontend/reorderable_cells.ts
+++ b/ui/src/frontend/reorderable_cells.ts
@@ -20,8 +20,13 @@
import {globals} from './globals';
+export interface ReorderableCell {
+ content: m.Children;
+ extraClass?: string;
+}
+
export interface ReorderableCellGroupAttrs {
- cells: m.Children[];
+ cells: ReorderableCell[];
onReorder: (from: number, to: number, side: DropDirection) => void;
}
@@ -62,7 +67,7 @@
view(vnode: m.Vnode<ReorderableCellGroupAttrs>): m.Children {
return vnode.attrs.cells.map(
(cell, index) => m(
- 'td.reorderable-cell',
+ `td.reorderable-cell${cell.extraClass ?? ''}`,
{
draggable: 'draggable',
class: this.getClassForIndex(index),
@@ -138,7 +143,7 @@
globals.rafScheduler.scheduleFullRedraw();
},
},
- cell));
+ cell.content));
}
oncreate(vnode: m.VnodeDOM<ReorderableCellGroupAttrs, this>) {
diff --git a/ui/src/frontend/tickmark_panel.ts b/ui/src/frontend/tickmark_panel.ts
index 6cc8037..aea1085 100644
--- a/ui/src/frontend/tickmark_panel.ts
+++ b/ui/src/frontend/tickmark_panel.ts
@@ -13,11 +13,16 @@
// limitations under the License.
import * as m from 'mithril';
+
import {fromNs} from '../common/time';
import {TRACK_SHELL_WIDTH} from './css_constants';
import {globals} from './globals';
-import {gridlines} from './gridline_helper';
+import {
+ TickGenerator,
+ TickType,
+ timeScaleForVisibleWindow,
+} from './gridline_helper';
import {Panel, PanelSize} from './panel';
// This is used to display the summary of search results.
@@ -31,9 +36,11 @@
ctx.fillStyle = '#999';
ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
- for (const xAndTime of gridlines(
- size.width, visibleWindowTime, timeScale)) {
- ctx.fillRect(xAndTime[0], 0, 1, size.height);
+ const relScale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
+ if (relScale.timeSpan.duration > 0 && relScale.widthPx > 0) {
+ for (const {type, position} of new TickGenerator(relScale)) {
+ if (type === TickType.MAJOR) ctx.fillRect(position, 0, 1, size.height);
+ }
}
const data = globals.searchSummary;
diff --git a/ui/src/frontend/time_axis_panel.ts b/ui/src/frontend/time_axis_panel.ts
index 2f39881..aeb396b 100644
--- a/ui/src/frontend/time_axis_panel.ts
+++ b/ui/src/frontend/time_axis_panel.ts
@@ -18,7 +18,11 @@
import {TRACK_SHELL_WIDTH} from './css_constants';
import {globals} from './globals';
-import {gridlines} from './gridline_helper';
+import {
+ TickGenerator,
+ TickType,
+ timeScaleForVisibleWindow,
+} from './gridline_helper';
import {Panel, PanelSize} from './panel';
export class TimeAxisPanel extends Panel {
@@ -27,27 +31,23 @@
}
renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
- const timeScale = globals.frontendLocalState.timeScale;
- const range = globals.frontendLocalState.visibleWindowTime;
ctx.fillStyle = '#999';
-
- // Write trace offset time + line.
- ctx.font = '12px Roboto Condensed';
-
- ctx.textAlign = 'right';
- const offsetTime =
- timeToString(range.start - globals.state.traceTime.startSec);
- ctx.fillText(offsetTime, TRACK_SHELL_WIDTH - 6, 11);
-
+ ctx.font = '10px Roboto Condensed';
ctx.textAlign = 'left';
+
const startTime = timeToString(globals.state.traceTime.startSec);
ctx.fillText(startTime + ' +', 6, 11);
// Draw time axis.
- ctx.font = '10px Roboto Condensed';
- for (const [x, time] of gridlines(size.width, range, timeScale)) {
- ctx.fillRect(x, 0, 1, size.height);
- ctx.fillText('+' + timeToString(time - range.start), x + 5, 10);
+ const timeScale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
+ if (timeScale.timeSpan.duration > 0 && timeScale.widthPx > 0) {
+ const tickGen = new TickGenerator(timeScale);
+ for (const {type, time, position} of tickGen) {
+ if (type === TickType.MAJOR) {
+ ctx.fillRect(position, 0, 1, size.height);
+ ctx.fillText(time.toFixed(tickGen.digits) + ' s', position + 5, 10);
+ }
+ }
}
ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
diff --git a/ui/src/frontend/time_scale.ts b/ui/src/frontend/time_scale.ts
index cdf3ca2..6f0307a 100644
--- a/ui/src/frontend/time_scale.ts
+++ b/ui/src/frontend/time_scale.ts
@@ -79,6 +79,14 @@
get endPx(): number {
return this._endPx;
}
+
+ get widthPx(): number {
+ return this._endPx - this._startPx;
+ }
+
+ get timeSpan(): TimeSpan {
+ return this.timeBounds;
+ }
}
export function computeZoom(
diff --git a/ui/src/frontend/time_selection_panel.ts b/ui/src/frontend/time_selection_panel.ts
index f1391da..5fc8a9f 100644
--- a/ui/src/frontend/time_selection_panel.ts
+++ b/ui/src/frontend/time_selection_panel.ts
@@ -19,7 +19,11 @@
import {TRACK_SHELL_WIDTH} from './css_constants';
import {globals} from './globals';
-import {gridlines} from './gridline_helper';
+import {
+ TickGenerator,
+ TickType,
+ timeScaleForVisibleWindow,
+} from './gridline_helper';
import {Panel, PanelSize} from './panel';
export interface BBox {
@@ -120,13 +124,15 @@
}
renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
- const range = globals.frontendLocalState.visibleWindowTime;
- const timeScale = globals.frontendLocalState.timeScale;
-
ctx.fillStyle = '#999';
ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
- for (const xAndTime of gridlines(size.width, range, timeScale)) {
- ctx.fillRect(xAndTime[0], 0, 1, size.height);
+ const scale = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
+ if (scale.timeSpan.duration > 0 && scale.widthPx > 0) {
+ for (const {position, type} of new TickGenerator(scale)) {
+ if (type === TickType.MAJOR) {
+ ctx.fillRect(position, 0, 1, size.height);
+ }
+ }
}
const localArea = globals.frontendLocalState.selectedArea;
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index 34a7ec4..0c1df65 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -215,8 +215,6 @@
drawGridLines(
ctx,
- globals.frontendLocalState.timeScale,
- globals.frontendLocalState.visibleWindowTime,
size.width,
size.height);
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 588900f..5ee1a55 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -364,8 +364,6 @@
drawGridLines(
ctx,
- globals.frontendLocalState.timeScale,
- globals.frontendLocalState.visibleWindowTime,
size.width,
size.height);