ui: tp: Move Choreographer stage from core to extension
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/input.sql b/src/trace_processor/perfetto_sql/stdlib/android/input.sql
index a27f51f..09d2d67 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/input.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/input.sql
@@ -552,7 +552,6 @@
   ts_dispatch LONG,
   ts_receive LONG,
   ts_consume LONG,
-  ts_frame LONG,
   -- InputReader Stage
   id_reader LONG,
   track_reader LONG,
@@ -569,10 +568,6 @@
   id_consume LONG,
   track_consume LONG,
   dur_consume LONG,
-  -- Choreographer Frame Stage
-  id_frame LONG,
-  track_frame LONG,
-  dur_frame LONG,
   is_speculative_frame BOOL
 )
 AS
@@ -584,7 +579,6 @@
   e.dispatch_ts AS ts_dispatch,
   e.receive_ts AS ts_receive,
   s_cons.ts AS ts_consume,
-  s_frame.ts AS ts_frame,
   s_read.id AS id_reader,
   s_read.track_id AS track_reader,
   s_read.dur AS dur_reader,
@@ -597,9 +591,6 @@
   s_cons.id AS id_consume,
   s_cons.track_id AS track_consume,
   s_cons.dur AS dur_consume,
-  s_frame.id AS id_frame,
-  s_frame.track_id AS track_frame,
-  s_frame.dur AS dur_frame,
   e.is_speculative_frame
 FROM android_input_events AS e
 LEFT JOIN slice AS s_read
@@ -613,7 +604,16 @@
   AND s_recv.track_id = e.receive_track_id
 LEFT JOIN _input_consumers_lookup AS s_cons
   ON s_cons.cookie = e.event_seq
-LEFT JOIN _frame_choreographer_lookup AS s_frame
-  ON s_frame.frame_id = CAST(e.frame_id AS LONG)
 WHERE
-  $slice_id IN (s_read.id, s_disp.id, s_recv.id, s_cons.id, s_frame.id);
+  $slice_id IN (s_read.id, s_disp.id, s_recv.id, s_cons.id);
+
+CREATE PERFETTO VIEW _android_input_frames AS
+SELECT
+  chor.upid,
+  chor.frame_id,
+  chor.id AS id_do_frame,
+  s_chor.track_id AS track_do_frame,
+  chor.ts AS ts_do_frame,
+  s_chor.dur AS dur_do_frame
+FROM android_frames_choreographer_do_frame AS chor
+JOIN slice AS s_chor ON s_chor.id = chor.id;
diff --git a/ui/src/plugins/com.android.AndroidInputLifecycle/android_input_event_source.ts b/ui/src/plugins/com.android.AndroidInputLifecycle/android_input_event_source.ts
index bebbb3f..cd82b02 100644
--- a/ui/src/plugins/com.android.AndroidInputLifecycle/android_input_event_source.ts
+++ b/ui/src/plugins/com.android.AndroidInputLifecycle/android_input_event_source.ts
@@ -263,13 +263,4 @@
     tsField: 'ts_consume',
     durField: 'dur_consume',
   },
-  {
-    key: 'frame',
-    headerName: 'App Frame',
-    sequenceNumber: 5000,
-    idField: 'id_frame',
-    trackField: 'track_frame',
-    tsField: 'ts_frame',
-    durField: 'dur_frame',
-  },
 ];
diff --git a/ui/src/plugins/com.android.AndroidInputLifecycle/extensions/android_frames_extension.ts b/ui/src/plugins/com.android.AndroidInputLifecycle/extensions/android_frames_extension.ts
new file mode 100644
index 0000000..1328cbb
--- /dev/null
+++ b/ui/src/plugins/com.android.AndroidInputLifecycle/extensions/android_frames_extension.ts
@@ -0,0 +1,75 @@
+// Copyright (C) 2026 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 type {Trace} from '../../../public/trace';
+import type {
+  InputLifecycleExtension,
+  StageDefinition,
+  SqlJoinSpec,
+} from './interface';
+import {STR_NULL} from '../../../trace_processor/query_result';
+
+export class AndroidFramesInputLifecycleExtension
+  implements InputLifecycleExtension
+{
+  readonly id = 'android.InputLifecycleAndroidFrames';
+  readonly name = 'Android Frame Pipeline';
+
+  async isEligible(trace: Trace): Promise<boolean> {
+    const result = await trace.engine.query(
+      `SELECT 1 FROM slice WHERE name GLOB 'Choreographer#doFrame *' LIMIT 1`,
+    );
+    return result.numRows() > 0;
+  }
+
+  getSqlJoinSpec(): SqlJoinSpec {
+    return {
+      tableName: '_android_input_frames',
+      tableAlias: 'frames',
+      joinOn: 'frames.frame_id = a_evt.frame_id AND frames.upid = a_evt.upid',
+    };
+  }
+
+  getStages(): StageDefinition[] {
+    return [
+      {
+        key: 'choreographer_do_frame',
+        headerName: 'Choreographer#doFrame',
+        sequenceNumber: 5000,
+        idField: 'id_do_frame',
+        trackField: 'track_do_frame',
+        tsField: 'ts_do_frame',
+        durField: 'dur_do_frame',
+      },
+    ];
+  }
+
+  async resolveInputId(
+    trace: Trace,
+    sliceId: number,
+  ): Promise<string | undefined> {
+    const query = `
+      SELECT e.input_event_id AS input_id
+      FROM android_input_events e
+      JOIN android_frames_choreographer_do_frame chor 
+        ON chor.frame_id = e.frame_id
+        AND chor.upid = e.upid
+      WHERE chor.id = ${sliceId}
+      LIMIT 1
+    `;
+    const result = await trace.engine.query(query);
+    const it = result.iter({input_id: STR_NULL});
+    return it.valid() ? it.input_id ?? undefined : undefined;
+  }
+}
diff --git a/ui/src/plugins/com.android.AndroidInputLifecycle/index.ts b/ui/src/plugins/com.android.AndroidInputLifecycle/index.ts
index 71920fe..9f50747 100644
--- a/ui/src/plugins/com.android.AndroidInputLifecycle/index.ts
+++ b/ui/src/plugins/com.android.AndroidInputLifecycle/index.ts
@@ -32,9 +32,11 @@
 import type {QueryResult} from '../../base/query_slot';
 import type {InputLifecycleExtension, NavTarget} from './extensions/interface';
 import {PixelInputLifecycleExtension} from './extensions/pixel_extension';
+import {AndroidFramesInputLifecycleExtension} from './extensions/android_frames_extension';
 
 const EXTENSIONS: InputLifecycleExtension[] = [
   new PixelInputLifecycleExtension(),
+  new AndroidFramesInputLifecycleExtension(),
 ];
 
 export default class AndroidInputLifecyclePlugin implements PerfettoPlugin {