Support perfetto transition trace parsing in Winscope

Bug: 276431795

Test: npm run test:presubmit
Change-Id: If02ef76e93cf809dbd165502fd6bfb91be5d74cb
diff --git a/tools/winscope/src/flickerlib/common.js b/tools/winscope/src/flickerlib/common.js
index 1bb79c9..397dde8 100644
--- a/tools/winscope/src/flickerlib/common.js
+++ b/tools/winscope/src/flickerlib/common.js
@@ -103,6 +103,7 @@
 const RectF = require('flickerlib/flicker').android.tools.common.datatypes.RectF;
 const WindowingMode = require('flickerlib/flicker').android.tools.common.traces.wm.WindowingMode;
 const CrossPlatform = require('flickerlib/flicker').android.tools.common.CrossPlatform;
+const Timestamp = require('flickerlib/flicker').android.tools.common.Timestamp;
 const TimestampFactory = require('flickerlib/flicker').android.tools.common.TimestampFactory;
 
 const NoCache = require('flickerlib/flicker').android.tools.common.NoCache;
@@ -341,6 +342,7 @@
   Rotation,
   WindowingMode,
   CrossPlatform,
+  Timestamp,
   TimestampFactory,
   NoCache,
   // Service
diff --git a/tools/winscope/src/parsers/perfetto/parser_factory.ts b/tools/winscope/src/parsers/perfetto/parser_factory.ts
index 8f64522..9159e53 100644
--- a/tools/winscope/src/parsers/perfetto/parser_factory.ts
+++ b/tools/winscope/src/parsers/perfetto/parser_factory.ts
@@ -20,11 +20,16 @@
 import {Parser} from 'trace/parser';
 import {TraceFile} from 'trace/trace_file';
 import {initWasm, resetEngineWorker, WasmEngineProxy} from 'trace_processor/wasm_engine_proxy';
+import {ParserTransitions} from './parser_transitions';
 import {ParserSurfaceFlinger} from './parser_surface_flinger';
 import {ParserTransactions} from './parser_transactions';
 
 export class ParserFactory {
-  private static readonly PARSERS = [ParserSurfaceFlinger, ParserTransactions];
+  private static readonly PARSERS = [
+    ParserSurfaceFlinger,
+    ParserTransactions,
+    ParserTransitions,
+  ];
   private static readonly CHUNK_SIZE_BYTES = 50 * 1024 * 1024;
   private static traceProcessor?: WasmEngineProxy;
 
diff --git a/tools/winscope/src/parsers/perfetto/parser_transitions.ts b/tools/winscope/src/parsers/perfetto/parser_transitions.ts
new file mode 100644
index 0000000..83f6b68
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_transitions.ts
@@ -0,0 +1,150 @@
+/*
+ * 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 {assertDefined} from 'common/assert_utils';
+import {TimestampType} from 'common/time';
+import {
+  CrossPlatform,
+  ShellTransitionData,
+  Timestamp,
+  Transition,
+  TransitionChange,
+  TransitionType,
+  WmTransitionData,
+} from 'flickerlib/common';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {TraceFile} from 'trace/trace_file';
+import {TraceType} from 'trace/trace_type';
+import {WasmEngineProxy} from 'trace_processor/wasm_engine_proxy';
+import {AbstractParser} from './abstract_parser';
+import {FakeProto, FakeProtoBuilder} from './fake_proto_builder';
+
+export class ParserTransitions extends AbstractParser<Transition> {
+  constructor(traceFile: TraceFile, traceProcessor: WasmEngineProxy) {
+    super(traceFile, traceProcessor);
+  }
+
+  override getTraceType(): TraceType {
+    return TraceType.TRANSITION;
+  }
+
+  override async getEntry(index: number, timestampType: TimestampType): Promise<LayerTraceEntry> {
+    const transitionProto = await this.queryTransition(index);
+
+    if (this.handlerIdToName === undefined) {
+      const handlers = await this.queryHandlers();
+      this.handlerIdToName = {};
+      handlers.forEach((it) => (assertDefined(this.handlerIdToName)[it.id] = it.name));
+    }
+
+    return new Transition(
+      Number(transitionProto.id),
+      new WmTransitionData(
+        this.toTimestamp(transitionProto.createTimeNs),
+        this.toTimestamp(transitionProto.sendTimeNs),
+        this.toTimestamp(transitionProto.wmAbortTimeNs),
+        this.toTimestamp(transitionProto.finishTimeNs),
+        this.toTimestamp(transitionProto.startingWindowRemoveTimeNs),
+        transitionProto.startTransactionId.toString(),
+        transitionProto.finishTransactionId.toString(),
+        TransitionType.Companion.fromInt(Number(transitionProto.type)),
+        transitionProto.targets.map(
+          (it: any) =>
+            new TransitionChange(
+              TransitionType.Companion.fromInt(Number(it.mode)),
+              Number(it.layerId),
+              Number(it.windowId)
+            )
+        )
+      ),
+      new ShellTransitionData(
+        this.toTimestamp(transitionProto.dispatchTimeNs),
+        this.toTimestamp(transitionProto.mergeRequestTimeNs),
+        this.toTimestamp(transitionProto.mergeTimeNs),
+        this.toTimestamp(transitionProto.shellAbortTimeNs),
+        this.handlerIdToName[Number(transitionProto.handler)],
+        transitionProto.mergeTarget ? Number(transitionProto.mergeTarget) : null
+      )
+    );
+  }
+
+  private toTimestamp(n: BigInt | undefined | null): Timestamp | null {
+    if (n === undefined || n === null) {
+      return null;
+    }
+
+    const realToElapsedTimeOffsetNs = assertDefined(this.realToElapsedTimeOffsetNs);
+    const unixNs = BigInt(n.toString()) + realToElapsedTimeOffsetNs;
+
+    return CrossPlatform.timestamp.fromString(n.toString(), null, unixNs.toString());
+  }
+
+  protected override getTableName(): string {
+    return 'window_manager_shell_transitions';
+  }
+
+  private async queryTransition(index: number): Promise<FakeProto> {
+    const protoBuilder = new FakeProtoBuilder();
+
+    const sql = `
+      SELECT
+        transitions.transition_id,
+        args.key,
+        args.value_type,
+        args.int_value,
+        args.string_value,
+        args.real_value
+      FROM
+        window_manager_shell_transitions as transitions
+        INNER JOIN args ON transitions.arg_set_id = args.arg_set_id
+      WHERE transitions.id = ${index};
+    `;
+    const result = await this.traceProcessor.query(sql).waitAllRows();
+
+    for (const it = result.iter({}); it.valid(); it.next()) {
+      protoBuilder.addArg(
+        it.get('key') as string,
+        it.get('value_type') as string,
+        it.get('int_value') as bigint | undefined,
+        it.get('real_value') as number | undefined,
+        it.get('string_value') as string | undefined
+      );
+    }
+
+    return protoBuilder.build();
+  }
+
+  private async queryHandlers(): Promise<TransitionHandler[]> {
+    const sql = 'SELECT handler_id, handler_name FROM window_manager_shell_transition_handlers;';
+    const result = await this.traceProcessor.query(sql).waitAllRows();
+
+    const handlers: TransitionHandler[] = [];
+    for (const it = result.iter({}); it.valid(); it.next()) {
+      handlers.push({
+        id: it.get('handler_id') as number,
+        name: it.get('handler_name') as string,
+      });
+    }
+
+    return handlers;
+  }
+
+  private handlerIdToName: {[id: number]: string} | undefined = undefined;
+}
+
+interface TransitionHandler {
+  id: number;
+  name: string;
+}
diff --git a/tools/winscope/src/parsers/perfetto/parser_transitions_test.ts b/tools/winscope/src/parsers/perfetto/parser_transitions_test.ts
new file mode 100644
index 0000000..02113d5
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_transitions_test.ts
@@ -0,0 +1,90 @@
+/*
+ * 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 {assertDefined} from 'common/assert_utils';
+import {ElapsedTimestamp, RealTimestamp, TimestampType} from 'common/time';
+import {Transition, TransitionType} from 'flickerlib/common';
+import {TraceBuilder} from 'test/unit/trace_builder';
+import {UnitTestUtils} from 'test/unit/utils';
+import {Parser} from 'trace/parser';
+import {Trace} from 'trace/trace';
+import {TraceType} from 'trace/trace_type';
+
+describe('Perfetto ParserTransitions', () => {
+  describe('valid trace', () => {
+    let parser: Parser<Transition>;
+    let trace: Trace<Transition>;
+
+    beforeAll(async () => {
+      parser = await UnitTestUtils.getPerfettoParser(
+        TraceType.TRANSITION,
+        'traces/perfetto/shell_transitions_trace.perfetto-trace'
+      );
+      trace = new TraceBuilder().setType(TraceType.TRANSITION).setParser(parser).build();
+    });
+
+    it('has expected trace type', () => {
+      expect(parser.getTraceType()).toEqual(TraceType.TRANSITION);
+    });
+
+    it('provides elapsed timestamps', () => {
+      const expected = [
+        new ElapsedTimestamp(479602824452n),
+        new ElapsedTimestamp(480676958445n),
+        new ElapsedTimestamp(487195167758n),
+      ];
+      const actual = assertDefined(parser.getTimestamps(TimestampType.ELAPSED)).slice(0, 3);
+      expect(actual).toEqual(expected);
+    });
+
+    it('provides real timestamps', () => {
+      const expected = [
+        new RealTimestamp(1700573903102738218n),
+        new RealTimestamp(1700573904176872211n),
+        new RealTimestamp(1700573910695081524n),
+      ];
+      const actual = assertDefined(parser.getTimestamps(TimestampType.REAL)).slice(0, 3);
+      expect(actual).toEqual(expected);
+    });
+
+    it('decodes transition properties', async () => {
+      const entry = await parser.getEntry(0, TimestampType.REAL);
+
+      expect(entry.id).toEqual(32);
+      expect(entry.createTime.elapsedNanos.toString()).toEqual('479583450794');
+      expect(entry.sendTime.elapsedNanos.toString()).toEqual('479596405791');
+      expect(entry.abortTime).toEqual(null);
+      expect(entry.finishTime.elapsedNanos.toString()).toEqual('480124777862');
+      expect(entry.startingWindowRemoveTime.elapsedNanos.toString()).toEqual('479719652658');
+      expect(entry.dispatchTime.elapsedNanos.toString()).toEqual('479602824452');
+      expect(entry.mergeRequestTime).toEqual(null);
+      expect(entry.mergeTime).toEqual(null);
+      expect(entry.shellAbortTime).toEqual(null);
+      expect(entry.startTransactionId.toString()).toEqual('5811090758076');
+      expect(entry.finishTransactionId.toString()).toEqual('5811090758077');
+      expect(entry.type).toEqual(TransitionType.OPEN);
+      expect(entry.mergeTarget).toEqual(null);
+      expect(entry.handler).toEqual('com.android.wm.shell.transition.DefaultMixedHandler');
+      expect(entry.merged).toEqual(false);
+      expect(entry.played).toEqual(true);
+      expect(entry.aborted).toEqual(false);
+      expect(entry.changes.length).toEqual(2);
+      expect(entry.changes[0].layerId).toEqual(398);
+      expect(entry.changes[1].layerId).toEqual(47);
+      expect(entry.changes[0].transitMode).toEqual(TransitionType.TO_FRONT);
+      expect(entry.changes[1].transitMode).toEqual(TransitionType.TO_BACK);
+    });
+  });
+});
diff --git a/tools/winscope/src/test/fixtures/traces/perfetto/shell_transitions_trace.perfetto-trace b/tools/winscope/src/test/fixtures/traces/perfetto/shell_transitions_trace.perfetto-trace
new file mode 100644
index 0000000..3625318
--- /dev/null
+++ b/tools/winscope/src/test/fixtures/traces/perfetto/shell_transitions_trace.perfetto-trace
Binary files differ