Merge "[ui] Fixed some merge issues regarding uri and track keys." into main
diff --git a/docs/contributing/ui-plugins.md b/docs/contributing/ui-plugins.md
index 939e334..809018a 100644
--- a/docs/contributing/ui-plugins.md
+++ b/docs/contributing/ui-plugins.md
@@ -14,7 +14,7 @@
```sh
git clone https://android.googlesource.com/platform/external/perfetto/
cd perfetto
-./tool/install-build-deps --ui
+./tools/install-build-deps --ui
```
### Copy the plugin skeleton
diff --git a/src/trace_processor/db/BUILD.gn b/src/trace_processor/db/BUILD.gn
index 604929d..869c9da 100644
--- a/src/trace_processor/db/BUILD.gn
+++ b/src/trace_processor/db/BUILD.gn
@@ -66,6 +66,7 @@
":view_unittest",
"../../../gn:default_deps",
"../../../gn:gtest_and_gmock",
+ "../../../include/perfetto/trace_processor:basic_types",
"../../base",
"../tables",
"../views",
diff --git a/src/trace_processor/db/query_executor.cc b/src/trace_processor/db/query_executor.cc
index 1151079..c77cb38 100644
--- a/src/trace_processor/db/query_executor.cc
+++ b/src/trace_processor/db/query_executor.cc
@@ -24,6 +24,7 @@
#include "perfetto/base/logging.h"
#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/basic_types.h"
#include "src/trace_processor/containers/string_pool.h"
#include "src/trace_processor/db/query_executor.h"
#include "src/trace_processor/db/storage/arrangement_storage.h"
@@ -160,10 +161,14 @@
use_legacy = use_legacy || (col.overlay().size() != column_size &&
col.overlay().row_map().IsRange());
- // Mismatched types.
+ // Comparing ints with doubles and doubles with ints.
+ bool int_with_double =
+ col.type() == SqlValue::kLong && c.value.type == SqlValue::kDouble;
+ bool double_with_int =
+ col.type() == SqlValue::kDouble && c.value.type == SqlValue::kLong;
use_legacy = use_legacy ||
(c.op != FilterOp::kIsNull && c.op != FilterOp::kIsNotNull &&
- col.type() != c.value.type && !c.value.is_null());
+ (int_with_double || double_with_int));
// Dense columns.
use_legacy = use_legacy || col.IsDense();
diff --git a/src/trace_processor/db/query_executor_unittest.cc b/src/trace_processor/db/query_executor_unittest.cc
index 95ce5ef..a2c05d8 100644
--- a/src/trace_processor/db/query_executor_unittest.cc
+++ b/src/trace_processor/db/query_executor_unittest.cc
@@ -16,6 +16,7 @@
#include "src/trace_processor/db/query_executor.h"
+#include "perfetto/trace_processor/basic_types.h"
#include "src/trace_processor/db/storage/arrangement_storage.h"
#include "src/trace_processor/db/storage/fake_storage.h"
#include "src/trace_processor/db/storage/id_storage.h"
@@ -547,6 +548,17 @@
ASSERT_EQ(res.Get(0), 0u);
}
+TEST(QueryExecutor, MismatchedTypeIdWithString) {
+ IdStorage storage(5);
+
+ // Filter.
+ Constraint c{0, FilterOp::kGe, SqlValue::String("cheese")};
+ QueryExecutor exec({&storage}, 5);
+ RowMap res = exec.Filter({c});
+
+ ASSERT_EQ(res.size(), 0u);
+}
+
#if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
TEST(QueryExecutor, StringBinarySearchRegex) {
StringPool pool;
diff --git a/src/trace_processor/db/storage/numeric_storage_unittest.cc b/src/trace_processor/db/storage/numeric_storage_unittest.cc
index e82883f..36fa51a 100644
--- a/src/trace_processor/db/storage/numeric_storage_unittest.cc
+++ b/src/trace_processor/db/storage/numeric_storage_unittest.cc
@@ -30,7 +30,7 @@
using Range = RowMap::Range;
-TEST(IdStorageUnittest, InvalidSearchConstraintsGeneralChecks) {
+TEST(NumericStorageUnittest, InvalidSearchConstraintsGeneralChecks) {
std::vector<uint32_t> data_vec(128);
std::iota(data_vec.begin(), data_vec.end(), 0);
NumericStorage<uint32_t> storage(&data_vec, ColumnType::kUint32);
@@ -66,7 +66,7 @@
ASSERT_EQ(search_result, empty_range);
}
-TEST(IdStorageUnittest, InvalidValueBoundsUint32) {
+TEST(NumericStorageUnittest, InvalidValueBoundsUint32) {
std::vector<uint32_t> data_vec(128);
std::iota(data_vec.begin(), data_vec.end(), 0);
NumericStorage<uint32_t> storage(&data_vec, ColumnType::kUint32);
@@ -120,7 +120,7 @@
ASSERT_EQ(search_result, empty_range);
}
-TEST(IdStorageUnittest, InvalidValueBoundsInt32) {
+TEST(NumericStorageUnittest, InvalidValueBoundsInt32) {
std::vector<int32_t> data_vec(128);
std::iota(data_vec.begin(), data_vec.end(), 0);
NumericStorage<int32_t> storage(&data_vec, ColumnType::kInt32);
diff --git a/src/trace_processor/db/storage/set_id_storage.cc b/src/trace_processor/db/storage/set_id_storage.cc
index 570afb7..94b94dc 100644
--- a/src/trace_processor/db/storage/set_id_storage.cc
+++ b/src/trace_processor/db/storage/set_id_storage.cc
@@ -139,6 +139,8 @@
std::to_string(static_cast<uint32_t>(op)));
});
+ PERFETTO_DCHECK(search_range.end <= size());
+
// After this switch we assume the search is valid.
switch (ValidateSearchConstraints(sql_val, op)) {
case SearchValidationResult::kOk:
@@ -149,7 +151,6 @@
return RangeOrBitVector(Range());
}
- PERFETTO_DCHECK(search_range.end <= size());
uint32_t val = static_cast<uint32_t>(sql_val.AsLong());
if (op == FilterOp::kNe) {
diff --git a/src/trace_processor/db/storage/set_id_storage_unittest.cc b/src/trace_processor/db/storage/set_id_storage_unittest.cc
index 658b731..5023345 100644
--- a/src/trace_processor/db/storage/set_id_storage_unittest.cc
+++ b/src/trace_processor/db/storage/set_id_storage_unittest.cc
@@ -29,7 +29,7 @@
using Range = RowMap::Range;
-TEST(IdStorageUnittest, InvalidSearchConstraints) {
+TEST(SetIdStorageUnittest, InvalidSearchConstraints) {
std::vector<uint32_t> storage_data{0, 0, 0, 3, 3, 3, 6, 6, 6, 9, 9, 9};
SetIdStorage storage(&storage_data);
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/scroll_jank_cause_map.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/scroll_jank_cause_map.sql
index f049075..0a21d27 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/scroll_jank_cause_map.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/scroll_jank_cause_map.sql
@@ -2,6 +2,8 @@
-- Use of this source code is governed by a BSD-style license that can be
-- found in the LICENSE file.
+INCLUDE PERFETTO MODULE chrome.event_latency_description;
+
-- Source of truth of the descriptions of EventLatency-based scroll jank causes.
CREATE PERFETTO TABLE chrome_scroll_jank_cause_descriptions (
-- The name of the EventLatency stage.
@@ -92,3 +94,27 @@
cause_thread,
cause_description
FROM cause_descriptions;
+
+-- Combined description of scroll jank cause and associated event latency stage.
+CREATE PERFETTO VIEW chrome_scroll_jank_causes_with_event_latencies(
+ -- The name of the EventLatency stage.
+ name STRING,
+ -- Description of the EventLatency stage.
+ description STRING,
+ -- The process name that may cause scroll jank.
+ cause_process STRING,
+ -- The thread name that may cause scroll jank. The thread will be on the
+ -- cause_process.
+ cause_thread STRING,
+ -- Description of the cause of scroll jank on this process and thread.
+ cause_description STRING
+) AS
+SELECT
+ stages.name,
+ stages.description,
+ causes.cause_process,
+ causes.cause_thread,
+ causes.cause_description
+FROM chrome_event_latency_stage_descriptions stages
+LEFT JOIN chrome_scroll_jank_cause_descriptions causes
+ ON causes.event_latency_stage = stages.name;
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/scroll_jank_cause_utils.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/scroll_jank_cause_utils.sql
index 2c6d0d0..35780fb 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/scroll_jank_cause_utils.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/scroll_jank_cause_utils.sql
@@ -3,46 +3,181 @@
-- found in the LICENSE file.
--- Retrieve the thread id of the thread on a particular process, if the name of
--- that process is known. Returns an error if there are multiple threads in
--- the given process with the same name.
-CREATE PERFETTO FUNCTION internal_find_utid_by_upid_and_name(
- -- Unique process id
- upid INT,
- -- The name of the thread
- thread_name STRING)
+-- Function to retrieve the upid for a surfaceflinger, as these are attributed
+-- to the GPU but are recorded on a different data source (and track group).
+CREATE PERFETTO FUNCTION internal_get_process_id_for_surfaceflinger()
+-- The process id for surfaceflinger.
+RETURNS INT AS
+SELECT
+ upid
+FROM process
+WHERE name GLOB '*surfaceflinger*'
+LIMIT 1;
+
+-- Map a generic process type to a specific name or substring of a name that
+-- can be found in the trace process table.
+CREATE PERFETTO TABLE internal_process_type_to_name (
+ -- The process type: one of 'Browser' or 'GPU'.
+ process_type STRING,
+ -- The process name for Chrome traces.
+ process_name STRING,
+ -- Substring identifying the process for system traces.
+ process_glob STRING
+) AS
+WITH process_names (
+ process_type,
+ process_name,
+ process_glob
+ )
+AS (
+VALUES
+ ('Browser', 'Browser', '*.chrome'),
+ ('GPU', 'Gpu', '*.chrome:privileged_process*'))
+SELECT
+ process_type,
+ process_name,
+ process_glob
+FROM process_names;
+
+CREATE PERFETTO FUNCTION internal_get_process_name(
+ -- The process type: one of 'Browser' or 'GPU'.
+ type STRING
+)
+-- The process name
+RETURNS STRING AS
+SELECT
+ process_name
+FROM internal_process_type_to_name
+WHERE process_type = $type
+LIMIT 1;
+
+CREATE PERFETTO FUNCTION internal_get_process_glob(
+ -- The process type: one of 'Browser' or 'GPU'.
+ type STRING
+)
+-- A substring of the process name that can be used in GLOB calculations.
+RETURNS STRING AS
+SELECT
+ process_glob
+FROM internal_process_type_to_name
+WHERE process_type = $type
+LIMIT 1;
+
+-- TODO(b/309937901): Add chrome instance id for multiple chromes/webviews in a
+-- trace, as this may result in multiple browser and GPU processes.
+-- Function to retrieve the chrome process ID for a specific process type. Does
+-- not retrieve the Renderer process, as this is determined when the
+-- EventLatency is known. See function
+-- internal_get_renderer_upid_for_event_latency below.
+CREATE PERFETTO FUNCTION internal_get_process_id_by_type(
+ -- The process type: one of 'Browser' or 'GPU'.
+ type STRING
+)
RETURNS TABLE (
- -- Unique thread id.
- utid INT
+ -- The process id for the process type.
+ upid INT
) AS
SELECT
- DISTINCT utid
-FROM thread
-WHERE upid = $upid
- AND name = $thread_name;
+ upid
+FROM process
+WHERE name = internal_get_process_name($type)
+ OR name GLOB internal_get_process_glob($type);
--- Function to retrieve the track id of the thread on a particular process if
+-- Function to retrieve the chrome process ID that a given EventLatency slice
+-- occurred on. This is the Renderer process.
+CREATE PERFETTO FUNCTION internal_get_renderer_upid_for_event_latency(
+ -- The slice id for an EventLatency slice.
+ id INT
+)
+-- The process id for an EventLatency slice. This is the Renderer process.
+RETURNS INT AS
+SELECT
+ upid
+FROM process_slice
+WHERE id = $id;
+
+-- Helper function to retrieve all of the upids for a given process, thread,
+-- or EventLatency.
+CREATE PERFETTO FUNCTION internal_processes_by_type_for_event_latency(
+ -- The process type that the thread is on: one of 'Browser', 'Renderer' or
+ -- 'GPU'.
+ type STRING,
+ -- The name of the thread.
+ thread STRING,
+ -- The slice id of an EventLatency slice.
+ event_latency_id INT)
+RETURNS TABLE (
+ upid INT
+) AS
+WITH all_upids AS (
+ -- Renderer process upids
+ SELECT
+ $type AS process,
+ $thread AS thread,
+ $event_latency_id AS event_latency_id,
+ internal_get_renderer_upid_for_event_latency($event_latency_id) AS upid
+ WHERE $type = 'Renderer'
+ UNION ALL
+ -- surfaceflinger upids
+ SELECT
+ $type AS process,
+ $thread AS thread,
+ $event_latency_id AS event_latency_id,
+ internal_get_process_id_for_surfaceflinger() AS upid
+ WHERE $type = 'GPU' AND $thread = 'surfaceflinger'
+ UNION ALL
+ -- Generic Browser and GPU process upids
+ SELECT
+ $type AS process,
+ $thread AS thread,
+ $event_latency_id AS event_latency_id,
+ upid
+ FROM internal_get_process_id_by_type($type)
+ WHERE $type = 'Browser'
+ OR ($type = 'GPU' AND $thread != 'surfaceflinger')
+)
+SELECT
+ upid
+FROM all_upids;
+
+-- Function to retrieve the thread id of the thread on a particular process if
-- there are any slices during a particular EventLatency slice duration; this
-- upid/thread combination refers to a cause of Scroll Jank.
-CREATE PERFETTO FUNCTION chrome_select_scroll_jank_cause_track(
+CREATE PERFETTO FUNCTION chrome_select_scroll_jank_cause_thread(
-- The slice id of an EventLatency slice.
event_latency_id INT,
- -- The process id that the thread is on.
- upid INT,
+ -- The process type that the thread is on: one of 'Browser', 'Renderer' or
+ -- 'GPU'.
+ process_type STRING,
-- The name of the thread.
thread_name STRING)
RETURNS TABLE (
- -- The track id associated with |thread| on the process with |upid|.
- track_id INT
+ -- The utid associated with |thread| on the process with |upid|.
+ utid INT
) AS
+WITH threads AS (
+ SELECT
+ utid
+ FROM thread
+ WHERE upid IN
+ (
+ SELECT DISTINCT
+ upid
+ FROM internal_processes_by_type_for_event_latency(
+ $process_type,
+ $thread_name,
+ $event_latency_id)
+ )
+ AND name = $thread_name
+)
SELECT
- DISTINCT track_id
+ DISTINCT utid
FROM thread_slice
WHERE utid IN
(
SELECT
utid
- FROM internal_find_utid_by_upid_and_name($upid, $thread_name)
+ FROM threads
)
AND ts >= (SELECT ts FROM slice WHERE id = $event_latency_id LIMIT 1)
AND ts <= (SELECT ts + dur FROM slice WHERE id = $event_latency_id LIMIT 1);
diff --git a/ui/src/base/dom_utils.ts b/ui/src/base/dom_utils.ts
index d7c7582..5caf9d9 100644
--- a/ui/src/base/dom_utils.ts
+++ b/ui/src/base/dom_utils.ts
@@ -82,3 +82,25 @@
return {x: e.offsetX, y: e.offsetY};
}
+
+function calculateScrollbarWidth() {
+ const outer = document.createElement('div');
+ outer.style.overflowY = 'scroll';
+ const inner = document.createElement('div');
+ outer.appendChild(inner);
+ document.body.appendChild(outer);
+ const width =
+ outer.getBoundingClientRect().width - inner.getBoundingClientRect().width;
+ document.body.removeChild(outer);
+ return width;
+}
+
+let cachedScrollBarWidth: number|undefined = undefined;
+
+// Calculate the space a scrollbar takes up.
+export function getScrollbarWidth() {
+ if (cachedScrollBarWidth === undefined) {
+ cachedScrollBarWidth = calculateScrollbarWidth();
+ }
+ return cachedScrollBarWidth;
+}
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index 7f83b36..5311cb2 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -35,7 +35,7 @@
NewBottomTabArgs,
} from './bottom_tab';
import {FlowPoint, globals} from './globals';
-import {renderArguments} from './slice_args';
+import {hasArgs, renderArguments} from './slice_args';
import {renderDetails} from './slice_details';
import {getSlice, SliceDetails, SliceRef} from './sql/slice';
import {
@@ -288,7 +288,10 @@
private renderRhs(engine: EngineProxy, slice: SliceDetails): m.Children {
const precFlows = this.renderPrecedingFlows(slice);
const followingFlows = this.renderFollowingFlows(slice);
- const args = renderArguments(engine, slice);
+ const args = hasArgs(slice) &&
+ m(Section,
+ {title: 'Arguments'},
+ m(Tree, renderArguments(engine, slice)));
if (precFlows ?? followingFlows ?? args) {
return m(
GridLayoutColumn,
diff --git a/ui/src/frontend/frontend_local_state.ts b/ui/src/frontend/frontend_local_state.ts
index 86c30db..a649966 100644
--- a/ui/src/frontend/frontend_local_state.ts
+++ b/ui/src/frontend/frontend_local_state.ts
@@ -26,7 +26,6 @@
VisibleState,
} from '../common/state';
import {raf} from '../core/raf_scheduler';
-import {HttpRpcState} from '../trace_processor/http_rpc_engine';
import {globals} from './globals';
import {ratelimit} from './rate_limiters';
@@ -47,20 +46,6 @@
return current;
}
-// Calculate the space a scrollbar takes up so that we can subtract it from
-// the canvas width.
-function calculateScrollbarWidth() {
- const outer = document.createElement('div');
- outer.style.overflowY = 'scroll';
- const inner = document.createElement('div');
- outer.appendChild(inner);
- document.body.appendChild(outer);
- const width =
- outer.getBoundingClientRect().width - inner.getBoundingClientRect().width;
- document.body.removeChild(outer);
- return width;
-}
-
// Immutable object describing a (high precision) time window, providing methods
// for common mutation operations (pan, zoom), and accessors for common
// properties such as spans and durations in several formats.
@@ -155,17 +140,10 @@
private visibleWindow = new TimeWindow();
private _timeScale = this.visibleWindow.createTimeScale(0, 0);
private _windowSpan = PxSpan.ZERO;
- showPanningHint = false;
- showCookieConsent = false;
- scrollToTrackKey?: string|number;
- httpRpcState: HttpRpcState = {connected: false};
- newVersionAvailable = false;
// This is used to calculate the tracks within a Y range for area selection.
areaY: Range = {};
- private scrollBarWidth?: number;
-
private _visibleState: VisibleState = {
lastUpdate: 0,
start: Time.ZERO,
@@ -179,18 +157,6 @@
// and a |timeScale| have a notion of time range. That should live in one
// place only.
- getScrollbarWidth() {
- if (this.scrollBarWidth === undefined) {
- this.scrollBarWidth = calculateScrollbarWidth();
- }
- return this.scrollBarWidth;
- }
-
- setHttpRpcState(httpRpcState: HttpRpcState) {
- this.httpRpcState = httpRpcState;
- raf.scheduleFullRedraw();
- }
-
zoomVisibleWindow(ratio: number, centerPoint: number) {
this.visibleWindow = this.visibleWindow.zoom(ratio, centerPoint);
this._timeScale = this.visibleWindow.createTimeScale(
@@ -231,7 +197,6 @@
assertTrue(
end >= start,
`Impossible select area: start [${start}] >= end [${end}]`);
- this.showPanningHint = true;
this._selectedArea = {start, end, tracks};
raf.scheduleFullRedraw();
}
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 7323ba5..435bf99 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -47,6 +47,7 @@
import {setPerfHooks} from '../core/perf';
import {raf} from '../core/raf_scheduler';
import {Engine} from '../trace_processor/engine';
+import {HttpRpcState} from '../trace_processor/http_rpc_engine';
import {Analytics, initAnalytics} from './analytics';
import {BottomTabList} from './bottom_tab';
@@ -283,6 +284,11 @@
private _utcOffset = Time.ZERO;
private _openQueryHandler?: OpenQueryHandler;
+ scrollToTrackKey?: string|number;
+ httpRpcState: HttpRpcState = {connected: false};
+ newVersionAvailable = false;
+ showPanningHint = false;
+
// TODO(hjd): Remove once we no longer need to update UUID on redraw.
private _publishRedraw?: () => void = undefined;
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 14550e7..5497d8e 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -15,6 +15,7 @@
import m from 'mithril';
import {Trash} from '../base/disposable';
+import {getScrollbarWidth} from '../base/dom_utils';
import {assertExists, assertFalse} from '../base/logging';
import {SimpleResizeObserver} from '../base/resize_observer';
import {
@@ -333,8 +334,7 @@
// On non-MacOS if there is a solid scroll bar it can cover important
// pixels, reduce the size of the canvas so it doesn't overlap with
// the scroll bar.
- this.parentWidth =
- clientRect.width - globals.frontendLocalState.getScrollbarWidth();
+ this.parentWidth = clientRect.width - getScrollbarWidth();
this.parentHeight = clientRect.height;
return this.parentHeight !== oldHeight || this.parentWidth !== oldWidth;
}
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
index 8ed0c2d..d23be62 100644
--- a/ui/src/frontend/publish.ts
+++ b/ui/src/frontend/publish.ts
@@ -25,6 +25,7 @@
import {MetricResult} from '../common/metric_data';
import {CurrentSearchResults, SearchSummary} from '../common/search_data';
import {raf} from '../core/raf_scheduler';
+import {HttpRpcState} from '../trace_processor/http_rpc_engine';
import {
CounterDetails,
@@ -81,6 +82,11 @@
globals.publishRedraw();
}
+export function publishHttpRpcState(httpRpcState: HttpRpcState) {
+ globals.httpRpcState = httpRpcState;
+ raf.scheduleFullRedraw();
+}
+
export function publishCounterDetails(click: CounterDetails) {
globals.counterDetails = click;
globals.publishRedraw();
@@ -216,3 +222,8 @@
globals.ftracePanelData = data;
globals.publishRedraw();
}
+
+export function publishShowPanningHint() {
+ globals.showPanningHint = true;
+ globals.publishRedraw();
+}
diff --git a/ui/src/frontend/rpc_http_dialog.ts b/ui/src/frontend/rpc_http_dialog.ts
index c2a64a2..54796b8 100644
--- a/ui/src/frontend/rpc_http_dialog.ts
+++ b/ui/src/frontend/rpc_http_dialog.ts
@@ -22,6 +22,7 @@
import {globals} from './globals';
import {showModal} from './modal';
+import {publishHttpRpcState} from './publish';
const CURRENT_API_VERSION =
TraceProcessorApiVersion.TRACE_PROCESSOR_CURRENT_API_VERSION;
@@ -79,7 +80,7 @@
// having to open a trace).
export async function CheckHttpRpcConnection(): Promise<void> {
const state = await HttpRpcEngine.checkConnection();
- globals.frontendLocalState.setHttpRpcState(state);
+ publishHttpRpcState(state);
if (!state.connected) return;
const tpStatus = assertExists(state.status);
diff --git a/ui/src/frontend/scroll_helper.ts b/ui/src/frontend/scroll_helper.ts
index dccad06..b514b61 100644
--- a/ui/src/frontend/scroll_helper.ts
+++ b/ui/src/frontend/scroll_helper.ts
@@ -135,7 +135,7 @@
// group and scroll to the track or just scroll to the track group.
if (openGroup) {
// After the track exists in the dom, it will be scrolled to.
- globals.frontendLocalState.scrollToTrackKey = trackKey;
+ globals.scrollToTrackKey = trackKey;
globals.dispatch(Actions.toggleTrackGroupCollapsed({trackGroupId}));
return;
} else {
diff --git a/ui/src/frontend/service_worker_controller.ts b/ui/src/frontend/service_worker_controller.ts
index a6082fe..de5323e 100644
--- a/ui/src/frontend/service_worker_controller.ts
+++ b/ui/src/frontend/service_worker_controller.ts
@@ -88,7 +88,7 @@
// Ctrl+Shift+R). In these cases, we are already at the last
// version.
if (sw !== this._initialWorker && this._initialWorker) {
- globals.frontendLocalState.newVersionAvailable = true;
+ globals.newVersionAvailable = true;
}
}
}
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index c0c23bf..6d3e79b 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -631,7 +631,7 @@
// RPC server is shut down after we load the UI and cached httpRpcState)
// this will eventually become consistent once the engine is created.
if (mode === undefined) {
- if (globals.frontendLocalState.httpRpcState.connected &&
+ if (globals.httpRpcState.connected &&
globals.state.newEngineMode === 'USE_HTTP_RPC_IF_AVAILABLE') {
mode = 'HTTP_RPC';
} else {
diff --git a/ui/src/frontend/slice_args.ts b/ui/src/frontend/slice_args.ts
index 70beacb..9e1798f 100644
--- a/ui/src/frontend/slice_args.ts
+++ b/ui/src/frontend/slice_args.ts
@@ -30,8 +30,7 @@
} from '../tracks/visualised_args';
import {Anchor} from '../widgets/anchor';
import {MenuItem, PopupMenu2} from '../widgets/menu';
-import {Section} from '../widgets/section';
-import {Tree, TreeNode} from '../widgets/tree';
+import {TreeNode} from '../widgets/tree';
import {addTab} from './bottom_tab';
import {globals} from './globals';
@@ -40,20 +39,21 @@
import {SqlTableTab} from './sql_table/tab';
import {SqlTables} from './sql_table/well_known_tables';
-// Renders slice arguments (key/value pairs) into a Tree widget.
+// Renders slice arguments (key/value pairs) as a subtree.
export function renderArguments(
engine: EngineProxy, slice: SliceDetails): m.Children {
if (slice.args && slice.args.length > 0) {
const tree = convertArgsToTree(slice.args);
- return m(
- Section,
- {title: 'Arguments'},
- m(Tree, renderArgTreeNodes(engine, tree)));
+ return renderArgTreeNodes(engine, tree);
} else {
return undefined;
}
}
+export function hasArgs(slice: SliceDetails): boolean {
+ return exists(slice.args) && slice.args.length > 0;
+}
+
function renderArgTreeNodes(
engine: EngineProxy, args: ArgNode<Arg>[]): m.Children {
return args.map((arg) => {
diff --git a/ui/src/frontend/tables/table.ts b/ui/src/frontend/tables/table.ts
index 4d1b773..eb12883 100644
--- a/ui/src/frontend/tables/table.ts
+++ b/ui/src/frontend/tables/table.ts
@@ -89,6 +89,11 @@
return new ColumnDescriptor<T>(name, getter, {contextMenu, sortKey: getter});
}
+export function widgetColumn<T>(
+ name: string, getter: (t: T) => m.Child): ColumnDescriptor<T> {
+ return new ColumnDescriptor<T>(name, getter);
+}
+
interface SortingInfo<T> {
columnId: string;
direction: SortDirection;
diff --git a/ui/src/frontend/topbar.ts b/ui/src/frontend/topbar.ts
index 70aed74..4bea985 100644
--- a/ui/src/frontend/topbar.ts
+++ b/ui/src/frontend/topbar.ts
@@ -45,7 +45,7 @@
m('button.notification-btn.preferred',
{
onclick: () => {
- globals.frontendLocalState.newVersionAvailable = false;
+ globals.newVersionAvailable = false;
raf.scheduleFullRedraw();
},
},
@@ -61,7 +61,7 @@
// does not persist for iFrames. The host is responsible for communicating
// to users that they can press '?' for help.
if (globals.embeddedMode || dismissed === 'true' ||
- !globals.frontendLocalState.showPanningHint) {
+ !globals.showPanningHint) {
return;
}
return m(
@@ -72,7 +72,7 @@
m('button.hint-dismiss-button',
{
onclick: () => {
- globals.frontendLocalState.showPanningHint = false;
+ globals.showPanningHint = false;
localStorage.setItem(DISMISSED_PANNING_HINT_KEY, 'true');
raf.scheduleFullRedraw();
},
@@ -113,9 +113,7 @@
return m(
'.topbar',
{class: globals.state.sidebarVisible ? '' : 'hide-sidebar'},
- globals.frontendLocalState.newVersionAvailable ?
- m(NewVersionNotification) :
- omnibox,
+ globals.newVersionAvailable ? m(NewVersionNotification) : omnibox,
m(Progress),
m(HelpPanningNotification),
m(TraceErrorIcon));
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index c877ad1..56318d1 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -297,9 +297,9 @@
}
oncreate({attrs}: m.CVnode<TrackComponentAttrs>) {
- if (globals.frontendLocalState.scrollToTrackKey === attrs.trackState.key) {
+ if (globals.scrollToTrackKey === attrs.trackState.key) {
verticalScrollToTrack(attrs.trackState.key);
- globals.frontendLocalState.scrollToTrackKey = undefined;
+ globals.scrollToTrackKey = undefined;
}
}
}
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 8aeabde..57aa0a3 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -14,6 +14,7 @@
import m from 'mithril';
+import {getScrollbarWidth} from '../base/dom_utils';
import {clamp} from '../base/math_utils';
import {Time} from '../base/time';
import {Actions} from '../common/actions';
@@ -28,6 +29,7 @@
import {createPage} from './pages';
import {PanAndZoomHandler} from './pan_and_zoom_handler';
import {AnyAttrsVnode, PanelContainer} from './panel_container';
+import {publishShowPanningHint} from './publish';
import {TickmarkPanel} from './tickmark_panel';
import {TimeAxisPanel} from './time_axis_panel';
import {TimeSelectionPanel} from './time_selection_panel';
@@ -96,9 +98,7 @@
const updateDimensions = () => {
const rect = vnode.dom.getBoundingClientRect();
frontendLocalState.updateLocalLimits(
- 0,
- rect.width - TRACK_SHELL_WIDTH -
- frontendLocalState.getScrollbarWidth());
+ 0, rect.width - TRACK_SHELL_WIDTH - getScrollbarWidth());
};
updateDimensions();
@@ -193,6 +193,7 @@
);
frontendLocalState.areaY.start = dragStartY;
frontendLocalState.areaY.end = currentY;
+ publishShowPanningHint();
}
raf.scheduleRedraw();
},
diff --git a/ui/src/tracks/chrome_critical_user_interactions/index.ts b/ui/src/tracks/chrome_critical_user_interactions/index.ts
index 2e0a381..8ddb3d4 100644
--- a/ui/src/tracks/chrome_critical_user_interactions/index.ts
+++ b/ui/src/tracks/chrome_critical_user_interactions/index.ts
@@ -24,6 +24,7 @@
NamedSliceTrackTypes,
} from '../../frontend/named_slice_track';
import {
+ NUM,
Plugin,
PluginContext,
PluginContextTrace,
@@ -40,17 +41,20 @@
} from '../custom_sql_table_slices';
import {PageLoadDetailsPanel} from './page_load_details_panel';
+import {StartupDetailsPanel} from './startup_details_panel';
export const CRITICAL_USER_INTERACTIONS_KIND =
'org.chromium.CriticalUserInteraction.track';
export const CRITICAL_USER_INTERACTIONS_ROW = {
...NAMED_ROW,
+ scopedId: NUM,
type: STR,
};
export type CriticalUserInteractionRow = typeof CRITICAL_USER_INTERACTIONS_ROW;
export interface CriticalUserInteractionSlice extends Slice {
+ scopedId: number;
type: string;
}
@@ -63,6 +67,7 @@
enum CriticalUserInteractionType {
UNKNOWN = 'Unknown',
PAGE_LOAD = 'chrome_page_loads',
+ STARTUP = 'chrome_startups',
}
function convertToCriticalUserInteractionType(cujType: string):
@@ -70,6 +75,8 @@
switch (cujType) {
case CriticalUserInteractionType.PAGE_LOAD:
return CriticalUserInteractionType.PAGE_LOAD;
+ case CriticalUserInteractionType.STARTUP:
+ return CriticalUserInteractionType.STARTUP;
default:
return CriticalUserInteractionType.UNKNOWN;
}
@@ -81,7 +88,17 @@
getSqlDataSource(): CustomSqlTableDefConfig {
return {
- columns: ['scoped_id AS id', 'name', 'ts', 'dur', 'type'],
+ columns: [
+ // The scoped_id is not a unique identifier within the table; generate
+ // a unique id from type and scoped_id on the fly to use for slice
+ // selection.
+ 'hash(type, scoped_id) AS id',
+ 'scoped_id AS scopedId',
+ 'name',
+ 'ts',
+ 'dur',
+ 'type',
+ ],
sqlTableName: 'chrome_interactions',
};
}
@@ -107,12 +124,37 @@
},
};
break;
+ case CriticalUserInteractionType.STARTUP:
+ detailsPanel = {
+ kind: StartupDetailsPanel.kind,
+ config: {
+ sqlTableName: this.tableName,
+ title: 'Chrome Startup',
+ },
+ };
+ break;
default:
break;
}
return detailsPanel;
}
+ onSliceClick(
+ args: OnSliceClickArgs<CriticalUserInteractionSliceTrackTypes['slice']>) {
+ const detailsPanelConfig = this.getDetailsPanel(args);
+ globals.makeSelection(Actions.selectGenericSlice({
+ id: args.slice.scopedId,
+ sqlTableName: this.tableName,
+ start: args.slice.ts,
+ duration: args.slice.dur,
+ trackKey: this.trackKey,
+ detailsPanelConfig: {
+ kind: detailsPanelConfig.kind,
+ config: detailsPanelConfig.config,
+ },
+ }));
+ }
+
getSqlImports(): CustomSqlImportConfig {
return {
modules: ['chrome.interactions'],
@@ -126,8 +168,9 @@
rowToSlice(row: CriticalUserInteractionSliceTrackTypes['row']):
CriticalUserInteractionSliceTrackTypes['slice'] {
const baseSlice = super.rowToSlice(row);
+ const scopedId = row.scopedId;
const type = row.type;
- return {...baseSlice, type};
+ return {...baseSlice, scopedId, type};
}
}
diff --git a/ui/src/tracks/chrome_critical_user_interactions/startup_details_panel.ts b/ui/src/tracks/chrome_critical_user_interactions/startup_details_panel.ts
new file mode 100644
index 0000000..dbcde70
--- /dev/null
+++ b/ui/src/tracks/chrome_critical_user_interactions/startup_details_panel.ts
@@ -0,0 +1,147 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+
+import {duration, Time, time} from '../../base/time';
+import {
+ BottomTab,
+ bottomTabRegistry,
+ NewBottomTabArgs,
+} from '../../frontend/bottom_tab';
+import {
+ GenericSliceDetailsTabConfig,
+} from '../../frontend/generic_slice_details_tab';
+import {DurationWidget} from '../../frontend/widgets/duration';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {LONG, NUM, STR, STR_NULL} from '../../trace_processor/query_result';
+import {DetailsShell} from '../../widgets/details_shell';
+import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
+import {Section} from '../../widgets/section';
+import {SqlRef} from '../../widgets/sql_ref';
+import {dictToTreeNodes, Tree} from '../../widgets/tree';
+import {asUpid, Upid} from '../../frontend/sql_types';
+
+interface Data {
+ startupId: number;
+ eventName: string;
+ startupBeginTs: time;
+ durToFirstVisibleContent: duration;
+ launchCause?: string;
+ upid: Upid;
+}
+
+export class StartupDetailsPanel extends
+ BottomTab<GenericSliceDetailsTabConfig> {
+ static readonly kind = 'org.perfetto.StartupDetailsPanel';
+ private loaded = false;
+ private data: Data|undefined;
+
+ static create(args: NewBottomTabArgs): StartupDetailsPanel {
+ return new StartupDetailsPanel(args);
+ }
+
+ constructor(args: NewBottomTabArgs) {
+ super(args);
+ this.loadData();
+ }
+
+ private async loadData() {
+ const queryResult = await this.engine.query(`
+ SELECT
+ activity_id AS startupId,
+ name,
+ startup_begin_ts AS startupBeginTs,
+ CASE
+ WHEN first_visible_content_ts IS NULL THEN 0
+ ELSE first_visible_content_ts - startup_begin_ts
+ END AS durTofirstVisibleContent,
+ launch_cause AS launchCause,
+ browser_upid AS upid
+ FROM chrome_startups
+ WHERE id = ${this.config.id};
+ `);
+
+ const iter = queryResult.firstRow({
+ startupId: NUM,
+ name: STR,
+ startupBeginTs: LONG,
+ durTofirstVisibleContent: LONG,
+ launchCause: STR_NULL,
+ upid: NUM,
+ });
+
+ this.data = {
+ startupId: iter.startupId,
+ eventName: iter.name,
+ startupBeginTs: Time.fromRaw(iter.startupBeginTs),
+ durToFirstVisibleContent: iter.durTofirstVisibleContent,
+ upid: asUpid(iter.upid),
+ };
+
+ if (iter.launchCause) {
+ this.data.launchCause = iter.launchCause;
+ }
+
+ this.loaded = true;
+ }
+
+ private getDetailsDictionary() {
+ const details: {[key: string]: m.Child} = {};
+ if (this.data === undefined) return details;
+ details['Activity ID'] = this.data.startupId;
+ details['Browser Upid'] = this.data.upid;
+ details['Startup Event'] = this.data.eventName;
+ details['Startup Timestamp'] = m(Timestamp, {ts: this.data.startupBeginTs});
+ details['Duration to First Visible Content'] =
+ m(DurationWidget, {dur: this.data.durToFirstVisibleContent});
+ if (this.data.launchCause) {
+ details['Launch Cause'] = this.data.launchCause;
+ }
+ details['SQL ID'] =
+ m(SqlRef, {table: 'chrome_startups', id: this.config.id});
+ return details;
+ }
+
+ viewTab() {
+ if (this.isLoading()) {
+ return m('h2', 'Loading');
+ }
+
+ return m(
+ DetailsShell,
+ {
+ title: this.getTitle(),
+ },
+ m(GridLayout,
+ m(
+ GridLayoutColumn,
+ m(
+ Section,
+ {title: 'Details'},
+ m(Tree, dictToTreeNodes(this.getDetailsDictionary())),
+ ),
+ )));
+ }
+
+ getTitle(): string {
+ return this.config.title;
+ }
+
+ isLoading() {
+ return !this.loaded;
+ }
+}
+
+bottomTabRegistry.register(StartupDetailsPanel);
diff --git a/ui/src/tracks/chrome_scroll_jank/event_latency_details_panel.ts b/ui/src/tracks/chrome_scroll_jank/event_latency_details_panel.ts
index 5014d14..0510eeb 100644
--- a/ui/src/tracks/chrome_scroll_jank/event_latency_details_panel.ts
+++ b/ui/src/tracks/chrome_scroll_jank/event_latency_details_panel.ts
@@ -14,7 +14,7 @@
import m from 'mithril';
-import {exists} from '../../base/utils';
+import {duration, time} from '../../base/time';
import {raf} from '../../core/raf_scheduler';
import {
BottomTab,
@@ -27,8 +27,14 @@
import {renderArguments} from '../../frontend/slice_args';
import {renderDetails} from '../../frontend/slice_details';
import {getSlice, SliceDetails, sliceRef} from '../../frontend/sql/slice';
-import {asSliceSqlId} from '../../frontend/sql_types';
-import {NUM} from '../../trace_processor/query_result';
+import {asSliceSqlId, SliceSqlId} from '../../frontend/sql_types';
+import {
+ ColumnDescriptor,
+ Table,
+ TableData,
+ widgetColumn,
+} from '../../frontend/tables/table';
+import {NUM, STR} from '../../trace_processor/query_result';
import {DetailsShell} from '../../widgets/details_shell';
import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
import {Section} from '../../widgets/section';
@@ -36,6 +42,14 @@
import {Tree, TreeNode} from '../../widgets/tree';
import {
+ EventLatencyCauseThreadTracks,
+ EventLatencyStage,
+ getCauseLink,
+ getEventLatencyCauseTracks,
+ getScrollJankCauseStage,
+} from './scroll_jank_cause_link_utils';
+import {ScrollJankCauseMap} from './scroll_jank_cause_map';
+import {
getScrollJankSlices,
getSliceForTrack,
ScrollJankSlice,
@@ -47,10 +61,22 @@
static readonly kind = 'dev.perfetto.EventLatencySliceDetailsPanel';
private loaded = false;
+ private name = '';
+ private topEventLatencyId: SliceSqlId|undefined = undefined;
private sliceDetails?: SliceDetails;
private jankySlice?: ScrollJankSlice;
+ // Whether this stage has caused jank. This is also true for top level
+ // EventLatency slices where a descendant is a cause of jank.
+ private isJankStage = false;
+
+ // For top level EventLatency slices - if any descendant is a cause of jank,
+ // this field stores information about that descendant slice. Otherwise, this
+ // is stores information about the current stage;
+ private relevantThreadStage: EventLatencyStage|undefined;
+ private relevantThreadTracks: EventLatencyCauseThreadTracks[] = [];
+
static create(args: NewBottomTabArgs): EventLatencySliceDetailsPanel {
return new EventLatencySliceDetailsPanel(args);
}
@@ -62,8 +88,22 @@
}
async loadData() {
+ const queryResult = await this.engine.query(`
+ SELECT
+ name
+ FROM ${this.config.sqlTableName}
+ WHERE id = ${this.config.id}
+ `);
+
+ const iter = queryResult.firstRow({
+ name: STR,
+ });
+
+ this.name = iter.name;
+
await this.loadSlice();
await this.loadJankSlice();
+ await this.loadRelevantThreads();
this.loaded = true;
}
@@ -74,37 +114,152 @@
}
async loadJankSlice() {
- if (exists(this.sliceDetails)) {
- // Get the id for the top-level EventLatency slice (this or parent), as
- // this id is used in the ScrollJankV3 track to identify the corresponding
- // janky interval.
- let eventLatencyId = -1;
- if (this.sliceDetails.name == 'EventLatency') {
- eventLatencyId = this.sliceDetails.id;
- } else {
- const queryResult = await this.engine.query(`
- SELECT
- id
- FROM ancestor_slice(${this.sliceDetails.id})
- WHERE name = 'EventLatency'
- `);
- const it = queryResult.iter({
- id: NUM,
- });
- for (; it.valid(); it.next()) {
- eventLatencyId = it.id;
- break;
- }
- }
-
- const possibleSlices =
- await getScrollJankSlices(this.engine, eventLatencyId);
- // We may not get any slices if the EventLatency doesn't indicate any
- // jank occurred.
- if (possibleSlices.length > 0) {
- this.jankySlice = possibleSlices[0];
- }
+ if (!this.sliceDetails) return;
+ // Get the id for the top-level EventLatency slice (this or parent), as
+ // this id is used in the ScrollJankV3 track to identify the corresponding
+ // janky interval.
+ if (this.sliceDetails.name === 'EventLatency') {
+ this.topEventLatencyId = this.sliceDetails.id;
+ } else {
+ this.topEventLatencyId =
+ asSliceSqlId(await this.getOldestAncestorSliceId());
}
+
+ const possibleSlices =
+ await getScrollJankSlices(this.engine, this.topEventLatencyId);
+ // We may not get any slices if the EventLatency doesn't indicate any
+ // jank occurred.
+ if (possibleSlices.length > 0) {
+ this.jankySlice = possibleSlices[0];
+ }
+ }
+
+ async loadRelevantThreads() {
+ if (!this.sliceDetails) return;
+ if (!this.topEventLatencyId) return;
+
+ // Relevant threads should only be available on a "Janky" EventLatency
+ // slice to allow the user to jump to the possible cause of jank.
+ if (this.sliceDetails.name === 'EventLatency' && !this.jankySlice) return;
+
+ const possibleScrollJankStage =
+ await getScrollJankCauseStage(this.engine, this.topEventLatencyId);
+ if (this.sliceDetails.name === 'EventLatency') {
+ this.isJankStage = true;
+ this.relevantThreadStage = possibleScrollJankStage;
+ } else {
+ if (possibleScrollJankStage &&
+ this.sliceDetails.name === possibleScrollJankStage.name) {
+ this.isJankStage = true;
+ }
+ this.relevantThreadStage = {
+ name: this.sliceDetails.name,
+ eventLatencyId: this.topEventLatencyId,
+ ts: this.sliceDetails.ts,
+ dur: this.sliceDetails.dur,
+ };
+ }
+
+ if (this.relevantThreadStage) {
+ this.relevantThreadTracks = await getEventLatencyCauseTracks(
+ this.engine, this.relevantThreadStage);
+ }
+ }
+
+ private getRelevantLinks(): m.Child {
+ if (!this.sliceDetails) return undefined;
+
+ // Relevant threads should only be available on a "Janky" EventLatency
+ // slice to allow the user to jump to the possible cause of jank.
+ if (this.sliceDetails.name === 'EventLatency' &&
+ !this.relevantThreadStage) {
+ return undefined;
+ }
+
+ const name = this.relevantThreadStage ? this.relevantThreadStage.name :
+ this.sliceDetails.name;
+ const ts = this.relevantThreadStage ? this.relevantThreadStage.ts :
+ this.sliceDetails.ts;
+ const dur = this.relevantThreadStage ? this.relevantThreadStage.dur :
+ this.sliceDetails.dur;
+ const stageDetails = ScrollJankCauseMap.getEventLatencyDetails(name);
+ if (stageDetails === undefined) return undefined;
+
+ const childWidgets: m.Child[] = [];
+ childWidgets.push(m(TextParagraph, {text: stageDetails.description}));
+
+ interface RelevantThreadRow {
+ description: string;
+ tracks: EventLatencyCauseThreadTracks;
+ ts: time;
+ dur: duration;
+ }
+
+ const columns: ColumnDescriptor<RelevantThreadRow>[] = [
+ widgetColumn<RelevantThreadRow>(
+ 'Relevant Thread', (x) => getCauseLink(x.tracks, x.ts, x.dur)),
+ widgetColumn<RelevantThreadRow>(
+ 'Description',
+ (x) => {
+ if (x.description === '') {
+ return x.description;
+ } else {
+ return m(TextParagraph, {text: x.description});
+ }
+ }),
+ ];
+
+ const trackLinks: RelevantThreadRow[] = [];
+
+ for (let i = 0; i < this.relevantThreadTracks.length; i++) {
+ const track = this.relevantThreadTracks[i];
+ let description = '';
+ if (i == 0 || track.thread != this.relevantThreadTracks[i - 1].thread) {
+ description = track.causeDescription;
+ }
+ trackLinks.push({
+ description: description,
+ tracks: this.relevantThreadTracks[i],
+ ts: ts,
+ dur: dur,
+ });
+ }
+
+ const tableData = new TableData(trackLinks);
+
+ if (trackLinks.length > 0) {
+ childWidgets.push(m(Table, {
+ data: tableData,
+ columns: columns,
+ }));
+ }
+
+ return m(
+ Section,
+ {title: this.isJankStage ? `Jank Cause: ${name}` : name},
+ childWidgets);
+ }
+
+ private async getOldestAncestorSliceId(): Promise<number> {
+ let eventLatencyId = -1;
+ if (!this.sliceDetails) return eventLatencyId;
+ const queryResult = await this.engine.query(`
+ SELECT
+ id
+ FROM ancestor_slice(${this.sliceDetails.id})
+ WHERE name = 'EventLatency'
+ `);
+
+ const it = queryResult.iter({
+ id: NUM,
+ });
+
+ for (; it.valid(); it.next()) {
+ eventLatencyId = it.id;
+ break;
+ }
+
+ return eventLatencyId;
}
private getLinksSection(): m.Child {
@@ -114,20 +269,20 @@
m(
Tree,
m(TreeNode, {
- left: exists(this.sliceDetails) ?
+ left: this.sliceDetails ?
sliceRef(
this.sliceDetails,
'EventLatency in context of other Input events') :
'EventLatency in context of other Input events',
- right: exists(this.sliceDetails) ? '' : 'N/A',
+ right: this.sliceDetails ? '' : 'N/A',
}),
m(TreeNode, {
- left: exists(this.jankySlice) ? getSliceForTrack(
- this.jankySlice,
- ScrollJankV3Track.kind,
- 'Jank Interval') :
- 'Jank Interval',
- right: exists(this.jankySlice) ? '' : 'N/A',
+ left: this.jankySlice ? getSliceForTrack(
+ this.jankySlice,
+ ScrollJankV3Track.kind,
+ 'Jank Interval') :
+ 'Jank Interval',
+ right: this.jankySlice ? '' : 'N/A',
}),
),
);
@@ -160,23 +315,34 @@
}
viewTab() {
- if (exists(this.sliceDetails)) {
+ if (this.sliceDetails) {
const slice = this.sliceDetails;
+
+ const rightSideWidgets: m.Child[] = [];
+ rightSideWidgets.push(
+ m(Section,
+ {title: 'Description'},
+ m('.div', this.getDescriptionText())));
+
+ const stageWidget = this.getRelevantLinks();
+ if (stageWidget) {
+ rightSideWidgets.push(stageWidget);
+ }
+ rightSideWidgets.push(this.getLinksSection());
+
return m(
DetailsShell,
{
title: 'Slice',
- description: slice.name,
+ description: this.name,
},
m(GridLayout,
m(GridLayoutColumn,
renderDetails(slice),
- renderArguments(this.engine, slice)),
- m(GridLayoutColumn,
m(Section,
- {title: 'Description'},
- m('.div', this.getDescriptionText())),
- this.getLinksSection())),
+ {title: 'Arguments'},
+ m(Tree, renderArguments(this.engine, slice)))),
+ m(GridLayoutColumn, rightSideWidgets)),
);
} else {
return m(DetailsShell, {title: 'Slice', description: 'Loading...'});
diff --git a/ui/src/tracks/chrome_scroll_jank/index.ts b/ui/src/tracks/chrome_scroll_jank/index.ts
index 583909f..34df4e1 100644
--- a/ui/src/tracks/chrome_scroll_jank/index.ts
+++ b/ui/src/tracks/chrome_scroll_jank/index.ts
@@ -38,6 +38,7 @@
EventLatencyTrack,
JANKY_LATENCY_NAME,
} from './event_latency_track';
+import {ScrollJankCauseMap} from './scroll_jank_cause_map';
import {
addScrollJankV3ScrollTrack,
ScrollJankV3Track,
@@ -117,7 +118,7 @@
}
}
-export async function getScrollJankTracks(_engine: Engine):
+export async function getScrollJankTracks(engine: Engine):
Promise<ScrollJankTrackGroup> {
const result: ScrollJankTracks = {
tracksToAdd: [],
@@ -149,6 +150,7 @@
fixedOrdering: true,
});
+ await ScrollJankCauseMap.initialize(engine);
return {tracks: result, addTrackGroup};
}
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_details_panel.ts b/ui/src/tracks/chrome_scroll_jank/scroll_details_panel.ts
index 63e913f..2837ce1 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_details_panel.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_details_panel.ts
@@ -31,6 +31,7 @@
numberColumn,
Table,
TableData,
+ widgetColumn,
} from '../../frontend/tables/table';
import {DurationWidget} from '../../frontend/widgets/duration';
import {Timestamp} from '../../frontend/widgets/timestamp';
@@ -55,11 +56,6 @@
} from './scroll_jank_slice';
import {ScrollJankV3Track} from './scroll_jank_v3_track';
-function widgetColumn<T>(
- name: string, getter: (t: T) => m.Child): ColumnDescriptor<T> {
- return new ColumnDescriptor<T>(name, getter);
-}
-
interface Data {
// Scroll ID.
id: number;
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_jank_cause_link_utils.ts b/ui/src/tracks/chrome_scroll_jank/scroll_jank_cause_link_utils.ts
new file mode 100644
index 0000000..7209356
--- /dev/null
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_jank_cause_link_utils.ts
@@ -0,0 +1,218 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+
+import {Icons} from '../../base/semantic_icons';
+import {duration, Time, time} from '../../base/time';
+import {exists} from '../../base/utils';
+import {Actions} from '../../common/actions';
+import {globals} from '../../frontend/globals';
+import {
+ focusHorizontalRange,
+ verticalScrollToTrack,
+} from '../../frontend/scroll_helper';
+import {SliceSqlId} from '../../frontend/sql_types';
+import {EngineProxy} from '../../trace_processor/engine';
+import {LONG, NUM, STR} from '../../trace_processor/query_result';
+import {Anchor} from '../../widgets/anchor';
+
+import {
+ CauseProcess,
+ CauseThread,
+ ScrollJankCauseMap,
+} from './scroll_jank_cause_map';
+
+const UNKNOWN_NAME = 'Unknown';
+
+export interface EventLatencyStage {
+ name: string;
+ // Slice id of the top level EventLatency slice (not a stage).
+ eventLatencyId: SliceSqlId;
+ ts: time;
+ dur: duration;
+}
+
+export interface EventLatencyCauseThreadTracks {
+ // A thread may have multiple tracks associated with it (e.g. from ATrace
+ // events).
+ trackIds: number[];
+ thread: CauseThread;
+ causeDescription: string;
+}
+
+export async function getScrollJankCauseStage(
+ engine: EngineProxy,
+ eventLatencyId: SliceSqlId): Promise<EventLatencyStage|undefined> {
+ const queryResult = await engine.query(`
+ SELECT
+ IFNULL(cause_of_jank, '${UNKNOWN_NAME}') AS causeOfJank,
+ IFNULL(sub_cause_of_jank, '${UNKNOWN_NAME}') AS subCauseOfJank,
+ IFNULL(substage.ts, -1) AS ts,
+ IFNULL(substage.dur, -1) AS dur
+ FROM chrome_janky_frame_presentation_intervals
+ JOIN descendant_slice(event_latency_id) substage
+ WHERE event_latency_id = ${eventLatencyId}
+ AND substage.name = COALESCE(sub_cause_of_jank, cause_of_jank)
+ `);
+
+ const causeIt = queryResult.iter({
+ causeOfJank: STR,
+ subCauseOfJank: STR,
+ ts: LONG,
+ dur: LONG,
+ });
+
+ for (; causeIt.valid(); causeIt.next()) {
+ const causeOfJank = causeIt.causeOfJank;
+ const subCauseOfJank = causeIt.subCauseOfJank;
+
+ if (causeOfJank == '' || causeOfJank == UNKNOWN_NAME) return undefined;
+ const cause = subCauseOfJank == UNKNOWN_NAME ? causeOfJank : subCauseOfJank;
+ const stageDetails: EventLatencyStage = {
+ name: cause,
+ eventLatencyId: eventLatencyId,
+ ts: Time.fromRaw(causeIt.ts),
+ dur: causeIt.dur,
+ };
+
+ return stageDetails;
+ }
+
+ return undefined;
+}
+
+export async function getEventLatencyCauseTracks(
+ engine: EngineProxy, scrollJankCauseStage: EventLatencyStage):
+ Promise<EventLatencyCauseThreadTracks[]> {
+ const threadTracks: EventLatencyCauseThreadTracks[] = [];
+ const causeDetails =
+ ScrollJankCauseMap.getEventLatencyDetails(scrollJankCauseStage.name);
+ if (causeDetails === undefined) return threadTracks;
+
+ for (const cause of causeDetails.jankCauses) {
+ switch (cause.process) {
+ case CauseProcess.RENDERER:
+ case CauseProcess.BROWSER:
+ case CauseProcess.GPU:
+ const tracksForProcess = await getChromeCauseTracks(
+ engine,
+ scrollJankCauseStage.eventLatencyId,
+ cause.process,
+ cause.thread);
+ for (const track of tracksForProcess) {
+ track.causeDescription = cause.description;
+ threadTracks.push(track);
+ }
+ break;
+ case CauseProcess.UNKNOWN:
+ default:
+ break;
+ }
+ }
+
+ return threadTracks;
+}
+
+async function getChromeCauseTracks(
+ engine: EngineProxy,
+ eventLatencySliceId: number,
+ processName: CauseProcess,
+ threadName: CauseThread): Promise<EventLatencyCauseThreadTracks[]> {
+ const queryResult = await engine.query(`
+ INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_cause_utils;
+
+ SELECT DISTINCT
+ utid,
+ id AS trackId
+ FROM thread_track
+ WHERE utid IN (
+ SELECT DISTINCT
+ utid
+ FROM chrome_select_scroll_jank_cause_thread(
+ ${eventLatencySliceId},
+ '${processName}',
+ '${threadName}'
+ )
+ );
+ `);
+
+ const it = queryResult.iter({
+ utid: NUM,
+ trackId: NUM,
+ });
+
+ const threadsWithTrack: {[id: number]: EventLatencyCauseThreadTracks;} = {};
+ const utids: number[] = [];
+ for (; it.valid(); it.next()) {
+ const utid = it.utid;
+ if (!(utid in threadsWithTrack)) {
+ threadsWithTrack[utid] = {
+ trackIds: [it.trackId],
+ thread: threadName,
+ causeDescription: '',
+ };
+ utids.push(utid);
+ } else {
+ threadsWithTrack[utid].trackIds.push(it.trackId);
+ }
+ }
+
+ return utids.map((each) => threadsWithTrack[each]);
+}
+
+export function getCauseLink(
+ threadTracks: EventLatencyCauseThreadTracks,
+ ts: time|undefined,
+ dur: duration|undefined): m.Child {
+ const trackKeys: string[] = [];
+ for (const trackId of threadTracks.trackIds) {
+ const trackKey = globals.state.trackKeyByTrackId[trackId];
+ if (trackKey === undefined) {
+ return `Could not locate track ${trackId} for thread ${
+ threadTracks.thread} in the global state`;
+ }
+ trackKeys.push(trackKey);
+ }
+
+ if (trackKeys.length == 0) {
+ return `No valid tracks for thread ${threadTracks.thread}.`;
+ }
+
+ // Fixed length of a container to ensure that the icon does not overlap with
+ // the text due to table formatting.
+ return m(
+ `div[style='width:250px']`,
+ m(Anchor,
+ {
+ icon: Icons.UpdateSelection,
+ onclick: () => {
+ verticalScrollToTrack(trackKeys[0], true);
+ if (exists(ts) && exists(dur)) {
+ focusHorizontalRange(ts, Time.fromRaw(ts + dur), 0.3);
+ globals.frontendLocalState.selectArea(
+ ts, Time.fromRaw(ts + dur), trackKeys);
+
+ globals.dispatch(Actions.selectArea({
+ area: {
+ start: ts,
+ end: Time.fromRaw(ts + dur),
+ tracks: trackKeys,
+ },
+ }));
+ }
+ },
+ },
+ threadTracks.thread));
+}
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_jank_cause_map.ts b/ui/src/tracks/chrome_scroll_jank/scroll_jank_cause_map.ts
new file mode 100644
index 0000000..c4842ef
--- /dev/null
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_jank_cause_map.ts
@@ -0,0 +1,149 @@
+// 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 {exists} from '../../base/utils';
+import {Engine} from '../../trace_processor/engine';
+import {STR} from '../../trace_processor/query_result';
+
+export enum CauseProcess {
+ UNKNOWN,
+ BROWSER = 'Browser',
+ RENDERER = 'Renderer',
+ GPU = 'GPU',
+}
+
+export enum CauseThread {
+ UNKNOWN,
+ BROWSER_MAIN = 'CrBrowserMain',
+ RENDERER_MAIN = 'CrRendererMain',
+ COMPOSITOR = 'Compositor',
+ CHROME_CHILD_IO_THREAD = 'Chrome_ChildIOThread',
+ VIZ_COMPOSITOR = 'VizCompositorThread',
+ SURFACE_FLINGER = 'surfaceflinger'
+}
+
+export interface ScrollJankCause {
+ description: string;
+ process: CauseProcess;
+ thread: CauseThread;
+}
+
+export interface EventLatencyStageDetails {
+ description: string;
+ jankCauses: ScrollJankCause[];
+}
+
+export interface ScrollJankCauseMapInternal {
+ // Key corresponds with the EventLatency stage.
+ [key: string]: EventLatencyStageDetails;
+}
+
+function getScrollJankProcess(process: string): CauseProcess {
+ switch (process) {
+ case CauseProcess.BROWSER:
+ return CauseProcess.BROWSER;
+ case CauseProcess.RENDERER:
+ return CauseProcess.RENDERER;
+ case CauseProcess.GPU:
+ return CauseProcess.GPU;
+ default:
+ return CauseProcess.UNKNOWN;
+ }
+}
+
+function getScrollJankThread(thread: string): CauseThread {
+ switch (thread) {
+ case CauseThread.BROWSER_MAIN:
+ return CauseThread.BROWSER_MAIN;
+ case CauseThread.RENDERER_MAIN:
+ return CauseThread.RENDERER_MAIN;
+ case CauseThread.CHROME_CHILD_IO_THREAD:
+ return CauseThread.CHROME_CHILD_IO_THREAD;
+ case CauseThread.COMPOSITOR:
+ return CauseThread.COMPOSITOR;
+ case CauseThread.VIZ_COMPOSITOR:
+ return CauseThread.VIZ_COMPOSITOR;
+ case CauseThread.SURFACE_FLINGER:
+ return CauseThread.SURFACE_FLINGER;
+ default:
+ return CauseThread.UNKNOWN;
+ }
+}
+
+export class ScrollJankCauseMap {
+ private static instance: ScrollJankCauseMap;
+ private causes: ScrollJankCauseMapInternal;
+
+ private constructor() {
+ this.causes = {};
+ }
+
+ private async initializeCauseMap(engine: Engine) {
+ const queryResult = await engine.query(`
+ INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_cause_map;
+
+ SELECT
+ IFNULL(name, '') AS name,
+ IFNULL(description, '') AS description,
+ IFNULL(cause_process, '') AS causeProcess,
+ IFNULL(cause_thread, '') AS causeThread,
+ IFNULL(cause_description, '') AS causeDescription
+ FROM chrome_scroll_jank_causes_with_event_latencies;
+ `);
+
+ const iter = queryResult.iter({
+ name: STR,
+ description: STR,
+ causeProcess: STR,
+ causeThread: STR,
+ causeDescription: STR,
+ });
+
+ for (; iter.valid(); iter.next()) {
+ const eventLatencyStage = iter.name;
+ if (!(eventLatencyStage in this.causes)) {
+ this.causes[eventLatencyStage] = {
+ description: iter.description,
+ jankCauses: [] as ScrollJankCause[],
+ };
+ }
+
+ const causeProcess = getScrollJankProcess(iter.causeProcess);
+ const causeThread = getScrollJankThread(iter.causeThread);
+
+ this.causes[eventLatencyStage].jankCauses.push({
+ description: iter.causeDescription,
+ process: causeProcess,
+ thread: causeThread,
+ });
+ }
+ }
+
+ // Must be called before this item is accessed, as the object is populated
+ // from SQL data.
+ public static async initialize(engine: Engine) {
+ if (!exists(ScrollJankCauseMap.instance)) {
+ ScrollJankCauseMap.instance = new ScrollJankCauseMap();
+ await ScrollJankCauseMap.instance.initializeCauseMap(engine);
+ }
+ }
+
+ public static getEventLatencyDetails(eventLatency: string):
+ EventLatencyStageDetails|undefined {
+ if (eventLatency in ScrollJankCauseMap.instance.causes) {
+ return ScrollJankCauseMap.instance.causes[eventLatency];
+ }
+ return undefined;
+ }
+}
diff --git a/ui/src/tracks/debug/details_tab.ts b/ui/src/tracks/debug/details_tab.ts
index 3523f4f..5616a6b 100644
--- a/ui/src/tracks/debug/details_tab.ts
+++ b/ui/src/tracks/debug/details_tab.ts
@@ -24,6 +24,7 @@
import {
GenericSliceDetailsTabConfig,
} from '../../frontend/generic_slice_details_tab';
+import {hasArgs, renderArguments} from '../../frontend/slice_args';
import {
getSlice,
SliceDetails,
@@ -162,11 +163,24 @@
left: sliceRef(this.slice, 'Slice'),
right: '',
},
- renderTreeContents({
- 'Name': this.slice.name,
- 'Thread': getThreadName(this.slice.thread),
- 'Process': getProcessName(this.slice.process),
- }));
+ m(TreeNode, {
+ left: 'Name',
+ right: this.slice.name,
+ }),
+ m(TreeNode, {
+ left: 'Thread',
+ right: getThreadName(this.slice.thread),
+ }),
+ m(TreeNode, {
+ left: 'Process',
+ right: getProcessName(this.slice.process),
+ }),
+ hasArgs(this.slice) &&
+ m(TreeNode,
+ {
+ left: 'Args',
+ },
+ renderArguments(this.engine, this.slice)));
}