Merge "docs: note that iid 0 is not a valid iid" into main
diff --git a/include/perfetto/public/protos/common/builtin_clock.pzc.h b/include/perfetto/public/protos/common/builtin_clock.pzc.h
index 51218e4..a7fa7e3 100644
--- a/include/perfetto/public/protos/common/builtin_clock.pzc.h
+++ b/include/perfetto/public/protos/common/builtin_clock.pzc.h
@@ -33,6 +33,7 @@
PERFETTO_PB_ENUM_ENTRY(perfetto_protos_BUILTIN_CLOCK_MONOTONIC_COARSE) = 4,
PERFETTO_PB_ENUM_ENTRY(perfetto_protos_BUILTIN_CLOCK_MONOTONIC_RAW) = 5,
PERFETTO_PB_ENUM_ENTRY(perfetto_protos_BUILTIN_CLOCK_BOOTTIME) = 6,
+ PERFETTO_PB_ENUM_ENTRY(perfetto_protos_BUILTIN_CLOCK_TSC) = 9,
PERFETTO_PB_ENUM_ENTRY(perfetto_protos_BUILTIN_CLOCK_MAX_ID) = 63,
};
diff --git a/include/perfetto/public/protos/config/data_source_config.pzc.h b/include/perfetto/public/protos/config/data_source_config.pzc.h
index ed7632b..5a78ca5 100644
--- a/include/perfetto/public/protos/config/data_source_config.pzc.h
+++ b/include/perfetto/public/protos/config/data_source_config.pzc.h
@@ -43,6 +43,7 @@
PERFETTO_PB_MSG_DECL(perfetto_protos_NetworkPacketTraceConfig);
PERFETTO_PB_MSG_DECL(perfetto_protos_PackagesListConfig);
PERFETTO_PB_MSG_DECL(perfetto_protos_PerfEventConfig);
+PERFETTO_PB_MSG_DECL(perfetto_protos_PixelModemConfig);
PERFETTO_PB_MSG_DECL(perfetto_protos_ProcessStatsConfig);
PERFETTO_PB_MSG_DECL(perfetto_protos_ProtoLogConfig);
PERFETTO_PB_MSG_DECL(perfetto_protos_StatsdTracingConfig);
@@ -244,6 +245,11 @@
android_input_event_config,
128);
PERFETTO_PB_FIELD(perfetto_protos_DataSourceConfig,
+ MSG,
+ perfetto_protos_PixelModemConfig,
+ pixel_modem_config,
+ 129);
+PERFETTO_PB_FIELD(perfetto_protos_DataSourceConfig,
STRING,
const char*,
legacy_config,
diff --git a/include/perfetto/public/protos/config/trace_config.pzc.h b/include/perfetto/public/protos/config/trace_config.pzc.h
index 76492e3..cea2610 100644
--- a/include/perfetto/public/protos/config/trace_config.pzc.h
+++ b/include/perfetto/public/protos/config/trace_config.pzc.h
@@ -36,6 +36,7 @@
PERFETTO_PB_MSG_DECL(perfetto_protos_TraceConfig_IncidentReportConfig);
PERFETTO_PB_MSG_DECL(perfetto_protos_TraceConfig_IncrementalStateConfig);
PERFETTO_PB_MSG_DECL(perfetto_protos_TraceConfig_ProducerConfig);
+PERFETTO_PB_MSG_DECL(perfetto_protos_TraceConfig_SessionSemaphore);
PERFETTO_PB_MSG_DECL(perfetto_protos_TraceConfig_StatsdMetadata);
PERFETTO_PB_MSG_DECL(perfetto_protos_TraceConfig_TraceFilter);
PERFETTO_PB_MSG_DECL(perfetto_protos_TraceConfig_TraceFilter_StringFilterChain);
@@ -275,6 +276,23 @@
perfetto_protos_TraceConfig_CmdTraceStartDelay,
cmd_trace_start_delay,
35);
+PERFETTO_PB_FIELD(perfetto_protos_TraceConfig,
+ MSG,
+ perfetto_protos_TraceConfig_SessionSemaphore,
+ session_semaphores,
+ 39);
+
+PERFETTO_PB_MSG(perfetto_protos_TraceConfig_SessionSemaphore);
+PERFETTO_PB_FIELD(perfetto_protos_TraceConfig_SessionSemaphore,
+ STRING,
+ const char*,
+ name,
+ 1);
+PERFETTO_PB_FIELD(perfetto_protos_TraceConfig_SessionSemaphore,
+ VARINT,
+ uint64_t,
+ max_other_session_count,
+ 2);
PERFETTO_PB_MSG(perfetto_protos_TraceConfig_CmdTraceStartDelay);
PERFETTO_PB_FIELD(perfetto_protos_TraceConfig_CmdTraceStartDelay,
diff --git a/include/perfetto/public/protos/trace/clock_snapshot.pzc.h b/include/perfetto/public/protos/trace/clock_snapshot.pzc.h
new file mode 100644
index 0000000..cc00030
--- /dev/null
+++ b/include/perfetto/public/protos/trace/clock_snapshot.pzc.h
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+// Autogenerated by the ProtoZero C compiler plugin.
+// Invoked by tools/gen_c_protos
+// DO NOT EDIT.
+#ifndef INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_CLOCK_SNAPSHOT_PZC_H_
+#define INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_CLOCK_SNAPSHOT_PZC_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "perfetto/public/pb_macros.h"
+#include "perfetto/public/protos/common/builtin_clock.pzc.h"
+
+PERFETTO_PB_MSG_DECL(perfetto_protos_ClockSnapshot_Clock);
+
+PERFETTO_PB_ENUM_IN_MSG(perfetto_protos_ClockSnapshot_Clock, BuiltinClocks){
+ PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_ClockSnapshot_Clock,
+ UNKNOWN) = 0,
+ PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_ClockSnapshot_Clock,
+ REALTIME) = 1,
+ PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_ClockSnapshot_Clock,
+ REALTIME_COARSE) = 2,
+ PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_ClockSnapshot_Clock,
+ MONOTONIC) = 3,
+ PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_ClockSnapshot_Clock,
+ MONOTONIC_COARSE) = 4,
+ PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_ClockSnapshot_Clock,
+ MONOTONIC_RAW) = 5,
+ PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_ClockSnapshot_Clock,
+ BOOTTIME) = 6,
+ PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_ClockSnapshot_Clock,
+ BUILTIN_CLOCK_MAX_ID) = 63,
+};
+
+PERFETTO_PB_MSG(perfetto_protos_ClockSnapshot);
+PERFETTO_PB_FIELD(perfetto_protos_ClockSnapshot,
+ MSG,
+ perfetto_protos_ClockSnapshot_Clock,
+ clocks,
+ 1);
+PERFETTO_PB_FIELD(perfetto_protos_ClockSnapshot,
+ VARINT,
+ enum perfetto_protos_BuiltinClock,
+ primary_trace_clock,
+ 2);
+
+PERFETTO_PB_MSG(perfetto_protos_ClockSnapshot_Clock);
+PERFETTO_PB_FIELD(perfetto_protos_ClockSnapshot_Clock,
+ VARINT,
+ uint32_t,
+ clock_id,
+ 1);
+PERFETTO_PB_FIELD(perfetto_protos_ClockSnapshot_Clock,
+ VARINT,
+ uint64_t,
+ timestamp,
+ 2);
+PERFETTO_PB_FIELD(perfetto_protos_ClockSnapshot_Clock,
+ VARINT,
+ bool,
+ is_incremental,
+ 3);
+PERFETTO_PB_FIELD(perfetto_protos_ClockSnapshot_Clock,
+ VARINT,
+ uint64_t,
+ unit_multiplier_ns,
+ 4);
+
+#endif // INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_CLOCK_SNAPSHOT_PZC_H_
diff --git a/include/perfetto/public/protos/trace/interned_data/interned_data.pzc.h b/include/perfetto/public/protos/trace/interned_data/interned_data.pzc.h
index 71e26fa..d2c5a1b 100644
--- a/include/perfetto/public/protos/trace/interned_data/interned_data.pzc.h
+++ b/include/perfetto/public/protos/trace/interned_data/interned_data.pzc.h
@@ -193,5 +193,25 @@
perfetto_protos_InternedString,
protolog_stacktrace,
37);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+ MSG,
+ perfetto_protos_InternedString,
+ viewcapture_package_name,
+ 38);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+ MSG,
+ perfetto_protos_InternedString,
+ viewcapture_window_name,
+ 39);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+ MSG,
+ perfetto_protos_InternedString,
+ viewcapture_view_id,
+ 40);
+PERFETTO_PB_FIELD(perfetto_protos_InternedData,
+ MSG,
+ perfetto_protos_InternedString,
+ viewcapture_class_name,
+ 41);
#endif // INCLUDE_PERFETTO_PUBLIC_PROTOS_TRACE_INTERNED_DATA_INTERNED_DATA_PZC_H_
diff --git a/include/perfetto/public/protos/trace/trace_packet.pzc.h b/include/perfetto/public/protos/trace/trace_packet.pzc.h
index d83ab18..0151821 100644
--- a/include/perfetto/public/protos/trace/trace_packet.pzc.h
+++ b/include/perfetto/public/protos/trace/trace_packet.pzc.h
@@ -29,13 +29,13 @@
PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidCameraSessionStats);
PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidEnergyEstimationBreakdown);
PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidGameInterventionList);
-PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidInputEvent);
PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidLogPacket);
PERFETTO_PB_MSG_DECL(perfetto_protos_AndroidSystemProperty);
PERFETTO_PB_MSG_DECL(perfetto_protos_BatteryCounters);
PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeBenchmarkMetadata);
PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeEventBundle);
PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeMetadataPacket);
+PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeTrigger);
PERFETTO_PB_MSG_DECL(perfetto_protos_ClockSnapshot);
PERFETTO_PB_MSG_DECL(perfetto_protos_CpuInfo);
PERFETTO_PB_MSG_DECL(perfetto_protos_DeobfuscationMapping);
@@ -62,6 +62,8 @@
PERFETTO_PB_MSG_DECL(perfetto_protos_PackagesList);
PERFETTO_PB_MSG_DECL(perfetto_protos_PerfSample);
PERFETTO_PB_MSG_DECL(perfetto_protos_PerfettoMetatrace);
+PERFETTO_PB_MSG_DECL(perfetto_protos_PixelModemEvents);
+PERFETTO_PB_MSG_DECL(perfetto_protos_PixelModemTokenDatabase);
PERFETTO_PB_MSG_DECL(perfetto_protos_PowerRails);
PERFETTO_PB_MSG_DECL(perfetto_protos_ProcessDescriptor);
PERFETTO_PB_MSG_DECL(perfetto_protos_ProcessStats);
@@ -101,6 +103,7 @@
PERFETTO_PB_MSG_DECL(perfetto_protos_V8WasmCode);
PERFETTO_PB_MSG_DECL(perfetto_protos_VulkanApiEvent);
PERFETTO_PB_MSG_DECL(perfetto_protos_VulkanMemoryEvent);
+PERFETTO_PB_MSG_DECL(perfetto_protos_WinscopeExtensions);
PERFETTO_PB_ENUM_IN_MSG(perfetto_protos_TracePacket, SequenceFlags){
PERFETTO_PB_ENUM_IN_MSG_ENTRY(perfetto_protos_TracePacket,
@@ -215,6 +218,11 @@
46);
PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
MSG,
+ perfetto_protos_ChromeTrigger,
+ chrome_trigger,
+ 109);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+ MSG,
perfetto_protos_PackagesList,
packages_list,
47);
@@ -455,6 +463,11 @@
105);
PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
MSG,
+ perfetto_protos_WinscopeExtensions,
+ winscope_extensions,
+ 112);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+ MSG,
perfetto_protos_EtwTraceEventBundle,
etw_events,
95);
@@ -485,16 +498,21 @@
103);
PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
MSG,
- perfetto_protos_AndroidInputEvent,
- android_input_event,
- 106);
-PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
- MSG,
perfetto_protos_RemoteClockSync,
remote_clock_sync,
107);
PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
MSG,
+ perfetto_protos_PixelModemEvents,
+ pixel_modem_events,
+ 110);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+ MSG,
+ perfetto_protos_PixelModemTokenDatabase,
+ pixel_modem_token_database,
+ 111);
+PERFETTO_PB_FIELD(perfetto_protos_TracePacket,
+ MSG,
perfetto_protos_TestEvent,
for_testing,
900);
diff --git a/include/perfetto/public/protos/trace/track_event/track_descriptor.pzc.h b/include/perfetto/public/protos/trace/track_event/track_descriptor.pzc.h
index ee3d978..7f0a5ad 100644
--- a/include/perfetto/public/protos/trace/track_event/track_descriptor.pzc.h
+++ b/include/perfetto/public/protos/trace/track_event/track_descriptor.pzc.h
@@ -44,6 +44,11 @@
name,
2);
PERFETTO_PB_FIELD(perfetto_protos_TrackDescriptor,
+ STRING,
+ const char*,
+ static_name,
+ 10);
+PERFETTO_PB_FIELD(perfetto_protos_TrackDescriptor,
MSG,
perfetto_protos_ProcessDescriptor,
process,
diff --git a/include/perfetto/public/protos/trace/track_event/track_event.pzc.h b/include/perfetto/public/protos/trace/track_event/track_event.pzc.h
index 2ee81f1..e9e3247 100644
--- a/include/perfetto/public/protos/trace/track_event/track_event.pzc.h
+++ b/include/perfetto/public/protos/trace/track_event/track_event.pzc.h
@@ -41,6 +41,7 @@
PERFETTO_PB_MSG_DECL(perfetto_protos_ChromeWindowHandleEventInfo);
PERFETTO_PB_MSG_DECL(perfetto_protos_DebugAnnotation);
PERFETTO_PB_MSG_DECL(perfetto_protos_LogMessage);
+PERFETTO_PB_MSG_DECL(perfetto_protos_PixelModemEventInsight);
PERFETTO_PB_MSG_DECL(perfetto_protos_Screenshot);
PERFETTO_PB_MSG_DECL(perfetto_protos_SourceLocation);
PERFETTO_PB_MSG_DECL(perfetto_protos_TaskExecution);
@@ -252,6 +253,11 @@
50);
PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
MSG,
+ perfetto_protos_PixelModemEventInsight,
+ pixel_modem_event_insight,
+ 51);
+PERFETTO_PB_FIELD(perfetto_protos_TrackEvent,
+ MSG,
perfetto_protos_SourceLocation,
source_location,
33);
diff --git a/python/tools/check_imports.py b/python/tools/check_imports.py
index a114982..7f732a8 100755
--- a/python/tools/check_imports.py
+++ b/python/tools/check_imports.py
@@ -227,6 +227,21 @@
r'/core/.*',
'instead plugins should depend on the API exposed at ui/src/public.',
),
+ NoDirectDep(
+ r"/frontend/.*",
+ r"/core_plugins/.*",
+ "core code should not depend on plugins.",
+ ),
+ NoDirectDep(
+ r"/core/.*",
+ r"/core_plugins/.*",
+ "core code should not depend on plugins.",
+ ),
+ NoDirectDep(
+ r"/base/.*",
+ r"/core_plugins/.*",
+ "core code should not depend on plugins.",
+ ),
#NoDirectDep(
# r'/tracks/.*',
# r'/core/.*',
diff --git a/src/traceconv/trace_to_firefox.cc b/src/traceconv/trace_to_firefox.cc
index 251400d..976cf75 100644
--- a/src/traceconv/trace_to_firefox.cc
+++ b/src/traceconv/trace_to_firefox.cc
@@ -33,8 +33,8 @@
void ExportFirefoxProfile(trace_processor::TraceProcessor& tp,
std::ostream* output) {
auto it = tp.ExecuteQuery(R"(
- INCLUDE PERFETTO MODULE export.firefox;
- SELECT CAST(export_firefox_profile() AS BLOB);
+ INCLUDE PERFETTO MODULE export.to_firefox_profile;
+ SELECT CAST(export_to_firefox_profile() AS BLOB);
)");
PERFETTO_CHECK(it.Next());
diff --git a/tools/gen_c_protos b/tools/gen_c_protos
index 370e59a..cefc5e3 100755
--- a/tools/gen_c_protos
+++ b/tools/gen_c_protos
@@ -30,6 +30,7 @@
'protos/perfetto/config/data_source_config.proto',
'protos/perfetto/config/trace_config.proto',
'protos/perfetto/config/track_event/track_event_config.proto',
+ 'protos/perfetto/trace/clock_snapshot.proto',
'protos/perfetto/trace/interned_data/interned_data.proto',
'protos/perfetto/trace/test_event.proto',
'protos/perfetto/trace/trace.proto',
diff --git a/ui/PRESUBMIT.py b/ui/PRESUBMIT.py
index 5eeab58..d4a90cc 100644
--- a/ui/PRESUBMIT.py
+++ b/ui/PRESUBMIT.py
@@ -109,7 +109,10 @@
cmd = [prettier_path, '--check'] + paths
if subprocess.call(cmd):
s = ' '.join(cmd)
- return [output_api.PresubmitError(f"prettier errors. Run: $ {s}")]
+ return [
+ output_api.PresubmitError(f"""Prettier errors. To fix, run:
+{prettier_path} -w {'_'.join(paths)}""")
+ ]
return []
diff --git a/ui/src/base/disposable.ts b/ui/src/base/disposable.ts
index 15b0346..f251c05 100644
--- a/ui/src/base/disposable.ts
+++ b/ui/src/base/disposable.ts
@@ -18,60 +18,29 @@
dispose(): void;
}
-// Perform some operation using a disposable object guaranteeing it is disposed
-// of after the operation completes.
-// This can be replaced by the native "using" when Typescript 5.2 lands.
-// See: https://www.totaltypescript.com/typescript-5-2-new-keyword-using
-// Usage:
-// using(createDisposable(), (x) => {doSomethingWith(x)});
-export function using<T extends Disposable>(x: T, func?: (x: T) => void) {
- try {
- func && func(x);
- } finally {
- x.dispose();
- }
-}
-
-export class DisposableCallback implements Disposable {
- private callback?: () => void;
-
- constructor(callback: () => void) {
- this.callback = callback;
- }
-
- static from(callback: () => void): Disposable {
- return new DisposableCallback(callback);
- }
-
- dispose() {
- if (this.callback) {
- this.callback();
- this.callback = undefined;
- }
- }
-}
-
-export class NullDisposable implements Disposable {
- dispose() {}
+export interface AsyncDisposable {
+ disposeAsync(): Promise<void>;
}
// A collection of Disposables.
// Disposables can be added one by one, (e.g. during the lifecycle of a
// component) then can all be disposed at once (e.g. when the component
// is destroyed). Resources are disposed LIFO.
-export class Trash implements Disposable {
+export class DisposableStack implements Disposable {
private resources: Disposable[];
constructor() {
this.resources = [];
}
- add(d: Disposable) {
+ use(d: Disposable) {
this.resources.push(d);
}
- addCallback(callback: () => void) {
- this.add(DisposableCallback.from(callback));
+ defer(onDispose: () => void) {
+ this.use({
+ dispose: onDispose,
+ });
}
dispose() {
@@ -84,3 +53,27 @@
}
}
}
+
+export class AsyncDisposableStack implements AsyncDisposable {
+ private resources: AsyncDisposable[] = [];
+
+ use(d: AsyncDisposable) {
+ this.resources.push(d);
+ }
+
+ defer(onDispose: () => Promise<void>) {
+ this.use({
+ disposeAsync: onDispose,
+ });
+ }
+
+ async disposeAsync(): Promise<void> {
+ while (true) {
+ const d = this.resources.pop();
+ if (d === undefined) {
+ break;
+ }
+ await d.disposeAsync();
+ }
+ }
+}
diff --git a/ui/src/base/disposable_unittest.ts b/ui/src/base/disposable_unittest.ts
index 7774ad8..7a3dd84 100644
--- a/ui/src/base/disposable_unittest.ts
+++ b/ui/src/base/disposable_unittest.ts
@@ -12,15 +12,36 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {DisposableCallback, Trash} from './disposable';
+import {AsyncDisposableStack, DisposableStack} from './disposable';
-test('trash', () => {
+test('DisposableStack', () => {
const order: number[] = [];
- const trash = new Trash();
- trash.add(DisposableCallback.from(() => order.push(3)));
- trash.add(DisposableCallback.from(() => order.push(2)));
- trash.add(DisposableCallback.from(() => order.push(1)));
+ const trash = new DisposableStack();
+ trash.use({dispose: () => order.push(3)});
+ trash.use({dispose: () => order.push(2)});
+ trash.defer(() => order.push(1));
expect(order).toEqual([]);
trash.dispose();
expect(order).toEqual([1, 2, 3]);
});
+
+test('AsyncDisposableStack', async () => {
+ const order: number[] = [];
+ const trash = new AsyncDisposableStack();
+ trash.use({
+ disposeAsync: async () => {
+ order.push(3);
+ },
+ });
+ trash.use({
+ disposeAsync: async () => {
+ order.push(2);
+ },
+ });
+ trash.defer(async () => {
+ order.push(1);
+ });
+ expect(order).toEqual([]);
+ await trash.disposeAsync();
+ expect(order).toEqual([1, 2, 3]);
+});
diff --git a/ui/src/base/hotkeys.ts b/ui/src/base/hotkeys.ts
index f224aa4..80672be 100644
--- a/ui/src/base/hotkeys.ts
+++ b/ui/src/base/hotkeys.ts
@@ -97,8 +97,8 @@
| 'Ctrl+'
| 'Alt+'
| 'Mod+Shift+'
- | 'Mod+Alt'
- | 'Mod+Shift+Alt'
+ | 'Mod+Alt+'
+ | 'Mod+Shift+Alt+'
| 'Ctrl+Shift+'
| 'Ctrl+Alt'
| 'Ctrl+Shift+Alt';
diff --git a/ui/src/base/store_unittest.ts b/ui/src/base/store_unittest.ts
index f90cde2..8500c17 100644
--- a/ui/src/base/store_unittest.ts
+++ b/ui/src/base/store_unittest.ts
@@ -14,7 +14,6 @@
import {Draft} from 'immer';
-import {using} from './disposable';
import {createStore} from './store';
interface Bar {
@@ -145,7 +144,7 @@
const callback = jest.fn();
// Subscribe then immediately unsubscribe
- using(store.subscribe(callback));
+ store.subscribe(callback).dispose();
// Make an arbitrary edit
store.edit((draft) => {
@@ -236,7 +235,7 @@
const callback = jest.fn();
// Subscribe then immediately unsubscribe
- using(subStore.subscribe(callback));
+ subStore.subscribe(callback).dispose();
// Make an arbitrary edit
subStore.edit((draft) => {
diff --git a/ui/src/base/utils.ts b/ui/src/base/utils.ts
index d3c5f4b..c6f3069 100644
--- a/ui/src/base/utils.ts
+++ b/ui/src/base/utils.ts
@@ -24,3 +24,6 @@
export type Result<T, E = {}> =
| {success: true; result: T}
| {success: false; error: E};
+
+// Generic "optional" type
+export type Optional<T> = T | undefined;
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 0083843..50a67fd 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -30,7 +30,6 @@
tableColumnEquals,
toggleEnabled,
} from '../frontend/pivot_table_types';
-import {PrimaryTrackSortKey} from '../public/index';
import {
computeIntervals,
@@ -53,6 +52,7 @@
OmniboxState,
PendingDeeplinkState,
PivotTableResult,
+ PrimaryTrackSortKey,
ProfileType,
RecordingTarget,
SCROLLING_TRACK_GROUP,
@@ -73,7 +73,6 @@
labels?: string[];
trackSortKey: TrackSortKey;
trackGroup?: string;
- params?: unknown;
closeable?: boolean;
}
@@ -215,7 +214,6 @@
trackGroup: track.trackGroup,
labels: track.labels,
uri: track.uri,
- params: track.params,
closeable: track.closeable,
};
if (track.trackGroup === SCROLLING_TRACK_GROUP) {
@@ -552,22 +550,6 @@
}
},
- selectCounter(
- state: StateDraft,
- args: {leftTs: time; rightTs: time; id: number; trackKey: string},
- ): void {
- state.selection = {
- kind: 'legacy',
- legacySelection: {
- kind: 'COUNTER',
- leftTs: args.leftTs,
- rightTs: args.rightTs,
- id: args.id,
- trackKey: args.trackKey,
- },
- };
- },
-
selectHeapProfile(
state: StateDraft,
args: {id: number; upid: number; ts: time; type: ProfileType},
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
index 468755e..af68b07 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -18,7 +18,6 @@
import {PrimaryTrackSortKey} from '../public';
import {HEAP_PROFILE_TRACK_KIND} from '../core_plugins/heap_profile';
import {PROCESS_SCHEDULING_TRACK_KIND} from '../core_plugins/process_summary/process_scheduling_track';
-import {THREAD_STATE_TRACK_KIND} from '../core_plugins/thread_state';
import {StateActions} from './actions';
import {createEmptyState} from './empty_state';
@@ -29,7 +28,10 @@
TraceUrlSource,
TrackSortKey,
} from './state';
-import {THREAD_SLICE_TRACK_KIND} from '../core_plugins/thread_slice/thread_slice_track';
+import {
+ THREAD_SLICE_TRACK_KIND,
+ THREAD_STATE_TRACK_KIND,
+} from '../core/track_kinds';
function fakeTrack(
state: State,
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index 648bf06..a0ab84e 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -14,7 +14,7 @@
import {v4 as uuidv4} from 'uuid';
-import {Disposable, Trash} from '../base/disposable';
+import {Disposable, DisposableStack} from '../base/disposable';
import {Registry} from '../base/registry';
import {Span, duration, time} from '../base/time';
import {TraceContext, globals} from '../frontend/globals';
@@ -52,7 +52,7 @@
// plugins.
// The PluginContext exists for the whole duration a plugin is active.
export class PluginContextImpl implements PluginContext, Disposable {
- private trash = new Trash();
+ private trash = new DisposableStack();
private alive = true;
readonly sidebar = {
@@ -80,7 +80,7 @@
if (!this.alive) return;
const disposable = globals.commandManager.registerCommand(cmd);
- this.trash.add(disposable);
+ this.trash.use(disposable);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -101,13 +101,13 @@
// The PluginContextTrace exists for the whole duration a plugin is active AND a
// trace is loaded.
class PluginContextTraceImpl implements PluginContextTrace, Disposable {
- private trash = new Trash();
+ private trash = new DisposableStack();
private alive = true;
readonly engine: Engine;
constructor(private ctx: PluginContext, engine: EngineBase) {
const engineProxy = engine.getProxy(ctx.pluginId);
- this.trash.add(engineProxy);
+ this.trash.use(engineProxy);
this.engine = engineProxy;
}
@@ -116,7 +116,7 @@
if (!this.alive) return;
const dispose = globals.commandManager.registerCommand(cmd);
- this.trash.add(dispose);
+ this.trash.use(dispose);
}
registerTrack(trackDesc: TrackDescriptor): void {
@@ -124,7 +124,7 @@
if (!this.alive) return;
const dispose = globals.trackManager.registerTrack(trackDesc);
- this.trash.add(dispose);
+ this.trash.use(dispose);
}
addDefaultTrack(track: TrackRef): void {
@@ -132,7 +132,7 @@
if (!this.alive) return;
const dispose = globals.trackManager.addPotentialTrack(track);
- this.trash.add(dispose);
+ this.trash.use(dispose);
}
registerStaticTrack(track: TrackDescriptor & TrackRef): void {
@@ -149,12 +149,12 @@
if (!this.alive) return;
const unregister = globals.tabManager.registerTab(desc);
- this.trash.add(unregister);
+ this.trash.use(unregister);
}
addDefaultTab(uri: string): void {
const remove = globals.tabManager.addDefaultTab(uri);
- this.trash.add(remove);
+ this.trash.use(remove);
}
registerDetailsPanel(detailsPanel: LegacyDetailsPanel): void {
@@ -162,7 +162,7 @@
const tabMan = globals.tabManager;
const unregister = tabMan.registerLegacyDetailsPanel(detailsPanel);
- this.trash.add(unregister);
+ this.trash.use(unregister);
}
get sidebar() {
@@ -189,14 +189,13 @@
readonly timeline = {
// Add a new track to the timeline, returning its key.
- addTrack(uri: string, displayName: string, params?: unknown): string {
+ addTrack(uri: string, displayName: string): string {
const trackKey = uuidv4();
globals.dispatch(
Actions.addTrack({
key: trackKey,
uri,
name: displayName,
- params,
trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
trackGroup: SCROLLING_TRACK_GROUP,
}),
@@ -315,7 +314,6 @@
return {
displayName: trackState.name,
uri: trackState.uri,
- params: trackState.params,
key: trackState.key,
groupName: group?.name,
isPinned: pinnedTracks.includes(trackState.key),
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 4a53a5d..23233c6 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -20,7 +20,6 @@
PivotTree,
TableColumn,
} from '../frontend/pivot_table_types';
-import {PrimaryTrackSortKey} from '../public/index';
import {
selectionToLegacySelection,
@@ -33,7 +32,6 @@
SelectionKind,
NoteSelection,
SliceSelection,
- CounterSelection,
HeapProfileSelection,
PerfSamplesSelection,
LegacySelection,
@@ -43,6 +41,36 @@
CpuProfileSampleSelection,
} from '../core/selection_manager';
+// Tracks within track groups (usually corresponding to processes) are sorted.
+// As we want to group all tracks related to a given thread together, we use
+// two keys:
+// - Primary key corresponds to a priority of a track block (all tracks related
+// to a given thread or a single track if it's not thread-associated).
+// - Secondary key corresponds to a priority of a given thread-associated track
+// within its thread track block.
+// Each track will have a sort key, which either a primary sort key
+// (for non-thread tracks) or a tid and secondary sort key (mapping of tid to
+// primary sort key is done independently).
+export enum PrimaryTrackSortKey {
+ DEBUG_TRACK,
+ NULL_TRACK,
+ PROCESS_SCHEDULING_TRACK,
+ PROCESS_SUMMARY_TRACK,
+ EXPECTED_FRAMES_SLICE_TRACK,
+ ACTUAL_FRAMES_SLICE_TRACK,
+ PERF_SAMPLES_PROFILE_TRACK,
+ HEAP_PROFILE_TRACK,
+ MAIN_THREAD,
+ RENDER_THREAD,
+ GPU_COMPLETION_THREAD,
+ CHROME_IO_THREAD,
+ CHROME_COMPOSITOR_THREAD,
+ ORDINARY_THREAD,
+ COUNTER_TRACK,
+ ASYNC_SLICE_TRACK,
+ ORDINARY_TRACK,
+}
+
/**
* A plain js object, holding objects of type |Class| keyed by string id.
* We use this instead of using |Map| object since it is simpler and faster to
@@ -152,7 +180,8 @@
// 58. Remove area map.
// 59. Deprecate old area selection type.
// 60. Deprecate old note selection type.
-export const STATE_VERSION = 60;
+// 61. Remove params/state from TrackState.
+export const STATE_VERSION = 61;
export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
@@ -237,8 +266,6 @@
labels?: string[];
trackSortKey: TrackSortKey;
trackGroup?: string;
- params?: unknown;
- state?: unknown;
closeable?: boolean;
}
diff --git a/ui/src/common/tab_registry.ts b/ui/src/common/tab_registry.ts
index fd9cc65..0df6284 100644
--- a/ui/src/common/tab_registry.ts
+++ b/ui/src/common/tab_registry.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {Disposable, DisposableCallback} from '../base/disposable';
+import {Disposable} from '../base/disposable';
import {DetailsPanel, LegacyDetailsPanel, TabDescriptor} from '../public';
export interface ResolvedTab {
@@ -41,30 +41,30 @@
registerTab(desc: TabDescriptor): Disposable {
this._registry.set(desc.uri, desc);
- return new DisposableCallback(() => {
- this._registry.delete(desc.uri);
- });
+ return {
+ dispose: () => this._registry.delete(desc.uri),
+ };
}
addDefaultTab(uri: string): Disposable {
this._defaultTabs.add(uri);
- return new DisposableCallback(() => {
- this._defaultTabs.delete(uri);
- });
+ return {
+ dispose: () => this._defaultTabs.delete(uri),
+ };
}
registerLegacyDetailsPanel(section: LegacyDetailsPanel): Disposable {
this._legacyDetailsPanelRegistry.add(section);
- return new DisposableCallback(() => {
- this._legacyDetailsPanelRegistry.delete(section);
- });
+ return {
+ dispose: () => this._legacyDetailsPanelRegistry.delete(section),
+ };
}
registerDetailsPanel(section: DetailsPanel): Disposable {
this._detailsPanelRegistry.add(section);
- return new DisposableCallback(() => {
- this._detailsPanelRegistry.delete(section);
- });
+ return {
+ dispose: () => this._detailsPanelRegistry.delete(section),
+ };
}
resolveTab(uri: string): TabDescriptor | undefined {
diff --git a/ui/src/common/track_cache.ts b/ui/src/common/track_cache.ts
index 5d05a13..08ec2ac 100644
--- a/ui/src/common/track_cache.ts
+++ b/ui/src/common/track_cache.ts
@@ -12,18 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {Disposable, DisposableCallback} from '../base/disposable';
+import {Disposable} from '../base/disposable';
import {exists} from '../base/utils';
import {Registry} from '../base/registry';
import {Store} from '../base/store';
import {PanelSize} from '../frontend/panel';
-import {
- Migrate,
- Track,
- TrackContext,
- TrackDescriptor,
- TrackRef,
-} from '../public';
+import {Track, TrackContext, TrackDescriptor, TrackRef} from '../public';
import {ObjectByKey, State, TrackState} from './state';
@@ -82,9 +76,9 @@
addPotentialTrack(track: TrackRef): Disposable {
this.defaultTracks.add(track);
- return new DisposableCallback(() => {
- this.defaultTracks.delete(track);
- });
+ return {
+ dispose: () => this.defaultTracks.delete(track),
+ };
}
findPotentialTracks(): TrackRef[] {
@@ -103,11 +97,7 @@
// Creates a new track using |uri| and |params| or retrieves a cached track if
// |key| exists in the cache.
- resolveTrack(
- key: string,
- trackDesc: TrackDescriptor,
- params?: unknown,
- ): TrackCacheEntry {
+ resolveTrack(key: string, trackDesc: TrackDescriptor): TrackCacheEntry {
// Search for a cached version of this track,
const cached = this.currentTracks.get(key);
@@ -126,11 +116,6 @@
// Cached track doesn't exist or is out of date, create a new one.
const trackContext: TrackContext = {
trackKey: key,
- mountStore: <T>(migrate: Migrate<T>) => {
- const path = ['tracks', key, 'state'];
- return this.store.createSubStore(path, migrate);
- },
- params,
};
const track = trackDesc.trackFactory(trackContext);
const entry = new TrackFSM(track, trackDesc, trackContext);
diff --git a/ui/src/controller/aggregation/cpu_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
index 6b9083e..d3fcd88 100644
--- a/ui/src/controller/aggregation/cpu_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
@@ -15,9 +15,9 @@
import {exists} from '../../base/utils';
import {ColumnDef} from '../../common/aggregation_data';
import {Area, Sorting} from '../../common/state';
+import {CPU_SLICE_TRACK_KIND} from '../../core/track_kinds';
import {globals} from '../../frontend/globals';
import {Engine} from '../../trace_processor/engine';
-import {CPU_SLICE_TRACK_KIND} from '../../core_plugins/cpu_slices';
import {AggregationController} from './aggregation_controller';
diff --git a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
index 03f29f2..578c874 100644
--- a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
@@ -17,7 +17,7 @@
import {Area, Sorting} from '../../common/state';
import {globals} from '../../frontend/globals';
import {Engine} from '../../trace_processor/engine';
-import {CPU_SLICE_TRACK_KIND} from '../../core_plugins/cpu_slices';
+import {CPU_SLICE_TRACK_KIND} from '../../core/track_kinds';
import {AggregationController} from './aggregation_controller';
diff --git a/ui/src/controller/aggregation/frame_aggregation_controller.ts b/ui/src/controller/aggregation/frame_aggregation_controller.ts
index 100e47b..d905ede 100644
--- a/ui/src/controller/aggregation/frame_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/frame_aggregation_controller.ts
@@ -14,9 +14,9 @@
import {ColumnDef} from '../../common/aggregation_data';
import {Area, Sorting} from '../../common/state';
+import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../../core/track_kinds';
import {globals} from '../../frontend/globals';
import {Engine} from '../../trace_processor/engine';
-import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../../core_plugins/frames';
import {AggregationController} from './aggregation_controller';
diff --git a/ui/src/controller/aggregation/slice_aggregation_controller.ts b/ui/src/controller/aggregation/slice_aggregation_controller.ts
index 89cdfb1..def6765 100644
--- a/ui/src/controller/aggregation/slice_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/slice_aggregation_controller.ts
@@ -16,10 +16,12 @@
import {Area, Sorting} from '../../common/state';
import {globals} from '../../frontend/globals';
import {Engine} from '../../trace_processor/engine';
-import {ASYNC_SLICE_TRACK_KIND} from '../../core_plugins/async_slices';
-import {THREAD_SLICE_TRACK_KIND} from '../../core_plugins/thread_slice/thread_slice_track';
import {AggregationController} from './aggregation_controller';
+import {
+ ASYNC_SLICE_TRACK_KIND,
+ THREAD_SLICE_TRACK_KIND,
+} from '../../core/track_kinds';
export function getSelectedTrackKeys(area: Area): number[] {
const selectedTrackKeys: number[] = [];
diff --git a/ui/src/controller/aggregation/thread_aggregation_controller.ts b/ui/src/controller/aggregation/thread_aggregation_controller.ts
index 98e5c77..64eedeb 100644
--- a/ui/src/controller/aggregation/thread_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/thread_aggregation_controller.ts
@@ -16,10 +16,10 @@
import {ColumnDef, ThreadStateExtra} from '../../common/aggregation_data';
import {Area, Sorting} from '../../common/state';
import {translateState} from '../../common/thread_state';
+import {THREAD_STATE_TRACK_KIND} from '../../core/track_kinds';
import {globals} from '../../frontend/globals';
import {Engine} from '../../trace_processor/engine';
import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
-import {THREAD_STATE_TRACK_KIND} from '../../core_plugins/thread_state';
import {AggregationController} from './aggregation_controller';
diff --git a/ui/src/controller/flow_events_controller.ts b/ui/src/controller/flow_events_controller.ts
index 0501014..8769c00 100644
--- a/ui/src/controller/flow_events_controller.ts
+++ b/ui/src/controller/flow_events_controller.ts
@@ -20,11 +20,13 @@
import {asSliceSqlId} from '../frontend/sql_types';
import {Engine} from '../trace_processor/engine';
import {LONG, NUM, STR_NULL} from '../trace_processor/query_result';
-import {THREAD_SLICE_TRACK_KIND} from '../core_plugins/thread_slice/thread_slice_track';
-import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../core_plugins/frames';
import {Controller} from './controller';
import {Monitor} from '../base/monitor';
+import {
+ ACTUAL_FRAMES_SLICE_TRACK_KIND,
+ THREAD_SLICE_TRACK_KIND,
+} from '../core/track_kinds';
export interface FlowEventsControllerArgs {
engine: Engine;
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
index bd6a3b1..21892e7 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -21,12 +21,12 @@
SearchSummary,
} from '../common/search_data';
import {OmniboxState} from '../common/state';
+import {CPU_SLICE_TRACK_KIND} from '../core/track_kinds';
import {globals} from '../frontend/globals';
import {publishSearch, publishSearchResult} from '../frontend/publish';
import {Engine} from '../trace_processor/engine';
import {LONG, NUM, STR} from '../trace_processor/query_result';
import {escapeSearchQuery} from '../trace_processor/query_utils';
-import {CPU_SLICE_TRACK_KIND} from '../core_plugins/cpu_slices';
import {Controller} from './controller';
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index 6a75fa6..b52b73f 100644
--- a/ui/src/controller/selection_controller.ts
+++ b/ui/src/controller/selection_controller.ts
@@ -20,14 +20,9 @@
ThreadSliceSelection,
getLegacySelection,
} from '../common/state';
+import {THREAD_SLICE_TRACK_KIND} from '../core/track_kinds';
+import {globals, SliceDetails, ThreadStateDetails} from '../frontend/globals';
import {
- CounterDetails,
- globals,
- SliceDetails,
- ThreadStateDetails,
-} from '../frontend/globals';
-import {
- publishCounterDetails,
publishSliceDetails,
publishThreadStateDetails,
} from '../frontend/publish';
@@ -41,7 +36,6 @@
STR_NULL,
timeFromSql,
} from '../trace_processor/query_result';
-import {THREAD_SLICE_TRACK_KIND} from '../core_plugins/thread_slice/thread_slice_track';
import {Controller} from './controller';
@@ -77,7 +71,6 @@
const selectWithId: SelectionKind[] = [
'SLICE',
- 'COUNTER',
'SCHED_SLICE',
'HEAP_PROFILE',
'THREAD_STATE',
@@ -97,21 +90,7 @@
if (selectedId === undefined) return;
- if (selection.kind === 'COUNTER') {
- this.counterDetails(
- selection.leftTs,
- selection.rightTs,
- selection.id,
- ).then((results) => {
- if (
- results !== undefined &&
- selection.kind === selectedKind &&
- selection.id === selectedId
- ) {
- publishCounterDetails(results);
- }
- });
- } else if (selection.kind === 'SCHED_SLICE') {
+ if (selection.kind === 'SCHED_SLICE') {
this.schedSliceDetails(selectedId as number);
} else if (selection.kind === 'THREAD_STATE') {
this.threadStateDetails(selection.id);
@@ -424,35 +403,6 @@
}
}
- async counterDetails(
- ts: time,
- rightTs: time,
- id: number,
- ): Promise<CounterDetails> {
- const counter = await this.args.engine.query(
- `SELECT value, track_id as trackId FROM counter WHERE id = ${id}`,
- );
- const row = counter.iter({
- value: NUM,
- trackId: NUM,
- });
- const value = row.value;
- const trackId = row.trackId;
- // Finding previous value. If there isn't previous one, it will return 0 for
- // ts and value.
- const previous = await this.args.engine.query(`SELECT
- MAX(ts),
- IFNULL(value, 0) as value
- FROM counter WHERE ts < ${ts} and track_id = ${trackId}`);
- const previousValue = previous.firstRow({value: NUM}).value;
- const endTs = rightTs !== -1n ? rightTs : globals.traceContext.end;
- const delta = value - previousValue;
- const duration = endTs - ts;
- const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
- const name = trackKey ? globals.state.tracks[trackKey].name : undefined;
- return {startTime: ts, value, delta, duration, name};
- }
-
async schedulingDetails(ts: time, utid: number) {
// Find the ts of the first wakeup before the current slice.
const wakeResult = await this.args.engine.query(`
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 065f690..dea50d7 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -95,7 +95,8 @@
TraceStream,
} from '../core/trace_stream';
import {decideTracks} from './track_decider';
-import {FlamegraphCache, profileType} from '../frontend/flamegraph_panel';
+import {profileType} from '../frontend/flamegraph_panel';
+import {FlamegraphCache} from '../core/flamegraph_cache';
type States = 'init' | 'loading_trace' | 'ready';
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index 36fb251..fb5114a 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -29,20 +29,20 @@
import {getTrackName} from '../public/utils';
import {Engine, EngineBase} from '../trace_processor/engine';
import {NUM, NUM_NULL, STR, STR_NULL} from '../trace_processor/query_result';
-import {ASYNC_SLICE_TRACK_KIND} from '../core_plugins/async_slices';
import {
ENABLE_SCROLL_JANK_PLUGIN_V2,
getScrollJankTracks,
} from '../core_plugins/chrome_scroll_jank';
import {decideTracks as scrollJankDecideTracks} from '../core_plugins/chrome_scroll_jank/chrome_tasks_scroll_jank_track';
import {COUNTER_TRACK_KIND} from '../core_plugins/counter';
+import {decideTracks as screenshotDecideTracks} from '../core_plugins/screenshots';
import {
ACTUAL_FRAMES_SLICE_TRACK_KIND,
+ ASYNC_SLICE_TRACK_KIND,
EXPECTED_FRAMES_SLICE_TRACK_KIND,
-} from '../core_plugins/frames';
-import {decideTracks as screenshotDecideTracks} from '../core_plugins/screenshots';
-import {THREAD_STATE_TRACK_KIND} from '../core_plugins/thread_state';
-import {THREAD_SLICE_TRACK_KIND} from '../core_plugins/thread_slice/thread_slice_track';
+ THREAD_SLICE_TRACK_KIND,
+ THREAD_STATE_TRACK_KIND,
+} from '../core/track_kinds';
const MEM_DMA_COUNTER_NAME = 'mem.dma_heap';
const MEM_DMA = 'mem.dma_buffer';
@@ -1400,7 +1400,6 @@
// 'sort keys' which the user can use to choose a sort order.
trackSortKey: info.sortKey ?? PrimaryTrackSortKey.ORDINARY_TRACK,
trackGroup: groupUuid,
- params: info.params,
});
}
}
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index 983f37e..56a6b19 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -45,7 +45,7 @@
'perfetto.CpuProfile',
'perfetto.CpuSlices',
'perfetto.CriticalUserInteraction',
- 'perfetto.DebugSlices',
+ 'perfetto.DebugTracks',
'perfetto.Flows',
'perfetto.Frames',
'perfetto.FtraceRaw',
@@ -56,7 +56,6 @@
'perfetto.Sched',
'perfetto.Screenshots',
'perfetto.ThreadState',
- 'perfetto.VisualisedArgs',
'org.kernel.LinuxKernelDevices',
'perfetto.TrackUtils',
'com.google.PixelMemory',
diff --git a/ui/src/core/flamegraph_cache.ts b/ui/src/core/flamegraph_cache.ts
new file mode 100644
index 0000000..d0f7dad
--- /dev/null
+++ b/ui/src/core/flamegraph_cache.ts
@@ -0,0 +1,52 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use size 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 {Engine} from '../trace_processor/engine';
+
+export class FlamegraphCache {
+ private cache: Map<string, string>;
+ private prefix: string;
+ private tableId: number;
+ private cacheSizeLimit: number;
+
+ constructor(prefix: string) {
+ this.cache = new Map<string, string>();
+ this.prefix = prefix;
+ this.tableId = 0;
+ this.cacheSizeLimit = 10;
+ }
+
+ async getTableName(engine: Engine, query: string): Promise<string> {
+ let tableName = this.cache.get(query);
+ if (tableName === undefined) {
+ // TODO(hjd): This should be LRU.
+ if (this.cache.size > this.cacheSizeLimit) {
+ for (const name of this.cache.values()) {
+ await engine.query(`drop table ${name}`);
+ }
+ this.cache.clear();
+ }
+ tableName = `${this.prefix}_${this.tableId++}`;
+ await engine.query(
+ `create temp table if not exists ${tableName} as ${query}`,
+ );
+ this.cache.set(query, tableName);
+ }
+ return tableName;
+ }
+
+ hasQuery(query: string): boolean {
+ return this.cache.get(query) !== undefined;
+ }
+}
diff --git a/ui/src/core/selection_manager.ts b/ui/src/core/selection_manager.ts
index b954380..5764f51 100644
--- a/ui/src/core/selection_manager.ts
+++ b/ui/src/core/selection_manager.ts
@@ -32,13 +32,6 @@
id: number;
}
-export interface CounterSelection {
- kind: 'COUNTER';
- leftTs: time;
- rightTs: time;
- id: number;
-}
-
export interface HeapProfileSelection {
kind: 'HEAP_PROFILE';
id: number;
@@ -92,7 +85,6 @@
export type LegacySelection = (
| SliceSelection
- | CounterSelection
| HeapProfileSelection
| CpuProfileSampleSelection
| ThreadSliceSelection
@@ -112,7 +104,7 @@
export interface SingleSelection {
kind: 'single';
trackKey: string;
- eventId: string;
+ eventId: number;
}
export interface AreaSelection {
@@ -225,7 +217,7 @@
setEvent(
trackKey: string,
- eventId: string,
+ eventId: number,
legacySelection?: LegacySelection,
) {
this.clear();
@@ -234,7 +226,7 @@
addEvent(
trackKey: string,
- eventId: string,
+ eventId: number,
legacySelection?: LegacySelection,
) {
this.addSelection({
diff --git a/ui/src/core/track_kinds.ts b/ui/src/core/track_kinds.ts
new file mode 100644
index 0000000..bd36d6c
--- /dev/null
+++ b/ui/src/core/track_kinds.ts
@@ -0,0 +1,24 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file contains a list of well known (to the core) track kinds.
+// This file exists purely to keep legacy systems in place without introducing a
+// ton of circular imports.
+export const CPU_SLICE_TRACK_KIND = 'CpuSliceTrack';
+export const THREAD_STATE_TRACK_KIND = 'ThreadStateTrack';
+export const THREAD_SLICE_TRACK_KIND = 'ThreadSliceTrack';
+export const EXPECTED_FRAMES_SLICE_TRACK_KIND = 'ExpectedFramesSliceTrack';
+export const ACTUAL_FRAMES_SLICE_TRACK_KIND = 'ActualFramesSliceTrack';
+export const ASYNC_SLICE_TRACK_KIND = 'AsyncSliceTrack';
+export const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack';
diff --git a/ui/src/core_plugins/annotation/index.ts b/ui/src/core_plugins/annotation/index.ts
index d2f1627..4b29cd9 100644
--- a/ui/src/core_plugins/annotation/index.ts
+++ b/ui/src/core_plugins/annotation/index.ts
@@ -13,13 +13,11 @@
// limitations under the License.
import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {
- ThreadSliceTrack,
- THREAD_SLICE_TRACK_KIND,
-} from '../thread_slice/thread_slice_track';
+import {ThreadSliceTrack} from '../../frontend/thread_slice_track';
import {NUM, NUM_NULL, STR} from '../../trace_processor/query_result';
import {COUNTER_TRACK_KIND} from '../counter';
import {TraceProcessorCounterTrack} from '../counter/trace_processor_counter_track';
+import {THREAD_SLICE_TRACK_KIND} from '../../public';
class AnnotationPlugin implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
diff --git a/ui/src/core_plugins/async_slices/index.ts b/ui/src/core_plugins/async_slices/index.ts
index b01cb38..530f3c7 100644
--- a/ui/src/core_plugins/async_slices/index.ts
+++ b/ui/src/core_plugins/async_slices/index.ts
@@ -12,14 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {ASYNC_SLICE_TRACK_KIND} from '../../public';
import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
import {getTrackName} from '../../public/utils';
import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
import {AsyncSliceTrack} from './async_slice_track';
-export const ASYNC_SLICE_TRACK_KIND = 'AsyncSliceTrack';
-
class AsyncSlicePlugin implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
await this.addGlobalAsyncTracks(ctx);
diff --git a/ui/src/core_plugins/counter/counter_details_panel.ts b/ui/src/core_plugins/counter/counter_details_panel.ts
new file mode 100644
index 0000000..0ee28bd
--- /dev/null
+++ b/ui/src/core_plugins/counter/counter_details_panel.ts
@@ -0,0 +1,165 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {AsyncLimiter} from '../../base/async_limiter';
+import {Time, duration, time} from '../../base/time';
+import {raf} from '../../core/raf_scheduler';
+import {
+ Engine,
+ LONG,
+ LONG_NULL,
+ NUM,
+ NUM_NULL,
+ TrackSelectionDetailsPanel,
+} from '../../public';
+import m from 'mithril';
+import {DetailsShell} from '../../widgets/details_shell';
+import {GridLayout} from '../../widgets/grid_layout';
+import {Section} from '../../widgets/section';
+import {Tree, TreeNode} from '../../widgets/tree';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {DurationWidget} from '../../frontend/widgets/duration';
+
+interface CounterDetails {
+ // The "left" timestamp of the counter sample T(N)
+ ts: time;
+
+ // The delta between this sample and the next one's timestamps T(N+1) - T(N)
+ duration: duration;
+
+ // The value of the counter sample F(N)
+ value: number;
+
+ // The delta between this sample's value and the previous one F(N) - F(N-1)
+ delta: number;
+}
+
+export class CounterDetailsPanel implements TrackSelectionDetailsPanel {
+ private readonly queryLimiter = new AsyncLimiter();
+ private readonly engine: Engine;
+ private readonly trackId: number;
+ private readonly rootTable: string;
+ private readonly trackName: string;
+ private id?: number;
+ private counterDetails?: CounterDetails;
+
+ constructor(
+ engine: Engine,
+ trackId: number,
+ trackName: string,
+ rootTable = 'counter',
+ ) {
+ this.engine = engine;
+ this.trackId = trackId;
+ this.trackName = trackName;
+ this.rootTable = rootTable;
+ }
+
+ render(id: number): m.Children {
+ if (id !== this.id) {
+ this.id = id;
+ this.queryLimiter.schedule(async () => {
+ this.counterDetails = await loadCounterDetails(
+ this.engine,
+ this.trackId,
+ id,
+ this.rootTable,
+ );
+ raf.scheduleFullRedraw();
+ });
+ }
+
+ return this.renderView();
+ }
+
+ private renderView() {
+ const counterInfo = this.counterDetails;
+ if (counterInfo) {
+ return m(
+ DetailsShell,
+ {title: 'Counter', description: `${this.trackName}`},
+ m(
+ GridLayout,
+ m(
+ Section,
+ {title: 'Properties'},
+ m(
+ Tree,
+ m(TreeNode, {left: 'Name', right: `${this.trackName}`}),
+ m(TreeNode, {
+ left: 'Start time',
+ right: m(Timestamp, {ts: counterInfo.ts}),
+ }),
+ m(TreeNode, {
+ left: 'Value',
+ right: `${counterInfo.value.toLocaleString()}`,
+ }),
+ m(TreeNode, {
+ left: 'Delta',
+ right: `${counterInfo.delta.toLocaleString()}`,
+ }),
+ m(TreeNode, {
+ left: 'Duration',
+ right: m(DurationWidget, {dur: counterInfo.duration}),
+ }),
+ ),
+ ),
+ ),
+ );
+ } else {
+ return m(DetailsShell, {title: 'Counter', description: 'Loading...'});
+ }
+ }
+
+ isLoading(): boolean {
+ return this.counterDetails === undefined;
+ }
+}
+
+async function loadCounterDetails(
+ engine: Engine,
+ trackId: number,
+ id: number,
+ rootTable: string,
+): Promise<CounterDetails> {
+ const query = `
+ WITH CTE AS (
+ SELECT
+ id,
+ ts as leftTs,
+ value,
+ LAG(value) OVER (ORDER BY ts) AS prevValue,
+ LEAD(ts) OVER (ORDER BY ts) AS rightTs
+ FROM ${rootTable}
+ WHERE track_id = ${trackId}
+ )
+ SELECT * FROM CTE WHERE id = ${id}
+ `;
+
+ const counter = await engine.query(query);
+ const row = counter.iter({
+ value: NUM,
+ prevValue: NUM_NULL,
+ leftTs: LONG,
+ rightTs: LONG_NULL,
+ });
+ const value = row.value;
+ const leftTs = Time.fromRaw(row.leftTs);
+ const rightTs = row.rightTs !== null ? Time.fromRaw(row.rightTs) : leftTs;
+ const prevValue = row.prevValue !== null ? row.prevValue : value;
+
+ const delta = value - prevValue;
+ const duration = rightTs - leftTs;
+ return {ts: leftTs, value, delta, duration};
+}
diff --git a/ui/src/core_plugins/counter/index.ts b/ui/src/core_plugins/counter/index.ts
index 40865fa..2af0836 100644
--- a/ui/src/core_plugins/counter/index.ts
+++ b/ui/src/core_plugins/counter/index.ts
@@ -12,9 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import m from 'mithril';
-
-import {CounterDetailsPanel} from '../../frontend/counter_panel';
import {
NUM_NULL,
STR_NULL,
@@ -25,10 +22,15 @@
PluginDescriptor,
PrimaryTrackSortKey,
STR,
+ LONG,
+ Engine,
} from '../../public';
import {getTrackName} from '../../public/utils';
import {CounterOptions} from '../../frontend/base_counter_track';
import {TraceProcessorCounterTrack} from './trace_processor_counter_track';
+import {CounterDetailsPanel} from './counter_details_panel';
+import {Time, duration, time} from '../../base/time';
+import {Optional} from '../../base/utils';
export const COUNTER_TRACK_KIND = 'CounterTrack';
@@ -105,6 +107,34 @@
return options;
}
+async function getCounterEventBounds(
+ engine: Engine,
+ trackId: number,
+ id: number,
+): Promise<Optional<{ts: time; dur: duration}>> {
+ const query = `
+ WITH CTE AS (
+ SELECT
+ id,
+ ts as leftTs,
+ LEAD(ts) OVER (ORDER BY ts) AS rightTs
+ FROM counter
+ WHERE track_id = ${trackId}
+ )
+ SELECT * FROM CTE WHERE id = ${id}
+ `;
+
+ const counter = await engine.query(query);
+ const row = counter.iter({
+ leftTs: LONG,
+ rightTs: LONG_NULL,
+ });
+ const leftTs = Time.fromRaw(row.leftTs);
+ const rightTs = row.rightTs !== null ? Time.fromRaw(row.rightTs) : leftTs;
+ const duration = rightTs - leftTs;
+ return {ts: leftTs, dur: duration};
+}
+
class CounterPlugin implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
await this.addCounterTracks(ctx);
@@ -113,16 +143,6 @@
await this.addCpuPerfCounterTracks(ctx);
await this.addThreadCounterTracks(ctx);
await this.addProcessCounterTracks(ctx);
-
- ctx.registerDetailsPanel({
- render: (sel) => {
- if (sel.kind === 'COUNTER') {
- return m(CounterDetailsPanel);
- } else {
- return undefined;
- }
- },
- });
}
private async addCounterTracks(ctx: PluginContextTrace) {
@@ -170,6 +190,10 @@
});
},
sortKey: PrimaryTrackSortKey.COUNTER_TRACK,
+ detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, displayName),
+ getEventBounds: async (id) => {
+ return await getCounterEventBounds(ctx.engine, trackId, id);
+ },
});
}
}
@@ -229,6 +253,10 @@
options: getDefaultCounterOptions(name),
});
},
+ detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name),
+ getEventBounds: async (id) => {
+ return await getCounterEventBounds(ctx.engine, trackId, id);
+ },
});
}
}
@@ -288,6 +316,10 @@
options: getDefaultCounterOptions(name),
});
},
+ detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name),
+ getEventBounds: async (id) => {
+ return await getCounterEventBounds(ctx.engine, trackId, id);
+ },
});
}
}
@@ -338,6 +370,10 @@
options: getDefaultCounterOptions(name),
});
},
+ detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name),
+ getEventBounds: async (id) => {
+ return await getCounterEventBounds(ctx.engine, trackId, id);
+ },
});
}
}
@@ -373,6 +409,10 @@
options: getDefaultCounterOptions(name),
});
},
+ detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name),
+ getEventBounds: async (id) => {
+ return await getCounterEventBounds(ctx.engine, trackId, id);
+ },
});
}
}
diff --git a/ui/src/core_plugins/counter/trace_processor_counter_track.ts b/ui/src/core_plugins/counter/trace_processor_counter_track.ts
index b47a03d..6a97273 100644
--- a/ui/src/core_plugins/counter/trace_processor_counter_track.ts
+++ b/ui/src/core_plugins/counter/trace_processor_counter_track.ts
@@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {Time} from '../../base/time';
-import {Actions} from '../../common/actions';
import {globals} from '../../frontend/globals';
import {LONG, LONG_NULL, NUM} from '../../public';
import {
@@ -80,23 +78,8 @@
if (!it.valid()) {
return;
}
- const trackKey = this.trackKey;
const id = it.id;
- const leftTs = Time.fromRaw(it.leftTs);
-
- // TODO(stevegolton): Don't try to guess times and durations here, make it
- // obvious to the user that this counter sample has no duration as it's
- // the last one in the series
- const rightTs = Time.fromRaw(it.rightTs ?? leftTs);
-
- globals.makeSelection(
- Actions.selectCounter({
- leftTs,
- rightTs,
- id,
- trackKey,
- }),
- );
+ globals.selectSingleEvent(this.trackKey, id);
});
return true;
diff --git a/ui/src/core_plugins/cpu_slices/index.ts b/ui/src/core_plugins/cpu_slices/index.ts
index a862732..51ac18b 100644
--- a/ui/src/core_plugins/cpu_slices/index.ts
+++ b/ui/src/core_plugins/cpu_slices/index.ts
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {CPU_SLICE_TRACK_KIND} from '../../public';
import {SliceDetailsPanel} from '../../frontend/slice_details_panel';
import {
Engine,
@@ -22,8 +23,6 @@
import {NUM, STR_NULL} from '../../trace_processor/query_result';
import {CpuSliceTrack} from './cpu_slice_track';
-export const CPU_SLICE_TRACK_KIND = 'CpuSliceTrack';
-
class CpuSlices implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
const cpus = ctx.trace.cpus;
diff --git a/ui/src/core_plugins/debug/counter_track.ts b/ui/src/core_plugins/debug/counter_track.ts
deleted file mode 100644
index 5c0022d..0000000
--- a/ui/src/core_plugins/debug/counter_track.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-// 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 {BaseCounterTrack} from '../../frontend/base_counter_track';
-import {TrackContext} from '../../public';
-import {Engine} from '../../trace_processor/engine';
-import {CounterDebugTrackConfig} from '../../frontend/debug_tracks';
-import {Disposable, DisposableCallback} from '../../base/disposable';
-import {uuidv4Sql} from '../../base/uuid';
-
-export class DebugCounterTrack extends BaseCounterTrack {
- private config: CounterDebugTrackConfig;
- private sqlTableName: string;
-
- constructor(engine: Engine, ctx: TrackContext) {
- super({
- engine,
- trackKey: ctx.trackKey,
- });
-
- // TODO(stevegolton): Validate params before type asserting.
- // TODO(stevegolton): Avoid just pushing this config up for some base
- // class to use. Be more explicit.
- this.config = ctx.params as CounterDebugTrackConfig;
- this.sqlTableName = `__debug_counter_${uuidv4Sql(this.trackKey)}`;
- }
-
- async onInit(): Promise<Disposable> {
- await this.createTrackTable();
- return new DisposableCallback(() => {
- this.dropTrackTable();
- });
- }
-
- getTrackShellButtons(): m.Children {
- return this.getCounterContextMenu();
- }
-
- getSqlSource(): string {
- return `select * from ${this.sqlTableName}`;
- }
-
- private async createTrackTable(): Promise<void> {
- await this.engine.query(`
- create table ${this.sqlTableName} as
- with data as (
- ${this.config.data.sqlSource}
- )
- select
- ${this.config.columns.ts} as ts,
- ${this.config.columns.value} as value
- from data
- order by ts;`);
- }
-
- private async dropTrackTable(): Promise<void> {
- this.engine.tryQuery(`drop table if exists ${this.sqlTableName}`);
- }
-}
diff --git a/ui/src/core_plugins/debug/index.ts b/ui/src/core_plugins/debug/index.ts
index fad9cae..0c9b1e6 100644
--- a/ui/src/core_plugins/debug/index.ts
+++ b/ui/src/core_plugins/debug/index.ts
@@ -14,9 +14,9 @@
import {uuidv4} from '../../base/uuid';
import {
- DEBUG_COUNTER_TRACK_URI,
- DEBUG_SLICE_TRACK_URI,
-} from '../../frontend/debug_tracks';
+ addDebugCounterTrack,
+ addDebugSliceTrack,
+} from '../../frontend/debug_tracks/debug_tracks';
import {
BottomTabToSCSAdapter,
Plugin,
@@ -24,18 +24,55 @@
PluginDescriptor,
} from '../../public';
-import {DebugCounterTrack} from './counter_track';
-import {DebugSliceDetailsTab} from './details_tab';
-import {DebugTrackV2} from './slice_track';
+import {DebugSliceDetailsTab} from '../../frontend/debug_tracks/details_tab';
import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
+import {Optional, exists} from '../../base/utils';
-class DebugTrackPlugin implements Plugin {
+class DebugTracksPlugin implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
- ctx.registerTrack({
- uri: DEBUG_SLICE_TRACK_URI,
- trackFactory: (trackCtx) => new DebugTrackV2(ctx.engine, trackCtx),
+ ctx.registerCommand({
+ id: 'perfetto.DebugTracks#addDebugSliceTrack',
+ name: 'Add debug slice track',
+ callback: async (arg: unknown) => {
+ // This command takes a query and creates a debug track out of it The
+ // query can be passed in using the first arg, or if this is not defined
+ // or is the wrong type, we prompt the user for it.
+ const query = await getStringFromArgOrPrompt(ctx, arg);
+ if (exists(query)) {
+ await addDebugSliceTrack(
+ ctx,
+ {
+ sqlSource: query,
+ },
+ 'Debug slice track',
+ {ts: 'ts', dur: 'dur', name: 'name'},
+ [],
+ );
+ }
+ },
});
+ ctx.registerCommand({
+ id: 'perfetto.DebugTracks#addDebugCounterTrack',
+ name: 'Add debug counter track',
+ callback: async (arg: unknown) => {
+ const query = await getStringFromArgOrPrompt(ctx, arg);
+ if (exists(query)) {
+ await addDebugCounterTrack(
+ ctx,
+ {
+ sqlSource: query,
+ },
+ 'Debug slice track',
+ {ts: 'ts', value: 'value'},
+ );
+ }
+ },
+ });
+
+ // TODO(stevegolton): While debug tracks are in their current state, we rely
+ // on this plugin to provide the details panel for them. In the future, this
+ // details panel will become part of the debug track's definition.
ctx.registerDetailsPanel(
new BottomTabToSCSAdapter({
tabFactory: (selection) => {
@@ -54,15 +91,29 @@
},
}),
);
+ }
+}
- ctx.registerTrack({
- uri: DEBUG_COUNTER_TRACK_URI,
- trackFactory: (trackCtx) => new DebugCounterTrack(ctx.engine, trackCtx),
- });
+// If arg is a string, return it, otherwise prompt the user for a string. An
+// exception is thrown if the prompt is cancelled, so this function handles this
+// and returns undefined in this case.
+async function getStringFromArgOrPrompt(
+ ctx: PluginContextTrace,
+ arg: unknown,
+): Promise<Optional<string>> {
+ if (typeof arg === 'string') {
+ return arg;
+ } else {
+ try {
+ return await ctx.prompt('Enter a query...');
+ } catch {
+ // Prompt was ignored
+ return undefined;
+ }
}
}
export const plugin: PluginDescriptor = {
- pluginId: 'perfetto.DebugSlices',
- plugin: DebugTrackPlugin,
+ pluginId: 'perfetto.DebugTracks',
+ plugin: DebugTracksPlugin,
};
diff --git a/ui/src/core_plugins/debug/slice_track.ts b/ui/src/core_plugins/debug/slice_track.ts
deleted file mode 100644
index a0734d9..0000000
--- a/ui/src/core_plugins/debug/slice_track.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-// 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 {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
-import {TrackContext} from '../../public';
-import {Engine} from '../../trace_processor/engine';
-import {
- CustomSqlDetailsPanelConfig,
- CustomSqlTableDefConfig,
- CustomSqlTableSliceTrack,
-} from '../../frontend/tracks/custom_sql_table_slice_track';
-
-import {DebugSliceDetailsTab} from './details_tab';
-import {
- ARG_PREFIX,
- SliceColumns,
- SqlDataSource,
-} from '../../frontend/debug_tracks';
-import {DisposableCallback} from '../../base/disposable';
-import {uuidv4Sql} from '../../base/uuid';
-
-export interface DebugTrackV2Config {
- data: SqlDataSource;
- columns: SliceColumns;
- argColumns: string[];
-}
-
-export class DebugTrackV2 extends CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
- private config: DebugTrackV2Config;
- private sqlTableName: string;
-
- constructor(engine: Engine, ctx: TrackContext) {
- super({
- engine,
- trackKey: ctx.trackKey,
- });
-
- // TODO(stevegolton): Validate params before type asserting.
- // TODO(stevegolton): Avoid just pushing this config up for some base
- // class to use. Be more explicit.
- this.config = ctx.params as DebugTrackV2Config;
- this.sqlTableName = `__debug_slice_${uuidv4Sql(ctx.trackKey)}`;
- }
-
- async getSqlDataSource(): Promise<CustomSqlTableDefConfig> {
- await this.createTrackTable(
- this.config.data,
- this.config.columns,
- this.config.argColumns,
- );
- return {
- sqlTableName: this.sqlTableName,
- dispose: new DisposableCallback(() => this.destroyTrackTable()),
- };
- }
-
- getDetailsPanel(): CustomSqlDetailsPanelConfig {
- return {
- kind: DebugSliceDetailsTab.kind,
- config: {
- sqlTableName: this.sqlTableName,
- title: 'Debug Slice',
- },
- };
- }
-
- private async createTrackTable(
- data: SqlDataSource,
- sliceColumns: SliceColumns,
- argColumns: string[],
- ): Promise<void> {
- // If the view has clashing names (e.g. "name" coming from joining two
- // different tables, we will see names like "name_1", "name_2", but they
- // won't be addressable from the SQL. So we explicitly name them through a
- // list of columns passed to CTE.
- const dataColumns =
- data.columns !== undefined ? `(${data.columns.join(', ')})` : '';
-
- // TODO(altimin): Support removing this table when the track is closed.
- const dur = sliceColumns.dur === '0' ? 0 : sliceColumns.dur;
- await this.engine.query(`
- create perfetto table ${this.sqlTableName} as
- with data${dataColumns} as (
- ${data.sqlSource}
- ),
- prepared_data as (
- select
- ${sliceColumns.ts} as ts,
- ifnull(cast(${dur} as int), -1) as dur,
- printf('%s', ${sliceColumns.name}) as name
- ${argColumns.length > 0 ? ',' : ''}
- ${argColumns.map((c) => `${c} as ${ARG_PREFIX}${c}`).join(',\n')}
- from data
- )
- select
- row_number() over (order by ts) as id,
- *
- from prepared_data
- order by ts;`);
- }
-
- private async destroyTrackTable() {
- await this.engine.tryQuery(`DROP TABLE IF EXISTS ${this.sqlTableName}`);
- }
-}
diff --git a/ui/src/core_plugins/frames/index.ts b/ui/src/core_plugins/frames/index.ts
index 8e67de9..6ebca8e 100644
--- a/ui/src/core_plugins/frames/index.ts
+++ b/ui/src/core_plugins/frames/index.ts
@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {
+ ACTUAL_FRAMES_SLICE_TRACK_KIND,
+ EXPECTED_FRAMES_SLICE_TRACK_KIND,
+} from '../../public';
import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
import {getTrackName} from '../../public/utils';
import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
@@ -19,9 +23,6 @@
import {ActualFramesTrack} from './actual_frames_track';
import {ExpectedFramesTrack} from './expected_frames_track';
-export const EXPECTED_FRAMES_SLICE_TRACK_KIND = 'ExpectedFramesSliceTrack';
-export const ACTUAL_FRAMES_SLICE_TRACK_KIND = 'ActualFramesSliceTrack';
-
class FramesPlugin implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
this.addExpectedFrames(ctx);
diff --git a/ui/src/core_plugins/ftrace/index.ts b/ui/src/core_plugins/ftrace/index.ts
index 9539c78..11be0a4 100644
--- a/ui/src/core_plugins/ftrace/index.ts
+++ b/ui/src/core_plugins/ftrace/index.ts
@@ -22,7 +22,7 @@
PluginDescriptor,
} from '../../public';
import {NUM} from '../../trace_processor/query_result';
-import {Trash} from '../../base/disposable';
+import {DisposableStack} from '../../base/disposable';
import {FtraceFilter, FtracePluginState} from './common';
import {FtraceRawTrack} from './ftrace_track';
@@ -36,7 +36,7 @@
};
class FtraceRawPlugin implements Plugin {
- private trash = new Trash();
+ private trash = new DisposableStack();
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
const store = ctx.mountStore<FtracePluginState>((init: unknown) => {
@@ -51,13 +51,13 @@
return DEFAULT_STATE;
}
});
- this.trash.add(store);
+ this.trash.use(store);
const filterStore = store.createSubStore(
['filter'],
(x) => x as FtraceFilter,
);
- this.trash.add(filterStore);
+ this.trash.use(filterStore);
const cpus = await this.lookupCpuCores(ctx.engine);
for (const cpuNum of cpus) {
diff --git a/ui/src/core_plugins/heap_profile/index.ts b/ui/src/core_plugins/heap_profile/index.ts
index 193f227..8e0b5ba 100644
--- a/ui/src/core_plugins/heap_profile/index.ts
+++ b/ui/src/core_plugins/heap_profile/index.ts
@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {FlamegraphCache} from '../../core/flamegraph_cache';
import {
- FlamegraphCache,
FlamegraphDetailsPanel,
profileType,
} from '../../frontend/flamegraph_panel';
diff --git a/ui/src/core_plugins/perf_samples_profile/index.ts b/ui/src/core_plugins/perf_samples_profile/index.ts
index eb4b220..8517739 100644
--- a/ui/src/core_plugins/perf_samples_profile/index.ts
+++ b/ui/src/core_plugins/perf_samples_profile/index.ts
@@ -13,8 +13,9 @@
// limitations under the License.
import {TrackData} from '../../common/track_data';
+import {PERF_SAMPLES_PROFILE_TRACK_KIND} from '../../public';
+import {FlamegraphCache} from '../../core/flamegraph_cache';
import {
- FlamegraphCache,
FlamegraphDetailsPanel,
profileType,
} from '../../frontend/flamegraph_panel';
@@ -22,8 +23,6 @@
import {NUM} from '../../trace_processor/query_result';
import {PerfSamplesProfileTrack} from './perf_samples_profile_track';
-export const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack';
-
export interface Data extends TrackData {
tsStarts: BigInt64Array;
}
diff --git a/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts b/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
index aca33de..a9b3ca9 100644
--- a/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
+++ b/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
@@ -25,8 +25,6 @@
import {Engine, Track} from '../../public';
import {LONG} from '../../trace_processor/query_result';
-export const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack';
-
export interface Data extends TrackData {
tsStarts: BigInt64Array;
}
diff --git a/ui/src/core_plugins/sched/active_cpu_count.ts b/ui/src/core_plugins/sched/active_cpu_count.ts
index f3a4eee..6d12ca7 100644
--- a/ui/src/core_plugins/sched/active_cpu_count.ts
+++ b/ui/src/core_plugins/sched/active_cpu_count.ts
@@ -14,58 +14,30 @@
import m from 'mithril';
-import {NullDisposable} from '../../base/disposable';
import {sqliteString} from '../../base/string_utils';
-import {uuidv4} from '../../base/uuid';
-import {Actions} from '../../common/actions';
-import {SCROLLING_TRACK_GROUP} from '../../common/state';
import {
BaseCounterTrack,
CounterOptions,
} from '../../frontend/base_counter_track';
import {CloseTrackButton} from '../../frontend/close_track_button';
-import {globals} from '../../frontend/globals';
-import {Engine, PrimaryTrackSortKey, TrackContext} from '../../public';
+import {Engine, TrackContext} from '../../public';
+import {DisposableStack} from '../../base/disposable';
-export function addActiveCPUCountTrack(cpuType?: string) {
- const cpuTypeName = cpuType === undefined ? '' : ` ${cpuType} `;
-
- const key = uuidv4();
-
- globals.dispatchMultiple([
- Actions.addTrack({
- key,
- uri: ActiveCPUCountTrack.kind,
- name: `Active ${cpuTypeName}CPU count`,
- trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
- trackGroup: SCROLLING_TRACK_GROUP,
- params: {
- cpuType,
- },
- }),
- Actions.toggleTrackPinned({trackKey: key}),
- ]);
-}
-
-export interface ActiveCPUCountTrackConfig {
- cpuType?: string;
+export enum CPUType {
+ Big = 'big',
+ Mid = 'mid',
+ Little = 'little',
}
export class ActiveCPUCountTrack extends BaseCounterTrack {
- private config: ActiveCPUCountTrackConfig;
+ private readonly cpuType?: CPUType;
- static readonly kind = 'dev.perfetto.Sched.ActiveCPUCount';
-
- constructor(ctx: TrackContext, engine: Engine) {
+ constructor(ctx: TrackContext, engine: Engine, cpuType?: CPUType) {
super({
engine,
trackKey: ctx.trackKey,
});
-
- // TODO(stevegolton): Validate params before type asserting.
- // TODO(stevegolton): Avoid just pushing this config up for some base
- // class to use. Be more explicit.
- this.config = ctx.params as ActiveCPUCountTrackConfig;
+ this.cpuType = cpuType;
}
getTrackShellButtons(): m.Children {
@@ -85,16 +57,14 @@
await this.engine.query(
`INCLUDE PERFETTO MODULE sched.thread_level_parallelism`,
);
- return new NullDisposable();
+ return new DisposableStack();
}
getSqlSource() {
const sourceTable =
- this.config!.cpuType === undefined
+ this.cpuType === undefined
? 'sched_active_cpu_count'
- : `sched_active_cpu_count_for_core_type(${sqliteString(
- this.config!.cpuType,
- )})`;
+ : `sched_active_cpu_count_for_core_type(${sqliteString(this.cpuType)})`;
return `
select
ts,
diff --git a/ui/src/core_plugins/sched/index.ts b/ui/src/core_plugins/sched/index.ts
index ccebaa1..1ac0b14 100644
--- a/ui/src/core_plugins/sched/index.ts
+++ b/ui/src/core_plugins/sched/index.ts
@@ -12,18 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {uuidv4} from '../../base/uuid';
+import {Actions} from '../../common/actions';
+import {SCROLLING_TRACK_GROUP} from '../../common/state';
+import {globals} from '../../frontend/globals';
import {
Plugin,
- PluginContext,
PluginContextTrace,
PluginDescriptor,
+ PrimaryTrackSortKey,
} from '../../public';
-import {ActiveCPUCountTrack, addActiveCPUCountTrack} from './active_cpu_count';
-import {
- addRunnableThreadCountTrack,
- RunnableThreadCountTrack,
-} from './runnable_thread_count';
+import {ActiveCPUCountTrack, CPUType} from './active_cpu_count';
+import {RunnableThreadCountTrack} from './runnable_thread_count';
class SchedPlugin implements Plugin {
async onTraceLoad(ctx: PluginContextTrace) {
@@ -35,33 +36,68 @@
trackKey: trackCtx.trackKey,
}),
});
- ctx.registerTrack({
- uri: ActiveCPUCountTrack.kind,
- trackFactory: (trackCtx) => new ActiveCPUCountTrack(trackCtx, ctx.engine),
- });
- }
-
- onActivate(ctx: PluginContext): void {
ctx.registerCommand({
id: 'dev.perfetto.Sched.AddRunnableThreadCountTrackCommand',
name: 'Add track: runnable thread count',
- callback: () => addRunnableThreadCountTrack(),
+ callback: () =>
+ addPinnedTrack(RunnableThreadCountTrack.kind, 'Runnable thread count'),
+ });
+
+ const uri = uriForActiveCPUCountTrack();
+ const title = 'Active ${cpuType} CPU count';
+ ctx.registerTrack({
+ uri,
+ displayName: title,
+ trackFactory: (trackCtx) => new ActiveCPUCountTrack(trackCtx, ctx.engine),
});
ctx.registerCommand({
id: 'dev.perfetto.Sched.AddActiveCPUCountTrackCommand',
name: 'Add track: active CPU count',
- callback: () => addActiveCPUCountTrack(),
+ callback: () => addPinnedTrack(uri, title),
});
- for (const cpuType of ['big', 'little', 'mid']) {
+
+ for (const cpuType of Object.values(CPUType)) {
+ const uri = uriForActiveCPUCountTrack(cpuType);
+ const title = `Active ${cpuType} CPU count`;
+ ctx.registerTrack({
+ uri,
+ displayName: title,
+ trackFactory: (trackCtx) =>
+ new ActiveCPUCountTrack(trackCtx, ctx.engine, cpuType),
+ });
+
ctx.registerCommand({
id: `dev.perfetto.Sched.AddActiveCPUCountTrackCommand.${cpuType}`,
name: `Add track: active ${cpuType} CPU count`,
- callback: () => addActiveCPUCountTrack(cpuType),
+ callback: () => addPinnedTrack(uri, title),
});
}
}
}
+function uriForActiveCPUCountTrack(cpuType?: CPUType): string {
+ const prefix = `perfetto.sched#ActiveCPUCount`;
+ if (cpuType) {
+ return `${prefix}.${cpuType}`;
+ } else {
+ return prefix;
+ }
+}
+
+function addPinnedTrack(uri: string, title: string) {
+ const key = uuidv4();
+ globals.dispatchMultiple([
+ Actions.addTrack({
+ key,
+ uri,
+ name: title,
+ trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
+ trackGroup: SCROLLING_TRACK_GROUP,
+ }),
+ Actions.toggleTrackPinned({trackKey: key}),
+ ]);
+}
+
export const plugin: PluginDescriptor = {
pluginId: 'perfetto.Sched',
plugin: SchedPlugin,
diff --git a/ui/src/core_plugins/sched/runnable_thread_count.ts b/ui/src/core_plugins/sched/runnable_thread_count.ts
index 894824e..7c9423c 100644
--- a/ui/src/core_plugins/sched/runnable_thread_count.ts
+++ b/ui/src/core_plugins/sched/runnable_thread_count.ts
@@ -14,32 +14,13 @@
import m from 'mithril';
-import {NullDisposable} from '../../base/disposable';
-import {uuidv4} from '../../base/uuid';
-import {Actions} from '../../common/actions';
-import {SCROLLING_TRACK_GROUP} from '../../common/state';
import {
BaseCounterTrack,
CounterOptions,
} from '../../frontend/base_counter_track';
import {CloseTrackButton} from '../../frontend/close_track_button';
-import {globals} from '../../frontend/globals';
import {NewTrackArgs} from '../../frontend/track';
-import {PrimaryTrackSortKey} from '../../public';
-
-export function addRunnableThreadCountTrack() {
- const key = uuidv4();
- globals.dispatchMultiple([
- Actions.addTrack({
- key,
- uri: RunnableThreadCountTrack.kind,
- name: `Runnable thread count`,
- trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
- trackGroup: SCROLLING_TRACK_GROUP,
- }),
- Actions.toggleTrackPinned({trackKey: key}),
- ]);
-}
+import {DisposableStack} from '../../base/disposable';
export class RunnableThreadCountTrack extends BaseCounterTrack {
static readonly kind = 'dev.perfetto.Sched.RunnableThreadCount';
@@ -65,7 +46,7 @@
await this.engine.query(
`INCLUDE PERFETTO MODULE sched.thread_level_parallelism`,
);
- return new NullDisposable();
+ return new DisposableStack();
}
getSqlSource() {
diff --git a/ui/src/core_plugins/thread_slice/index.ts b/ui/src/core_plugins/thread_slice/index.ts
index 611b061..6b7da4a 100644
--- a/ui/src/core_plugins/thread_slice/index.ts
+++ b/ui/src/core_plugins/thread_slice/index.ts
@@ -13,6 +13,7 @@
// limitations under the License.
import {uuidv4} from '../../base/uuid';
+import {THREAD_SLICE_TRACK_KIND} from '../../public';
import {ThreadSliceDetailsTab} from '../../frontend/thread_slice_details_tab';
import {
BottomTabToSCSAdapter,
@@ -22,7 +23,7 @@
} from '../../public';
import {getTrackName} from '../../public/utils';
import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
-import {ThreadSliceTrack, THREAD_SLICE_TRACK_KIND} from './thread_slice_track';
+import {ThreadSliceTrack} from '../../frontend/thread_slice_track';
class ThreadSlicesPlugin implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
diff --git a/ui/src/core_plugins/thread_state/index.ts b/ui/src/core_plugins/thread_state/index.ts
index ab0bcc5..864a744 100644
--- a/ui/src/core_plugins/thread_state/index.ts
+++ b/ui/src/core_plugins/thread_state/index.ts
@@ -13,6 +13,7 @@
// limitations under the License.
import {uuidv4} from '../../base/uuid';
+import {THREAD_STATE_TRACK_KIND} from '../../public';
import {asThreadStateSqlId} from '../../frontend/sql_types';
import {ThreadStateTab} from '../../frontend/thread_state_tab';
import {
@@ -26,8 +27,6 @@
import {ThreadStateTrack} from './thread_state_track';
-export const THREAD_STATE_TRACK_KIND = 'ThreadStateTrack';
-
class ThreadState implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
const {engine} = ctx;
diff --git a/ui/src/core_plugins/track_utils/index.ts b/ui/src/core_plugins/track_utils/index.ts
index de39729..6e54875 100644
--- a/ui/src/core_plugins/track_utils/index.ts
+++ b/ui/src/core_plugins/track_utils/index.ts
@@ -31,8 +31,8 @@
ctx.registerCommand({
id: 'perfetto.RunQueryInSelectedTimeWindow',
name: `Run query in selected time window`,
- callback: () => {
- const window = getTimeSpanOfSelectionOrVisibleWindow();
+ callback: async () => {
+ const window = await getTimeSpanOfSelectionOrVisibleWindow();
globals.omnibox.setMode(OmniboxMode.Query);
globals.omnibox.setText(
`select where ts >= ${window.start} and ts < ${window.end}`,
diff --git a/ui/src/core_plugins/visualised_args/index.ts b/ui/src/core_plugins/visualised_args/index.ts
deleted file mode 100644
index 459a7ad..0000000
--- a/ui/src/core_plugins/visualised_args/index.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (C) 2022 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {
- VISUALISED_ARGS_SLICE_TRACK_URI,
- VisualisedArgsState,
-} from '../../frontend/visualized_args_tracks';
-import {VisualisedArgsTrack} from './visualized_args_track';
-
-class VisualisedArgsPlugin implements Plugin {
- async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
- ctx.registerTrack({
- uri: VISUALISED_ARGS_SLICE_TRACK_URI,
- tags: {
- metric: true, // TODO(stevegolton): Is this track really a metric?
- },
- trackFactory: (trackCtx) => {
- // TODO(stevegolton): Validate params properly. Note, this is no
- // worse than the situation we had before with track config.
- const params = trackCtx.params as VisualisedArgsState;
- return new VisualisedArgsTrack(
- {
- engine: ctx.engine,
- trackKey: trackCtx.trackKey,
- },
- params.trackId,
- params.maxDepth,
- params.argName,
- );
- },
- });
- }
-}
-
-export const plugin: PluginDescriptor = {
- pluginId: 'perfetto.VisualisedArgs',
- plugin: VisualisedArgsPlugin,
-};
diff --git a/ui/src/frontend/aggregation_tab.ts b/ui/src/frontend/aggregation_tab.ts
index 6cd1d0c..0c68f5f 100644
--- a/ui/src/frontend/aggregation_tab.ts
+++ b/ui/src/frontend/aggregation_tab.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import m from 'mithril';
-import {Disposable, Trash} from '../base/disposable';
+import {Disposable, DisposableStack} from '../base/disposable';
import {AggregationPanel} from './aggregation_panel';
import {globals} from './globals';
import {isEmptyData} from '../common/aggregation_data';
@@ -28,9 +28,9 @@
FlamegraphSelectionParams,
} from './flamegraph_panel';
import {ProfileType, TrackState} from '../common/state';
-import {PERF_SAMPLES_PROFILE_TRACK_KIND} from '../core_plugins/perf_samples_profile';
import {assertExists} from '../base/logging';
import {Monitor} from '../base/monitor';
+import {PERF_SAMPLES_PROFILE_TRACK_KIND} from '../core/track_kinds';
interface View {
key: string;
@@ -187,7 +187,7 @@
}
export class AggregationsTabs implements Disposable {
- private trash = new Trash();
+ private trash = new DisposableStack();
constructor() {
const unregister = globals.tabManager.registerDetailsPanel({
@@ -200,7 +200,7 @@
},
});
- this.trash.add(unregister);
+ this.trash.use(unregister);
}
dispose(): void {
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index 97907ef..59a5079 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -15,7 +15,7 @@
import m from 'mithril';
import {copyToClipboard} from '../base/clipboard';
-import {Trash} from '../base/disposable';
+import {DisposableStack} from '../base/disposable';
import {findRef} from '../base/dom_utils';
import {FuzzyFinder} from '../base/fuzzy';
import {assertExists, assertUnreachable} from '../base/logging';
@@ -56,7 +56,7 @@
import {OmniboxMode, PromptOption} from './omnibox_manager';
import {Utid} from './sql_types';
import {getThreadInfo} from './thread_and_process_info';
-import {THREAD_STATE_TRACK_KIND} from '../core_plugins/thread_state';
+import {THREAD_STATE_TRACK_KIND} from '../core/track_kinds';
function renderPermalink(): m.Children {
const hash = globals.permalinkHash;
@@ -112,14 +112,14 @@
];
export class App implements m.ClassComponent {
- private trash = new Trash();
+ private trash = new DisposableStack();
static readonly OMNIBOX_INPUT_REF = 'omnibox';
private omniboxInputEl?: HTMLInputElement;
private recentCommands: string[] = [];
constructor() {
- this.trash.add(new Notes());
- this.trash.add(new AggregationsTabs());
+ this.trash.use(new Notes());
+ this.trash.use(new AggregationsTabs());
}
private getEngine(): Engine | undefined {
@@ -210,7 +210,7 @@
name: `Critical path lite`,
callback: async () => {
const trackUtid = this.getFirstUtidOfSelectionOrVisibleWindow();
- const window = getTimeSpanOfSelectionOrVisibleWindow();
+ const window = await getTimeSpanOfSelectionOrVisibleWindow();
const engine = this.getEngine();
if (engine !== undefined && trackUtid != 0) {
@@ -218,7 +218,13 @@
`INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
);
await addDebugSliceTrack(
- engine,
+ // NOTE(stevegolton): This is a temporary patch, this menu should
+ // become part of a critical path plugin, at which point we can just
+ // use the plugin's context object.
+ {
+ engine,
+ registerTrack: (x) => globals.trackManager.registerTrack(x),
+ },
{
sqlSource: `
SELECT
@@ -252,7 +258,7 @@
name: `Critical path`,
callback: async () => {
const trackUtid = this.getFirstUtidOfSelectionOrVisibleWindow();
- const window = getTimeSpanOfSelectionOrVisibleWindow();
+ const window = await getTimeSpanOfSelectionOrVisibleWindow();
const engine = this.getEngine();
if (engine !== undefined && trackUtid != 0) {
@@ -260,7 +266,13 @@
`INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
);
await addDebugSliceTrack(
- engine,
+ // NOTE(stevegolton): This is a temporary patch, this menu should
+ // become part of a critical path plugin, at which point we can just
+ // use the plugin's context object.
+ {
+ engine,
+ registerTrack: (x) => globals.trackManager.registerTrack(x),
+ },
{
sqlSource: `
SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
@@ -283,9 +295,9 @@
{
id: 'perfetto.CriticalPathPprof',
name: `Critical path pprof`,
- callback: () => {
+ callback: async () => {
const trackUtid = this.getFirstUtidOfSelectionOrVisibleWindow();
- const window = getTimeSpanOfSelectionOrVisibleWindow();
+ const window = await getTimeSpanOfSelectionOrVisibleWindow();
const engine = this.getEngine();
if (engine !== undefined && trackUtid != 0) {
@@ -368,8 +380,8 @@
{
id: 'perfetto.CopyTimeWindow',
name: `Copy selected time window to clipboard`,
- callback: () => {
- const window = getTimeSpanOfSelectionOrVisibleWindow();
+ callback: async () => {
+ const window = await getTimeSpanOfSelectionOrVisibleWindow();
const query = `ts >= ${window.start} and ts < ${window.end}`;
copyToClipboard(query);
},
@@ -393,8 +405,8 @@
{
id: 'perfetto.SetTemporarySpanNote',
name: 'Set the temporary span note based on the current selection',
- callback: () => {
- const range = globals.findTimeRangeOfSelection();
+ callback: async () => {
+ const range = await globals.findTimeRangeOfSelection();
if (range) {
globals.dispatch(
Actions.addSpanNote({
@@ -410,8 +422,8 @@
{
id: 'perfetto.AddSpanNote',
name: 'Add a new span note based on the current selection',
- callback: () => {
- const range = globals.findTimeRangeOfSelection();
+ callback: async () => {
+ const range = await globals.findTimeRangeOfSelection();
if (range) {
globals.dispatch(
Actions.addSpanNote({start: range.start, end: range.end}),
@@ -807,7 +819,7 @@
// Register each command with the command manager
this.cmds.forEach((cmd) => {
const dispose = globals.commandManager.registerCommand(cmd);
- this.trash.add(dispose);
+ this.trash.use(dispose);
});
}
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index d010f95..7e20cf2 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -15,16 +15,18 @@
import m from 'mithril';
import {searchSegment} from '../base/binary_search';
-import {Disposable, NullDisposable} from '../base/disposable';
+import {Disposable, DisposableStack} from '../base/disposable';
import {assertTrue, assertUnreachable} from '../base/logging';
import {Time, time} from '../base/time';
import {uuidv4Sql} from '../base/uuid';
import {drawTrackHoverTooltip} from '../common/canvas_utils';
import {raf} from '../core/raf_scheduler';
import {CacheKey} from '../core/timeline_cache';
-import {Engine, LONG, NUM, Track} from '../public';
+import {Track} from '../public';
import {Button} from '../widgets/button';
import {MenuDivider, MenuItem, PopupMenu2} from '../widgets/menu';
+import {Engine} from '../trace_processor/engine';
+import {LONG, NUM} from '../trace_processor/query_result';
import {checkerboardExcept} from './checkerboard';
import {globals} from './globals';
@@ -233,7 +235,7 @@
// state in trace_processor should be cleaned up when dispose is
// called on the returned hook.
async onInit(): Promise<Disposable> {
- return new NullDisposable();
+ return new DisposableStack();
}
// This should be an SQL expression returning the columns `ts` and `value`.
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 84e53cf..0e0f00d 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {Disposable, NullDisposable} from '../base/disposable';
+import {Disposable, DisposableStack} from '../base/disposable';
import {assertExists} from '../base/logging';
import {clamp, floatEqual} from '../base/math_utils';
import {Time, time} from '../base/time';
@@ -224,7 +224,7 @@
// called on the returned hook. In the common case of where
// the data for this track is a SQL fragment this does nothing.
async onInit(): Promise<Disposable> {
- return new NullDisposable();
+ return new DisposableStack();
}
// This should be an SQL expression returning all the columns listed
diff --git a/ui/src/frontend/counter_panel.ts b/ui/src/frontend/counter_panel.ts
deleted file mode 100644
index fb34212..0000000
--- a/ui/src/frontend/counter_panel.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size 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 {DetailsShell} from '../widgets/details_shell';
-import {GridLayout} from '../widgets/grid_layout';
-import {Section} from '../widgets/section';
-import {Tree, TreeNode} from '../widgets/tree';
-
-import {globals} from './globals';
-import {DurationWidget} from './widgets/duration';
-import {Timestamp} from './widgets/timestamp';
-
-export class CounterDetailsPanel implements m.ClassComponent {
- view() {
- const counterInfo = globals.counterDetails;
- if (
- counterInfo.startTime &&
- counterInfo.name !== undefined &&
- counterInfo.value !== undefined &&
- counterInfo.delta !== undefined &&
- counterInfo.duration !== undefined
- ) {
- return m(
- DetailsShell,
- {title: 'Counter', description: `${counterInfo.name}`},
- m(
- GridLayout,
- m(
- Section,
- {title: 'Properties'},
- m(
- Tree,
- m(TreeNode, {left: 'Name', right: `${counterInfo.name}`}),
- m(TreeNode, {
- left: 'Start time',
- right: m(Timestamp, {ts: counterInfo.startTime}),
- }),
- m(TreeNode, {
- left: 'Value',
- right: `${counterInfo.value.toLocaleString()}`,
- }),
- m(TreeNode, {
- left: 'Delta',
- right: `${counterInfo.delta.toLocaleString()}`,
- }),
- m(TreeNode, {
- left: 'Duration',
- right: m(DurationWidget, {dur: counterInfo.duration}),
- }),
- ),
- ),
- ),
- );
- } else {
- return m(DetailsShell, {title: 'Counter', description: 'Loading...'});
- }
- }
-
- renderCanvas() {}
-}
diff --git a/ui/src/frontend/debug_tracks.ts b/ui/src/frontend/debug_tracks.ts
deleted file mode 100644
index 1aa97f8..0000000
--- a/ui/src/frontend/debug_tracks.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {uuidv4} from '../base/uuid';
-import {Actions, DeferredAction} from '../common/actions';
-import {SCROLLING_TRACK_GROUP} from '../common/state';
-import {DebugTrackV2Config} from '../core_plugins/debug/slice_track';
-import {Engine, PrimaryTrackSortKey} from '../public';
-import {matchesSqlValue} from '../trace_processor/sql_utils';
-
-import {globals} from './globals';
-
-export const ARG_PREFIX = 'arg_';
-export const DEBUG_SLICE_TRACK_URI = 'perfetto.DebugSlices';
-export const DEBUG_COUNTER_TRACK_URI = 'perfetto.DebugCounter';
-
-// Names of the columns of the underlying view to be used as
-// ts / dur / name / pivot.
-export interface SliceColumns {
- ts: string;
- dur: string;
- name: string;
- pivot?: string;
-}
-
-export interface DebugTrackV2CreateConfig {
- pinned?: boolean; // default true
- closeable?: boolean; // default true
-}
-
-let debugTrackCount = 0;
-
-export interface SqlDataSource {
- // SQL source selecting the necessary data.
- sqlSource: string;
-
- // Optional: Rename columns from the query result.
- // If omitted, original column names from the query are used instead.
- // The caller is responsible for ensuring that the number of items in this
- // list matches the number of columns returned by sqlSource.
- columns?: string[];
-}
-
-// Creates actions to add a debug track. The actions must be dispatched to
-// have an effect. Use this variant if you want to create many tracks at
-// once or want to tweak the actions once produced. Otherwise, use
-// addDebugSliceTrack().
-export async function createDebugSliceTrackActions(
- _engine: Engine,
- data: SqlDataSource,
- trackName: string,
- sliceColumns: SliceColumns,
- argColumns: string[],
- config?: DebugTrackV2CreateConfig,
-): Promise<DeferredAction<{}>[]> {
- const debugTrackId = ++debugTrackCount;
- const closeable = config?.closeable ?? true;
- const trackKey = uuidv4();
-
- const trackConfig: DebugTrackV2Config = {
- data,
- columns: sliceColumns,
- argColumns,
- };
-
- const actions: DeferredAction<{}>[] = [
- Actions.addTrack({
- key: trackKey,
- name: trackName.trim() || `Debug Track ${debugTrackId}`,
- uri: DEBUG_SLICE_TRACK_URI,
- trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
- trackGroup: SCROLLING_TRACK_GROUP,
- params: trackConfig,
- closeable,
- }),
- ];
- if (config?.pinned ?? true) {
- actions.push(Actions.toggleTrackPinned({trackKey}));
- }
- return actions;
-}
-
-export async function addPivotDebugSliceTracks(
- engine: Engine,
- data: SqlDataSource,
- trackName: string,
- sliceColumns: SliceColumns,
- argColumns: string[],
- config?: DebugTrackV2CreateConfig,
-) {
- if (sliceColumns.pivot) {
- // Get distinct values to group by
- const pivotValues = await engine.query(`
- with all_vals as (${data.sqlSource})
- select DISTINCT ${sliceColumns.pivot} from all_vals;`);
-
- const iter = pivotValues.iter({});
-
- for (; iter.valid(); iter.next()) {
- const pivotDataSource: SqlDataSource = {
- sqlSource: `select * from
- (${data.sqlSource})
- where ${sliceColumns.pivot} ${matchesSqlValue(
- iter.get(sliceColumns.pivot),
- )}`,
- };
-
- const actions = await createDebugSliceTrackActions(
- engine,
- pivotDataSource,
- `${trackName.trim() || 'Pivot Track'}: ${iter.get(sliceColumns.pivot)}`,
- sliceColumns,
- argColumns,
- config,
- );
-
- globals.dispatchMultiple(actions);
- }
- }
-}
-
-// Adds a debug track immediately. Use createDebugSliceTrackActions() if you
-// want to create many tracks at once.
-export async function addDebugSliceTrack(
- engine: Engine,
- data: SqlDataSource,
- trackName: string,
- sliceColumns: SliceColumns,
- argColumns: string[],
- config?: DebugTrackV2CreateConfig,
-) {
- const actions = await createDebugSliceTrackActions(
- engine,
- data,
- trackName,
- sliceColumns,
- argColumns,
- config,
- );
- globals.dispatchMultiple(actions);
-}
-
-// Names of the columns of the underlying view to be used as ts / dur / name.
-export interface CounterColumns {
- ts: string;
- value: string;
-}
-
-export interface CounterDebugTrackConfig {
- data: SqlDataSource;
- columns: CounterColumns;
-}
-
-export interface CounterDebugTrackCreateConfig {
- pinned?: boolean; // default true
- closeable?: boolean; // default true
-}
-
-// Creates actions to add a debug track. The actions must be dispatched to
-// have an effect. Use this variant if you want to create many tracks at
-// once or want to tweak the actions once produced. Otherwise, use
-// addDebugCounterTrack().
-export async function createDebugCounterTrackActions(
- data: SqlDataSource,
- trackName: string,
- columns: CounterColumns,
- config?: CounterDebugTrackCreateConfig,
-) {
- // To prepare displaying the provided data as a track, materialize it and
- // compute depths.
- const debugTrackId = ++debugTrackCount;
-
- const closeable = config?.closeable ?? true;
- const params: CounterDebugTrackConfig = {
- data,
- columns,
- };
-
- const trackKey = uuidv4();
- const actions: DeferredAction<{}>[] = [
- Actions.addTrack({
- key: trackKey,
- uri: DEBUG_COUNTER_TRACK_URI,
- name: trackName.trim() || `Debug Track ${debugTrackId}`,
- trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
- trackGroup: SCROLLING_TRACK_GROUP,
- params,
- closeable,
- }),
- ];
- if (config?.pinned ?? true) {
- actions.push(Actions.toggleTrackPinned({trackKey}));
- }
- return actions;
-}
-
-// Adds a debug track immediately. Use createDebugCounterTrackActions() if you
-// want to create many tracks at once.
-export async function addDebugCounterTrack(
- data: SqlDataSource,
- trackName: string,
- columns: CounterColumns,
- config?: CounterDebugTrackCreateConfig,
-) {
- const actions = await createDebugCounterTrackActions(
- data,
- trackName,
- columns,
- config,
- );
- globals.dispatchMultiple(actions);
-}
diff --git a/ui/src/core_plugins/debug/add_debug_track_menu.ts b/ui/src/frontend/debug_tracks/add_debug_track_menu.ts
similarity index 83%
rename from ui/src/core_plugins/debug/add_debug_track_menu.ts
rename to ui/src/frontend/debug_tracks/add_debug_track_menu.ts
index a0c5b31..6c39bac 100644
--- a/ui/src/core_plugins/debug/add_debug_track_menu.ts
+++ b/ui/src/frontend/debug_tracks/add_debug_track_menu.ts
@@ -26,9 +26,8 @@
addDebugCounterTrack,
addDebugSliceTrack,
addPivotDebugSliceTracks,
-} from '../../frontend/debug_tracks';
-
-export const ARG_PREFIX = 'arg_';
+} from './debug_tracks';
+import {globals} from '../globals';
export function uuidToViewName(uuid: string): string {
return `view_${uuid.split('-').join('_')}`;
@@ -186,7 +185,13 @@
case 'slice':
if (this.renderParams.pivot === '') {
addDebugSliceTrack(
- vnode.attrs.engine,
+ // NOTE(stevegolton): This is a temporary patch, this menu
+ // should become part of the debug tracks plugin, at which
+ // point we can just use the plugin's context object.
+ {
+ engine: vnode.attrs.engine,
+ registerTrack: (x) => globals.trackManager.registerTrack(x),
+ },
vnode.attrs.dataSource,
this.name,
{
@@ -198,7 +203,10 @@
);
} else {
addPivotDebugSliceTracks(
- vnode.attrs.engine,
+ {
+ engine: vnode.attrs.engine,
+ registerTrack: globals.trackManager.registerTrack,
+ },
vnode.attrs.dataSource,
this.name,
{
@@ -212,10 +220,21 @@
}
break;
case 'counter':
- addDebugCounterTrack(vnode.attrs.dataSource, this.name, {
- ts: this.renderParams.ts,
- value: this.renderParams.value,
- });
+ addDebugCounterTrack(
+ // TODO(stevegolton): This is a temporary patch, this menu
+ // should become part of the debug tracks plugin, at which
+ // point we can just use the plugin's context object.
+ {
+ engine: vnode.attrs.engine,
+ registerTrack: (x) => globals.trackManager.registerTrack(x),
+ },
+ vnode.attrs.dataSource,
+ this.name,
+ {
+ ts: this.renderParams.ts,
+ value: this.renderParams.value,
+ },
+ );
break;
}
},
diff --git a/ui/src/frontend/debug_tracks/counter_track.ts b/ui/src/frontend/debug_tracks/counter_track.ts
new file mode 100644
index 0000000..d1b915e
--- /dev/null
+++ b/ui/src/frontend/debug_tracks/counter_track.ts
@@ -0,0 +1,39 @@
+// 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 {BaseCounterTrack} from '../../frontend/base_counter_track';
+import {TrackContext} from '../../public';
+import {Engine} from '../../trace_processor/engine';
+
+export class DebugCounterTrack extends BaseCounterTrack {
+ private readonly sqlTableName: string;
+
+ constructor(engine: Engine, ctx: TrackContext, tableName: string) {
+ super({
+ engine,
+ trackKey: ctx.trackKey,
+ });
+ this.sqlTableName = tableName;
+ }
+
+ getTrackShellButtons(): m.Children {
+ return this.getCounterContextMenu();
+ }
+
+ getSqlSource(): string {
+ return `select * from ${this.sqlTableName}`;
+ }
+}
diff --git a/ui/src/frontend/debug_tracks/debug_tracks.ts b/ui/src/frontend/debug_tracks/debug_tracks.ts
new file mode 100644
index 0000000..83ca517
--- /dev/null
+++ b/ui/src/frontend/debug_tracks/debug_tracks.ts
@@ -0,0 +1,265 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {uuidv4, uuidv4Sql} from '../../base/uuid';
+import {Actions, DeferredAction} from '../../common/actions';
+import {PrimaryTrackSortKey, SCROLLING_TRACK_GROUP} from '../../common/state';
+import {globals} from '../globals';
+import {TrackDescriptor} from '../../public';
+import {DebugSliceTrack} from './slice_track';
+import {
+ createPerfettoTable,
+ matchesSqlValue,
+} from '../../trace_processor/sql_utils';
+import {Engine} from '../../trace_processor/engine';
+import {DebugCounterTrack} from './counter_track';
+import {ARG_PREFIX} from './details_tab';
+
+// We need to add debug tracks from the core and from plugins. In order to add a
+// debug track we need to pass a context through with we can add the track. This
+// is different for plugins vs the core. This interface defines the generic
+// shape of this context, which can be supplied from a plugin or built from
+// globals.
+//
+// TODO(stevegolton): In the future, both the core and plugins should
+// have access to some Context object which implements the various things we
+// want to do in a generic way, so that we don't have to do this mangling to get
+// this to work.
+interface Context {
+ engine: Engine;
+ registerTrack(track: TrackDescriptor): unknown;
+}
+
+// Names of the columns of the underlying view to be used as
+// ts / dur / name / pivot.
+export interface SliceColumns {
+ ts: string;
+ dur: string;
+ name: string;
+ pivot?: string;
+}
+
+let debugTrackCount = 0;
+
+export interface SqlDataSource {
+ // SQL source selecting the necessary data.
+ sqlSource: string;
+
+ // Optional: Rename columns from the query result.
+ // If omitted, original column names from the query are used instead.
+ // The caller is responsible for ensuring that the number of items in this
+ // list matches the number of columns returned by sqlSource.
+ columns?: string[];
+}
+
+// Creates actions to add a debug track. The actions must be dispatched to
+// have an effect. Use this variant if you want to create many tracks at
+// once or want to tweak the actions once produced. Otherwise, use
+// addDebugSliceTrack().
+function createAddDebugTrackActions(
+ trackName: string,
+ uri: string,
+): DeferredAction<{}>[] {
+ const debugTrackId = ++debugTrackCount;
+ const trackKey = uuidv4();
+
+ const actions: DeferredAction<{}>[] = [
+ Actions.addTrack({
+ key: trackKey,
+ name: trackName.trim() || `Debug Track ${debugTrackId}`,
+ uri,
+ trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
+ trackGroup: SCROLLING_TRACK_GROUP,
+ closeable: true,
+ }),
+ Actions.toggleTrackPinned({trackKey}),
+ ];
+
+ return actions;
+}
+
+export async function addPivotDebugSliceTracks(
+ ctx: Context,
+ data: SqlDataSource,
+ trackName: string,
+ sliceColumns: SliceColumns,
+ argColumns: string[],
+) {
+ if (sliceColumns.pivot) {
+ // Get distinct values to group by
+ const pivotValues = await ctx.engine.query(`
+ with all_vals as (${data.sqlSource})
+ select DISTINCT ${sliceColumns.pivot} from all_vals;`);
+
+ const iter = pivotValues.iter({});
+
+ for (; iter.valid(); iter.next()) {
+ const pivotDataSource: SqlDataSource = {
+ sqlSource: `select * from
+ (${data.sqlSource})
+ where ${sliceColumns.pivot} ${matchesSqlValue(
+ iter.get(sliceColumns.pivot),
+ )}`,
+ };
+
+ await addDebugSliceTrack(
+ ctx,
+ pivotDataSource,
+ `${trackName.trim() || 'Pivot Track'}: ${iter.get(sliceColumns.pivot)}`,
+ sliceColumns,
+ argColumns,
+ );
+ }
+ }
+}
+
+// Adds a debug track immediately. Use createDebugSliceTrackActions() if you
+// want to create many tracks at once.
+export async function addDebugSliceTrack(
+ ctx: Context,
+ data: SqlDataSource,
+ trackName: string,
+ sliceColumns: SliceColumns,
+ argColumns: string[],
+): Promise<void> {
+ // Create a new table from the debug track definition. This will be used as
+ // the backing data source for our track and its details panel.
+ const tableName = `__debug_slice_${uuidv4Sql()}`;
+
+ // TODO(stevegolton): Right now we ignore the AsyncDisposable that this
+ // function returns, and so never clean up this table. The problem is we have
+ // no where sensible to do this cleanup.
+ // - If we did it in the track's onDestroy function, we could drop the table
+ // while the details panel still needs access to it.
+ // - If we did it in the plugin's onTraceUnload function, we could risk
+ // dropping it n the middle of a track update cycle as track lifecycles are
+ // not synchronized with plugin lifecycles.
+ await createPerfettoTable(
+ ctx.engine,
+ tableName,
+ createDebugSliceTrackTableExpr(data, sliceColumns, argColumns),
+ );
+
+ const uri = `debug.slice.${uuidv4()}`;
+ ctx.registerTrack({
+ uri,
+ trackFactory: (trackCtx) => {
+ return new DebugSliceTrack(ctx.engine, trackCtx, tableName);
+ },
+ });
+
+ // Create the actions to add this track to the tracklist
+ const actions = await createAddDebugTrackActions(trackName, uri);
+ globals.dispatchMultiple(actions);
+}
+
+function createDebugSliceTrackTableExpr(
+ data: SqlDataSource,
+ sliceColumns: SliceColumns,
+ argColumns: string[],
+): string {
+ const dataColumns =
+ data.columns !== undefined ? `(${data.columns.join(', ')})` : '';
+ const dur = sliceColumns.dur === '0' ? 0 : sliceColumns.dur;
+ return `
+ with data${dataColumns} as (
+ ${data.sqlSource}
+ ),
+ prepared_data as (
+ select
+ ${sliceColumns.ts} as ts,
+ ifnull(cast(${dur} as int), -1) as dur,
+ printf('%s', ${sliceColumns.name}) as name
+ ${argColumns.length > 0 ? ',' : ''}
+ ${argColumns.map((c) => `${c} as ${ARG_PREFIX}${c}`).join(',\n')}
+ from data
+ )
+ select
+ row_number() over (order by ts) as id,
+ *
+ from prepared_data
+ order by ts
+ `;
+}
+
+// Names of the columns of the underlying view to be used as ts / dur / name.
+export interface CounterColumns {
+ ts: string;
+ value: string;
+}
+
+export interface CounterDebugTrackConfig {
+ data: SqlDataSource;
+ columns: CounterColumns;
+}
+
+export interface CounterDebugTrackCreateConfig {
+ pinned?: boolean; // default true
+ closeable?: boolean; // default true
+}
+
+// Adds a debug track immediately. Use createDebugCounterTrackActions() if you
+// want to create many tracks at once.
+export async function addDebugCounterTrack(
+ ctx: Context,
+ data: SqlDataSource,
+ trackName: string,
+ columns: CounterColumns,
+): Promise<void> {
+ // Create a new table from the debug track definition. This will be used as
+ // the backing data source for our track and its details panel.
+ const tableName = `__debug_counter_${uuidv4Sql()}`;
+
+ // TODO(stevegolton): Right now we ignore the AsyncDisposable that this
+ // function returns, and so never clean up this table. The problem is we have
+ // no where sensible to do this cleanup.
+ // - If we did it in the track's onDestroy function, we could drop the table
+ // while the details panel still needs access to it.
+ // - If we did it in the plugin's onTraceUnload function, we could risk
+ // dropping it n the middle of a track update cycle as track lifecycles are
+ // not synchronized with plugin lifecycles.
+ await createPerfettoTable(
+ ctx.engine,
+ tableName,
+ createDebugCounterTrackTableExpr(data, columns),
+ );
+
+ const uri = `debug.counter.${uuidv4()}`;
+ ctx.registerTrack({
+ uri,
+ trackFactory: (trackCtx) => {
+ return new DebugCounterTrack(ctx.engine, trackCtx, tableName);
+ },
+ });
+
+ // Create the actions to add this track to the tracklist
+ const actions = await createAddDebugTrackActions(trackName, uri);
+ globals.dispatchMultiple(actions);
+}
+
+function createDebugCounterTrackTableExpr(
+ data: SqlDataSource,
+ columns: CounterColumns,
+): string {
+ return `
+ with data as (
+ ${data.sqlSource}
+ )
+ select
+ ${columns.ts} as ts,
+ ${columns.value} as value
+ from data
+ order by ts
+ `;
+}
diff --git a/ui/src/core_plugins/debug/details_tab.ts b/ui/src/frontend/debug_tracks/details_tab.ts
similarity index 90%
rename from ui/src/core_plugins/debug/details_tab.ts
rename to ui/src/frontend/debug_tracks/details_tab.ts
index 0926909..5cc0f6f 100644
--- a/ui/src/core_plugins/debug/details_tab.ts
+++ b/ui/src/frontend/debug_tracks/details_tab.ts
@@ -16,23 +16,15 @@
import {duration, Time, time} from '../../base/time';
import {raf} from '../../core/raf_scheduler';
-import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
-import {ARG_PREFIX} from '../../frontend/debug_tracks';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {hasArgs, renderArguments} from '../../frontend/slice_args';
-import {getSlice, SliceDetails, sliceRef} from '../../frontend/sql/slice';
-import {asSliceSqlId, Utid} from '../../frontend/sql_types';
-import {
- getProcessName,
- getThreadName,
-} from '../../frontend/thread_and_process_info';
-import {
- getThreadState,
- ThreadState,
- threadStateRef,
-} from '../../frontend/thread_state';
-import {DurationWidget} from '../../frontend/widgets/duration';
-import {Timestamp} from '../../frontend/widgets/timestamp';
+import {BottomTab, NewBottomTabArgs} from '../bottom_tab';
+import {GenericSliceDetailsTabConfig} from '../generic_slice_details_tab';
+import {hasArgs, renderArguments} from '../slice_args';
+import {getSlice, SliceDetails, sliceRef} from '../sql/slice';
+import {asSliceSqlId, Utid} from '../sql_types';
+import {getProcessName, getThreadName} from '../thread_and_process_info';
+import {getThreadState, ThreadState, threadStateRef} from '../thread_state';
+import {DurationWidget} from '../widgets/duration';
+import {Timestamp} from '../widgets/timestamp';
import {
ColumnType,
durationFromSql,
@@ -46,6 +38,8 @@
import {Section} from '../../widgets/section';
import {dictToTree, dictToTreeNodes, Tree, TreeNode} from '../../widgets/tree';
+export const ARG_PREFIX = 'arg_';
+
function sqlValueToNumber(value?: ColumnType): number | undefined {
if (typeof value === 'bigint') return Number(value);
if (typeof value !== 'number') return undefined;
diff --git a/ui/src/frontend/debug_tracks/slice_track.ts b/ui/src/frontend/debug_tracks/slice_track.ts
new file mode 100644
index 0000000..fbc9770
--- /dev/null
+++ b/ui/src/frontend/debug_tracks/slice_track.ts
@@ -0,0 +1,51 @@
+// 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 {NamedSliceTrackTypes} from '../named_slice_track';
+import {
+ CustomSqlDetailsPanelConfig,
+ CustomSqlTableDefConfig,
+ CustomSqlTableSliceTrack,
+} from '../tracks/custom_sql_table_slice_track';
+import {TrackContext} from '../../public';
+import {Engine} from '../../trace_processor/engine';
+import {DebugSliceDetailsTab} from './details_tab';
+
+export class DebugSliceTrack extends CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
+ private readonly sqlTableName: string;
+
+ constructor(engine: Engine, ctx: TrackContext, tableName: string) {
+ super({
+ engine,
+ trackKey: ctx.trackKey,
+ });
+ this.sqlTableName = tableName;
+ }
+
+ async getSqlDataSource(): Promise<CustomSqlTableDefConfig> {
+ return {
+ sqlTableName: this.sqlTableName,
+ };
+ }
+
+ getDetailsPanel(): CustomSqlDetailsPanelConfig {
+ return {
+ kind: DebugSliceDetailsTab.kind,
+ config: {
+ sqlTableName: this.sqlTableName,
+ title: 'Debug Slice',
+ },
+ };
+ }
+}
diff --git a/ui/src/frontend/drag_handle.ts b/ui/src/frontend/drag_handle.ts
index dd43077..039e514 100644
--- a/ui/src/frontend/drag_handle.ts
+++ b/ui/src/frontend/drag_handle.ts
@@ -14,7 +14,7 @@
import m from 'mithril';
-import {Trash} from '../base/disposable';
+import {DisposableStack} from '../base/disposable';
import {raf} from '../core/raf_scheduler';
import {Button} from '../widgets/button';
import {MenuItem, PopupMenu2} from '../widgets/menu';
@@ -103,7 +103,7 @@
// We can't get real fullscreen height until the pan_and_zoom_handler
// exists.
private fullscreenHeight = 0;
- private trash = new Trash();
+ private trash = new DisposableStack();
oncreate({dom, attrs}: m.CVnodeDOM<DragHandleAttrs>) {
this.resize = attrs.resize;
@@ -111,7 +111,7 @@
this.isClosed = this.height <= 0;
this.fullscreenHeight = getFullScreenHeight();
const elem = dom as HTMLElement;
- this.trash.add(
+ this.trash.use(
new DragGestureHandler(
elem,
this.onDrag.bind(this),
@@ -127,7 +127,7 @@
this.toggleVisibility();
},
});
- this.trash.add(cmd);
+ this.trash.use(cmd);
}
private toggleVisibility() {
diff --git a/ui/src/frontend/flamegraph_panel.ts b/ui/src/frontend/flamegraph_panel.ts
index d81ffec..c45a248 100644
--- a/ui/src/frontend/flamegraph_panel.ts
+++ b/ui/src/frontend/flamegraph_panel.ts
@@ -50,6 +50,7 @@
import {getCurrentTrace} from './sidebar';
import {convertTraceToPprofAndDownload} from './trace_converter';
import {AsyncLimiter} from '../base/async_limiter';
+import {FlamegraphCache} from '../core/flamegraph_cache';
const HEADER_HEIGHT = 30;
@@ -102,43 +103,6 @@
const MIN_PIXEL_DISPLAYED = 1;
-export class FlamegraphCache {
- private cache: Map<string, string>;
- private prefix: string;
- private tableId: number;
- private cacheSizeLimit: number;
-
- constructor(prefix: string) {
- this.cache = new Map<string, string>();
- this.prefix = prefix;
- this.tableId = 0;
- this.cacheSizeLimit = 10;
- }
-
- async getTableName(engine: Engine, query: string): Promise<string> {
- let tableName = this.cache.get(query);
- if (tableName === undefined) {
- // TODO(hjd): This should be LRU.
- if (this.cache.size > this.cacheSizeLimit) {
- for (const name of this.cache.values()) {
- await engine.query(`drop table ${name}`);
- }
- this.cache.clear();
- }
- tableName = `${this.prefix}_${this.tableId++}`;
- await engine.query(
- `create temp table if not exists ${tableName} as ${query}`,
- );
- this.cache.set(query, tableName);
- }
- return tableName;
- }
-
- hasQuery(query: string): boolean {
- return this.cache.get(query) !== undefined;
- }
-}
-
function toSelectedCallsite(c: CallsiteInfo | undefined): string {
if (c !== undefined && c.name !== undefined) {
return c.name;
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index ef36631..d2a1c57 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -53,10 +53,10 @@
import {SliceSqlId} from './sql_types';
import {PxSpan, TimeScale} from './time_scale';
import {SelectionManager, LegacySelection} from '../core/selection_manager';
-import {exists} from '../base/utils';
+import {Optional, exists} from '../base/utils';
import {OmniboxManager} from './omnibox_manager';
import {CallsiteInfo} from '../common/flamegraph_util';
-import {FlamegraphCache} from './flamegraph_panel';
+import {FlamegraphCache} from '../core/flamegraph_cache';
const INSTANT_FOCUS_DURATION = 1n;
const INCOMPLETE_SLICE_DURATION = 30_000n;
@@ -132,14 +132,6 @@
name?: string;
}
-export interface CounterDetails {
- startTime?: time;
- value?: number;
- delta?: number;
- duration?: duration;
- name?: string;
-}
-
export interface ThreadStateDetails {
ts?: time;
dur?: duration;
@@ -260,7 +252,6 @@
private _connectedFlows?: Flow[] = undefined;
private _selectedFlows?: Flow[] = undefined;
private _visibleFlowCategories?: Map<string, boolean> = undefined;
- private _counterDetails?: CounterDetails = undefined;
private _cpuProfileDetails?: CpuProfileDetails = undefined;
private _numQueriesQueued = 0;
private _bufferUsage?: number = undefined;
@@ -334,7 +325,6 @@
this._connectedFlows = [];
this._selectedFlows = [];
this._visibleFlowCategories = new Map<string, boolean>();
- this._counterDetails = {};
this._threadStateDetails = {};
this._cpuProfileDetails = {};
this.engines.clear();
@@ -446,14 +436,6 @@
this._visibleFlowCategories = assertExists(visibleFlowCategories);
}
- get counterDetails() {
- return assertExists(this._counterDetails);
- }
-
- set counterDetails(click: CounterDetails) {
- this._counterDetails = assertExists(click);
- }
-
get aggregateDataStore(): AggregateDataStore {
return assertExists(this._aggregateDataStore);
}
@@ -616,20 +598,38 @@
setLegacySelection(
legacySelection: LegacySelection,
- args: LegacySelectionArgs,
+ args: Partial<LegacySelectionArgs> = {},
): void {
this._selectionManager.setLegacy(legacySelection);
- if (args.clearSearch) {
+ this.handleSelectionArgs(args);
+ }
+
+ selectSingleEvent(
+ trackKey: string,
+ eventId: number,
+ args: Partial<LegacySelectionArgs> = {},
+ ): void {
+ this._selectionManager.setEvent(trackKey, eventId);
+ this.handleSelectionArgs(args);
+ }
+
+ private handleSelectionArgs(args: Partial<LegacySelectionArgs> = {}): void {
+ const {
+ clearSearch = true,
+ switchToCurrentSelectionTab = true,
+ pendingScrollId = undefined,
+ } = args;
+ if (clearSearch) {
globals.dispatch(Actions.setSearchIndex({index: -1}));
}
- if (args.pendingScrollId !== undefined) {
+ if (pendingScrollId !== undefined) {
globals.dispatch(
Actions.setPendingScrollId({
- pendingScrollId: args.pendingScrollId,
+ pendingScrollId,
}),
);
}
- if (args.switchToCurrentSelectionTab) {
+ if (switchToCurrentSelectionTab) {
globals.dispatch(Actions.showTab({uri: 'current_selection'}));
}
}
@@ -762,7 +762,9 @@
return Time.sub(ts, this.timestampOffset());
}
- findTimeRangeOfSelection(): {start: time; end: time} | undefined {
+ async findTimeRangeOfSelection(): Promise<
+ Optional<{start: time; end: time}>
+ > {
const sel = globals.state.selection;
if (sel.kind === 'area') {
return sel;
@@ -786,6 +788,20 @@
assertUnreachable(kind);
}
}
+ } else if (sel.kind === 'single') {
+ const uri = globals.state.tracks[sel.trackKey]?.uri;
+ if (uri) {
+ const bounds = await globals.trackManager
+ .resolveTrackInfo(uri)
+ ?.getEventBounds?.(sel.eventId);
+ if (bounds) {
+ return {
+ start: bounds.ts,
+ end: Time.add(bounds.ts, bounds.dur),
+ };
+ }
+ }
+ return undefined;
}
const selection = getLegacySelection(this.state);
@@ -799,8 +815,6 @@
} else if (selection.kind === 'THREAD_STATE') {
const threadState = this.threadStateDetails;
return findTimeRangeOfSlice(threadState);
- } else if (selection.kind === 'COUNTER') {
- return {start: selection.leftTs, end: selection.rightTs};
} else if (selection.kind === 'LOG') {
// TODO(hjd): Make focus selection work for logs.
} else if (selection.kind === 'GENERIC_SLICE') {
@@ -850,8 +864,10 @@
// Returns the time span of the current selection, or the visible window if
// there is no current selection.
-export function getTimeSpanOfSelectionOrVisibleWindow(): Span<time, duration> {
- const range = globals.findTimeRangeOfSelection();
+export async function getTimeSpanOfSelectionOrVisibleWindow(): Promise<
+ Span<time, duration>
+> {
+ const range = await globals.findTimeRangeOfSelection();
if (exists(range)) {
return new TimeSpan(range.start, range.end);
} else {
diff --git a/ui/src/frontend/keyboard_event_handler.ts b/ui/src/frontend/keyboard_event_handler.ts
index d597376..53d3352 100644
--- a/ui/src/frontend/keyboard_event_handler.ts
+++ b/ui/src/frontend/keyboard_event_handler.ts
@@ -117,11 +117,11 @@
}
}
-export function findCurrentSelection() {
+export async function findCurrentSelection() {
const selection = getLegacySelection(globals.state);
if (selection === null) return;
- const range = globals.findTimeRangeOfSelection();
+ const range = await globals.findTimeRangeOfSelection();
if (exists(range)) {
focusHorizontalRange(range.start, range.end);
}
diff --git a/ui/src/frontend/notes.ts b/ui/src/frontend/notes.ts
index a7f9898..d408861 100644
--- a/ui/src/frontend/notes.ts
+++ b/ui/src/frontend/notes.ts
@@ -1,4 +1,4 @@
-import {Disposable, Trash} from '../base/disposable';
+import {Disposable, DisposableStack} from '../base/disposable';
import {globals} from './globals';
import {NotesManager} from './notes_manager';
@@ -11,14 +11,14 @@
* Notes are core functionality thus don't really belong in a plugin.
*/
export class Notes implements Disposable {
- private trash = new Trash();
+ private trash = new DisposableStack();
constructor() {
- this.trash.add(
+ this.trash.use(
globals.tabManager.registerDetailsPanel(new NotesEditorTab()),
);
- this.trash.add(
+ this.trash.use(
globals.tabManager.registerTab({
uri: 'notes.manager',
isEphemeral: false,
diff --git a/ui/src/frontend/pan_and_zoom_handler.ts b/ui/src/frontend/pan_and_zoom_handler.ts
index 00d0116..b889deb 100644
--- a/ui/src/frontend/pan_and_zoom_handler.ts
+++ b/ui/src/frontend/pan_and_zoom_handler.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {Disposable, Trash} from '../base/disposable';
+import {Disposable, DisposableStack} from '../base/disposable';
import {currentTargetOffset, elementIsEditable} from '../base/dom_utils';
import {raf} from '../core/raf_scheduler';
@@ -120,7 +120,7 @@
editing: boolean,
) => void;
private endSelection: (edit: boolean) => void;
- private trash: Trash;
+ private trash: DisposableStack;
constructor({
element,
@@ -150,13 +150,13 @@
this.editSelection = editSelection;
this.onSelection = onSelection;
this.endSelection = endSelection;
- this.trash = new Trash();
+ this.trash = new DisposableStack();
document.body.addEventListener('keydown', this.boundOnKeyDown);
document.body.addEventListener('keyup', this.boundOnKeyUp);
this.element.addEventListener('mousemove', this.boundOnMouseMove);
this.element.addEventListener('wheel', this.boundOnWheel, {passive: true});
- this.trash.addCallback(() => {
+ this.trash.defer(() => {
this.element.removeEventListener('wheel', this.boundOnWheel);
this.element.removeEventListener('mousemove', this.boundOnMouseMove);
document.body.removeEventListener('keyup', this.boundOnKeyUp);
@@ -167,7 +167,7 @@
let dragStartX = -1;
let dragStartY = -1;
let edit = false;
- this.trash.add(
+ this.trash.use(
new DragGestureHandler(
this.element,
(x, y) => {
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 5611a19..19331b6 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -14,7 +14,7 @@
import m from 'mithril';
-import {Trash} from '../base/disposable';
+import {DisposableStack} from '../base/disposable';
import {findRef, toHTMLElement} from '../base/dom_utils';
import {assertExists, assertFalse} from '../base/logging';
import {time} from '../base/time';
@@ -103,7 +103,7 @@
private ctx?: CanvasRenderingContext2D;
- private readonly trash = new Trash();
+ private readonly trash = new DisposableStack();
private readonly OVERLAY_REF = 'overlay';
private readonly PANEL_STACK_REF = 'panel-stack';
@@ -194,12 +194,12 @@
constructor() {
const onRedraw = () => this.renderCanvas();
raf.addRedrawCallback(onRedraw);
- this.trash.addCallback(() => {
+ this.trash.defer(() => {
raf.removeRedrawCallback(onRedraw);
});
perfDisplay.addContainer(this);
- this.trash.addCallback(() => {
+ this.trash.defer(() => {
perfDisplay.removeContainer(this);
});
}
@@ -216,7 +216,7 @@
const virtualCanvas = new VirtualCanvas(overlayElement, dom, {
overdrawPx: CANVAS_OVERDRAW_PX,
});
- this.trash.add(virtualCanvas);
+ this.trash.use(virtualCanvas);
this.virtualCanvas = virtualCanvas;
const ctx = virtualCanvas.canvasElement.getContext('2d');
@@ -242,7 +242,7 @@
);
// Listen for when the panel stack changes size
- this.trash.add(
+ this.trash.use(
new SimpleResizeObserver(panelStackElement, () => {
attrs.onPanelStackResize?.(
panelStackElement.clientWidth,
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
index a704cbb..7dbcc82 100644
--- a/ui/src/frontend/publish.ts
+++ b/ui/src/frontend/publish.ts
@@ -22,7 +22,6 @@
import {getLegacySelection} from '../common/state';
import {
- CounterDetails,
CpuProfileDetails,
Flow,
globals,
@@ -75,11 +74,6 @@
raf.scheduleFullRedraw();
}
-export function publishCounterDetails(click: CounterDetails) {
- globals.counterDetails = click;
- globals.publishRedraw();
-}
-
export function publishCpuProfileDetails(details: CpuProfileDetails) {
globals.cpuProfileDetails = details;
globals.publishRedraw();
diff --git a/ui/src/frontend/query_result_tab.ts b/ui/src/frontend/query_result_tab.ts
index e6feda3..c85df5d 100644
--- a/ui/src/frontend/query_result_tab.ts
+++ b/ui/src/frontend/query_result_tab.ts
@@ -22,7 +22,7 @@
import {
AddDebugTrackMenu,
uuidToViewName,
-} from '../core_plugins/debug/add_debug_track_menu';
+} from './debug_tracks/add_debug_track_menu';
import {Button} from '../widgets/button';
import {PopupMenu2} from '../widgets/menu';
import {PopupPosition} from '../widgets/popup';
diff --git a/ui/src/frontend/simple_counter_track.ts b/ui/src/frontend/simple_counter_track.ts
index 7bf0f0e..5f18545 100644
--- a/ui/src/frontend/simple_counter_track.ts
+++ b/ui/src/frontend/simple_counter_track.ts
@@ -15,8 +15,8 @@
import m from 'mithril';
import {Engine, TrackContext} from '../public';
import {BaseCounterTrack, CounterOptions} from './base_counter_track';
-import {CounterColumns, SqlDataSource} from './debug_tracks';
-import {Disposable, DisposableCallback} from '../base/disposable';
+import {CounterColumns, SqlDataSource} from './debug_tracks/debug_tracks';
+import {Disposable} from '../base/disposable';
import {uuidv4Sql} from '../base/uuid';
export type SimpleCounterTrackConfig = {
@@ -46,10 +46,12 @@
async onInit(): Promise<Disposable> {
const trash = await super.onInit();
await this.createTrackTable();
- return new DisposableCallback(() => {
- trash.dispose();
- this.dropTrackTable();
- });
+ return {
+ dispose: () => {
+ trash.dispose();
+ this.dropTrackTable();
+ },
+ };
}
getTrackShellButtons(): m.Children {
diff --git a/ui/src/frontend/simple_slice_track.ts b/ui/src/frontend/simple_slice_track.ts
index 3eebb83..4b65d4e 100644
--- a/ui/src/frontend/simple_slice_track.ts
+++ b/ui/src/frontend/simple_slice_track.ts
@@ -19,10 +19,9 @@
CustomSqlTableSliceTrack,
} from './tracks/custom_sql_table_slice_track';
import {NamedSliceTrackTypes} from './named_slice_track';
-import {ARG_PREFIX, SliceColumns, SqlDataSource} from './debug_tracks';
+import {SliceColumns, SqlDataSource} from './debug_tracks/debug_tracks';
import {uuidv4Sql} from '../base/uuid';
-import {DisposableCallback} from '../base/disposable';
-import {DebugSliceDetailsTab} from '../core_plugins/debug/details_tab';
+import {ARG_PREFIX, DebugSliceDetailsTab} from './debug_tracks/details_tab';
export interface SimpleSliceTrackConfig {
data: SqlDataSource;
@@ -56,7 +55,9 @@
);
return {
sqlTableName: this.sqlTableName,
- dispose: new DisposableCallback(() => this.destroyTrackTable()),
+ dispose: {
+ dispose: () => this.destroyTrackTable(),
+ },
};
}
diff --git a/ui/src/frontend/slice_args.ts b/ui/src/frontend/slice_args.ts
index ec9d609..497e967 100644
--- a/ui/src/frontend/slice_args.ts
+++ b/ui/src/frontend/slice_args.ts
@@ -13,30 +13,22 @@
// limitations under the License.
import m from 'mithril';
-import {v4 as uuidv4} from 'uuid';
import {isString} from '../base/object_utils';
import {Icons} from '../base/semantic_icons';
import {sqliteString} from '../base/string_utils';
import {exists} from '../base/utils';
-import {Actions, AddTrackArgs} from '../common/actions';
-import {InThreadTrackSortKey} from '../common/state';
import {ArgNode, convertArgsToTree, Key} from '../controller/args_parser';
import {Engine} from '../trace_processor/engine';
-import {NUM} from '../trace_processor/query_result';
-import {
- VISUALISED_ARGS_SLICE_TRACK_URI,
- VisualisedArgsState,
-} from './visualized_args_tracks';
+import {addVisualisedArgTracks} from './visualized_args_tracks';
import {Anchor} from '../widgets/anchor';
import {MenuItem, PopupMenu2} from '../widgets/menu';
import {TreeNode} from '../widgets/tree';
-import {globals} from './globals';
import {Arg} from './sql/args';
import {addSqlTableTab} from './sql_table/tab';
import {SqlTables} from './sql_table/well_known_tables';
-import {assertExists} from '../base/logging';
+import {globals} from './globals';
// Renders slice arguments (key/value pairs) as a subtree.
export function renderArguments(engine: Engine, args: Arg[]): m.Children {
@@ -111,85 +103,19 @@
label: 'Visualise argument values',
icon: 'query_stats',
onclick: () => {
- addVisualisedArg(engine, fullKey);
+ addVisualisedArgTracks(
+ {
+ engine,
+ registerTrack: (t) => globals.trackManager.registerTrack(t),
+ },
+ fullKey,
+ );
},
}),
);
}
}
-async function addVisualisedArg(engine: Engine, argName: string) {
- const escapedArgName = argName.replace(/[^a-zA-Z]/g, '_');
- const tableName = `__arg_visualisation_helper_${escapedArgName}_slice`;
-
- const result = await engine.query(`
- drop table if exists ${tableName};
-
- create table ${tableName} as
- with slice_with_arg as (
- select
- slice.id,
- slice.track_id,
- slice.ts,
- slice.dur,
- slice.thread_dur,
- NULL as cat,
- args.display_value as name
- from slice
- join args using (arg_set_id)
- where args.key='${argName}'
- )
- select
- *,
- (select count()
- from ancestor_slice(s1.id) s2
- join slice_with_arg s3 on s2.id=s3.id
- ) as depth
- from slice_with_arg s1
- order by id;
-
- select
- track_id as trackId,
- max(depth) as maxDepth
- from ${tableName}
- group by track_id;
- `);
-
- const tracksToAdd: AddTrackArgs[] = [];
- const it = result.iter({trackId: NUM, maxDepth: NUM});
- const addedTrackKeys: string[] = [];
- for (; it.valid(); it.next()) {
- const trackKey = globals.trackManager.trackKeyByTrackId.get(it.trackId);
- const track = globals.state.tracks[assertExists(trackKey)];
- const utid = (track.trackSortKey as {utid?: number}).utid;
- const key = uuidv4();
- addedTrackKeys.push(key);
-
- const params: VisualisedArgsState = {
- maxDepth: it.maxDepth,
- trackId: it.trackId,
- argName: argName,
- };
-
- tracksToAdd.push({
- key,
- trackGroup: track.trackGroup,
- name: argName,
- trackSortKey:
- utid === undefined
- ? track.trackSortKey
- : {utid, priority: InThreadTrackSortKey.VISUALISED_ARGS_TRACK},
- params,
- uri: VISUALISED_ARGS_SLICE_TRACK_URI,
- });
- }
-
- globals.dispatchMultiple([
- Actions.addTracks({tracks: tracksToAdd}),
- Actions.sortThreadTracks({}),
- ]);
-}
-
function renderArgValue({value}: Arg): m.Children {
if (isWebLink(value)) {
return renderWebLink(value);
diff --git a/ui/src/frontend/slice_details_panel.ts b/ui/src/frontend/slice_details_panel.ts
index 3887577..a3683e2 100644
--- a/ui/src/frontend/slice_details_panel.ts
+++ b/ui/src/frontend/slice_details_panel.ts
@@ -16,7 +16,6 @@
import {Actions} from '../common/actions';
import {translateState} from '../common/thread_state';
-import {THREAD_STATE_TRACK_KIND} from '../core_plugins/thread_state';
import {Anchor} from '../widgets/anchor';
import {DetailsShell} from '../widgets/details_shell';
import {GridLayout} from '../widgets/grid_layout';
@@ -29,6 +28,7 @@
import {SlicePanel} from './slice_panel';
import {DurationWidget} from './widgets/duration';
import {Timestamp} from './widgets/timestamp';
+import {THREAD_STATE_TRACK_KIND} from '../core/track_kinds';
const MIN_NORMAL_SCHED_PRIORITY = 100;
diff --git a/ui/src/frontend/sql_table/tab.ts b/ui/src/frontend/sql_table/tab.ts
index 52b7d58..0affa5f 100644
--- a/ui/src/frontend/sql_table/tab.ts
+++ b/ui/src/frontend/sql_table/tab.ts
@@ -17,7 +17,7 @@
import {copyToClipboard} from '../../base/clipboard';
import {Icons} from '../../base/semantic_icons';
import {exists} from '../../base/utils';
-import {AddDebugTrackMenu} from '../../core_plugins/debug/add_debug_track_menu';
+import {AddDebugTrackMenu} from '../debug_tracks/add_debug_track_menu';
import {Button} from '../../widgets/button';
import {DetailsShell} from '../../widgets/details_shell';
import {Popup, PopupPosition} from '../../widgets/popup';
diff --git a/ui/src/frontend/tab_panel.ts b/ui/src/frontend/tab_panel.ts
index 8bade50..ac4cdcb 100644
--- a/ui/src/frontend/tab_panel.ts
+++ b/ui/src/frontend/tab_panel.ts
@@ -145,6 +145,23 @@
};
}
+ // Show single selection panels if they are registered
+ if (currentSelection.kind === 'single') {
+ const trackKey = currentSelection.trackKey;
+ const uri = globals.state.tracks[trackKey]?.uri;
+
+ if (uri) {
+ const trackDesc = globals.trackManager.resolveTrackInfo(uri);
+ const panel = trackDesc?.detailsPanel;
+ if (panel) {
+ return {
+ content: panel.render(currentSelection.eventId),
+ isLoading: panel.isLoading?.() ?? false,
+ };
+ }
+ }
+ }
+
// Get the first "truthy" details panel
let detailsPanels = globals.tabManager.detailsPanels.map((dp) => {
return {
diff --git a/ui/src/frontend/thread_slice_details_tab.ts b/ui/src/frontend/thread_slice_details_tab.ts
index 8c051eb..666b43f 100644
--- a/ui/src/frontend/thread_slice_details_tab.ts
+++ b/ui/src/frontend/thread_slice_details_tab.ts
@@ -38,7 +38,7 @@
} from './sql/thread_state';
import {asSliceSqlId} from './sql_types';
import {DurationWidget} from './widgets/duration';
-import {addDebugSliceTrack} from './debug_tracks';
+import {addDebugSliceTrack} from './debug_tracks/debug_tracks';
import {addQueryResultsTab} from './query_result_tab';
interface ContextMenuItem {
@@ -112,7 +112,13 @@
)
.then(() =>
addDebugSliceTrack(
- engine,
+ // NOTE(stevegolton): This is a temporary patch, this menu should
+ // become part of another plugin, at which point we can just use the
+ // plugin's context object.
+ {
+ engine,
+ registerTrack: (x) => globals.trackManager.registerTrack(x),
+ },
{
sqlSource: `
WITH merged AS (
diff --git a/ui/src/core_plugins/thread_slice/thread_slice_track.ts b/ui/src/frontend/thread_slice_track.ts
similarity index 83%
rename from ui/src/core_plugins/thread_slice/thread_slice_track.ts
rename to ui/src/frontend/thread_slice_track.ts
index 90e9f90..f7bcdd5 100644
--- a/ui/src/core_plugins/thread_slice/thread_slice_track.ts
+++ b/ui/src/frontend/thread_slice_track.ts
@@ -12,20 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {BigintMath as BIMath} from '../../base/bigint_math';
-import {clamp} from '../../base/math_utils';
-import {OnSliceClickArgs} from '../../frontend/base_slice_track';
-import {globals} from '../../frontend/globals';
+import {BigintMath as BIMath} from '../base/bigint_math';
+import {clamp} from '../base/math_utils';
+import {OnSliceClickArgs} from './base_slice_track';
+import {globals} from './globals';
import {
NAMED_ROW,
NamedSliceTrack,
NamedSliceTrackTypes,
-} from '../../frontend/named_slice_track';
-import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../frontend/slice_layout';
-import {NewTrackArgs} from '../../frontend/track';
-import {LONG_NULL} from '../../trace_processor/query_result';
-
-export const THREAD_SLICE_TRACK_KIND = 'ThreadSliceTrack';
+} from './named_slice_track';
+import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from './slice_layout';
+import {NewTrackArgs} from './track';
+import {LONG_NULL} from '../trace_processor/query_result';
export const THREAD_SLICE_ROW = {
// Base columns (tsq, ts, dur, id, depth).
diff --git a/ui/src/frontend/thread_state.ts b/ui/src/frontend/thread_state.ts
index d98eb30..6c2ed29 100644
--- a/ui/src/frontend/thread_state.ts
+++ b/ui/src/frontend/thread_state.ts
@@ -19,8 +19,6 @@
import {exists} from '../base/utils';
import {Actions} from '../common/actions';
import {translateState} from '../common/thread_state';
-import {CPU_SLICE_TRACK_KIND} from '../core_plugins/cpu_slices';
-import {THREAD_STATE_TRACK_KIND} from '../core_plugins/thread_state';
import {Engine} from '../trace_processor/engine';
import {LONG, NUM, NUM_NULL, STR_NULL} from '../trace_processor/query_result';
import {
@@ -34,6 +32,10 @@
import {scrollToTrackAndTs} from './scroll_helper';
import {asUtid, SchedSqlId, ThreadStateSqlId, Utid} from './sql_types';
import {getThreadInfo, ThreadInfo} from './thread_and_process_info';
+import {
+ CPU_SLICE_TRACK_KIND,
+ THREAD_STATE_TRACK_KIND,
+} from '../core/track_kinds';
// Representation of a single thread state object, corresponding to
// a row for the |thread_slice| table.
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index f028b15..8ce6eb7 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -42,7 +42,8 @@
} from './thread_state';
import {DurationWidget, renderDuration} from './widgets/duration';
import {Timestamp} from './widgets/timestamp';
-import {addDebugSliceTrack} from './debug_tracks';
+import {addDebugSliceTrack} from './debug_tracks/debug_tracks';
+import {globals} from './globals';
interface ThreadStateTabConfig {
// Id into |thread_state| sql table.
@@ -325,7 +326,13 @@
.query(`INCLUDE PERFETTO MODULE sched.thread_executing_span;`)
.then(() =>
addDebugSliceTrack(
- this.engine,
+ // NOTE(stevegolton): This is a temporary patch, this menu
+ // should become part of a critical path plugin, at which point
+ // we can just use the plugin's context object.
+ {
+ engine: this.engine,
+ registerTrack: (x) => globals.trackManager.registerTrack(x),
+ },
{
sqlSource: `
SELECT
@@ -363,7 +370,13 @@
)
.then(() =>
addDebugSliceTrack(
- this.engine,
+ // NOTE(stevegolton): This is a temporary patch, this menu
+ // should become part of a critical path plugin, at which point
+ // we can just use the plugin's context object.
+ {
+ engine: this.engine,
+ registerTrack: (x) => globals.trackManager.registerTrack(x),
+ },
{
sqlSource: `
SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
diff --git a/ui/src/frontend/tracks/custom_sql_table_slice_track.ts b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
index cd31610..73235b5 100644
--- a/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
+++ b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
@@ -14,7 +14,7 @@
import {v4 as uuidv4} from 'uuid';
-import {Disposable, DisposableCallback} from '../../base/disposable';
+import {Disposable} from '../../base/disposable';
import {Actions} from '../../common/actions';
import {generateSqlWithInternalLayout} from '../../common/internal_layout_utils';
import {LegacySelection} from '../../common/state';
@@ -89,10 +89,12 @@
whereClause: config.whereClause,
});
await this.engine.query(sql);
- return DisposableCallback.from(() => {
- this.engine.tryQuery(`DROP VIEW ${this.tableName}`);
- config.dispose?.dispose();
- });
+ return {
+ dispose: () => {
+ this.engine.tryQuery(`DROP VIEW ${this.tableName}`);
+ config.dispose?.dispose();
+ },
+ };
}
getSqlSource(): string {
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index b3c5b9f..6b24faa 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -333,10 +333,10 @@
// Resolve a track and its metadata through the track cache
private resolveTrack(key: string): TrackBundle {
const trackState = globals.state.tracks[key];
- const {uri, params, name, labels, closeable} = trackState;
+ const {uri, name, labels, closeable} = trackState;
const trackDesc = globals.trackManager.resolveTrackInfo(uri);
const trackCacheEntry =
- trackDesc && globals.trackManager.resolveTrack(key, trackDesc, params);
+ trackDesc && globals.trackManager.resolveTrack(key, trackDesc);
const trackFSM = trackCacheEntry;
const tags = trackCacheEntry?.desc.tags;
const trackIds = trackCacheEntry?.desc.trackIds;
diff --git a/ui/src/frontend/virtual_canvas.ts b/ui/src/frontend/virtual_canvas.ts
index 4035614..24b0d8a 100644
--- a/ui/src/frontend/virtual_canvas.ts
+++ b/ui/src/frontend/virtual_canvas.ts
@@ -32,7 +32,7 @@
* using the "floating" canvas technique described above.
*/
-import {Disposable, Trash} from '../base/disposable';
+import {Disposable, DisposableStack} from '../base/disposable';
import {
Rect,
Size,
@@ -66,7 +66,7 @@
}
export class VirtualCanvas implements Disposable {
- private readonly _trash = new Trash();
+ private readonly _trash = new DisposableStack();
private readonly _canvasElement: HTMLCanvasElement;
private readonly _targetElement: HTMLElement;
@@ -153,7 +153,7 @@
containerElement.addEventListener('scroll', updateCanvas, {
passive: true,
});
- this._trash.addCallback(() =>
+ this._trash.defer(() =>
containerElement.removeEventListener('scroll', updateCanvas),
);
@@ -164,7 +164,7 @@
resizeObserver.observe(containerElement);
resizeObserver.observe(targetElement);
- this._trash.addCallback(() => {
+ this._trash.defer(() => {
resizeObserver.disconnect();
});
@@ -174,7 +174,7 @@
const canvas = document.createElement('canvas');
canvas.style.position = 'absolute';
targetElement.appendChild(canvas);
- this._trash.addCallback(() => {
+ this._trash.defer(() => {
targetElement.removeChild(canvas);
});
diff --git a/ui/src/core_plugins/visualised_args/visualized_args_track.ts b/ui/src/frontend/visualized_args_track.ts
similarity index 70%
rename from ui/src/core_plugins/visualised_args/visualized_args_track.ts
rename to ui/src/frontend/visualized_args_track.ts
index 03f9f98..43b51cd 100644
--- a/ui/src/core_plugins/visualised_args/visualized_args_track.ts
+++ b/ui/src/frontend/visualized_args_track.ts
@@ -14,34 +14,44 @@
import m from 'mithril';
-import {Actions} from '../../common/actions';
-import {globals} from '../../frontend/globals';
-import {Button} from '../../widgets/button';
-import {Icons} from '../../base/semantic_icons';
-import {ThreadSliceTrack} from '../thread_slice/thread_slice_track';
-import {uuidv4Sql} from '../../base/uuid';
-import {NewTrackArgs} from '../../frontend/track';
-import {Disposable, DisposableCallback} from '../../base/disposable';
+import {Actions} from '../common/actions';
+import {globals} from './globals';
+import {Button} from '../widgets/button';
+import {Icons} from '../base/semantic_icons';
+import {ThreadSliceTrack} from './thread_slice_track';
+import {uuidv4Sql} from '../base/uuid';
+import {Disposable} from '../base/disposable';
+import {Engine} from '../trace_processor/engine';
-// Similar to a SliceTrack, but creates a view
+export interface VisualizedArgsTrackAttrs {
+ readonly trackKey: string;
+ readonly engine: Engine;
+ readonly trackId: number;
+ readonly maxDepth: number;
+ readonly argName: string;
+}
+
export class VisualisedArgsTrack extends ThreadSliceTrack {
- private viewName: string;
+ private readonly viewName: string;
+ private readonly argName: string;
- constructor(
- args: NewTrackArgs,
- trackId: number,
- maxDepth: number,
- private argName: string,
- ) {
+ constructor({
+ trackKey,
+ engine,
+ trackId,
+ maxDepth,
+ argName,
+ }: VisualizedArgsTrackAttrs) {
const uuid = uuidv4Sql();
const escapedArgName = argName.replace(/[^a-zA-Z]/g, '_');
const viewName = `__arg_visualisation_helper_${escapedArgName}_${uuid}_slice`;
- super(args, trackId, maxDepth, viewName);
+
+ super({engine, trackKey}, trackId, maxDepth, viewName);
this.viewName = viewName;
+ this.argName = argName;
}
async onInit(): Promise<Disposable> {
- // Create the helper view - just one which is relevant to this slice
await this.engine.query(`
create view ${this.viewName} as
with slice_with_arg as (
@@ -67,9 +77,9 @@
order by id;
`);
- return new DisposableCallback(() => {
- this.engine.tryQuery(`drop view ${this.viewName}`);
- });
+ return {
+ dispose: () => this.engine.tryQuery(`drop view ${this.viewName}`),
+ };
}
getTrackShellButtons(): m.Children {
diff --git a/ui/src/frontend/visualized_args_tracks.ts b/ui/src/frontend/visualized_args_tracks.ts
index cf498cb..465e354 100644
--- a/ui/src/frontend/visualized_args_tracks.ts
+++ b/ui/src/frontend/visualized_args_tracks.ts
@@ -12,10 +12,110 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-export const VISUALISED_ARGS_SLICE_TRACK_URI = 'perfetto.VisualisedArgs';
+import {assertExists} from '../base/logging';
+import {uuidv4} from '../base/uuid';
+import {Actions, AddTrackArgs} from '../common/actions';
+import {InThreadTrackSortKey} from '../common/state';
+import {Engine, NUM, TrackDescriptor} from '../public';
+import {globals} from './globals';
+import {VisualisedArgsTrack} from './visualized_args_track';
-export interface VisualisedArgsState {
- argName: string;
- maxDepth: number;
- trackId: number;
+const VISUALISED_ARGS_SLICE_TRACK_URI_PREFIX = 'perfetto.VisualisedArgs';
+
+// We need to add tracks from the core and from plugins. In order to add a debug
+// track we need to pass a context through with we can add the track. This is
+// different for plugins vs the core. This interface defines the generic shape
+// of this context, which can be supplied from a plugin or built from globals.
+//
+// TODO(stevegolton): In the future, both the core and plugins should have
+// access to some Context object which implements the various things we want to
+// do in a generic way, so that we don't have to do this mangling to get this to
+// work.
+interface Context {
+ engine: Engine;
+ registerTrack(track: TrackDescriptor): unknown;
+}
+
+export async function addVisualisedArgTracks(ctx: Context, argName: string) {
+ const escapedArgName = argName.replace(/[^a-zA-Z]/g, '_');
+ const tableName = `__arg_visualisation_helper_${escapedArgName}_slice`;
+
+ const result = await ctx.engine.query(`
+ drop table if exists ${tableName};
+
+ create table ${tableName} as
+ with slice_with_arg as (
+ select
+ slice.id,
+ slice.track_id,
+ slice.ts,
+ slice.dur,
+ slice.thread_dur,
+ NULL as cat,
+ args.display_value as name
+ from slice
+ join args using (arg_set_id)
+ where args.key='${argName}'
+ )
+ select
+ *,
+ (select count()
+ from ancestor_slice(s1.id) s2
+ join slice_with_arg s3 on s2.id=s3.id
+ ) as depth
+ from slice_with_arg s1
+ order by id;
+
+ select
+ track_id as trackId,
+ max(depth) as maxDepth
+ from ${tableName}
+ group by track_id;
+ `);
+
+ const tracksToAdd: AddTrackArgs[] = [];
+ const it = result.iter({trackId: NUM, maxDepth: NUM});
+ const addedTrackKeys: string[] = [];
+ for (; it.valid(); it.next()) {
+ const trackId = it.trackId;
+ const maxDepth = it.maxDepth;
+ const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
+ const track = globals.state.tracks[assertExists(trackKey)];
+ const utid = (track.trackSortKey as {utid?: number}).utid;
+ const key = uuidv4();
+ addedTrackKeys.push(key);
+
+ const uri = `${VISUALISED_ARGS_SLICE_TRACK_URI_PREFIX}#${uuidv4()}`;
+ ctx.registerTrack({
+ uri,
+ tags: {
+ metric: true, // TODO(stevegolton): Is this track really a metric?
+ },
+ trackFactory: (trackCtx) => {
+ return new VisualisedArgsTrack({
+ engine: ctx.engine,
+ trackKey: trackCtx.trackKey,
+ trackId,
+ maxDepth,
+ argName,
+ });
+ },
+ });
+
+ tracksToAdd.push({
+ key,
+ trackGroup: track.trackGroup,
+ name: argName,
+ trackSortKey:
+ utid === undefined
+ ? track.trackSortKey
+ : {utid, priority: InThreadTrackSortKey.VISUALISED_ARGS_TRACK},
+ uri,
+ });
+ }
+
+ globals.dispatchMultiple([
+ Actions.addTracks({tracks: tracksToAdd}),
+ Actions.sortThreadTracks({}),
+ ]);
}
diff --git a/ui/src/plugins/com.google.PixelMemory/index.ts b/ui/src/plugins/com.google.PixelMemory/index.ts
index c79faf2..0412e19 100644
--- a/ui/src/plugins/com.google.PixelMemory/index.ts
+++ b/ui/src/plugins/com.google.PixelMemory/index.ts
@@ -14,7 +14,7 @@
import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {addDebugCounterTrack} from '../../frontend/debug_tracks';
+import {addDebugCounterTrack} from '../../frontend/debug_tracks/debug_tracks';
class PixelMemory implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
@@ -39,6 +39,7 @@
`;
await ctx.engine.query(RSS_ALL);
await addDebugCounterTrack(
+ ctx,
{
sqlSource: `
SELECT
diff --git a/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts b/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts
index d60fdb7..7470c48 100644
--- a/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts
@@ -173,7 +173,7 @@
for (; it.valid(); it.next()) {
if (it.tstate_upid !== null) {
await addDebugSliceTrack(
- ctx.engine,
+ ctx,
{
sqlSource: `
SELECT ts, dur, name
@@ -187,7 +187,7 @@
);
}
await addDebugSliceTrack(
- ctx.engine,
+ ctx,
{
sqlSource: `
SELECT ts, dur, name
diff --git a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
index b4f20ca..b011143 100644
--- a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
@@ -171,7 +171,7 @@
callback: () => {
ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() => {
addDebugSliceTrack(
- ctx.engine,
+ ctx,
{
sqlSource: JANK_CUJ_QUERY,
columns: JANK_COLUMNS,
@@ -199,7 +199,7 @@
name: 'Add track: Android latency CUJs',
callback: () => {
addDebugSliceTrack(
- ctx.engine,
+ ctx,
{
sqlSource: LATENCY_CUJ_QUERY,
columns: LATENCY_COLUMNS,
@@ -224,7 +224,7 @@
callback: () => {
ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() =>
addDebugSliceTrack(
- ctx.engine,
+ ctx,
{
sqlSource: BLOCKING_CALLS_DURING_CUJS_QUERY,
columns: BLOCKING_CALLS_DURING_CUJS_COLUMNS,
diff --git a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
index 03ed11c..bfaac60 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
@@ -24,6 +24,15 @@
SimpleCounterTrackConfig,
} from '../../frontend/simple_counter_track';
+interface ContainedTrace {
+ uuid: string;
+ subscription: string;
+ trigger: string;
+ // NB: these are millis.
+ ts: number;
+ dur: number;
+}
+
const DEFAULT_NETWORK = `
with base as (
select
@@ -557,7 +566,7 @@
with_ratio as (
select
ts,
- iif(dur is null, 0, 100.0 * cpu_dur / dur) as value,
+ iif(dur is null, 0, max(0, 100.0 * cpu_dur / dur)) as value,
case cluster when 0 then 'little' when 1 then 'mid' when 2 then 'big' else 'cl-' || cluster end as cluster,
case
when uid = 0 then 'AID_ROOT'
@@ -1259,7 +1268,7 @@
str_value AS name,
ifnull(
(select package_name from package_list where uid = int_value % 100000),
- int_value) as package
+ "uid="||int_value) as package
FROM android_battery_stats_event_slices
WHERE track_name = "battery_stats.longwake"`,
undefined,
@@ -1723,6 +1732,38 @@
);
}
+ async addContainedTraces(
+ ctx: PluginContextTrace,
+ containedTraces: ContainedTrace[],
+ ): Promise<void> {
+ const bySubscription = new Map<string, ContainedTrace[]>();
+ for (const trace of containedTraces) {
+ if (!bySubscription.has(trace.subscription)) {
+ bySubscription.set(trace.subscription, []);
+ }
+ bySubscription.get(trace.subscription)!.push(trace);
+ }
+
+ bySubscription.forEach((traces, subscription) =>
+ this.addSliceTrack(
+ ctx,
+ subscription,
+ traces
+ .map(
+ (t) => `SELECT
+ CAST(${t.ts} * 1e6 AS int) AS ts,
+ CAST(${t.dur} * 1e6 AS int) AS dur,
+ '${t.trigger === '' ? 'Trace' : t.trigger}' AS name,
+ 'http://go/trace-uuid/${t.uuid}' AS link
+ `,
+ )
+ .join(' UNION ALL '),
+ 'Other traces',
+ ['link'],
+ ),
+ );
+ }
+
async findFeatures(e: Engine): Promise<Set<string>> {
const features = new Set<string>();
@@ -1762,6 +1803,9 @@
async addTracks(ctx: PluginContextTrace): Promise<void> {
const features: Set<string> = await this.findFeatures(ctx.engine);
+ const containedTraces = (ctx.openerPluginArgs?.containedTraces ??
+ []) as ContainedTrace[];
+
await this.addNetworkSummary(ctx, features),
await this.addModemDetail(ctx, features);
await this.addKernelWakelocks(ctx, features);
@@ -1769,6 +1813,7 @@
await this.addDeviceState(ctx, features);
await this.addHighCpu(ctx, features);
await this.addBluetooth(ctx, features);
+ await this.addContainedTraces(ctx, containedTraces);
}
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
diff --git a/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts b/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
index 34fd087..1c85c98 100644
--- a/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
@@ -14,20 +14,19 @@
import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
import {addDebugSliceTrack} from '../../public';
-import {Engine} from '../../trace_processor/engine';
class AndroidNetwork implements Plugin {
// Adds a debug track using the provided query and given columns. The columns
// must be start with ts, dur, and a name column. The name column and all
// following columns are shown as arguments in slice details.
async addSimpleTrack(
- engine: Engine,
+ ctx: PluginContextTrace,
trackName: string,
tableOrQuery: string,
columns: string[],
): Promise<void> {
await addDebugSliceTrack(
- engine,
+ ctx,
{
sqlSource: `SELECT ${columns.join(',')} FROM ${tableOrQuery}`,
columns: columns,
@@ -50,7 +49,7 @@
await ctx.engine.query(`SELECT IMPORT('android.battery_stats');`);
await this.addSimpleTrack(
- ctx.engine,
+ ctx,
track,
`(SELECT *
FROM android_battery_stats_event_slices
@@ -89,7 +88,7 @@
// The first group column is used for the slice name.
const groupCols = groupby.replaceAll(' ', '').split(',');
await this.addSimpleTrack(
- ctx.engine,
+ ctx,
trackName || 'Network Activity',
`android_network_activity_${suffix}`,
['ts', 'dur', ...groupCols, 'packet_length', 'packet_count'],
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
index 25f11f4..b256bb3 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
@@ -18,11 +18,10 @@
PluginContextTrace,
PluginDescriptor,
} from '../../public';
-import {Engine} from '../../trace_processor/engine';
class AndroidPerf implements Plugin {
async addAppProcessStartsDebugTrack(
- engine: Engine,
+ ctx: PluginContextTrace,
reason: string,
sliceName: string,
): Promise<void> {
@@ -36,7 +35,7 @@
'table_name',
];
await addDebugSliceTrack(
- engine,
+ ctx,
{
sqlSource: `
SELECT
@@ -172,11 +171,7 @@
const startReason = ['activity', 'service', 'broadcast', 'provider'];
for (const reason of startReason) {
- await this.addAppProcessStartsDebugTrack(
- ctx.engine,
- reason,
- 'process_name',
- );
+ await this.addAppProcessStartsDebugTrack(ctx, reason, 'process_name');
}
},
});
@@ -191,11 +186,7 @@
const startReason = ['activity', 'service', 'broadcast'];
for (const reason of startReason) {
- await this.addAppProcessStartsDebugTrack(
- ctx.engine,
- reason,
- 'intent',
- );
+ await this.addAppProcessStartsDebugTrack(ctx, reason, 'intent');
}
},
});
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
index b8feb57..4270a76 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
@@ -83,7 +83,7 @@
`;
await addDebugSliceTrack(
- ctx.engine,
+ ctx,
{
sqlSource:
sqlPrefix +
diff --git a/ui/src/plugins/dev.perfetto.Chaos/index.ts b/ui/src/plugins/dev.perfetto.Chaos/index.ts
index c18ff7f..028b8dd 100644
--- a/ui/src/plugins/dev.perfetto.Chaos/index.ts
+++ b/ui/src/plugins/dev.perfetto.Chaos/index.ts
@@ -52,7 +52,7 @@
name: 'Chaos: add crashing debug track',
callback: () => {
addDebugSliceTrack(
- ctx.engine,
+ ctx,
{
sqlSource: `
syntactically
diff --git a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
index f15d3eb..f07b62d 100644
--- a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
+++ b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
@@ -79,6 +79,12 @@
name: 'Disable timeline sync',
callback: () => this.disableTimelineSync(this._sessionId),
});
+ ctx.registerCommand({
+ id: `dev.perfetto.SplitScreen#toggleTimelineSync`,
+ name: 'Toggle timeline sync with other PerfettoUI tabs',
+ callback: () => this.toggleTimelineSync(),
+ defaultHotkey: 'Mod+Alt+S',
+ });
// Start advertising this tab. This allows the command run in other
// instances to discover us.
@@ -128,6 +134,14 @@
} as SyncMessage);
}
+ private toggleTimelineSync() {
+ if (this._sessionId === 0) {
+ this.showTimelineSyncDialog();
+ } else {
+ this.disableTimelineSync(this._sessionId);
+ }
+ }
+
private showTimelineSyncDialog() {
let clientsSelect: HTMLSelectElement;
diff --git a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
index 9f627b8..60f72cc 100644
--- a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
+++ b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
@@ -19,8 +19,8 @@
PluginDescriptor,
STR_NULL,
} from '../../public';
-import {ASYNC_SLICE_TRACK_KIND} from '../../core_plugins/async_slices';
import {AsyncSliceTrack} from '../../core_plugins/async_slices/async_slice_track';
+import {ASYNC_SLICE_TRACK_KIND} from '../../public';
// This plugin renders visualizations of runtime power state transitions for
// Linux kernel devices (devices managed by Linux drivers).
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index 293f4bd..f5c4bde 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -18,12 +18,13 @@
import {Span, duration, time} from '../base/time';
import {Migrate, Store} from '../base/store';
import {ColorScheme} from '../core/colorizer';
-import {LegacySelection, Selection} from '../common/state';
+import {LegacySelection, PrimaryTrackSortKey, Selection} from '../common/state';
import {PanelSize} from '../frontend/panel';
import {Engine} from '../trace_processor/engine';
import {UntypedEventSet} from '../core/event_set';
import {TraceContext} from '../frontend/globals';
import {PromptOption} from '../frontend/omnibox_manager';
+import {Optional} from '../base/utils';
export {Engine} from '../trace_processor/engine';
export {
@@ -37,12 +38,10 @@
export {BottomTabToSCSAdapter} from './utils';
export {createStore, Migrate, Store} from '../base/store';
export {PromptOption} from '../frontend/omnibox_manager';
+export {PrimaryTrackSortKey} from '../common/state';
-// This is a temporary fix until this is available in the plugin API.
-export {
- createDebugSliceTrackActions,
- addDebugSliceTrack,
-} from '../frontend/debug_tracks';
+export {addDebugSliceTrack} from '../frontend/debug_tracks/debug_tracks';
+export * from '../core/track_kinds';
export interface Slice {
// These properties are updated only once per query result when the Slice
@@ -155,19 +154,6 @@
export interface TrackContext {
// This track's key, used for making selections et al.
trackKey: string;
-
- // Set of params passed in when the track was created.
- params: unknown;
-
- // Creates a new store overlaying the track instance's state object.
- // A migrate function must be passed to convert any existing state to a
- // compatible format.
- // When opening a fresh trace, the value of |init| will be undefined, and
- // state should be updated to an appropriate default value.
- // When loading a permalink, the value of |init| will be whatever was saved
- // when the permalink was shared, which might be from an old version of this
- // track.
- mountStore<State>(migrate: Migrate<State>): Store<State>;
}
export interface SliceRect {
@@ -258,36 +244,12 @@
// Placeholder - presently unused.
displayName?: string;
-}
-// Tracks within track groups (usually corresponding to processes) are sorted.
-// As we want to group all tracks related to a given thread together, we use
-// two keys:
-// - Primary key corresponds to a priority of a track block (all tracks related
-// to a given thread or a single track if it's not thread-associated).
-// - Secondary key corresponds to a priority of a given thread-associated track
-// within its thread track block.
-// Each track will have a sort key, which either a primary sort key
-// (for non-thread tracks) or a tid and secondary sort key (mapping of tid to
-// primary sort key is done independently).
-export enum PrimaryTrackSortKey {
- DEBUG_TRACK,
- NULL_TRACK,
- PROCESS_SCHEDULING_TRACK,
- PROCESS_SUMMARY_TRACK,
- EXPECTED_FRAMES_SLICE_TRACK,
- ACTUAL_FRAMES_SLICE_TRACK,
- PERF_SAMPLES_PROFILE_TRACK,
- HEAP_PROFILE_TRACK,
- MAIN_THREAD,
- RENDER_THREAD,
- GPU_COMPLETION_THREAD,
- CHROME_IO_THREAD,
- CHROME_COMPOSITOR_THREAD,
- ORDINARY_THREAD,
- COUNTER_TRACK,
- ASYNC_SLICE_TRACK,
- ORDINARY_TRACK,
+ // Optional: method to look up the start and duration of an event on this track
+ getEventBounds?: (id: number) => Promise<Optional<{ts: time; dur: duration}>>;
+
+ // Optional: A details panel to use when this track is selected.
+ detailsPanel?: TrackSelectionDetailsPanel;
}
export interface SliceTrackColNames {
@@ -348,6 +310,11 @@
isLoading?(): boolean;
}
+export interface TrackSelectionDetailsPanel {
+ render(id: number): m.Children;
+ isLoading?(): boolean;
+}
+
// Similar to PluginContext but with additional methods to operate on the
// currently loaded trace. Passed to trace-relevant hooks on a plugin instead of
// PluginContext.
@@ -485,9 +452,6 @@
// A human readable name for this track - displayed in the track shell.
displayName: string;
- // Optional: An opaque object used to customize this instance of the track.
- params?: unknown;
-
// Optional: Used to define default sort order for new traces.
// Note: This will be deprecated soon in favour of tags & sort rules.
sortKey?: PrimaryTrackSortKey;
diff --git a/ui/src/trace_processor/sql_utils.ts b/ui/src/trace_processor/sql_utils.ts
index bb3836d..b9f220b 100644
--- a/ui/src/trace_processor/sql_utils.ts
+++ b/ui/src/trace_processor/sql_utils.ts
@@ -13,6 +13,7 @@
// limitations under the License.
import {SortDirection} from '../base/comparison_utils';
+import {AsyncDisposable} from '../base/disposable';
import {isString} from '../base/object_utils';
import {sqliteString} from '../base/string_utils';
@@ -152,4 +153,39 @@
count: NUM,
}).count;
}
+
export {SqlValue};
+
+/**
+ * Asynchronously creates a 'perfetto' table using the given engine and returns
+ * an disposable object to handle its cleanup.
+ *
+ * @param engine - The database engine to execute the query.
+ * @param tableName - The name of the table to be created.
+ * @param expression - The SQL expression to define the table.
+ * @returns An AsyncDisposable which drops the created table when disposed.
+ *
+ * @example
+ * const engine = new Engine();
+ * const tableName = 'my_perfetto_table';
+ * const expression = 'SELECT * FROM source_table';
+ *
+ * const table = await createPerfettoTable(engine, tableName, expression);
+ *
+ * // Use the table...
+ *
+ * // Cleanup the table when done
+ * await table.disposeAsync();
+ */
+export async function createPerfettoTable(
+ engine: Engine,
+ tableName: string,
+ expression: string,
+): Promise<AsyncDisposable> {
+ await engine.query(`CREATE PERFETTO TABLE ${tableName} AS ${expression}`);
+ return {
+ disposeAsync: async () => {
+ await engine.tryQuery(`DROP TABLE IF EXISTS ${tableName}`);
+ },
+ };
+}
diff --git a/ui/src/widgets/virtual_scroll_helper.ts b/ui/src/widgets/virtual_scroll_helper.ts
index 4fbe5c1..f544f86 100644
--- a/ui/src/widgets/virtual_scroll_helper.ts
+++ b/ui/src/widgets/virtual_scroll_helper.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {Trash} from '../base/disposable';
+import {DisposableStack} from '../base/disposable';
import * as Geometry from '../base/geom';
export interface VirtualScrollHelperOpts {
@@ -30,7 +30,7 @@
}
export class VirtualScrollHelper {
- private readonly _trash = new Trash();
+ private readonly _trash = new DisposableStack();
private readonly _data: Data[] = [];
constructor(
@@ -51,7 +51,7 @@
containerElement.addEventListener('scroll', recalculateRects, {
passive: true,
});
- this._trash.addCallback(() =>
+ this._trash.defer(() =>
containerElement.removeEventListener('scroll', recalculateRects),
);
@@ -62,7 +62,7 @@
resizeObserver.observe(containerElement);
resizeObserver.observe(sliderElement);
- this._trash.addCallback(() => {
+ this._trash.defer(() => {
resizeObserver.disconnect();
});
}
diff --git a/ui/src/widgets/virtual_table.ts b/ui/src/widgets/virtual_table.ts
index 0b97b96..99e11e1 100644
--- a/ui/src/widgets/virtual_table.ts
+++ b/ui/src/widgets/virtual_table.ts
@@ -13,7 +13,7 @@
// limitations under the License.
import m from 'mithril';
-import {Trash} from '../base/disposable';
+import {DisposableStack} from '../base/disposable';
import {findRef, toHTMLElement} from '../base/dom_utils';
import {Rect} from '../base/geom';
import {assertExists} from '../base/logging';
@@ -144,7 +144,7 @@
export class VirtualTable implements m.ClassComponent<VirtualTableAttrs> {
private readonly CONTAINER_REF = 'CONTAINER';
private readonly SLIDER_REF = 'SLIDER';
- private readonly trash = new Trash();
+ private readonly trash = new DisposableStack();
private renderBounds = {rowStart: 0, rowEnd: 0};
view({attrs}: m.Vnode<VirtualTableAttrs>): m.Children {
@@ -253,7 +253,7 @@
},
},
]);
- this.trash.add(virtualScrollHelper);
+ this.trash.use(virtualScrollHelper);
}
onremove(_: m.VnodeDOM<VirtualTableAttrs>) {