ui: Create CounterTrack - a flexible new counter track impl Change-Id: I9921bf3708148d8ca5ecfa6c60392921f02bcc55
diff --git a/ui/src/components/tracks/breakdown_tracks.ts b/ui/src/components/tracks/breakdown_tracks.ts index 551b5a5..03d1543 100644 --- a/ui/src/components/tracks/breakdown_tracks.ts +++ b/ui/src/components/tracks/breakdown_tracks.ts
@@ -15,7 +15,6 @@ import {sqliteString} from '../../base/string_utils'; import {uuidv4} from '../../base/uuid'; import {SliceTrack} from './slice_track'; -import {createQueryCounterTrack} from '../../components/tracks/query_counter_track'; import {Trace} from '../../public/trace'; import {TrackNode} from '../../public/workspace'; import {SourceDataset} from '../../trace_processor/dataset'; @@ -25,7 +24,9 @@ NUM_NULL, STR, LONG_NULL, + NUM, } from '../../trace_processor/query_result'; +import {CounterTrack} from './counter_track'; import {TrackRenderer} from '../../public/track'; /** @@ -425,19 +426,19 @@ name, newFilters, (uri: string, filtersClause: string) => { - return createQueryCounterTrack({ + return CounterTrack.createMaterialized({ trace: this.props.trace, uri, - data: { - sqlSource: ` + dataset: new SourceDataset({ + src: ` SELECT ts, value FROM (${this.getAggregationQuery(filtersClause)}) `, - }, - columns: { - ts: 'ts', - value: 'value', - }, + schema: { + ts: LONG, + value: NUM, + }, + }), }); }, (filterClause) => this.getCounterTrackSortOrder(filterClause),
diff --git a/ui/src/components/tracks/counter_track.ts b/ui/src/components/tracks/counter_track.ts new file mode 100644 index 0000000..96a87c8 --- /dev/null +++ b/ui/src/components/tracks/counter_track.ts
@@ -0,0 +1,349 @@ +// Copyright (C) 2025 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 {Time} from '../../base/time'; +import {TrackEventDetailsPanel} from '../../public/details_panel'; +import {TrackEventDetails, TrackEventSelection} from '../../public/selection'; +import {Trace} from '../../public/trace'; +import {DatasetSchema, SourceDataset} from '../../trace_processor/dataset'; +import {SqlValue, LONG, NUM} from '../../trace_processor/query_result'; +import {createPerfettoTable} from '../../trace_processor/sql_utils'; +import {removeFalsyValues} from '../../base/array_utils'; +import {CounterTrackDetailsPanel} from './counter_track_details_panel'; +import {BaseCounterTrack, CounterOptions} from './base_counter_track'; +import {TrackMouseEvent} from '../../public/track'; + +export interface CounterTrackAttrs<T extends DatasetSchema> { + /** + * The trace object used by the track for accessing the query engine and other + * trace-related resources. + */ + readonly trace: Trace; + + /** + * The URI of this track, which must match the URI specified in the track + * descriptor. + */ + readonly uri: string; + + /** + * The source dataset defining the content of this track. + * + * A source dataset consists of a SQL select statement or table name with a + * column schema and optional filtering information. It represents a set of + * instructions to extract counter-like rows from trace processor that + * represents the content of this track, which avoids the need to materialize + * all counter values into JavaScript beforehand. This approach minimizes + * memory usage and improves performance by only materializing the necessary + * rows on demand. + * + * Required columns: + * - `ts` (LONG): Timestamp of each counter value (in nanoseconds). + * - `value` (NUM): The counter value at the given timestamp. + * + * Auto-generated columns (if not provided): + * - `id` (NUM): Unique identifier for counter points in the track. If not + * provided in the dataset, will be automatically generated using ROW_NUMBER() + * ordered by timestamp. + */ + readonly dataset: SourceDataset<T> | (() => SourceDataset<T>); + + /** + * An optional root table name for the track's data source. + * + * This typically represents a well-known table name and serves as the root + * `id` namespace for the track. It is primarily used for resolving events + * with a combination of table name and `id`. + */ + readonly rootTableName?: string; + + /** + * Default counter options that will be merged with user preferences. + * These provide the initial configuration for y-axis display, scaling, + * and other counter-specific rendering options. + */ + readonly defaultOptions?: Partial<CounterOptions>; + + /** + * An optional callback to customize the details panel for counter points on + * this track. Called whenever a counter point is selected. + * + * If omitted, a default details panel will be created that displays all + * fields from the dataset with appropriate formatting for common counter + * properties (ts, value). + */ + detailsPanel?(row: T): TrackEventDetailsPanel; + + /** + * An optional function to override the tooltip content for each counter point. + * If omitted, a default counter tooltip will be used. + */ + tooltip?(row: T, formattedValue: string): m.Children; + + /** + * An optional function to define buttons which are displayed on the track + * shell. This function is called every Mithril render cycle. + */ + shellButtons?(): m.Children; +} + +export type CounterRowSchema = { + readonly id?: number; + readonly ts: bigint; + readonly value: number; +} & DatasetSchema; + +function getDataset<T extends DatasetSchema>( + attrs: CounterTrackAttrs<T>, +): SourceDataset<T> { + const dataset = attrs.dataset; + return typeof dataset === 'function' ? dataset() : dataset; +} + +export class CounterTrack<T extends CounterRowSchema> extends BaseCounterTrack { + readonly rootTableName?: string; + + /** + * Factory function to create a CounterTrack. This is purely an alias for new + * CounterTrack() but exists for symmetry with createMaterialized() + * below. + * + * @param attrs The track attributes + * @returns A fully initialized CounterTrack + */ + static create<T extends CounterRowSchema>( + attrs: CounterTrackAttrs<T>, + ): CounterTrack<T> { + return new CounterTrack(attrs); + } + + /** + * Async factory function to create a CounterTrack, first materializing + * the dataset into a perfetto table. This can be more efficient if for + * example the dataset is a complex query with multiple joins or window + * functions, so materializing it up front can improve rendering performance, + * for a one-time cost. + * + * However, it does have some downsides: + * - You're front loading the cost of materialization, which can slow down + * trace load times. + * - It uses more memory, as the entire dataset is materialized in memory as a + * new table. + * - It means that this dataset track has a new root source table, which makes + * it impossible to combine with other tracks for the purposes of bulk + * operations such as aggregations or search. + * + * @param attrs The track attributes + * @returns A fully initialized CounterTrack + */ + static async createMaterialized<T extends CounterRowSchema>( + attrs: CounterTrackAttrs<T>, + ): Promise<CounterTrack<T>> { + const originalDataset = getDataset(attrs); + // Create materialized table from the render query - we might as well + // materialize the calculated columns that are missing from the source + // dataset while we're here as this will improve performance at runtime. + const materializedTable = await createPerfettoTable({ + engine: attrs.trace.engine, + as: generateRenderQuery(originalDataset), + }); + + // Create a new dataset that queries the materialized table + const materializedDataset = new SourceDataset({ + src: materializedTable.name, + schema: { + ...originalDataset.schema, + + // We know we must have these columns now as they are injected in + // generateRenderQuery(), so we can add them to the schema to avoid the + // DST from adding them again. + id: NUM, + ts: LONG, + value: NUM, + }, + }); + + return new CounterTrack({ + ...attrs, + dataset: materializedDataset, + }); + } + + private constructor(private readonly attrs: CounterTrackAttrs<T>) { + super(attrs.trace, attrs.uri, attrs.defaultOptions ?? {}); + this.rootTableName = attrs.rootTableName; + } + + override getSqlSource(): string { + const dataset = getDataset(this.attrs); + return generateRenderQuery(dataset); + } + + getDataset() { + return getDataset(this.attrs); + } + + detailsPanel(sel: TrackEventSelection): TrackEventDetailsPanel | undefined { + if (this.attrs.detailsPanel) { + // This type assertion is required as a temporary patch while the + // specifics of selection details are being worked out. Eventually we will + // change the selection details to be purely based on dataset, but there + // are currently some use cases preventing us from doing so. For now, this + // type assertion is safe as we know we just returned the entire row from + // from getSelectionDetails() so we know it must at least implement the + // row's type `T`. + return this.attrs.detailsPanel(sel as unknown as T); + } else { + // Provide a default details panel that shows all dataset fields + const dataset = getDataset(this.attrs); + return new CounterTrackDetailsPanel( + this.trace, + dataset, + sel as unknown as T, + this.getSqlSource(), // Pass the SQL source for enhanced details + ); + } + } + + async getSelectionDetails( + id: number, + ): Promise<TrackEventDetails | undefined> { + const {trace} = this.attrs; + const dataset = getDataset(this.attrs); + + // If our dataset already has an id column, we can use it directly, + // otherwise we need to generate one using row number. + const query = (function () { + if (dataset.implements({id: NUM})) { + return dataset.query(); + } else { + return ` + SELECT + ROW_NUMBER() OVER (ORDER BY ts) AS id, + * + FROM (${dataset.query()}) + `; + } + })(); + + const result = await trace.engine.query(` + SELECT * + FROM (${query}) + WHERE id = ${id} + `); + + const row = result.iter(dataset.schema); + if (!row.valid()) return undefined; + + // Pull the fields out from the results + const data: {[key: string]: SqlValue} = {}; + for (const col of result.columns()) { + data[col] = row.get(col); + } + + return { + ...data, + ts: Time.fromRaw(row.ts), + }; + } + + override getTrackShellButtons(): m.Children { + const baseButtons = super.getTrackShellButtons(); + const customButtons = this.attrs.shellButtons?.(); + return [baseButtons, customButtons]; + } + + override renderTooltip(): m.Children { + // Get the base counter tooltip from BaseCounterTrack + const baseTooltip = super.renderTooltip(); + + // For now, we'll use the base tooltip since we can't access private members + // Custom tooltip functionality would need to be implemented differently + // or the BaseCounterTrack would need to expose the necessary methods + return baseTooltip; + } + + onMouseClick({x, timescale}: TrackMouseEvent): boolean { + const time = timescale.pxToHpTime(x).toTime('floor'); + const dataset = getDataset(this.attrs); + + // If our dataset already has an id column, we can use it directly, + // otherwise we need to generate one using row number. + const query = (function () { + if (dataset.implements({id: NUM})) { + return dataset.query(); + } else { + return ` + SELECT + ROW_NUMBER() OVER (ORDER BY ts) AS id, + * + FROM (${dataset.query()}) + `; + } + })(); + + this.engine + .query( + ` + SELECT + id + FROM (${query}) + WHERE + ts < ${time} + ORDER BY ts DESC + LIMIT 1 + `, + ) + .then((result) => { + const it = result.iter({ + id: NUM, + }); + if (!it.valid()) { + return; + } + const id = it.id; + this.trace.selection.selectTrackEvent(this.uri, id); + }); + + return true; + } +} + +// Generate a query to use for generating counter data to be rendered +function generateRenderQuery<T extends DatasetSchema>( + dataset: SourceDataset<T>, +) { + const hasId = dataset.implements({id: NUM}); + const hasTs = dataset.implements({ts: LONG}); + const hasValue = dataset.implements({value: NUM}); + + if (!hasTs) { + throw new Error('Counter dataset must have a "ts" column'); + } + if (!hasValue) { + throw new Error('Counter dataset must have a "value" column'); + } + + const cols = removeFalsyValues([ + // If we have no id, automatically generate one using row number. + !hasId && 'ROW_NUMBER() OVER (ORDER BY ts) AS id', + ]); + + if (cols.length === 0) { + return dataset.query(); + } else { + return `select ${cols.join(', ')}, * from (${dataset.query()})`; + } +}
diff --git a/ui/src/components/tracks/counter_track_details_panel.ts b/ui/src/components/tracks/counter_track_details_panel.ts new file mode 100644 index 0000000..a13072b --- /dev/null +++ b/ui/src/components/tracks/counter_track_details_panel.ts
@@ -0,0 +1,228 @@ +// Copyright (C) 2025 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 {TrackEventDetailsPanel} from '../../public/details_panel'; +import {Trace} from '../../public/trace'; +import {SourceDataset} from '../../trace_processor/dataset'; +import {sqlValueToReadableString} from '../../trace_processor/sql_utils'; +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 '../widgets/timestamp'; +import {CounterRowSchema} from './counter_track'; +import {exists} from '../../base/utils'; +import {duration, time, Time} from '../../base/time'; +import { + LONG, + LONG_NULL, + NUM, + NUM_NULL, +} from '../../trace_processor/query_result'; +import {Engine} from '../../trace_processor/engine'; +import {Arg} from '../sql_utils/args'; +import {DurationWidget} from '../widgets/duration'; + +/** + * Default details panel for CounterTrack that displays all fields from + * the dataset query. + * + * This panel provides a "better than nothing" experience when no custom + * details panel is specified. It automatically shows: + * - Common counter fields (ts, value) with appropriate formatting + * - All other dataset columns as readable strings + */ +export class CounterTrackDetailsPanel<T extends CounterRowSchema> + implements TrackEventDetailsPanel +{ + private counterDetails?: CounterDetails; + + constructor( + private readonly trace: Trace, + private readonly dataset: SourceDataset<T>, + private readonly data: T, + private readonly rootTableOrSubquery?: string, + ) {} + + async load() { + if (this.data.id !== undefined && this.rootTableOrSubquery) { + this.counterDetails = await loadCounterDetails( + this.trace.engine, + this.data.id, + this.rootTableOrSubquery, + ); + } + } + + render() { + const data = this.data; + const details = this.counterDetails; + + return m( + DetailsShell, + { + title: 'Counter', + }, + m( + GridLayout, + m( + Section, + {title: 'Details'}, + m(Tree, [ + // Basic counter information + exists(data.id) && + m(TreeNode, { + left: 'ID', + right: data.id, + }), + m(TreeNode, { + left: 'Timestamp', + right: m(Timestamp, { + trace: this.trace, + ts: Time.fromRaw(data.ts), + }), + }), + m(TreeNode, { + left: 'Value', + right: data.value.toLocaleString(), + }), + + // Enhanced details if available + details && [ + m(TreeNode, { + left: 'Duration to next', + right: m(DurationWidget, { + trace: this.trace, + dur: details.duration, + }), + }), + m(TreeNode, { + left: 'Delta from previous', + right: details.delta.toLocaleString(), + }), + // Show args if available + details.args && + details.args.length > 0 && [ + m(TreeNode, { + left: 'Arguments', + right: '', + }), + ...details.args.map((arg) => + m(TreeNode, { + left: ` ${arg.key}`, + right: arg.displayValue, + }), + ), + ], + ], + + // List all other fields from the dataset's schema + ...Object.keys(this.dataset.schema) + .filter((key) => !['id', 'ts', 'value'].includes(key)) + .map((key) => { + const value = data[key]; + return m(TreeNode, { + left: key, + right: sqlValueToReadableString(value), + }); + }), + ]), + ), + ), + ); + } +} + +interface CounterDetails { + // The "left" timestamp of the counter sample T(N) + readonly ts: time; + + // The delta between this sample and the next one's timestamps T(N+1) - T(N) + readonly duration: duration; + + // The value of the counter sample F(N) + readonly value: number; + + // The delta between this sample's value and the previous one F(N) - F(N-1) + readonly delta: number; + + readonly args?: Arg[]; +} + +async function loadCounterDetails( + engine: Engine, + id: number, + rootTableOrSubquery: string, +): Promise<CounterDetails> { + const query = ` + WITH data AS ( + SELECT * FROM (${rootTableOrSubquery}) + ), + CURRENT AS ( + SELECT + id, + ts, + value + FROM data + WHERE id = ${id} + ), + PREV AS ( + SELECT + value + FROM data + WHERE ts < (SELECT ts FROM CURRENT) + ORDER BY ts DESC + LIMIT 1 + ), + NEXT AS ( + SELECT + ts + FROM data + WHERE ts > (SELECT ts FROM CURRENT) + ORDER BY ts ASC + LIMIT 1 + ) + SELECT + id, + ts as leftTs, + value, + (SELECT value FROM PREV) as prevValue, + (SELECT ts FROM NEXT) as rightTs + FROM CURRENT + `; + + const counter = await engine.query(query); + const row = counter.iter({ + value: NUM, + prevValue: NUM_NULL, + leftTs: LONG, + rightTs: LONG_NULL, + }); + + if (!row.valid()) { + throw new Error(`Counter with id ${id} not found`); + } + + 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; + // const args = + // argSetId == null ? undefined : await getArgs(engine, asArgSetId(argSetId)); + return {ts: leftTs, value, delta, duration}; +}
diff --git a/ui/src/components/tracks/counter_track_example.ts b/ui/src/components/tracks/counter_track_example.ts new file mode 100644 index 0000000..a8eff45 --- /dev/null +++ b/ui/src/components/tracks/counter_track_example.ts
@@ -0,0 +1,94 @@ +// Copyright (C) 2025 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. + +/** + * Example usage of CounterTrack - this file demonstrates how to use the + * CounterTrack implementation and can be used for testing. + */ + +import {SourceDataset} from '../../trace_processor/dataset'; +import {LONG, NUM} from '../../trace_processor/query_result'; +import {CounterTrack, CounterTrackAttrs} from './counter_track'; +import {Trace} from '../../public/trace'; + +// Example: Creating a counter track for CPU usage +export function createCpuUsageCounterTrack(trace: Trace, uri: string) { + const dataset = new SourceDataset({ + src: ` + SELECT ts, value, cpu + FROM counter + WHERE name = 'CPU Usage' + ORDER BY ts + `, + schema: { + ts: LONG, + value: NUM, + cpu: NUM, + }, + }); + + const attrs: CounterTrackAttrs<typeof dataset.schema> = { + trace, + uri, + dataset, + defaultOptions: { + yMode: 'value', + yDisplay: 'zero', + yRange: 'all', + unit: '%', + yOverrideMaximum: 100, + yOverrideMinimum: 0, + }, + shellButtons: () => [], + }; + + return CounterTrack.create(attrs); +} + +// Example: Creating a memory usage counter track with materialization +export async function createMemoryUsageCounterTrack(trace: Trace, uri: string) { + const dataset = new SourceDataset({ + src: ` + SELECT + ts, + value / 1024 / 1024 as value, + name + FROM counter + WHERE name LIKE '%memory%' + ORDER BY ts + `, + schema: { + ts: LONG, + value: NUM, + name: 'STRING', + }, + }); + + const attrs: CounterTrackAttrs<typeof dataset.schema> = { + trace, + uri, + dataset, + defaultOptions: { + yMode: 'value', + yDisplay: 'zero', + unit: 'MB', + chartHeightSize: 4, + }, + tooltip: (row, formattedValue) => + `Memory: ${formattedValue} at ${row.name}`, + }; + + // Use materialized version for complex queries + return CounterTrack.createMaterialized(attrs); +}
diff --git a/ui/src/components/tracks/debug_tracks.ts b/ui/src/components/tracks/debug_tracks.ts index 104ab3c..ac0ee24 100644 --- a/ui/src/components/tracks/debug_tracks.ts +++ b/ui/src/components/tracks/debug_tracks.ts
@@ -27,10 +27,12 @@ RAW_PREFIX, DebugSliceTrackDetailsPanel, } from './debug_slice_track_details_panel'; -import { - CounterColumnMapping, - SqlTableCounterTrack, -} from './query_counter_track'; +import {BaseCounterTrack, CounterOptions} from './base_counter_track'; + +export interface CounterColumnMapping { + readonly ts: string; + readonly value: string; +} export interface SqlDataSource { // SQL source selecting the necessary data. @@ -398,3 +400,18 @@ const trackNode = new TrackNode({uri, name, removable: true}); trace.workspace.pinnedTracksNode.addChildLast(trackNode); } + +export class SqlTableCounterTrack extends BaseCounterTrack { + constructor( + trace: Trace, + uri: string, + private readonly sqlSource: string, + options?: Partial<CounterOptions>, + ) { + super(trace, uri, options); + } + + getSqlSource(): string { + return `select * from (${this.sqlSource})`; + } +}
diff --git a/ui/src/components/tracks/query_counter_track.ts b/ui/src/components/tracks/query_counter_track.ts deleted file mode 100644 index 401f8fb..0000000 --- a/ui/src/components/tracks/query_counter_track.ts +++ /dev/null
@@ -1,137 +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 {createPerfettoTable} from '../../trace_processor/sql_utils'; -import {Trace} from '../../public/trace'; -import {sqlNameSafe} from '../../base/string_utils'; -import {BaseCounterTrack, CounterOptions} from './base_counter_track'; -import {Engine} from '../../trace_processor/engine'; - -export interface QueryCounterTrackArgs { - // The trace object used to run queries. - readonly trace: Trace; - - // A unique, reproducible ID for this track. - readonly uri: string; - - // The query and optional column remapping. - readonly data: SqlDataSource; - - // Optional: Which columns should be used for ts, and value. If omitted, - // the defaults 'ts', and 'value' will be used. - readonly columns?: Partial<CounterColumnMapping>; - - // Optional: Display options for the counter track. - readonly options?: Partial<CounterOptions>; - - // Optional: Whether to materialize the query results. If omitted, we - // will materialize. - readonly materialize?: boolean; -} - -export interface SqlDataSource { - // SQL source selecting the necessary data. - readonly 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. - readonly columns?: string[]; -} - -export interface CounterColumnMapping { - readonly ts: string; - readonly value: string; -} - -/** - * Creates a counter track based on a query. - * - * The query must provide the following columns: - * - ts: INTEGER - The timestamp of each sample. - * - value: REAL | INTEGER - The value of each sample. - * - * The column names don't have to be 'ts' and 'value', and can be remapped if - * convenient using the config.columns parameter. - */ -export async function createQueryCounterTrack(args: QueryCounterTrackArgs) { - if (args.materialize === false) { - return new SqlTableCounterTrack( - args.trace, - args.uri, - wrapQueryForCounterTrack(args.data, args.columns), - args.options, - ); - } else { - const tableName = `__query_counter_track_${sqlNameSafe(args.uri)}`; - await createPerfettoTableForTrack( - args.trace.engine, - tableName, - args.data, - args.columns, - ); - return new SqlTableCounterTrack( - args.trace, - args.uri, - tableName, - args.options, - ); - } -} - -function wrapQueryForCounterTrack( - data: SqlDataSource, - columnMapping: Partial<CounterColumnMapping> = {}, -) { - const {ts = 'ts', value = 'value'} = columnMapping; - return ` - with data as ( - ${data.sqlSource} - ) - select - ${ts} as ts, - ${value} as value - from data - order by ts - `; -} - -async function createPerfettoTableForTrack( - engine: Engine, - tableName: string, - data: SqlDataSource, - columnMapping: Partial<CounterColumnMapping> = {}, -) { - return await createPerfettoTable({ - engine, - name: tableName, - as: wrapQueryForCounterTrack(data, columnMapping), - }); -} - -export class SqlTableCounterTrack extends BaseCounterTrack { - constructor( - trace: Trace, - uri: string, - private readonly sqlSource: string, - options?: Partial<CounterOptions>, - ) { - super(trace, uri, options); - } - - getSqlSource(): string { - return `select * from (${this.sqlSource})`; - } -}
diff --git a/ui/src/plugins/com.android.AndroidCounterTracks/index.ts b/ui/src/plugins/com.android.AndroidCounterTracks/index.ts index d0ba296..136ec90 100644 --- a/ui/src/plugins/com.android.AndroidCounterTracks/index.ts +++ b/ui/src/plugins/com.android.AndroidCounterTracks/index.ts
@@ -12,10 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {createQueryCounterTrack} from '../../components/tracks/query_counter_track'; +import { + CounterRowSchema, + CounterTrack, +} from '../../components/tracks/counter_track'; import {PerfettoPlugin} from '../../public/plugin'; import {Trace} from '../../public/trace'; import {TrackNode} from '../../public/workspace'; +import {SourceDataset} from '../../trace_processor/dataset'; +import {LONG, NUM} from '../../trace_processor/query_result'; export default class implements PerfettoPlugin { static readonly id = 'com.android.AndroidCounterTracks'; @@ -25,15 +30,21 @@ await engine.query(`include perfetto module android.binder;`); - const counterTrackSource = ` - select client_ts as ts, count(*) value - from android_binder_txns - group by ts - `; - const trackNode = await this.loadCounterTrack( ctx, - counterTrackSource, + new SourceDataset({ + src: ` + SELECT + client_ts AS ts, + COUNT(*) value + FROM android_binder_txns + GROUP BY ts + `, + schema: { + ts: LONG, + value: NUM, + }, + }), '/android_counter_track', 'Android Counter Track', ); @@ -42,17 +53,14 @@ private async loadCounterTrack( ctx: Trace, - sqlSource: string, + dataset: SourceDataset<CounterRowSchema>, uri: string, title: string, ) { - const track = await createQueryCounterTrack({ + const track = await CounterTrack.createMaterialized({ trace: ctx, uri, - data: { - sqlSource, - columns: ['ts', 'value'], - }, + dataset, }); ctx.tracks.registerTrack({
diff --git a/ui/src/plugins/com.android.AndroidDmabuf/index.ts b/ui/src/plugins/com.android.AndroidDmabuf/index.ts index dc80b36..7560e4a 100644 --- a/ui/src/plugins/com.android.AndroidDmabuf/index.ts +++ b/ui/src/plugins/com.android.AndroidDmabuf/index.ts
@@ -13,29 +13,30 @@ // limitations under the License. import { - createQueryCounterTrack, - SqlDataSource, -} from '../../components/tracks/query_counter_track'; + CounterRowSchema, + CounterTrack, +} from '../../components/tracks/counter_track'; import {PerfettoPlugin} from '../../public/plugin'; import {Trace} from '../../public/trace'; import {COUNTER_TRACK_KIND, SLICE_TRACK_KIND} from '../../public/track_kinds'; import {TrackNode} from '../../public/workspace'; -import {NUM, NUM_NULL, STR} from '../../trace_processor/query_result'; +import {SourceDataset} from '../../trace_processor/dataset'; +import {LONG, NUM, NUM_NULL, STR} from '../../trace_processor/query_result'; import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups'; import StandardGroupsPlugin from '../dev.perfetto.StandardGroups'; import TraceProcessorTrackPlugin from '../dev.perfetto.TraceProcessorTrack'; import {TraceProcessorCounterTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_counter_track'; import {createTraceProcessorSliceTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_slice_track'; -async function registerAllocsTrack( +async function registerAllocsTrack<T extends CounterRowSchema>( ctx: Trace, uri: string, - dataSource: SqlDataSource, + dataset: SourceDataset<T>, ) { - const track = await createQueryCounterTrack({ + const track = await CounterTrack.createMaterialized({ trace: ctx, uri, - data: dataSource, + dataset, }); ctx.tracks.registerTrack({ uri, @@ -55,29 +56,50 @@ const e = ctx.engine; await e.query(`INCLUDE PERFETTO MODULE android.memory.dmabuf`); - const pids = await e.query( - `SELECT DISTINCT upid, IIF(upid IS NULL, utid, NULL) AS utid FROM android_memory_cumulative_dmabuf`, - ); + const pids = await e.query(` + SELECT DISTINCT + upid, + IIF(upid IS NULL, utid, NULL) AS utid + FROM android_memory_cumulative_dmabuf + `); const it = pids.iter({upid: NUM_NULL, utid: NUM_NULL}); for (; it.valid(); it.next()) { if (it.upid != null) { const uri = `/android_process_dmabuf_upid_${it.upid}`; - const config: SqlDataSource = { - sqlSource: `SELECT ts, value FROM android_memory_cumulative_dmabuf - WHERE upid = ${it.upid}`, - }; - await registerAllocsTrack(ctx, uri, config); + const dataset = new SourceDataset({ + src: ` + SELECT + ts, + value + FROM android_memory_cumulative_dmabuf + WHERE upid = ${it.upid} + `, + schema: { + ts: LONG, + value: NUM, + }, + }); + await registerAllocsTrack(ctx, uri, dataset); ctx.plugins .getPlugin(ProcessThreadGroupsPlugin) .getGroupForProcess(it.upid) ?.addChildInOrder(new TrackNode({uri, name: 'dmabuf allocs'})); } else if (it.utid != null) { const uri = `/android_process_dmabuf_utid_${it.utid}`; - const config: SqlDataSource = { - sqlSource: `SELECT ts, value FROM android_memory_cumulative_dmabuf - WHERE utid = ${it.utid}`, - }; - await registerAllocsTrack(ctx, uri, config); + const dataset = new SourceDataset({ + src: ` + SELECT + ts, + value + FROM android_memory_cumulative_dmabuf + WHERE utid = ${it.utid} + `, + schema: { + ts: LONG, + value: NUM, + }, + }); + await registerAllocsTrack(ctx, uri, dataset); ctx.plugins .getPlugin(ProcessThreadGroupsPlugin) .getGroupForThread(it.utid)
diff --git a/ui/src/plugins/com.android.AndroidLongBatterySupport/index.ts b/ui/src/plugins/com.android.AndroidLongBatterySupport/index.ts index 925e216..0ff9f6f 100644 --- a/ui/src/plugins/com.android.AndroidLongBatterySupport/index.ts +++ b/ui/src/plugins/com.android.AndroidLongBatterySupport/index.ts
@@ -18,10 +18,10 @@ import {Engine} from '../../trace_processor/engine'; import {SliceTrack, RowSchema} from '../../components/tracks/slice_track'; import {CounterOptions} from '../../components/tracks/base_counter_track'; -import {createQueryCounterTrack} from '../../components/tracks/query_counter_track'; import {TrackNode} from '../../public/workspace'; import {SourceDataset} from '../../trace_processor/dataset'; -import {STR} from '../../trace_processor/query_result'; +import {LONG, NUM, STR} from '../../trace_processor/query_result'; +import {CounterTrack} from '../../components/tracks/counter_track'; export default class implements PerfettoPlugin { static readonly id = 'com.android.AndroidLongBatterySupport'; @@ -93,14 +93,17 @@ groupCollapsed = true, ) { const uri = `/long_battery_tracing_${name}`; - const track = await createQueryCounterTrack({ + const track = await CounterTrack.createMaterialized({ trace: ctx, uri, - data: { - sqlSource: query, - columns: ['ts', 'value'], - }, - options, + dataset: new SourceDataset({ + src: query, + schema: { + ts: LONG, + value: NUM, + }, + }), + defaultOptions: options, }); ctx.tracks.registerTrack({ uri,
diff --git a/ui/src/plugins/com.android.CpuPerUid/index.ts b/ui/src/plugins/com.android.CpuPerUid/index.ts index baf3e15..1b53078 100644 --- a/ui/src/plugins/com.android.CpuPerUid/index.ts +++ b/ui/src/plugins/com.android.CpuPerUid/index.ts
@@ -15,9 +15,13 @@ import {Trace} from '../../public/trace'; import {PerfettoPlugin} from '../../public/plugin'; import {TrackNode} from '../../public/workspace'; -import {createQueryCounterTrack} from '../../components/tracks/query_counter_track'; import StandardGroupsPlugin from '../dev.perfetto.StandardGroups'; -import {NUM, STR} from '../../trace_processor/query_result'; +import {LONG, NUM, STR} from '../../trace_processor/query_result'; +import { + CounterRowSchema, + CounterTrack, +} from '../../components/tracks/counter_track'; +import {SourceDataset} from '../../trace_processor/dataset'; export default class implements PerfettoPlugin { static readonly id = 'com.android.CpuPerUid'; @@ -25,29 +29,24 @@ private _topLevelGroup: TrackNode | undefined; - private async addCpuPerUidTrack( + private addCpuPerUidTrack( ctx: Trace, - sql: string, + dataset: SourceDataset<CounterRowSchema>, name: string, uri: string, group: TrackNode, sharing?: string, ) { - const track = await createQueryCounterTrack({ + const track = CounterTrack.create({ trace: ctx, uri, - data: { - sqlSource: sql, - columns: ['ts', 'value'], - }, - columns: {ts: 'ts', value: 'value'}, - options: { + dataset, + defaultOptions: { unit: '%', yOverrideMaximum: 100, yOverrideMinimum: 0, yRangeSharingKey: sharing, }, - materialize: false, }); ctx.tracks.registerTrack({ uri, @@ -95,11 +94,20 @@ for (; it.valid(); it.next()) { const name = `${it.type} (${clusterName(it.cluster)})`; - await this.addCpuPerUidTrack( + this.addCpuPerUidTrack( ctx, - `select ts, value - from _android_cpu_per_uid_summary - where type = '${it.type}' and cluster = ${it.cluster}`, + new SourceDataset({ + src: ` + SELECT + ts, + value + FROM _android_cpu_per_uid_summary + WHERE type = '${it.type}' AND cluster = ${it.cluster}`, + schema: { + ts: LONG, + value: NUM, + }, + }), name, `/cpu_per_uid_summary_${it.type}_${it.cluster}`, group, @@ -137,13 +145,25 @@ for (; it.valid(); it.next()) { const name = `${it.name} (${clusterName(it.cluster)})`; - await this.addCpuPerUidTrack( + this.addCpuPerUidTrack( ctx, - `select - ts, - min(100, 100 * cpu_ratio) as value - from android_cpu_per_uid_counter - where track_id = ${it.id}`, + new SourceDataset({ + src: ` + SELECT + ts, + MIN(100, 100 * cpu_ratio) AS value, + track_id + FROM android_cpu_per_uid_counter + `, + schema: { + ts: LONG, + value: NUM, + }, + filter: { + col: 'track_id', + eq: it.id, + }, + }), name, `/${uriPrefix}_${it.id}`, group,
diff --git a/ui/src/plugins/com.android.DayExplorer/index.ts b/ui/src/plugins/com.android.DayExplorer/index.ts index b87f933..0295560 100644 --- a/ui/src/plugins/com.android.DayExplorer/index.ts +++ b/ui/src/plugins/com.android.DayExplorer/index.ts
@@ -16,9 +16,8 @@ import {Trace} from '../../public/trace'; import StandardGroupsPlugin from '../dev.perfetto.StandardGroups'; import {PerfettoPlugin} from '../../public/plugin'; -import {createQueryCounterTrack} from '../../components/tracks/query_counter_track'; import {TrackNode} from '../../public/workspace'; -import {STR, LONG, LONG_NULL} from '../../trace_processor/query_result'; +import {STR, LONG, NUM, LONG_NULL} from '../../trace_processor/query_result'; import {SourceDataset} from '../../trace_processor/dataset'; import {AreaSelection, areaSelectionsEqual} from '../../public/selection'; import {Flamegraph} from '../../widgets/flamegraph'; @@ -27,6 +26,10 @@ QueryFlamegraph, } from '../../components/query_flamegraph'; import SupportPlugin from '../com.android.AndroidLongBatterySupport'; +import { + CounterRowSchema, + CounterTrack, +} from '../../components/tracks/counter_track'; const DAY_EXPLORER_TRACK_KIND = 'day_explorer_counter_track'; @@ -75,18 +78,30 @@ }); for (; childIter.valid(); childIter.next()) { - const query = ` - SELECT ts, power_mw AS value - FROM day_explorer_ui_hierarchy_per_ts - WHERE track_id = ${childIter.track_id} - `; + const dataset = new SourceDataset({ + src: ` + SELECT + ts, + power_mw AS value, + track_id + FROM day_explorer_ui_hierarchy_per_ts + `, + schema: { + ts: LONG, + value: NUM, + }, + filter: { + col: 'track_id', + eq: childIter.track_id, + }, + }); const groupKey = `_day_explorer_ui_hierarchy_under_${parentId}`; const trackName = `${childIter.display_name} - ${childIter.energy_mwh}mWh`; const node = await this.createDayExplorerTrack( ctx, trackName, groupKey, - query, + dataset, ); parent.addChildInOrder(node); await this.addDayExplorerRecursive(ctx, node, limit, childIter.track_id); @@ -97,20 +112,14 @@ ctx: Trace, name: string, groupKey: string, - query: string, + dataset: SourceDataset<CounterRowSchema>, ): Promise<TrackNode> { const uri = `/day_explorer_${uuidv4()}`; - const renderer = await createQueryCounterTrack({ + const renderer = await CounterTrack.createMaterialized({ trace: ctx, uri, - data: { - sqlSource: query, - }, - columns: { - ts: 'ts', - value: 'value', - }, - options: { + dataset, + defaultOptions: { yRangeSharingKey: groupKey, }, });
diff --git a/ui/src/plugins/com.google.PixelCpmTrace/index.ts b/ui/src/plugins/com.google.PixelCpmTrace/index.ts index 268df77..3072ca1 100644 --- a/ui/src/plugins/com.google.PixelCpmTrace/index.ts +++ b/ui/src/plugins/com.google.PixelCpmTrace/index.ts
@@ -12,12 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {createQueryCounterTrack} from '../../components/tracks/query_counter_track'; +import {CounterTrack} from '../../components/tracks/counter_track'; import {PerfettoPlugin} from '../../public/plugin'; import {Trace} from '../../public/trace'; import {COUNTER_TRACK_KIND} from '../../public/track_kinds'; import {TrackNode} from '../../public/workspace'; -import {NUM, STR} from '../../trace_processor/query_result'; +import {SourceDataset} from '../../trace_processor/dataset'; +import {LONG, NUM, STR} from '../../trace_processor/query_result'; export default class implements PerfettoPlugin { static readonly id = 'com.google.PixelCpmTrace'; @@ -42,18 +43,20 @@ for (let groupAdded = false; it.valid(); it.next()) { const {trackId, trackName} = it; const uri = `/cpm_trace_${trackName}`; - const track = await createQueryCounterTrack({ + const track = CounterTrack.create({ trace: ctx, uri, - data: { - sqlSource: ` - select ts, value - from counter - where track_id = ${trackId} - `, - columns: ['ts', 'value'], - }, - columns: {ts: 'ts', value: 'value'}, + dataset: new SourceDataset({ + src: 'counter', + schema: { + ts: LONG, + value: NUM, + }, + filter: { + col: 'track_id', + eq: trackId, + }, + }), }); ctx.tracks.registerTrack({ uri,
diff --git a/ui/src/plugins/dev.perfetto.CpuidleTimeInState/index.ts b/ui/src/plugins/dev.perfetto.CpuidleTimeInState/index.ts index fb54239..9f7ddcd 100644 --- a/ui/src/plugins/dev.perfetto.CpuidleTimeInState/index.ts +++ b/ui/src/plugins/dev.perfetto.CpuidleTimeInState/index.ts
@@ -16,9 +16,13 @@ import {PerfettoPlugin} from '../../public/plugin'; import {CounterOptions} from '../../components/tracks/base_counter_track'; import {TrackNode} from '../../public/workspace'; -import {createQueryCounterTrack} from '../../components/tracks/query_counter_track'; import StandardGroupsPlugin from '../dev.perfetto.StandardGroups'; -import {NUM, STR} from '../../trace_processor/query_result'; +import {LONG, NUM, STR} from '../../trace_processor/query_result'; +import { + CounterRowSchema, + CounterTrack, +} from '../../components/tracks/counter_track'; +import {SourceDataset} from '../../trace_processor/dataset'; export default class implements PerfettoPlugin { static readonly id = 'dev.perfetto.CpuidleTimeInState'; @@ -27,20 +31,16 @@ private async addCounterTrack( ctx: Trace, name: string, - query: string, + dataset: SourceDataset<CounterRowSchema>, group: TrackNode, - options?: Partial<CounterOptions>, + _options?: Partial<CounterOptions>, ) { const uri = `/cpuidle_time_in_state_${name}`; - const track = await createQueryCounterTrack({ + const track = await CounterTrack.createMaterialized({ trace: ctx, uri, - data: { - sqlSource: query, - columns: ['ts', 'value'], - }, - columns: {ts: 'ts', value: 'value'}, - options, + dataset, + // options, }); ctx.tracks.registerTrack({ uri, @@ -58,13 +58,19 @@ await this.addCounterTrack( ctx, `cpuidle.${state}`, - ` - select - ts, - idle_percentage as value - from linux_cpu_idle_time_in_state_counters - where state = '${state}' - `, + new SourceDataset({ + src: ` + SELECT + ts, + idle_percentage AS value + FROM linux_cpu_idle_time_in_state_counters + WHERE state = '${state}' + `, + schema: { + ts: LONG, + value: NUM, + }, + }), group, {unit: 'percent', yOverrideMaximum: 100, yOverrideMinimum: 0}, ); @@ -79,13 +85,19 @@ await this.addCounterTrack( ctx, `cpuidle.cpu${cpu}.${state} Residency`, - ` - select - ts, - idle_percentage as value - from linux_per_cpu_idle_time_in_state_counters - where state = '${state}' AND cpu = ${cpu} - `, + new SourceDataset({ + src: ` + SELECT + ts, + idle_percentage AS value + FROM linux_per_cpu_idle_time_in_state_counters + WHERE state = '${state}' AND cpu = ${cpu} + `, + schema: { + ts: LONG, + value: NUM, + }, + }), group, {unit: 'percent', yOverrideMaximum: 100, yOverrideMinimum: 0}, );
diff --git a/ui/src/plugins/dev.perfetto.EntityStateResidency/index.ts b/ui/src/plugins/dev.perfetto.EntityStateResidency/index.ts index 2c65366..f45a329 100644 --- a/ui/src/plugins/dev.perfetto.EntityStateResidency/index.ts +++ b/ui/src/plugins/dev.perfetto.EntityStateResidency/index.ts
@@ -13,12 +13,13 @@ // limitations under the License. import {createAggregationTab} from '../../components/aggregation_adapter'; -import {createQueryCounterTrack} from '../../components/tracks/query_counter_track'; +import {CounterTrack} from '../../components/tracks/counter_track'; import {PerfettoPlugin} from '../../public/plugin'; import {Trace} from '../../public/trace'; import {COUNTER_TRACK_KIND} from '../../public/track_kinds'; import {TrackNode} from '../../public/workspace'; -import {NUM, STR} from '../../trace_processor/query_result'; +import {SourceDataset} from '../../trace_processor/dataset'; +import {LONG, NUM, STR} from '../../trace_processor/query_result'; import StandardGroupsPlugin from '../dev.perfetto.StandardGroups'; import {EntityStateResidencySelectionAggregator} from './entity_state_residency_selection_aggregator'; @@ -73,21 +74,23 @@ // Create and register a track for the state residency. const uri = `/entity_state_residency_${it.entity}_${it.state}`; const name = it.state; - const track = await createQueryCounterTrack({ + const track = await CounterTrack.createMaterialized({ trace: ctx, uri, - data: { - sqlSource: ` + dataset: new SourceDataset({ + src: ` SELECT ts, state_time_since_boot / 1e9 AS value FROM android_entity_state_residency WHERE track_id = ${it.trackId} `, - columns: ['ts', 'value'], - }, - columns: {ts: 'ts', value: 'value'}, - options: { + schema: { + ts: LONG, + value: NUM, + }, + }), + defaultOptions: { yMode: 'rate', yRangeSharingKey: `entity_state_residency_${it.entity}`, unit: 's',
diff --git a/ui/src/plugins/dev.perfetto.PowerRails/index.ts b/ui/src/plugins/dev.perfetto.PowerRails/index.ts index be1320c..a478ea8 100644 --- a/ui/src/plugins/dev.perfetto.PowerRails/index.ts +++ b/ui/src/plugins/dev.perfetto.PowerRails/index.ts
@@ -13,13 +13,19 @@ // limitations under the License. import {createAggregationTab} from '../../components/aggregation_adapter'; -import {createQueryCounterTrack} from '../../components/tracks/query_counter_track'; +import {CounterTrack} from '../../components/tracks/counter_track'; import {PerfettoPlugin} from '../../public/plugin'; import {Trace} from '../../public/trace'; import {COUNTER_TRACK_KIND} from '../../public/track_kinds'; import {getTrackName} from '../../public/utils'; import {TrackNode} from '../../public/workspace'; -import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result'; +import {SourceDataset} from '../../trace_processor/dataset'; +import { + LONG, + NUM, + NUM_NULL, + STR_NULL, +} from '../../trace_processor/query_result'; import StandardGroupsPlugin from '../dev.perfetto.StandardGroups'; import {PowerCounterSelectionAggregator} from './power_counter_selection_aggregator'; @@ -77,19 +83,27 @@ machine, }); const uri = `/counter_${trackId}`; - const track = await createQueryCounterTrack({ + const track = await CounterTrack.createMaterialized({ trace: ctx, uri, - data: { - sqlSource: ` + dataset: new SourceDataset({ + src: ` SELECT ts, - value / 1000.0 AS value -- convert uJ to mJ + value / 1000.0 AS value, -- convert uJ to mJ + track_id FROM counter - WHERE track_id = ${trackId} `, - }, - options: { + schema: { + ts: LONG, + value: NUM, + }, + filter: { + col: 'track_id', + eq: trackId, + }, + }), + defaultOptions: { yMode: 'rate', yRangeSharingKey: 'power_rails', unit: 'mJ',
diff --git a/ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_counter_track.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_counter_track.ts index e0be4ac..66738b3 100644 --- a/ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_counter_track.ts +++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_counter_track.ts
@@ -113,6 +113,11 @@ } detailsPanel() { - return new CounterDetailsPanel(this.trace, this.trackId, this.trackName); + return new CounterDetailsPanel( + this.trace, + this.trackId, + this.trackName, + this.rootTable, + ); } }
diff --git a/ui/src/plugins/org.chromium.ETM/index.ts b/ui/src/plugins/org.chromium.ETM/index.ts index 9dd27c9..a43b70f 100644 --- a/ui/src/plugins/org.chromium.ETM/index.ts +++ b/ui/src/plugins/org.chromium.ETM/index.ts
@@ -14,9 +14,10 @@ import {Trace} from '../../public/trace'; import {PerfettoPlugin} from '../../public/plugin'; - -import {createQueryCounterTrack} from '../../components/tracks/query_counter_track'; import {TrackNode} from '../../public/workspace'; +import {CounterTrack} from '../../components/tracks/counter_track'; +import {SourceDataset} from '../../trace_processor/dataset'; +import {LONG, NUM} from '../../trace_processor/query_result'; export default class implements PerfettoPlugin { static readonly id = 'org.chromium.ETM'; @@ -24,17 +25,25 @@ async onTraceLoad(trace: Trace) { const title = 'ETM Session ID'; const uri = `${trace.pluginId}#ETMSessionID`; - const query = - 'select ts, value from counter inner join ' + - 'counter_track on counter_track.id = counter.track_id ' + - 'where name = "ETMSession"'; - - const renderer = await createQueryCounterTrack({ + const renderer = await CounterTrack.createMaterialized({ trace, uri, - data: { - sqlSource: query, - }, + dataset: new SourceDataset({ + src: ` + SELECT + counter.id AS id + ts, + value + FROM counter + INNER JOIN counter_track ON counter_track.id = counter.track_id + WHERE name = "ETMSession" + `, + schema: { + id: NUM, + ts: LONG, + value: NUM, + }, + }), }); trace.tracks.registerTrack({
diff --git a/ui/src/plugins/org.kernel.Io/index.ts b/ui/src/plugins/org.kernel.Io/index.ts index 0939292..7303e16 100644 --- a/ui/src/plugins/org.kernel.Io/index.ts +++ b/ui/src/plugins/org.kernel.Io/index.ts
@@ -15,9 +15,10 @@ import {TrackNode} from '../../public/workspace'; import {Trace} from '../../public/trace'; import {PerfettoPlugin} from '../../public/plugin'; -import {createQueryCounterTrack} from '../../components/tracks/query_counter_track'; import {Engine} from '../../trace_processor/engine'; -import {NUM} from '../../trace_processor/query_result'; +import {LONG, NUM} from '../../trace_processor/query_result'; +import {CounterTrack} from '../../components/tracks/counter_track'; +import {SourceDataset} from '../../trace_processor/dataset'; export default class implements PerfettoPlugin { static readonly id = 'dev.perfetto.Io'; @@ -32,16 +33,22 @@ for (const device of devices) { const uri = `/queued_io_request_count/device_${device['id']}`; const name = `dev major:${device['major']} minor:${device['minor']}`; - const track = await createQueryCounterTrack({ + const track = await CounterTrack.createMaterialized({ trace: ctx, uri, - data: { - sqlSource: ` - SELECT ts, ops_in_queue_or_device as value + dataset: new SourceDataset({ + src: ` + SELECT + ts, + ops_in_queue_or_device as value FROM linux_active_block_io_operations_by_device WHERE dev = ${String(device['id'])} `, - }, + schema: { + ts: LONG, + value: NUM, + }, + }), }); ctx.tracks.registerTrack({ uri,