| // Copyright (C) 2021 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 {removeFalsyValues} from '../../base/array_utils'; |
| import {AsyncLimiter} from '../../base/async_limiter'; |
| import {assertExists} from '../../base/assert'; |
| import {Time} from '../../base/time'; |
| import { |
| createAggregationTab, |
| createIITable, |
| } from '../../components/aggregation_adapter'; |
| import {sliceDistributionCellRenderers} from '../../components/details/slice_details'; |
| import {openDistributionTab} from '../../components/distribution_panel'; |
| import { |
| metricsFromTableOrSubquery, |
| type QueryFlamegraphMetric, |
| } from '../../components/query_flamegraph'; |
| import {FlamegraphPanel} from '../../components/flamegraph_panel'; |
| import type {MinimapRow} from '../../public/minimap'; |
| import type {PerfettoPlugin} from '../../public/plugin'; |
| import {type AreaSelection, areaSelectionsEqual} from '../../public/selection'; |
| import type {Trace} from '../../public/trace'; |
| import {COUNTER_TRACK_KIND, SLICE_TRACK_KIND} from '../../public/track_kinds'; |
| import {getTrackName} from '../../public/utils'; |
| import {TrackNode} from '../../public/workspace'; |
| import {SourceDataset} from '../../trace_processor/dataset'; |
| import { |
| LONG, |
| LONG_NULL, |
| NUM, |
| NUM_NULL, |
| STR, |
| STR_NULL, |
| } from '../../trace_processor/query_result'; |
| import {escapeSearchQuery} from '../../trace_processor/query_utils'; |
| import {Flamegraph, FLAMEGRAPH_STATE_SCHEMA} from '../../widgets/flamegraph'; |
| import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups'; |
| import StandardGroupsPlugin from '../dev.perfetto.StandardGroups'; |
| import {CounterSelectionAggregator} from './counter_selection_aggregator'; |
| import {COUNTER_TRACK_SCHEMAS} from './counter_tracks'; |
| import {PivotTableTab} from './pivot_table_tab'; |
| import {SliceSelectionAggregator} from './slice_selection_aggregator'; |
| import {SLICE_TRACK_SCHEMAS} from './slice_tracks'; |
| import {TraceProcessorCounterTrack} from './trace_processor_counter_track'; |
| import {createTraceProcessorSliceTrack} from './trace_processor_slice_track'; |
| import type {TopLevelTrackGroup, TrackGroupSchema} from './types'; |
| import type {Store} from '../../base/store'; |
| import {z} from 'zod'; |
| import { |
| createPerfettoIndex, |
| createPerfettoTable, |
| } from '../../trace_processor/sql_utils'; |
| import {ThreadSliceDetailsPanel} from '../../components/details/thread_slice_details_tab'; |
| import {CallstackDetailsSection} from './callstack_details_section'; |
| |
| const TRACE_PROCESSOR_TRACK_PLUGIN_STATE_SCHEMA = z.object({ |
| areaSelectionFlamegraphState: FLAMEGRAPH_STATE_SCHEMA.optional(), |
| }); |
| |
| type TraceProcessorTrackPluginState = z.infer< |
| typeof TRACE_PROCESSOR_TRACK_PLUGIN_STATE_SCHEMA |
| >; |
| |
| function createDetailsPanel(trace: Trace, utid: number | null) { |
| if (utid === null) { |
| return undefined; |
| } |
| // TrackEvent can end up in this path if it's a "merged" thread track |
| // with events from many different sources. In that case, show the callstack |
| // panel. |
| return () => |
| new ThreadSliceDetailsPanel(trace, { |
| rightSections: [new CallstackDetailsSection(trace)], |
| }); |
| } |
| |
| export default class TraceProcessorTrackPlugin implements PerfettoPlugin { |
| static readonly id = 'dev.perfetto.TraceProcessorTrack'; |
| static readonly dependencies = [ |
| ProcessThreadGroupsPlugin, |
| StandardGroupsPlugin, |
| ]; |
| |
| private groups = new Map<string, TrackNode>(); |
| private store?: Store<TraceProcessorTrackPluginState>; |
| |
| private migrateTraceProcessorTrackPluginState( |
| init: unknown, |
| ): TraceProcessorTrackPluginState { |
| const result = TRACE_PROCESSOR_TRACK_PLUGIN_STATE_SCHEMA.safeParse(init); |
| return result.data ?? {}; |
| } |
| |
| async onTraceLoad(ctx: Trace): Promise<void> { |
| this.store = ctx.mountStore(TraceProcessorTrackPlugin.id, (init) => |
| this.migrateTraceProcessorTrackPluginState(init), |
| ); |
| |
| await this.addCounters(ctx); |
| await this.addSlices(ctx); |
| this.addAggregations(ctx); |
| this.addMinimapContentProvider(ctx); |
| this.addSearchProviders(ctx); |
| } |
| |
| private async addCounters(ctx: Trace) { |
| const result = await ctx.engine.query(` |
| include perfetto module viz.threads; |
| |
| with tracks_summary as ( |
| select |
| ct.type, |
| ct.name, |
| ct.id, |
| ct.unit, |
| ct.machine_id as machine, |
| extract_arg(ct.dimension_arg_set_id, 'utid') as utid, |
| extract_arg(ct.dimension_arg_set_id, 'upid') as upid, |
| extract_arg(ct.dimension_arg_set_id, 'gpu') as gpu_id, |
| extract_arg(ct.source_arg_set_id, 'description') as description |
| from counter_track ct |
| join _counter_track_summary using (id) |
| order by ct.name |
| ) |
| select |
| s.*, |
| thread.tid, |
| thread.name as threadName, |
| ifnull(p.pid, tp.pid) as pid, |
| ifnull(p.name, tp.name) as processName, |
| ifnull(thread.is_main_thread, 0) as isMainThread, |
| ifnull(k.is_kernel_thread, 0) AS isKernelThread |
| from tracks_summary s |
| left join process p on s.upid = p.upid |
| left join thread using (utid) |
| left join _threads_with_kernel_flag k using (utid) |
| left join process tp on thread.upid = tp.upid |
| order by lower(s.name) |
| `); |
| |
| const schemas = new Map(COUNTER_TRACK_SCHEMAS.map((x) => [x.type, x])); |
| const it = result.iter({ |
| id: NUM, |
| type: STR, |
| name: STR_NULL, |
| unit: STR_NULL, |
| utid: NUM_NULL, |
| upid: NUM_NULL, |
| gpu_id: NUM_NULL, |
| threadName: STR_NULL, |
| processName: STR_NULL, |
| tid: LONG_NULL, |
| pid: LONG_NULL, |
| isMainThread: NUM, |
| isKernelThread: NUM, |
| machine: NUM, |
| description: STR_NULL, |
| }); |
| for (; it.valid(); it.next()) { |
| const { |
| type, |
| id: trackId, |
| name, |
| unit, |
| utid, |
| upid, |
| threadName, |
| processName, |
| tid, |
| pid, |
| isMainThread, |
| isKernelThread, |
| machine, |
| description, |
| } = it; |
| const schema = schemas.get(type); |
| if (schema === undefined) { |
| continue; |
| } |
| const {group, topLevelGroup} = schema; |
| const trackName = getTrackName({ |
| name, |
| tid, |
| threadName, |
| pid, |
| processName, |
| upid, |
| utid, |
| kind: COUNTER_TRACK_KIND, |
| threadTrack: utid !== undefined, |
| machine, |
| }); |
| const uri = `/counter_${trackId}`; |
| |
| const maybeDescriptionRenderer = schema.description?.({ |
| name: trackName ?? undefined, |
| description: description ?? undefined, |
| }); |
| |
| ctx.tracks.registerTrack({ |
| uri, |
| description: maybeDescriptionRenderer ?? description ?? undefined, |
| tags: { |
| kinds: [COUNTER_TRACK_KIND], |
| trackIds: [trackId], |
| type: type, |
| upid: upid ?? undefined, |
| utid: utid ?? undefined, |
| ...(isKernelThread === 1 && {kernelThread: true}), |
| }, |
| renderer: new TraceProcessorCounterTrack({ |
| trace: ctx, |
| uri, |
| yMode: schema.mode, |
| yRangeSharingKey: schema.shareYAxis ? it.type : undefined, |
| unit: schema.unit ?? unit ?? undefined, |
| trackId, |
| trackName, |
| }), |
| }); |
| this.addTrack( |
| ctx, |
| topLevelGroup, |
| group, |
| upid, |
| utid, |
| new TrackNode({ |
| uri, |
| name: trackName, |
| sortOrder: utid !== undefined || upid !== undefined ? 30 : 0, |
| chips: removeFalsyValues([ |
| isKernelThread === 0 && isMainThread === 1 && 'main thread', |
| ]), |
| }), |
| ); |
| } |
| } |
| |
| private async addSlices(ctx: Trace) { |
| await ctx.engine.query(` |
| include perfetto module viz.threads; |
| include perfetto module viz.track_event_callstacks; |
| `); |
| |
| // Step 1: Materialize track metadata |
| // Can be cleaned up at the end of this function as only tables and |
| // immediate queries depend on this. |
| await using _ = await createPerfettoTable({ |
| name: '__tracks_to_create', |
| engine: ctx.engine, |
| as: ` |
| with grouped as materialized ( |
| select |
| t.type, |
| min(t.name) as name, |
| lower(min(t.name)) as lower_name, |
| extract_arg(t.dimension_arg_set_id, 'utid') as utid, |
| extract_arg(t.dimension_arg_set_id, 'upid') as upid, |
| extract_arg(t.dimension_arg_set_id, 'gpu') as gpu_id, |
| extract_arg(t.source_arg_set_id, 'description') as description, |
| min(t.id) minTrackId, |
| group_concat(t.id) as trackIds, |
| count() as trackCount, |
| max(cs.track_id IS NOT NULL) as hasCallstacks, |
| CASE t.type |
| WHEN 'thread_execution' THEN 0 |
| WHEN 'art_method_tracing' THEN 1 |
| ELSE 99 |
| END as track_rank |
| from _slice_track_summary s |
| join track t using (id) |
| left join _track_event_tracks_with_callstacks cs on cs.track_id = t.id |
| group by type, upid, utid, gpu_id, t.track_group_id, ifnull(t.track_group_id, t.id) |
| ) |
| select |
| s.type, |
| s.name, |
| s.utid, |
| ifnull(s.upid, tp.upid) as upid, |
| s.gpu_id, |
| s.minTrackId as minTrackId, |
| s.trackIds as trackIds, |
| s.trackCount, |
| __max_layout_depth(s.trackCount, s.trackIds) as maxDepth, |
| thread.tid, |
| thread.name as threadName, |
| ifnull(p.pid, tp.pid) as pid, |
| ifnull(p.name, tp.name) as processName, |
| ifnull(thread.is_main_thread, 0) as isMainThread, |
| ifnull(k.is_kernel_thread, 0) AS isKernelThread, |
| s.description AS description, |
| s.hasCallstacks, |
| s.track_rank, |
| s.lower_name |
| from grouped s |
| left join process p on s.upid = p.upid |
| left join thread using (utid) |
| left join _threads_with_kernel_flag k using (utid) |
| left join process tp on thread.upid = tp.upid |
| order by s.track_rank, lower_name |
| `, |
| }); |
| |
| // Step 2: Create shared depth table by joining with |
| // experimental_slice_layout |
| await createPerfettoTable({ |
| name: '__tp_track_layout_depth', |
| engine: ctx.engine, |
| as: ` |
| select id, minTrackId, layout_depth as depth |
| from __tracks_to_create t |
| join experimental_slice_layout(t.trackIds) s |
| where trackCount > 1 |
| order by s.id |
| `, |
| }); |
| |
| // Step 3: Query materialized table and create tracks |
| const result = await ctx.engine.query('select * from __tracks_to_create'); |
| |
| const schemas = new Map(SLICE_TRACK_SCHEMAS.map((x) => [x.type, x])); |
| const it = result.iter({ |
| type: STR, |
| name: STR_NULL, |
| utid: NUM_NULL, |
| upid: NUM_NULL, |
| gpu_id: NUM_NULL, |
| trackIds: STR, |
| maxDepth: NUM, |
| tid: LONG_NULL, |
| threadName: STR_NULL, |
| pid: LONG_NULL, |
| processName: STR_NULL, |
| isMainThread: NUM, |
| isKernelThread: NUM, |
| hasCallstacks: NUM, |
| description: STR_NULL, |
| track_rank: NUM, |
| lower_name: STR_NULL, |
| }); |
| for (; it.valid(); it.next()) { |
| const { |
| trackIds: rawTrackIds, |
| type, |
| name, |
| maxDepth, |
| utid, |
| upid, |
| threadName, |
| processName, |
| tid, |
| pid, |
| isMainThread, |
| isKernelThread, |
| hasCallstacks, |
| description, |
| } = it; |
| const schema = schemas.get(type); |
| if (schema === undefined) { |
| continue; |
| } |
| const trackIds = rawTrackIds.split(',').map((v) => Number(v)); |
| const {group, topLevelGroup} = schema; |
| const trackName = getTrackName({ |
| name, |
| tid, |
| threadName, |
| pid, |
| processName, |
| upid, |
| utid, |
| kind: SLICE_TRACK_KIND, |
| threadTrack: utid !== undefined, |
| }); |
| const uri = `/slice_${trackIds[0]}`; |
| |
| // Apply displayName function from schema if available |
| const displayName = schema.displayName |
| ? schema.displayName(trackName) |
| : trackName; |
| |
| const maybeDescriptionRenderer = schema.description?.({ |
| name: trackName ?? undefined, |
| description: description ?? undefined, |
| }); |
| |
| ctx.tracks.registerTrack({ |
| uri, |
| description: maybeDescriptionRenderer ?? description ?? undefined, |
| tags: { |
| kinds: [SLICE_TRACK_KIND], |
| trackIds: trackIds, |
| type: type, |
| upid: upid ?? undefined, |
| utid: utid ?? undefined, |
| ...(isKernelThread === 1 && {kernelThread: true}), |
| hasCallstacks: hasCallstacks === 1, |
| }, |
| renderer: await createTraceProcessorSliceTrack({ |
| trace: ctx, |
| uri, |
| maxDepth, |
| trackIds, |
| detailsPanel: createDetailsPanel(ctx, utid), |
| depthTableName: |
| trackIds.length > 1 ? '__tp_track_layout_depth' : undefined, |
| }), |
| }); |
| this.addTrack( |
| ctx, |
| topLevelGroup, |
| group, |
| upid, |
| utid, |
| |
| new TrackNode({ |
| uri, |
| name: displayName, |
| sortOrder: utid !== undefined || upid !== undefined ? 20 : 0, |
| chips: removeFalsyValues([ |
| isKernelThread === 0 && isMainThread === 1 && 'main thread', |
| ]), |
| }), |
| ); |
| } |
| } |
| |
| private addTrack( |
| ctx: Trace, |
| topLevelGroup: TopLevelTrackGroup, |
| group: string | TrackGroupSchema | undefined, |
| upid: number | null, |
| utid: number | null, |
| track: TrackNode, |
| ) { |
| switch (topLevelGroup) { |
| case 'PROCESS': { |
| const process = assertExists( |
| ctx.plugins |
| .getPlugin(ProcessThreadGroupsPlugin) |
| .getGroupForProcess(assertExists(upid)), |
| ); |
| this.getGroupByName(process, group, upid).addChildInOrder(track); |
| break; |
| } |
| case 'THREAD': { |
| const thread = assertExists( |
| ctx.plugins |
| .getPlugin(ProcessThreadGroupsPlugin) |
| .getGroupForThread(assertExists(utid)), |
| ); |
| this.getGroupByName(thread, group, utid).addChildInOrder(track); |
| break; |
| } |
| case undefined: { |
| this.getGroupByName( |
| ctx.defaultWorkspace.tracks, |
| group, |
| upid, |
| ).addChildInOrder(track); |
| break; |
| } |
| default: { |
| const standardGroupsPlugin = |
| ctx.plugins.getPlugin(StandardGroupsPlugin); |
| const standardGroup = standardGroupsPlugin.getOrCreateStandardGroup( |
| ctx.defaultWorkspace, |
| topLevelGroup, |
| ); |
| |
| this.getGroupByName(standardGroup, group, null).addChildInOrder(track); |
| break; |
| } |
| } |
| } |
| |
| private getGroupByName( |
| node: TrackNode, |
| group: string | TrackGroupSchema | undefined, |
| scopeId: number | null, |
| ) { |
| if (group === undefined) { |
| return node; |
| } |
| // This is potentially dangerous - ids MUST be unique within the entire |
| // workspace - this seems to indicate that we could end up duplicating ids in |
| // different nodes. |
| const name = typeof group === 'string' ? group : group.name; |
| const expanded = |
| typeof group === 'string' ? false : group.expanded ?? false; |
| const groupId = `tp_group_${scopeId}_${name.toLowerCase().replace(' ', '_')}`; |
| const groupNode = this.groups.get(groupId); |
| if (groupNode) { |
| return groupNode; |
| } |
| const newGroup = new TrackNode({ |
| uri: `/${group}`, |
| isSummary: true, |
| name, |
| collapsed: !expanded, |
| }); |
| node.addChildInOrder(newGroup); |
| this.groups.set(groupId, newGroup); |
| return newGroup; |
| } |
| |
| private addAggregations(ctx: Trace) { |
| ctx.selection.registerAreaSelectionTab( |
| createAggregationTab(ctx, new CounterSelectionAggregator()), |
| ); |
| ctx.selection.registerAreaSelectionTab( |
| createAggregationTab(ctx, new SliceSelectionAggregator(ctx)), |
| ); |
| ctx.selection.registerAreaSelectionTab(new PivotTableTab(ctx)); |
| ctx.selection.registerAreaSelectionTab( |
| this.createSliceFlameGraphPanel(ctx), |
| ); |
| } |
| |
| private createSliceFlameGraphPanel(trace: Trace) { |
| let previousSelection: AreaSelection | undefined; |
| let computed: |
| | { |
| metrics: ReadonlyArray<QueryFlamegraphMetric>; |
| dependencies: ReadonlyArray<AsyncDisposable>; |
| } |
| | undefined; |
| let isLoading = false; |
| const limiter = new AsyncLimiter(); |
| |
| return { |
| id: 'slice_flamegraph_selection', |
| name: 'Slice Flamegraph', |
| render: (selection: AreaSelection) => { |
| const selectionChanged = |
| previousSelection === undefined || |
| !areaSelectionsEqual(previousSelection, selection); |
| previousSelection = selection; |
| if (selectionChanged) { |
| limiter.schedule(async () => { |
| computed = undefined; |
| isLoading = true; |
| computed = await this.computeSliceFlamegraph(trace, selection); |
| isLoading = false; |
| }); |
| } |
| if (computed === undefined && !isLoading) { |
| return undefined; |
| } |
| const store = assertExists(this.store); |
| return { |
| isLoading, |
| content: |
| computed && |
| m(FlamegraphPanel, { |
| trace, |
| metrics: computed.metrics, |
| dependencies: computed.dependencies, |
| state: store.state.areaSelectionFlamegraphState, |
| onStateChange: (state) => { |
| store.edit((draft) => { |
| draft.areaSelectionFlamegraphState = state; |
| }); |
| }, |
| }), |
| }; |
| }, |
| }; |
| } |
| |
| private async computeSliceFlamegraph( |
| trace: Trace, |
| currentSelection: AreaSelection, |
| ): Promise< |
| | { |
| metrics: ReadonlyArray<QueryFlamegraphMetric>; |
| dependencies: ReadonlyArray<AsyncDisposable>; |
| } |
| | undefined |
| > { |
| const trackIds = []; |
| for (const trackInfo of currentSelection.tracks) { |
| if (!trackInfo?.tags?.kinds?.includes(SLICE_TRACK_KIND)) { |
| continue; |
| } |
| if (trackInfo.tags?.trackIds === undefined) { |
| continue; |
| } |
| trackIds.push(...trackInfo.tags.trackIds); |
| } |
| if (trackIds.length === 0) { |
| return undefined; |
| } |
| |
| const dataset = new SourceDataset({ |
| src: ` |
| select |
| id, |
| dur, |
| ts, |
| parent_id, |
| name |
| from slice |
| where track_id in (${trackIds.join(',')}) |
| `, |
| schema: { |
| id: NUM, |
| ts: LONG, |
| dur: LONG, |
| parent_id: NUM_NULL, |
| name: STR_NULL, |
| }, |
| }); |
| |
| const iiTable = await createIITable( |
| trace.engine, |
| dataset, |
| currentSelection.start, |
| currentSelection.end, |
| ); |
| // Will be automatically cleaned up when `iiTable` is dropped. |
| await createPerfettoIndex({ |
| engine: trace.engine, |
| on: `${iiTable.name}(parent_id)`, |
| }); |
| |
| // Reuse the flamegraph's source dataset and add a time-window filter so |
| // "Find matching slices" reflects the same events the user is looking at. |
| const distributionDataset = new SourceDataset({ |
| src: ` |
| select * from (${dataset.query()}) |
| where ts < ${currentSelection.end} |
| and ts + dur > ${currentSelection.start} |
| `, |
| schema: dataset.schema, |
| }); |
| |
| const metrics = metricsFromTableOrSubquery({ |
| tableOrSubquery: `( |
| select * |
| from _viz_slice_ancestor_agg!( |
| ( |
| select s.id, s.dur |
| from ${iiTable.name} s |
| left join ${iiTable.name} t on t.parent_id = s.id |
| where t.id is null |
| ), |
| ${iiTable.name} |
| ) |
| )`, |
| tableMetrics: [ |
| { |
| name: 'Duration', |
| unit: 'ns', |
| columnName: 'self_dur', |
| }, |
| { |
| name: 'Samples', |
| unit: '', |
| columnName: 'self_count', |
| }, |
| ], |
| dependencySql: 'include perfetto module viz.slices;', |
| aggregatableProperties: [ |
| { |
| name: 'simple_count', |
| displayName: 'Slice Count', |
| mergeAggregation: 'SUM', |
| isVisible: (_) => true, |
| }, |
| ], |
| optionalActions: [ |
| { |
| name: 'Find matching slices', |
| execute: ({node}) => { |
| if (node === undefined) return; |
| openDistributionTab(trace, { |
| title: `${node.name} (in selection)`, |
| dataset: distributionDataset, |
| filter: {col: 'name', eq: node.name}, |
| valueColumn: 'dur', |
| idColumn: 'id', |
| sqlTable: 'slice', |
| displayColumns: ['ts', 'dur'], |
| cellRenderers: sliceDistributionCellRenderers(trace), |
| }); |
| }, |
| }, |
| ], |
| nameColumnLabel: 'Slice Name', |
| }); |
| const store = assertExists(this.store); |
| store.edit((draft) => { |
| draft.areaSelectionFlamegraphState = Flamegraph.updateState( |
| draft.areaSelectionFlamegraphState, |
| metrics, |
| ); |
| }); |
| return {metrics, dependencies: [iiTable]}; |
| } |
| |
| private addMinimapContentProvider(ctx: Trace) { |
| ctx.minimap.registerContentProvider({ |
| priority: 1, |
| getData: async (timeSpan, resolution) => { |
| const traceSpan = timeSpan.toTimeSpan(); |
| const sliceResult = await ctx.engine.query(` |
| SELECT |
| bucket, |
| upid, |
| IFNULL(SUM(utid_sum) / CAST(${resolution} AS FLOAT), 0) AS load |
| FROM thread |
| INNER JOIN ( |
| SELECT |
| IFNULL(CAST((ts - ${traceSpan.start}) / ${resolution} AS INT), 0) AS bucket, |
| SUM(dur) AS utid_sum, |
| utid |
| FROM slice |
| INNER JOIN thread_track ON slice.track_id = thread_track.id |
| GROUP BY |
| bucket, |
| utid |
| ) USING(utid) |
| WHERE |
| upid IS NOT NULL |
| GROUP BY |
| bucket, |
| upid; |
| `); |
| |
| const slicesData = new Map<number, MinimapRow>(); |
| const it = sliceResult.iter({bucket: LONG, upid: NUM, load: NUM}); |
| for (; it.valid(); it.next()) { |
| const bucket = it.bucket; |
| const upid = it.upid; |
| const load = it.load; |
| |
| const ts = Time.add(traceSpan.start, resolution * bucket); |
| |
| let loadArray = slicesData.get(upid); |
| if (loadArray === undefined) { |
| loadArray = []; |
| slicesData.set(upid, loadArray); |
| } |
| loadArray.push({ts, dur: resolution, load}); |
| } |
| |
| // Sort rows to match timeline ordering using actual workspace track order |
| const processGroupsPlugin = ctx.plugins.getPlugin( |
| ProcessThreadGroupsPlugin, |
| ); |
| const topLevelTracks = ctx.defaultWorkspace.children; |
| const upidOrderMap = new Map<number, number>(); |
| |
| // Get the position of each upid's process group in the top-level tracks |
| // Only include upids that have corresponding track groups |
| for (const upid of slicesData.keys()) { |
| const processGroup = processGroupsPlugin.getGroupForProcess(upid); |
| if (processGroup) { |
| const orderIndex = topLevelTracks.indexOf(processGroup); |
| if (orderIndex >= 0) { |
| upidOrderMap.set(upid, orderIndex); |
| } |
| } |
| } |
| |
| // Create rows array and sort by workspace track order |
| // Only process upids that have valid track groups |
| const rows: MinimapRow[] = []; |
| const sortedUpids = Array.from(upidOrderMap.keys()).sort((a, b) => { |
| const orderA = assertExists(upidOrderMap.get(a)); |
| const orderB = assertExists(upidOrderMap.get(b)); |
| return orderA - orderB; |
| }); |
| |
| for (const upid of sortedUpids) { |
| const row = slicesData.get(upid); |
| if (row) { |
| rows.push(row); |
| } |
| } |
| return rows; |
| }, |
| }); |
| } |
| |
| private addSearchProviders(ctx: Trace) { |
| ctx.search.registerSearchProvider({ |
| name: 'Slices by name', |
| selectTracks(tracks) { |
| return tracks |
| .filter((t) => t.tags?.kinds?.includes(SLICE_TRACK_KIND)) |
| .filter((t) => |
| t.renderer.getDataset?.()?.implements({name: STR_NULL}), |
| ); |
| }, |
| async getSearchFilter(searchTerm) { |
| return { |
| where: `name GLOB ${escapeSearchQuery(searchTerm)}`, |
| columns: {name: STR_NULL}, |
| }; |
| }, |
| }); |
| |
| ctx.search.registerSearchProvider({ |
| name: 'Slices by id', |
| selectTracks(tracks) { |
| return tracks |
| .filter((t) => t.tags?.kinds?.includes(SLICE_TRACK_KIND)) |
| .filter((t) => t.renderer.getDataset?.()?.implements({id: NUM_NULL})); |
| }, |
| async getSearchFilter(searchTerm) { |
| // Attempt to parse the search term as an integer. |
| const id = Number(searchTerm); |
| |
| // Note: Number.isInteger also returns false for NaN. |
| if (!Number.isInteger(id)) { |
| return undefined; |
| } |
| |
| return { |
| where: `id = ${searchTerm}`, |
| }; |
| }, |
| }); |
| |
| ctx.search.registerSearchProvider({ |
| name: 'Slice arguments', |
| selectTracks(tracks) { |
| return tracks |
| .filter((t) => t.tags?.kinds?.includes(SLICE_TRACK_KIND)) |
| .filter((t) => |
| t.renderer.getDataset?.()?.implements({arg_set_id: NUM_NULL}), |
| ); |
| }, |
| async getSearchFilter(searchTerm) { |
| const searchLiteral = escapeSearchQuery(searchTerm); |
| return { |
| join: `args USING(arg_set_id)`, |
| where: ` |
| args.string_value GLOB ${searchLiteral} |
| OR |
| args.key GLOB ${searchLiteral} |
| `, |
| columns: {arg_set_id: NUM_NULL}, |
| }; |
| }, |
| }); |
| } |
| } |