Handler for fullTraceJank

Adds debug track for full trace jank given jank type and process when passed with specific url patterns. Handler used in the PinAndroidPerfMetrics plugin.

URL Examples -
ui.perfetto.dev/#dev.perfetto.PinAndroidPerfMetrics:metrics=perfetto_ft_launcher-missed_sf_frames-mean

Test: pinFullTrace_unittest.ts. Run using ./ui/run-unittests
Bug: b/337774166
Change-Id: I9e0cbe6bd93f39d38cc7c18dd329944adf37c5bc
diff --git a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/fullTraceJankMetricHandler.ts b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/fullTraceJankMetricHandler.ts
new file mode 100644
index 0000000..afe75a9
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/fullTraceJankMetricHandler.ts
@@ -0,0 +1,143 @@
+// 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 {
+  expandProcessName,
+  FullTraceMetricData,
+  JankType,
+  MetricHandler,
+} from './metricUtils';
+import {PluginContextTrace} from '../../../public';
+import {
+  addAndPinSliceTrack,
+  TrackType,
+} from '../../dev.perfetto.AndroidCujs/trackUtils';
+import {SimpleSliceTrackConfig} from '../../../frontend/simple_slice_track';
+import {PLUGIN_ID} from '../pluginId';
+
+class FullTraceJankMetricHandler implements MetricHandler {
+  /**
+   * Matches metric key & return parsed data if successful.
+   *
+   * @param {string} metricKey The metric key to match.
+   * @returns {FullTraceMetricData | undefined} Parsed data or undefined if no match.
+   */
+  public match(metricKey: string): FullTraceMetricData | undefined {
+    const matcher =
+      /perfetto_ft_(?<process>.*)-missed_(?<jankType>frames|sf_frames|app_frames)/;
+    const match = matcher.exec(metricKey);
+    if (!match?.groups) {
+      return undefined;
+    }
+    const metricData: FullTraceMetricData = {
+      process: expandProcessName(match.groups.process),
+      jankType: match.groups.jankType as JankType,
+    };
+    return metricData;
+  }
+
+  /**
+   * Adds the debug track for full trace jank metrics
+   * The track contains missed sf/app frames for the process
+   * registerStaticTrack used when plugin adds tracks onTraceload()
+   * addDebugSliceTrack used for adding tracks using the command
+   *
+   * @param {FullTraceMetricData} metricData Parsed metric data for the full trace jank
+   * @param {PluginContextTrace} ctx PluginContextTrace for trace related properties and methods
+   * @param {TrackType} type 'static' for onTraceload and 'debug' for command
+   * @returns {void} Adds one track for Jank slice
+   */
+  public async addMetricTrack(
+    metricData: FullTraceMetricData,
+    ctx: PluginContextTrace,
+    type: TrackType,
+  ) {
+    const INCLUDE_PREQUERY = `
+    INCLUDE PERFETTO MODULE android.frames.jank_type;
+    INCLUDE PERFETTO MODULE slices.slices;
+    `;
+    const uri = `${PLUGIN_ID}#FullTraceJank#${metricData}`;
+    const {config: fullTraceJankConfig, trackName: trackName} =
+      this.fullTraceJankConfig(metricData);
+    await ctx.engine.query(INCLUDE_PREQUERY);
+    addAndPinSliceTrack(ctx, fullTraceJankConfig, trackName, type, uri);
+  }
+
+  private fullTraceJankConfig(metricData: FullTraceMetricData): {
+    config: SimpleSliceTrackConfig;
+    trackName: string;
+  } {
+    let jankTypeFilter;
+    let jankTypeDisplayName = 'all';
+    if (metricData.jankType?.includes('app')) {
+      jankTypeFilter = ' android_is_app_jank_type(display_value)';
+      jankTypeDisplayName = 'app';
+    } else if (metricData.jankType?.includes('sf')) {
+      jankTypeFilter = ' android_is_sf_jank_type(display_value)';
+      jankTypeDisplayName = 'sf';
+    }
+    const processName = metricData.process;
+
+    // TODO: b/324245198 - Refactor when jank_type added to android_frame_stats
+    const fullTraceJankQuery = `
+      WITH filtered_args AS (
+        SELECT DISTINCT arg_set_id
+        FROM args
+        WHERE key = 'Jank type'
+        ${jankTypeFilter ? 'AND ' + jankTypeFilter : ''}
+      )
+      SELECT
+        name,
+        ts as ts,
+        dur as dur,
+        track_id as track_id,
+        id as slice_id,
+        thread_dur as thread_dur,
+        category,
+        thread_name,
+        tid as tid,
+        process_name,
+        pid as pid
+      FROM _slice_with_thread_and_process_info
+      JOIN filtered_args ON filtered_args.arg_set_id = _slice_with_thread_and_process_info.arg_set_id
+      WHERE process_name = '${processName}'`;
+    const fullTraceJankColumns = [
+      'name',
+      'ts',
+      'dur',
+      'track_id',
+      'slice_id',
+      'thread_dur',
+      'category',
+      'thread_name',
+      'tid',
+      'process_name',
+      'pid',
+    ];
+    const fullTraceJankConfig: SimpleSliceTrackConfig = {
+      data: {
+        sqlSource: fullTraceJankQuery,
+        columns: fullTraceJankColumns,
+      },
+      columns: {ts: 'ts', dur: 'dur', name: 'name'},
+      argColumns: fullTraceJankColumns,
+    };
+
+    const trackName = jankTypeDisplayName + ' missed frames in ' + processName;
+
+    return {config: fullTraceJankConfig, trackName: trackName};
+  }
+}
+
+export const pinFullTraceJankInstance = new FullTraceJankMetricHandler();