Merge "Use gmock functions in version_script_parser_test" into main am: a11c87c968 am: a40a3656a0 am: 1b1e59bb03 am: 968eecf3d3 am: 535d6c5a01

Original change: https://android-review.googlesource.com/c/platform/development/+/2763026

Change-Id: I4782f2d38ee807b2938c0efc6dbc156816eedc5e
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/apps/Development/AndroidManifest.xml b/apps/Development/AndroidManifest.xml
index 016a245..7310f1c 100644
--- a/apps/Development/AndroidManifest.xml
+++ b/apps/Development/AndroidManifest.xml
@@ -43,9 +43,9 @@
     <uses-permission android:name="android.permission.WRITE_SETTINGS" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="com.google.android.googleapps.permission.ACCESS_GOOGLE_PASSWORD" />
-    <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH" />
-    <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.ALL_SERVICES" />
-    <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.YouTubeUser" />
+    <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH" android:maxSdkVersion="34"/>
+    <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.ALL_SERVICES" android:maxSdkVersion="34"/>
+    <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.YouTubeUser" android:maxSdkVersion="34"/>
 
     <application android:label="Dev Tools"
             android:icon="@mipmap/ic_launcher_devtools">
diff --git a/apps/PushApiAuthenticator/Android.bp b/apps/PushApiAuthenticator/Android.bp
index 48ffea0..1d45d79 100644
--- a/apps/PushApiAuthenticator/Android.bp
+++ b/apps/PushApiAuthenticator/Android.bp
@@ -11,7 +11,9 @@
     name: "PushApiAuthenticator",
     // Only compile source java files in this apk.
     srcs: ["src/**/*.java"],
-    sdk_version: "current",
+    sdk_version: "26",
+    target_sdk_version: "26",
+    min_sdk_version: "26",
     dex_preopt: {
         enabled: false,
     },
diff --git a/apps/PushApiAuthenticator/AndroidManifest.xml b/apps/PushApiAuthenticator/AndroidManifest.xml
index 2b641d1..d89aa8b 100644
--- a/apps/PushApiAuthenticator/AndroidManifest.xml
+++ b/apps/PushApiAuthenticator/AndroidManifest.xml
@@ -19,9 +19,10 @@
     package="com.example.android.pushapiauthenticator">
     <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
     <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
     <!-- Uses API introduced in O (26) -->
     <uses-sdk android:minSdkVersion="1"
-          android:targetSdkVersion="26"/>
+          android:targetSdkVersion="30"/>
     <application android:label="Auth Tester" android:icon="@drawable/push">
         <activity android:name="MainActivity">
             <intent-filter>
diff --git a/apps/PushApiAuthenticator/res/layout/activity_main.xml b/apps/PushApiAuthenticator/res/layout/activity_main.xml
index 715d90e..3199e4d 100644
--- a/apps/PushApiAuthenticator/res/layout/activity_main.xml
+++ b/apps/PushApiAuthenticator/res/layout/activity_main.xml
@@ -171,14 +171,14 @@
             <RadioButton
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:text="SET UM_NOT_VISIBLE(4)"
+                android:text="SET UM_NOT_VISIBLE (4)"
                 android:id="@+id/notVisibleButton"
                 android:checked="false" />
 
             <RadioButton
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:text="SET NOT_VISIBLE(3)"
+                android:text="SET NOT_VISIBLE (3)"
                 android:id="@+id/forcedNotVisibleButton"
                 android:checked="false" />
 
diff --git a/apps/PushApiAuthenticator/src/com/example/android/pushapiauthenticator/MainActivity.java b/apps/PushApiAuthenticator/src/com/example/android/pushapiauthenticator/MainActivity.java
index 9745823..826d844 100644
--- a/apps/PushApiAuthenticator/src/com/example/android/pushapiauthenticator/MainActivity.java
+++ b/apps/PushApiAuthenticator/src/com/example/android/pushapiauthenticator/MainActivity.java
@@ -161,7 +161,7 @@
                             am.setAccountVisibility(currentAccount, packageName,
                                     AccountManager.VISIBILITY_USER_MANAGED_VISIBLE);
                             Toast.makeText(
-                                    getApplicationContext(), "Set UM_VISIBLE(2) "
+                                    getApplicationContext(), "Set UM_VISIBLE (2) "
                                             + currentAccount.name + " to " + packageName,
                                     Toast.LENGTH_SHORT).show();
                             break;
@@ -169,7 +169,7 @@
                             am.setAccountVisibility(currentAccount, packageName,
                                     AccountManager.VISIBILITY_USER_MANAGED_NOT_VISIBLE);
                             Toast.makeText(
-                                    getApplicationContext(), "Set UM_NOT_VISIBLE(4) "
+                                    getApplicationContext(), "Set UM_NOT_VISIBLE (4) "
                                             + currentAccount.name + " to " + packageName,
                                     Toast.LENGTH_SHORT).show();
                             break;
@@ -177,20 +177,21 @@
                             am.setAccountVisibility(currentAccount, packageName,
                                     AccountManager.VISIBILITY_NOT_VISIBLE);
                             Toast.makeText(
-                                    getApplicationContext(), "Removing visibility(3) "
+                                    getApplicationContext(), "Removing visibility (3) "
                                             + currentAccount.name + " of " + packageName,
                                     Toast.LENGTH_SHORT).show();
                             break;
                         case R.id.getButton:
                             Toast.makeText(getApplicationContext(),
-                                    "Is " + currentAccount.name + " visible to " + packageName
-                                            + "?\n"
-                                            + am.getAccountVisibility(currentAccount, packageName),
+                                    "Visibility = "
+                                            + am.getAccountVisibility(currentAccount, packageName)
+                                            + " for " + currentAccount.name + ", app= "
+                                            + packageName,
                                     Toast.LENGTH_SHORT).show();
                             break;
                         case R.id.addAccountButton:
                             Toast.makeText(getApplicationContext(),
-                                    "Adding account explicitly!"
+                                    "Adding account "
                                             + am.addAccountExplicitly(currentAccount, null, null),
                                     Toast.LENGTH_SHORT).show();
                             break;
@@ -199,7 +200,7 @@
                             packageAndVisibilitys.put(packageName,
                                     AccountManager.VISIBILITY_USER_MANAGED_VISIBLE);
                             Toast.makeText(getApplicationContext(),
-                                    "Adding account explicitly!"
+                                    "Adding account "
                                             + am.addAccountExplicitly(currentAccount, null, null,
                                                     packageAndVisibilitys)
                                             + " with visibility for  " + packageName + "!",
@@ -207,7 +208,7 @@
                             break;
                         case R.id.removeAccount:
                             Toast.makeText(getApplicationContext(),
-                                    "Removing account explicitly!"
+                                    "Removing account "
                                             + am.removeAccountExplicitly(currentAccount),
                                     Toast.LENGTH_SHORT).show();
                             break;
diff --git a/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionEvent.java b/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionEvent.java
index c650aee..d56e5ec 100644
--- a/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionEvent.java
+++ b/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionEvent.java
@@ -19,6 +19,7 @@
 import android.app.ActivityManager;
 import android.app.AppGlobals;
 import android.app.IActivityManager;
+import android.content.Context;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.content.pm.PermissionInfo;
@@ -49,10 +50,11 @@
             Logger.out.println(String.format(":Permission %s %s to package %s",
                     grant ? "grant" : "revoke", mPermissionInfo.name, mPkg));
             if (grant) {
-                permissionManager.grantRuntimePermission(mPkg, mPermissionInfo.name, currentUser);
+                permissionManager.grantRuntimePermission(mPkg, mPermissionInfo.name,
+                        Context.DEVICE_ID_DEFAULT, currentUser);
             } else {
-                permissionManager.revokeRuntimePermission(mPkg, mPermissionInfo.name, currentUser,
-                        null);
+                permissionManager.revokeRuntimePermission(mPkg, mPermissionInfo.name,
+                        Context.DEVICE_ID_DEFAULT, currentUser, null);
             }
             return MonkeyEvent.INJECT_SUCCESS;
         } catch (RemoteException re) {
diff --git a/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionUtil.java b/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionUtil.java
index 21be743..b6f9e75 100644
--- a/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionUtil.java
+++ b/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionUtil.java
@@ -17,6 +17,7 @@
 package com.android.commands.monkey;
 
 import android.Manifest;
+import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageInfo;
@@ -106,7 +107,8 @@
     }
 
     private boolean shouldTargetPermission(String pkg, PermissionInfo pi) throws RemoteException {
-        int flags = mPermManager.getPermissionFlags(pkg, pi.name, UserHandle.myUserId());
+        int flags = mPermManager.getPermissionFlags(pkg, pi.name, Context.DEVICE_ID_DEFAULT,
+                UserHandle.myUserId());
         int fixedPermFlags = PackageManager.FLAG_PERMISSION_SYSTEM_FIXED
                 | PackageManager.FLAG_PERMISSION_POLICY_FIXED;
         return pi.group != null && pi.protectionLevel == PermissionInfo.PROTECTION_DANGEROUS
diff --git a/cmds/monkey/src/com/android/commands/monkey/MonkeyRotationEvent.java b/cmds/monkey/src/com/android/commands/monkey/MonkeyRotationEvent.java
index 08f66bc..ad6df5f 100644
--- a/cmds/monkey/src/com/android/commands/monkey/MonkeyRotationEvent.java
+++ b/cmds/monkey/src/com/android/commands/monkey/MonkeyRotationEvent.java
@@ -50,9 +50,9 @@
 
         // inject rotation event
         try {
-            iwm.freezeRotation(mRotationDegree);
+            iwm.freezeRotation(mRotationDegree, /* caller= */ "MonkeyRotationEven#injectEvent");
             if (!mPersist) {
-                iwm.thawRotation();
+                iwm.thawRotation(/* caller= */ "MonkeyRotationEven#injectEvent");
             }
             return MonkeyEvent.INJECT_SUCCESS;
         } catch (RemoteException ex) {
diff --git a/samples/AconfigDemo/aconfig_demo_flags.aconfig b/samples/AconfigDemo/aconfig_demo_flags.aconfig
index 2b68572..4f2197a 100644
--- a/samples/AconfigDemo/aconfig_demo_flags.aconfig
+++ b/samples/AconfigDemo/aconfig_demo_flags.aconfig
@@ -48,4 +48,11 @@
   description: "A read only flag for demo"
   bug: "298754733"
   is_fixed_read_only: true
-}
\ No newline at end of file
+}
+
+flag {
+  name: "test_flag_gantry"
+  namespace: "gantry"
+  description: "A flag used internally by the aconfig team for some testing. I'll be deleted soon!"
+  bug: "297503172"
+}
diff --git a/samples/PictureInPicture/ComposePip/.gitignore b/samples/PictureInPicture/ComposePip/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/samples/PictureInPicture/ComposePip/app/.gitignore b/samples/PictureInPicture/ComposePip/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/build.gradle b/samples/PictureInPicture/ComposePip/app/build.gradle
new file mode 100644
index 0000000..4773587
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/build.gradle
@@ -0,0 +1,66 @@
+plugins {
+    id 'com.android.application'
+    id 'org.jetbrains.kotlin.android'
+}
+
+android {
+    namespace 'com.example.samplepip'
+    compileSdk 33
+
+    defaultConfig {
+        applicationId "com.example.samplepip"
+        minSdk 24
+        targetSdk 33
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        vectorDrawables {
+            useSupportLibrary true
+        }
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    kotlinOptions {
+        jvmTarget = '1.8'
+    }
+    buildFeatures {
+        compose true
+    }
+    composeOptions {
+        kotlinCompilerExtensionVersion '1.4.0'
+    }
+    packagingOptions {
+        resources {
+            excludes += '/META-INF/{AL2.0,LGPL2.1}'
+        }
+    }
+}
+
+dependencies {
+    implementation 'androidx.core:core-ktx:1.10.1'
+    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
+    implementation 'androidx.activity:activity-compose:1.7.2'
+    implementation "androidx.compose.ui:ui:$compose_ui_version"
+    implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
+    implementation 'androidx.compose.material3:material3:1.0.0'
+    implementation 'androidx.compose.material:material:1.3.0'
+    implementation "androidx.media3:media3-exoplayer:$media3_version"
+    implementation "androidx.media3:media3-exoplayer-dash:$media3_version"
+    implementation "androidx.media3:media3-ui:$media3_version"
+    testImplementation 'junit:junit:4.13.2'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
+    debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
+    debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
+}
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/proguard-rules.pro b/samples/PictureInPicture/ComposePip/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/androidTest/java/com/example/samplepip/ExampleInstrumentedTest.kt b/samples/PictureInPicture/ComposePip/app/src/androidTest/java/com/example/samplepip/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..3fa54fe
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/androidTest/java/com/example/samplepip/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.example.samplepip
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+  @Test
+  fun useAppContext() {
+    // Context of the app under test.
+    val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+    assertEquals("com.example.samplepip", appContext.packageName)
+  }
+}
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/AndroidManifest.xml b/samples/PictureInPicture/ComposePip/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..febc75a
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+  <uses-permission android:name="android.permission.INTERNET" />
+
+  <application
+      android:allowBackup="true"
+      android:dataExtractionRules="@xml/data_extraction_rules"
+      android:fullBackupContent="@xml/backup_rules"
+      android:icon="@mipmap/ic_launcher"
+      android:label="@string/app_name"
+      android:roundIcon="@mipmap/ic_launcher_round"
+      android:supportsRtl="true"
+      android:theme="@style/Theme.SamplePip"
+      tools:targetApi="31">
+    <activity
+        android:name=".MainActivity"
+        android:exported="true"
+        android:supportsPictureInPicture="true"
+        android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
+        android:label="@string/app_name"
+        android:theme="@style/Theme.SamplePip">
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN"/>
+
+        <category android:name="android.intent.category.LAUNCHER"/>
+      </intent-filter>
+
+      <meta-data
+          android:name="android.app.lib_name"
+          android:value=""/>
+    </activity>
+  </application>
+
+</manifest>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/MainActivity.kt b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/MainActivity.kt
new file mode 100644
index 0000000..3946c16
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/MainActivity.kt
@@ -0,0 +1,68 @@
+package com.example.samplepip
+
+import android.app.PictureInPictureParams
+import android.graphics.Rect
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.util.Rational
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.media3.exoplayer.ExoPlayer
+import com.example.samplepip.R.raw
+import com.example.samplepip.ui.theme.PiPComposeSampleTheme
+
+
+class MainActivity : ComponentActivity() {
+  private var player: ExoPlayer? = null
+  @RequiresApi(Build.VERSION_CODES.O)
+  override fun onCreate(savedInstanceState: Bundle?) {
+    super.onCreate(savedInstanceState)
+    player = ExoPlayer.Builder(applicationContext).build()
+    setContent {
+      Column {
+        PiPComposeSampleTheme {
+          SampleVideoPlayer(
+            videoUri = Uri.parse("android.resource://$packageName/${raw.samplevideo}"),
+            modifier = Modifier.fillMaxWidth().pictureInPicture(onBoundsChange = ::onBoundsChange),
+            player = player!!
+
+            //TODO(b/276395464): only auto enter on unpaused state: add callback for playing and pausing
+            // as well as onUserLeaveHint
+          )
+        }
+
+        PipButton()
+      }
+    }
+  }
+
+  @RequiresApi(Build.VERSION_CODES.O)
+  @Composable
+  fun PipButton() {
+    Button(onClick = {
+      enterPictureInPictureMode()
+    }) {
+      Text(text = getString(R.string.start_pip_button))
+    }
+  }
+
+  private fun onBoundsChange(bounds: Rect) {
+    /**
+     * Whenever the players bounds change, we want to update our params
+     */
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+      val builder = PictureInPictureParams.Builder()
+        .setSourceRectHint(bounds)
+        .setAspectRatio(Rational(bounds.width(), bounds.height()))
+      setPictureInPictureParams(builder.build())
+    }
+  }
+}
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/SampleVideoPlayer.kt b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/SampleVideoPlayer.kt
new file mode 100644
index 0000000..d403e76
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/SampleVideoPlayer.kt
@@ -0,0 +1,42 @@
+package com.example.samplepip
+
+import android.graphics.Rect
+import android.net.Uri
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.toAndroidRectF
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.graphics.toRect
+import androidx.media3.common.MediaItem
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.ui.PlayerView
+
+@Composable
+fun SampleVideoPlayer(
+  videoUri: Uri,
+  modifier: Modifier,
+  player: ExoPlayer,
+) {
+  AndroidView(
+    factory = {
+      PlayerView(it).apply {
+        //TODO(b/276395464): set proper player functionality
+        setPlayer(player)
+        player.setMediaItem(MediaItem.fromUri(videoUri))
+        player.play()
+      }
+    },
+    modifier = modifier,
+  )
+}
+
+// callback to get player bounds, used to smoothly enter PIP
+fun Modifier.pictureInPicture(onBoundsChange: (Rect) -> Unit): Modifier {
+  return this then Modifier.onGloballyPositioned { layoutCoordinates ->
+    // Use onGloballyPositioned to get player bounds, which gets passed back to the main activity
+    val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
+    onBoundsChange(sourceRect)
+  }
+}
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Color.kt b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Color.kt
new file mode 100644
index 0000000..4b133b1
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Color.kt
@@ -0,0 +1,8 @@
+package com.example.samplepip.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple200 = Color(0xFFBB86FC)
+val Purple500 = Color(0xFF6200EE)
+val Purple700 = Color(0xFF3700B3)
+val Teal200 = Color(0xFF03DAC5)
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Shape.kt b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Shape.kt
new file mode 100644
index 0000000..ef230e8
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Shape.kt
@@ -0,0 +1,11 @@
+package com.example.samplepip.ui.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Shapes
+import androidx.compose.ui.unit.dp
+
+val Shapes = Shapes(
+  small = RoundedCornerShape(4.dp),
+  medium = RoundedCornerShape(4.dp),
+  large = RoundedCornerShape(0.dp)
+)
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Theme.kt b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Theme.kt
new file mode 100644
index 0000000..aeff16d
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Theme.kt
@@ -0,0 +1,60 @@
+package com.example.samplepip.ui.theme
+
+import android.app.Activity
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+private val DarkColorScheme = darkColorScheme(
+  primary = Purple200,
+  secondary = Purple700,
+  tertiary = Teal200
+)
+
+private val LightColorScheme = lightColorScheme(
+  primary = Purple500,
+  secondary = Purple700,
+  tertiary = Teal200
+
+  /* Other default colors to override
+    background = Color.White,
+    surface = Color.White,
+    onPrimary = Color.White,
+    onSecondary = Color.Black,
+    onBackground = Color.Black,
+    onSurface = Color.Black,
+    */
+)
+
+@Composable
+fun PiPComposeSampleTheme(
+  darkTheme: Boolean = isSystemInDarkTheme(),
+  content: @Composable () -> Unit
+) {
+  val colorScheme =
+    if (!darkTheme) {
+      LightColorScheme
+    } else {
+      DarkColorScheme
+    }
+  val view = LocalView.current
+  if (!view.isInEditMode) {
+    SideEffect {
+      val window = (view.context as Activity).window
+      window.statusBarColor = colorScheme.primary.toArgb()
+      WindowCompat.getInsetsController(window, view)?.isAppearanceLightStatusBars = darkTheme
+    }
+  }
+
+  MaterialTheme(
+    colorScheme = colorScheme,
+    typography = Typography,
+    content = content
+  )
+}
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Type.kt b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Type.kt
new file mode 100644
index 0000000..5525539
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Type.kt
@@ -0,0 +1,28 @@
+package com.example.samplepip.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+  bodyLarge = TextStyle(
+    fontFamily = FontFamily.Default,
+    fontWeight = FontWeight.Normal,
+    fontSize = 16.sp
+  )
+  /* Other default text styles to override
+    button = TextStyle(
+        fontFamily = FontFamily.Default,
+        fontWeight = FontWeight.W500,
+        fontSize = 14.sp
+    ),
+    caption = TextStyle(
+        fontFamily = FontFamily.Default,
+        fontWeight = FontWeight.Normal,
+        fontSize = 12.sp
+    )
+    */
+)
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..305ef03
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,31 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportHeight="108"
+    android:viewportWidth="108">
+  <path
+      android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+    <aapt:attr name="android:fillColor">
+      <gradient
+          android:endX="85.84757"
+          android:endY="92.4963"
+          android:startX="42.9492"
+          android:startY="49.59793"
+          android:type="linear">
+        <item
+            android:color="#44000000"
+            android:offset="0.0"/>
+        <item
+            android:color="#00000000"
+            android:offset="1.0"/>
+      </gradient>
+    </aapt:attr>
+  </path>
+  <path
+      android:fillColor="#FFFFFF"
+      android:fillType="nonZero"
+      android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+      android:strokeColor="#00000000"
+      android:strokeWidth="1"/>
+</vector>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/drawable/ic_launcher_background.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..2be47c2
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportHeight="108"
+    android:viewportWidth="108">
+  <path android:fillColor="#3DDC84"
+      android:pathData="M0,0h108v108h-108z"/>
+  <path android:fillColor="#00000000" android:pathData="M9,0L9,108"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+  <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
+      android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+</vector>
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6d5e5d0
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+  <background android:drawable="@drawable/ic_launcher_background"/>
+  <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6d5e5d0
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+  <background android:drawable="@drawable/ic_launcher_background"/>
+  <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/raw/samplevideo.mp4 b/samples/PictureInPicture/ComposePip/app/src/main/res/raw/samplevideo.mp4
new file mode 100644
index 0000000..7a5467e
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/raw/samplevideo.mp4
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/values/colors.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..09837df
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <color name="purple_200">#FFBB86FC</color>
+  <color name="purple_500">#FF6200EE</color>
+  <color name="purple_700">#FF3700B3</color>
+  <color name="teal_200">#FF03DAC5</color>
+  <color name="teal_700">#FF018786</color>
+  <color name="black">#FF000000</color>
+  <color name="white">#FFFFFFFF</color>
+</resources>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/values/strings.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..0fb3ab3
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+<resources>
+  <string name="app_name">SamplePip</string>
+  <string name="start_pip_button">Enter Pip</string>
+</resources>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/values/themes.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..7a2f21a
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/values/themes.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <style name="Theme.SamplePip"
+      parent="android:Theme.Material.Light.NoActionBar">
+    <item name="android:statusBarColor">@color/purple_700</item>
+  </style>
+</resources>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/xml/backup_rules.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..7a180f4
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   Sample backup rules file; uncomment and customize as necessary.
+   See https://developer.android.com/guide/topics/data/autobackup
+   for details.
+   Note: This file is ignored for devices older that API 31
+   See https://developer.android.com/about/versions/12/backup-restore
+-->
+<full-backup-content>
+  <!--
+   <include domain="sharedpref" path="."/>
+   <exclude domain="sharedpref" path="device.xml"/>
+-->
+</full-backup-content>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/xml/data_extraction_rules.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..84910a7
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   Sample data extraction rules file; uncomment and customize as necessary.
+   See https://developer.android.com/about/versions/12/backup-restore#xml-changes
+   for details.
+-->
+<data-extraction-rules>
+  <cloud-backup>
+    <!-- TODO: Use <include> and <exclude> to control what is backed up.
+        <include .../>
+        <exclude .../>
+        -->
+  </cloud-backup>
+  <!--
+    <device-transfer>
+        <include .../>
+        <exclude .../>
+    </device-transfer>
+    -->
+</data-extraction-rules>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/test/java/com/example/samplepip/ExampleUnitTest.kt b/samples/PictureInPicture/ComposePip/app/src/test/java/com/example/samplepip/ExampleUnitTest.kt
new file mode 100644
index 0000000..51ce218
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/test/java/com/example/samplepip/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.example.samplepip
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+  @Test
+  fun addition_isCorrect() {
+    assertEquals(4, 2 + 2)
+  }
+}
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/build.gradle b/samples/PictureInPicture/ComposePip/build.gradle
new file mode 100644
index 0000000..8a462e3
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/build.gradle
@@ -0,0 +1,11 @@
+buildscript {
+    ext {
+        compose_ui_version = '1.2.1'
+        media3_version = '1.1.0'
+    }
+}// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+    id 'com.android.application' version '7.3.0' apply false
+    id 'com.android.library' version '7.3.0' apply false
+    id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
+}
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/gradle.properties b/samples/PictureInPicture/ComposePip/gradle.properties
new file mode 100644
index 0000000..3c5031e
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/gradle/wrapper/gradle-wrapper.jar b/samples/PictureInPicture/ComposePip/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/gradle/wrapper/gradle-wrapper.properties b/samples/PictureInPicture/ComposePip/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..82abc92
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Jul 11 03:45:22 UTC 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/samples/PictureInPicture/ComposePip/gradlew b/samples/PictureInPicture/ComposePip/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# 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
+#
+#      https://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.
+#
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=`expr $i + 1`
+    done
+    case $i in
+        0) set -- ;;
+        1) set -- "$args0" ;;
+        2) set -- "$args0" "$args1" ;;
+        3) set -- "$args0" "$args1" "$args2" ;;
+        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/samples/PictureInPicture/ComposePip/gradlew.bat b/samples/PictureInPicture/ComposePip/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/gradlew.bat
@@ -0,0 +1,89 @@
+@rem

+@rem Copyright 2015 the original author or authors.

+@rem

+@rem Licensed under the Apache License, Version 2.0 (the "License");

+@rem you may not use this file except in compliance with the License.

+@rem You may obtain a copy of the License at

+@rem

+@rem      https://www.apache.org/licenses/LICENSE-2.0

+@rem

+@rem Unless required by applicable law or agreed to in writing, software

+@rem distributed under the License is distributed on an "AS IS" BASIS,

+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+@rem See the License for the specific language governing permissions and

+@rem limitations under the License.

+@rem

+

+@if "%DEBUG%" == "" @echo off

+@rem ##########################################################################

+@rem

+@rem  Gradle startup script for Windows

+@rem

+@rem ##########################################################################

+

+@rem Set local scope for the variables with windows NT shell

+if "%OS%"=="Windows_NT" setlocal

+

+set DIRNAME=%~dp0

+if "%DIRNAME%" == "" set DIRNAME=.

+set APP_BASE_NAME=%~n0

+set APP_HOME=%DIRNAME%

+

+@rem Resolve any "." and ".." in APP_HOME to make it shorter.

+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

+

+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

+

+@rem Find java.exe

+if defined JAVA_HOME goto findJavaFromJavaHome

+

+set JAVA_EXE=java.exe

+%JAVA_EXE% -version >NUL 2>&1

+if "%ERRORLEVEL%" == "0" goto execute

+

+echo.

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:findJavaFromJavaHome

+set JAVA_HOME=%JAVA_HOME:"=%

+set JAVA_EXE=%JAVA_HOME%/bin/java.exe

+

+if exist "%JAVA_EXE%" goto execute

+

+echo.

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:execute

+@rem Setup the command line

+

+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

+

+

+@rem Execute Gradle

+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*

+

+:end

+@rem End local scope for the variables with windows NT shell

+if "%ERRORLEVEL%"=="0" goto mainEnd

+

+:fail

+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of

+rem the _cmd.exe /c_ return code!

+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

+exit /b 1

+

+:mainEnd

+if "%OS%"=="Windows_NT" endlocal

+

+:omega

diff --git a/samples/PictureInPicture/ComposePip/settings.gradle b/samples/PictureInPicture/ComposePip/settings.gradle
new file mode 100644
index 0000000..1762602
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/settings.gradle
@@ -0,0 +1,16 @@
+pluginManagement {
+    repositories {
+        google()
+        mavenCentral()
+        gradlePluginPortal()
+    }
+}
+dependencyResolutionManagement {
+    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+rootProject.name = "SamplePip"
+include ':app'
diff --git a/tools/winscope/.eslintrc.js b/tools/winscope/.eslintrc.js
index 8edd759..e38cfad 100644
--- a/tools/winscope/.eslintrc.js
+++ b/tools/winscope/.eslintrc.js
@@ -30,6 +30,12 @@
     jasmine: true,
     protractor: true,
   },
+  ignorePatterns: [
+    // Perfetto trace processor sources. Either auto-generated (we want to keep them untouched)
+    // or copied from external/perfetto (we want to touch them as little as possible to allow
+    // future upgrading, diffing, conflicts merging, ...)
+    'src/trace_processor/',
+  ],
   rules: {
     'no-unused-vars': 'off', // not very robust rule
 
diff --git a/tools/winscope/.gitignore b/tools/winscope/.gitignore
index 74f32dd..7e97075 100644
--- a/tools/winscope/.gitignore
+++ b/tools/winscope/.gitignore
@@ -5,13 +5,14 @@
 /tmp
 /out-tsc
 /bazel-out
+/kotlin_build
 
 # Node
 /node_modules
 npm-debug.log
 
-# Kotlin transpiled code
-/kotlin_build
+# Dependencies build artifacts (flickerlib, perfetto's trace processor, ...)
+/deps_build
 
 # IDEs and editors
 .idea/
diff --git a/tools/winscope/.prettierignore b/tools/winscope/.prettierignore
new file mode 100644
index 0000000..d1ba0cd
--- /dev/null
+++ b/tools/winscope/.prettierignore
@@ -0,0 +1,6 @@
+
+# Perfetto trace processor sources. Either auto-generated (we want to keep them untouched)
+# or copied from external/perfetto (we want to touch them as little as possible to allow
+# future upgrading, diffing, conflicts merging, ...)
+src/trace_processor/
+
diff --git a/tools/winscope/karma.config.ci.js b/tools/winscope/karma.config.ci.js
new file mode 100644
index 0000000..e1b7474
--- /dev/null
+++ b/tools/winscope/karma.config.ci.js
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+const configCommon = require('./karma.config.common');
+
+const configCi = (config) => {
+  config.set({
+    singleRun: true,
+    browsers: ['ChromeHeadless'],
+  });
+};
+
+module.exports = (config) => {
+  configCommon(config);
+  configCi(config);
+};
diff --git a/tools/winscope/karma.conf.js b/tools/winscope/karma.config.common.js
similarity index 66%
rename from tools/winscope/karma.conf.js
rename to tools/winscope/karma.config.common.js
index bd03ace..b288ab2 100644
--- a/tools/winscope/karma.conf.js
+++ b/tools/winscope/karma.config.common.js
@@ -21,12 +21,23 @@
   config.set({
     frameworks: ['jasmine', 'webpack'],
     plugins: ['karma-webpack', 'karma-chrome-launcher', 'karma-jasmine', 'karma-sourcemap-loader'],
-    files: [{pattern: 'src/main_component_test.ts', watched: false}],
+    files: [
+      {pattern: 'src/main_unit_test.ts', watched: false},
+      {pattern: 'src/test/fixtures/**/*', included: false, served: true},
+      {
+        pattern: 'deps_build/trace_processor/to_be_served/engine_bundle.js',
+        included: false,
+        served: true,
+      },
+      {
+        pattern: 'deps_build/trace_processor/to_be_served/trace_processor.wasm',
+        included: false,
+        served: true,
+      },
+    ],
     preprocessors: {
-      'src/main_component_test.ts': ['webpack', 'sourcemap'],
+      'src/main_unit_test.ts': ['webpack', 'sourcemap'],
     },
-    singleRun: true,
-    browsers: ['ChromeHeadless'],
     webpack: webpackConfig,
   });
 };
diff --git a/tools/winscope/karma.config.dev.js b/tools/winscope/karma.config.dev.js
new file mode 100644
index 0000000..b9a280b
--- /dev/null
+++ b/tools/winscope/karma.config.dev.js
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+const configCommon = require('./karma.config.common');
+
+const configDev = (config) => {
+  config.set({
+    singleRun: false,
+    browsers: ['Chrome'],
+  });
+};
+
+module.exports = (config) => {
+  configCommon(config);
+  configDev(config);
+};
diff --git a/tools/winscope/package-lock.json b/tools/winscope/package-lock.json
index e041160..6bdf193 100644
--- a/tools/winscope/package-lock.json
+++ b/tools/winscope/package-lock.json
@@ -27,8 +27,6 @@
         "dateformat": "^5.0.3",
         "gl-matrix": "^3.4.3",
         "html-loader": "^3.1.0",
-        "html-webpack-inline-source-plugin": "^1.0.0-beta.2",
-        "html-webpack-plugin": "^5.5.0",
         "html2canvas": "^1.4.1",
         "jsbn": "^1.1.0",
         "jsbn-rsa": "^1.0.4",
@@ -40,7 +38,7 @@
         "three": "^0.143.0",
         "ts-loader": "^9.3.0",
         "tslib": "^2.3.0",
-        "typescript": "~4.7.2",
+        "typescript": "^4.8.0",
         "webgl-utils": "^1.0.1",
         "webgl-utils.js": "^1.1.0",
         "webpack-cli": "^4.10.0",
@@ -53,6 +51,7 @@
         "@ngxs/devtools-plugin": "^3.7.4",
         "@types/chrome": "^0.0.204",
         "@types/dateformat": "^5.0.0",
+        "@types/gtag.js": "^0.0.13",
         "@types/jasmine": "~4.3.1",
         "@types/jquery": "^3.5.14",
         "@types/jsbn": "^1.2.30",
@@ -62,9 +61,12 @@
         "@typescript-eslint/eslint-plugin": "^5.30.6",
         "@typescript-eslint/parser": "^5.30.6",
         "angular2-template-loader": "^0.6.2",
+        "copy-webpack-plugin": "^11.0.0",
         "eslint": "^8.19.0",
         "eslint-config-prettier": "^8.5.0",
         "eslint-plugin-prettier": "^4.2.1",
+        "html-webpack-inline-source-plugin": "^1.0.0-beta.2",
+        "html-webpack-plugin": "^5.5.0",
         "jasmine": "~4.3.0",
         "jasmine-core": "~4.1.0",
         "karma": "~6.3.0",
@@ -83,6 +85,15 @@
         "webpack": "^5.74.0"
       }
     },
+    "node_modules/@aashutoshrathi/word-wrap": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+      "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/@adobe/css-tools": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz",
@@ -243,6 +254,22 @@
         }
       }
     },
+    "node_modules/@angular-devkit/build-angular/node_modules/@ngtools/webpack": {
+      "version": "14.2.10",
+      "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.10.tgz",
+      "integrity": "sha512-sLHapZLVub6mEz5b19tf1VfIV1w3tYfg7FNPLeni79aldxu1FbP1v2WmiFAnMzrswqyK0bhTtxrl+Z/CLKqyoQ==",
+      "dev": true,
+      "engines": {
+        "node": "^14.15.0 || >=16.10.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      },
+      "peerDependencies": {
+        "@angular/compiler-cli": "^14.0.0",
+        "typescript": ">=4.6.2 <4.9",
+        "webpack": "^5.54.0"
+      }
+    },
     "node_modules/@angular-devkit/build-angular/node_modules/ajv": {
       "version": "6.12.6",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -964,9 +991,9 @@
       }
     },
     "node_modules/@babel/compat-data": {
-      "version": "7.20.10",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.10.tgz",
-      "integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==",
+      "version": "7.22.6",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz",
+      "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
@@ -1065,16 +1092,16 @@
       }
     },
     "node_modules/@babel/helper-compilation-targets": {
-      "version": "7.20.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz",
-      "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==",
+      "version": "7.22.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz",
+      "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==",
       "dev": true,
       "dependencies": {
-        "@babel/compat-data": "^7.20.5",
-        "@babel/helper-validator-option": "^7.18.6",
-        "browserslist": "^4.21.3",
-        "lru-cache": "^5.1.1",
-        "semver": "^6.3.0"
+        "@babel/compat-data": "^7.22.6",
+        "@babel/helper-validator-option": "^7.22.5",
+        "@nicolo-ribaudo/semver-v6": "^6.3.3",
+        "browserslist": "^4.21.9",
+        "lru-cache": "^5.1.1"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -1083,15 +1110,6 @@
         "@babel/core": "^7.0.0"
       }
     },
-    "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-      "dev": true,
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
     "node_modules/@babel/helper-create-class-features-plugin": {
       "version": "7.20.7",
       "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.7.tgz",
@@ -1383,9 +1401,9 @@
       }
     },
     "node_modules/@babel/helper-validator-option": {
-      "version": "7.18.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
-      "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==",
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz",
+      "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
@@ -3228,9 +3246,9 @@
       }
     },
     "node_modules/@jridgewell/source-map": {
-      "version": "0.3.2",
-      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
-      "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
+      "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
       "dependencies": {
         "@jridgewell/gen-mapping": "^0.3.0",
         "@jridgewell/trace-mapping": "^0.3.9"
@@ -3294,22 +3312,6 @@
         "rxjs": "^6.5.3 || ^7.5.0"
       }
     },
-    "node_modules/@ngtools/webpack": {
-      "version": "14.2.10",
-      "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.10.tgz",
-      "integrity": "sha512-sLHapZLVub6mEz5b19tf1VfIV1w3tYfg7FNPLeni79aldxu1FbP1v2WmiFAnMzrswqyK0bhTtxrl+Z/CLKqyoQ==",
-      "dev": true,
-      "engines": {
-        "node": "^14.15.0 || >=16.10.0",
-        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
-        "yarn": ">= 1.13.0"
-      },
-      "peerDependencies": {
-        "@angular/compiler-cli": "^14.0.0",
-        "typescript": ">=4.6.2 <4.9",
-        "webpack": "^5.54.0"
-      }
-    },
     "node_modules/@ngxs/devtools-plugin": {
       "version": "3.7.6",
       "resolved": "https://registry.npmjs.org/@ngxs/devtools-plugin/-/devtools-plugin-3.7.6.tgz",
@@ -3355,6 +3357,15 @@
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
       "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
     },
+    "node_modules/@nicolo-ribaudo/semver-v6": {
+      "version": "6.3.3",
+      "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz",
+      "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3768,6 +3779,12 @@
       "integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==",
       "dev": true
     },
+    "node_modules/@types/gtag.js": {
+      "version": "0.0.13",
+      "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.13.tgz",
+      "integrity": "sha512-yOXFkfnt1DQr1v9B4ERulJOGnbdVqnPHV8NG4nkQhnu4qbrJecQ06DlaKmSjI3nzIwBj5U9/X61LY4sTc2KbaQ==",
+      "dev": true
+    },
     "node_modules/@types/har-format": {
       "version": "1.2.10",
       "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.10.tgz",
@@ -3777,7 +3794,8 @@
     "node_modules/@types/html-minifier-terser": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
-      "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg=="
+      "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==",
+      "dev": true
     },
     "node_modules/@types/http-proxy": {
       "version": "1.17.9",
@@ -4137,6 +4155,7 @@
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
       "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==",
+      "dev": true,
       "dependencies": {
         "@webassemblyjs/helper-numbers": "1.11.1",
         "@webassemblyjs/helper-wasm-bytecode": "1.11.1"
@@ -4145,22 +4164,26 @@
     "node_modules/@webassemblyjs/floating-point-hex-parser": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz",
-      "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ=="
+      "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==",
+      "dev": true
     },
     "node_modules/@webassemblyjs/helper-api-error": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz",
-      "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg=="
+      "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==",
+      "dev": true
     },
     "node_modules/@webassemblyjs/helper-buffer": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz",
-      "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA=="
+      "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==",
+      "dev": true
     },
     "node_modules/@webassemblyjs/helper-numbers": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz",
       "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==",
+      "dev": true,
       "dependencies": {
         "@webassemblyjs/floating-point-hex-parser": "1.11.1",
         "@webassemblyjs/helper-api-error": "1.11.1",
@@ -4170,12 +4193,14 @@
     "node_modules/@webassemblyjs/helper-wasm-bytecode": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz",
-      "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q=="
+      "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==",
+      "dev": true
     },
     "node_modules/@webassemblyjs/helper-wasm-section": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz",
       "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==",
+      "dev": true,
       "dependencies": {
         "@webassemblyjs/ast": "1.11.1",
         "@webassemblyjs/helper-buffer": "1.11.1",
@@ -4187,6 +4212,7 @@
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz",
       "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==",
+      "dev": true,
       "dependencies": {
         "@xtuc/ieee754": "^1.2.0"
       }
@@ -4195,6 +4221,7 @@
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz",
       "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==",
+      "dev": true,
       "dependencies": {
         "@xtuc/long": "4.2.2"
       }
@@ -4202,12 +4229,14 @@
     "node_modules/@webassemblyjs/utf8": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz",
-      "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ=="
+      "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==",
+      "dev": true
     },
     "node_modules/@webassemblyjs/wasm-edit": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz",
       "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==",
+      "dev": true,
       "dependencies": {
         "@webassemblyjs/ast": "1.11.1",
         "@webassemblyjs/helper-buffer": "1.11.1",
@@ -4223,6 +4252,7 @@
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz",
       "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==",
+      "dev": true,
       "dependencies": {
         "@webassemblyjs/ast": "1.11.1",
         "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
@@ -4235,6 +4265,7 @@
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz",
       "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==",
+      "dev": true,
       "dependencies": {
         "@webassemblyjs/ast": "1.11.1",
         "@webassemblyjs/helper-buffer": "1.11.1",
@@ -4246,6 +4277,7 @@
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz",
       "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==",
+      "dev": true,
       "dependencies": {
         "@webassemblyjs/ast": "1.11.1",
         "@webassemblyjs/helper-api-error": "1.11.1",
@@ -4259,6 +4291,7 @@
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz",
       "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==",
+      "dev": true,
       "dependencies": {
         "@webassemblyjs/ast": "1.11.1",
         "@xtuc/long": "4.2.2"
@@ -4344,9 +4377,9 @@
       }
     },
     "node_modules/acorn": {
-      "version": "8.8.1",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
-      "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==",
+      "version": "8.10.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
+      "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
       "bin": {
         "acorn": "bin/acorn"
       },
@@ -4355,9 +4388,9 @@
       }
     },
     "node_modules/acorn-import-assertions": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
-      "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
+      "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
       "peerDependencies": {
         "acorn": "^8"
       }
@@ -4385,12 +4418,12 @@
       }
     },
     "node_modules/adm-zip": {
-      "version": "0.4.16",
-      "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz",
-      "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==",
+      "version": "0.5.10",
+      "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
+      "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
       "dev": true,
       "engines": {
-        "node": ">=0.3.0"
+        "node": ">=6.0"
       }
     },
     "node_modules/agent-base": {
@@ -4574,6 +4607,7 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
       "engines": {
         "node": ">=8"
       }
@@ -5028,7 +5062,8 @@
     "node_modules/boolbase": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
-      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
+      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+      "dev": true
     },
     "node_modules/brace-expansion": {
       "version": "2.0.1",
@@ -5060,9 +5095,9 @@
       }
     },
     "node_modules/browserslist": {
-      "version": "4.21.4",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
-      "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==",
+      "version": "4.21.9",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
+      "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
       "funding": [
         {
           "type": "opencollective",
@@ -5071,13 +5106,17 @@
         {
           "type": "tidelift",
           "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
         }
       ],
       "dependencies": {
-        "caniuse-lite": "^1.0.30001400",
-        "electron-to-chromium": "^1.4.251",
-        "node-releases": "^2.0.6",
-        "update-browserslist-db": "^1.0.9"
+        "caniuse-lite": "^1.0.30001503",
+        "electron-to-chromium": "^1.4.431",
+        "node-releases": "^2.0.12",
+        "update-browserslist-db": "^1.0.11"
       },
       "bin": {
         "browserslist": "cli.js"
@@ -5268,9 +5307,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001441",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz",
-      "integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg==",
+      "version": "1.0.30001513",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001513.tgz",
+      "integrity": "sha512-pnjGJo7SOOjAGytZZ203Em95MRM8Cr6jhCXNF/FAXTpCTRTECnqQWLpiTRqrFtdYcth8hf4WECUpkezuYsMVww==",
       "funding": [
         {
           "type": "opencollective",
@@ -5279,6 +5318,10 @@
         {
           "type": "tidelift",
           "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
         }
       ]
     },
@@ -5730,9 +5773,9 @@
       "dev": true
     },
     "node_modules/cookiejar": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz",
-      "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ=="
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
+      "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
     },
     "node_modules/copy-anything": {
       "version": "2.0.6",
@@ -6082,6 +6125,7 @@
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
       "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
+      "dev": true,
       "dependencies": {
         "boolbase": "^1.0.0",
         "css-what": "^6.0.1",
@@ -6097,6 +6141,7 @@
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
       "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
+      "dev": true,
       "engines": {
         "node": ">= 6"
       },
@@ -6693,6 +6738,7 @@
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
       "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==",
+      "dev": true,
       "dependencies": {
         "utila": "~0.4"
       }
@@ -6713,6 +6759,7 @@
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
       "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
+      "dev": true,
       "dependencies": {
         "domelementtype": "^2.0.1",
         "domhandler": "^4.2.0",
@@ -6726,6 +6773,7 @@
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
       "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+      "dev": true,
       "funding": [
         {
           "type": "github",
@@ -6737,6 +6785,7 @@
       "version": "4.3.1",
       "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
       "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
+      "dev": true,
       "dependencies": {
         "domelementtype": "^2.2.0"
       },
@@ -6751,6 +6800,7 @@
       "version": "2.8.0",
       "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
       "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
+      "dev": true,
       "dependencies": {
         "dom-serializer": "^1.0.1",
         "domelementtype": "^2.2.0",
@@ -6800,9 +6850,9 @@
       "dev": true
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.284",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz",
-      "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA=="
+      "version": "1.4.453",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.453.tgz",
+      "integrity": "sha512-BU8UtQz6CB3T7RIGhId4BjmjJVXQDujb0+amGL8jpcluFJr6lwspBOvkUbnttfpZCm4zFMHmjrX1QrdPWBBMjQ=="
     },
     "node_modules/emoji-regex": {
       "version": "8.0.0",
@@ -6852,9 +6902,9 @@
       }
     },
     "node_modules/engine.io": {
-      "version": "6.2.1",
-      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz",
-      "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==",
+      "version": "6.5.1",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.1.tgz",
+      "integrity": "sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==",
       "dev": true,
       "dependencies": {
         "@types/cookie": "^0.4.1",
@@ -6865,26 +6915,26 @@
         "cookie": "~0.4.1",
         "cors": "~2.8.5",
         "debug": "~4.3.1",
-        "engine.io-parser": "~5.0.3",
-        "ws": "~8.2.3"
+        "engine.io-parser": "~5.1.0",
+        "ws": "~8.11.0"
       },
       "engines": {
         "node": ">=10.0.0"
       }
     },
     "node_modules/engine.io-parser": {
-      "version": "5.0.4",
-      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
-      "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz",
+      "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==",
       "dev": true,
       "engines": {
         "node": ">=10.0.0"
       }
     },
     "node_modules/enhanced-resolve": {
-      "version": "5.12.0",
-      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
-      "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
+      "version": "5.15.0",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
+      "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
       "dependencies": {
         "graceful-fs": "^4.2.4",
         "tapable": "^2.2.0"
@@ -6903,6 +6953,7 @@
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
       "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
+      "dev": true,
       "funding": {
         "url": "https://github.com/fb55/entities?sponsor=1"
       }
@@ -6963,7 +7014,8 @@
     "node_modules/es-module-lexer": {
       "version": "0.9.3",
       "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz",
-      "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ=="
+      "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
+      "dev": true
     },
     "node_modules/es6-promise": {
       "version": "4.2.8",
@@ -7367,6 +7419,7 @@
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
       "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "dev": true,
       "engines": {
         "node": ">=0.8.0"
       }
@@ -8999,6 +9052,7 @@
       "version": "1.0.0-beta.2",
       "resolved": "https://registry.npmjs.org/html-webpack-inline-source-plugin/-/html-webpack-inline-source-plugin-1.0.0-beta.2.tgz",
       "integrity": "sha512-ydsEKdp0tnbmnqRAH2WSSMXerCNYhjes5b79uvP2BU3p6cyk+6ucNMsw5b5xD1QxphgvBBA3QqVmdcpu8QLlRQ==",
+      "dev": true,
       "dependencies": {
         "escape-string-regexp": "^1.0.5",
         "slash": "^1.0.0",
@@ -9009,6 +9063,7 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
       "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==",
+      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -9017,6 +9072,7 @@
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz",
       "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==",
+      "dev": true,
       "dependencies": {
         "@types/html-minifier-terser": "^6.0.0",
         "html-minifier-terser": "^6.0.2",
@@ -9051,6 +9107,7 @@
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
       "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
+      "dev": true,
       "funding": [
         "https://github.com/fb55/htmlparser2?sponsor=1",
         {
@@ -9066,9 +9123,9 @@
       }
     },
     "node_modules/http-cache-semantics": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
-      "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
+      "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
       "dev": true
     },
     "node_modules/http-deceiver": {
@@ -10118,9 +10175,9 @@
       }
     },
     "node_modules/jsonwebtoken/node_modules/semver": {
-      "version": "7.3.8",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
-      "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+      "version": "7.5.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
+      "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
       "dependencies": {
         "lru-cache": "^6.0.0"
       },
@@ -11807,9 +11864,9 @@
       }
     },
     "node_modules/node-releases": {
-      "version": "2.0.8",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
-      "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
+      "version": "2.0.13",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
+      "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
     },
     "node_modules/node-source-walk": {
       "version": "4.3.0",
@@ -12105,6 +12162,7 @@
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
       "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+      "dev": true,
       "dependencies": {
         "boolbase": "^1.0.0"
       },
@@ -12207,17 +12265,17 @@
       }
     },
     "node_modules/optionator": {
-      "version": "0.9.1",
-      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
-      "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+      "version": "0.9.3",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+      "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
       "dev": true,
       "dependencies": {
+        "@aashutoshrathi/word-wrap": "^1.2.3",
         "deep-is": "^0.1.3",
         "fast-levenshtein": "^2.0.6",
         "levn": "^0.4.1",
         "prelude-ls": "^1.2.1",
-        "type-check": "^0.4.0",
-        "word-wrap": "^1.2.3"
+        "type-check": "^0.4.0"
       },
       "engines": {
         "node": ">= 0.8.0"
@@ -13529,6 +13587,7 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
       "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==",
+      "dev": true,
       "dependencies": {
         "lodash": "^4.17.20",
         "renderkid": "^3.0.0"
@@ -14293,6 +14352,7 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
       "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==",
+      "dev": true,
       "dependencies": {
         "css-select": "^4.1.3",
         "dom-converter": "^0.2.0",
@@ -15055,9 +15115,9 @@
       "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA=="
     },
     "node_modules/serialize-javascript": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
-      "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
+      "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
       "dependencies": {
         "randombytes": "^2.1.0"
       }
@@ -15241,32 +15301,36 @@
       }
     },
     "node_modules/socket.io": {
-      "version": "4.5.4",
-      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz",
-      "integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==",
+      "version": "4.7.1",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.1.tgz",
+      "integrity": "sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw==",
       "dev": true,
       "dependencies": {
         "accepts": "~1.3.4",
         "base64id": "~2.0.0",
+        "cors": "~2.8.5",
         "debug": "~4.3.2",
-        "engine.io": "~6.2.1",
-        "socket.io-adapter": "~2.4.0",
-        "socket.io-parser": "~4.2.1"
+        "engine.io": "~6.5.0",
+        "socket.io-adapter": "~2.5.2",
+        "socket.io-parser": "~4.2.4"
       },
       "engines": {
         "node": ">=10.0.0"
       }
     },
     "node_modules/socket.io-adapter": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz",
-      "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==",
-      "dev": true
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz",
+      "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==",
+      "dev": true,
+      "dependencies": {
+        "ws": "~8.11.0"
+      }
     },
     "node_modules/socket.io-parser": {
-      "version": "4.2.1",
-      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz",
-      "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==",
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+      "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
       "dev": true,
       "dependencies": {
         "@socket.io/component-emitter": "~3.1.0",
@@ -15387,7 +15451,8 @@
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
       "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
-      "deprecated": "See https://github.com/lydell/source-map-url#deprecated"
+      "deprecated": "See https://github.com/lydell/source-map-url#deprecated",
+      "dev": true
     },
     "node_modules/sourcemap-codec": {
       "version": "1.4.8",
@@ -15570,6 +15635,7 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
       "dependencies": {
         "ansi-regex": "^5.0.1"
       },
@@ -15868,15 +15934,15 @@
       }
     },
     "node_modules/terser-webpack-plugin": {
-      "version": "5.3.6",
-      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz",
-      "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==",
+      "version": "5.3.9",
+      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz",
+      "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==",
       "dependencies": {
-        "@jridgewell/trace-mapping": "^0.3.14",
+        "@jridgewell/trace-mapping": "^0.3.17",
         "jest-worker": "^27.4.5",
         "schema-utils": "^3.1.1",
-        "serialize-javascript": "^6.0.0",
-        "terser": "^5.14.1"
+        "serialize-javascript": "^6.0.1",
+        "terser": "^5.16.8"
       },
       "engines": {
         "node": ">= 10.13.0"
@@ -15923,6 +15989,11 @@
         "ajv": "^6.9.1"
       }
     },
+    "node_modules/terser-webpack-plugin/node_modules/commander": {
+      "version": "2.20.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+    },
     "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -15945,6 +16016,23 @@
         "url": "https://opencollective.com/webpack"
       }
     },
+    "node_modules/terser-webpack-plugin/node_modules/terser": {
+      "version": "5.18.2",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.2.tgz",
+      "integrity": "sha512-Ah19JS86ypbJzTzvUCX7KOsEIhDaRONungA4aYBjEP3JZRf4ocuDzTg4QWZnPn9DEMiMYGJPiSOy7aykoCc70w==",
+      "dependencies": {
+        "@jridgewell/source-map": "^0.3.3",
+        "acorn": "^8.8.2",
+        "commander": "^2.20.0",
+        "source-map-support": "~0.5.20"
+      },
+      "bin": {
+        "terser": "bin/terser"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/terser/node_modules/commander": {
       "version": "2.20.3",
       "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -16204,9 +16292,9 @@
       }
     },
     "node_modules/tsconfig-paths/node_modules/json5": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
-      "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+      "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
       "dev": true,
       "dependencies": {
         "minimist": "^1.2.0"
@@ -16421,9 +16509,9 @@
       "dev": true
     },
     "node_modules/typescript": {
-      "version": "4.7.4",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
-      "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
+      "version": "4.8.4",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
+      "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -16433,9 +16521,9 @@
       }
     },
     "node_modules/ua-parser-js": {
-      "version": "0.7.32",
-      "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.32.tgz",
-      "integrity": "sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==",
+      "version": "0.7.35",
+      "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz",
+      "integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==",
       "dev": true,
       "funding": [
         {
@@ -16539,9 +16627,9 @@
       }
     },
     "node_modules/update-browserslist-db": {
-      "version": "1.0.10",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
-      "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
+      "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
       "funding": [
         {
           "type": "opencollective",
@@ -16550,6 +16638,10 @@
         {
           "type": "tidelift",
           "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
         }
       ],
       "dependencies": {
@@ -16557,7 +16649,7 @@
         "picocolors": "^1.0.0"
       },
       "bin": {
-        "browserslist-lint": "cli.js"
+        "update-browserslist-db": "cli.js"
       },
       "peerDependencies": {
         "browserslist": ">= 4.21.0"
@@ -16592,7 +16684,8 @@
     "node_modules/utila": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
-      "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA=="
+      "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==",
+      "dev": true
     },
     "node_modules/utils-merge": {
       "version": "1.0.1",
@@ -16727,12 +16820,12 @@
       }
     },
     "node_modules/webdriver-manager": {
-      "version": "12.1.8",
-      "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.8.tgz",
-      "integrity": "sha512-qJR36SXG2VwKugPcdwhaqcLQOD7r8P2Xiv9sfNbfZrKBnX243iAkOueX1yAmeNgIKhJ3YAT/F2gq6IiEZzahsg==",
+      "version": "12.1.9",
+      "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.9.tgz",
+      "integrity": "sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ==",
       "dev": true,
       "dependencies": {
-        "adm-zip": "^0.4.9",
+        "adm-zip": "^0.5.2",
         "chalk": "^1.1.1",
         "del": "^2.2.0",
         "glob": "^7.0.3",
@@ -16886,21 +16979,21 @@
       "integrity": "sha512-cmO2aPd6gR6bK/ttdk8ZIypJfZMOcTvsvXv/LxXZjAFu5TC6vXqFrZYudlPuKxVsA34Pc8Fysq2rCnflu+wuuA=="
     },
     "node_modules/webpack": {
-      "version": "5.75.0",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz",
-      "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==",
+      "version": "5.88.1",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.1.tgz",
+      "integrity": "sha512-FROX3TxQnC/ox4N+3xQoWZzvGXSuscxR32rbzjpXgEzWudJFEJBpdlkkob2ylrv5yzzufD1zph1OoFsLtm6stQ==",
       "dependencies": {
         "@types/eslint-scope": "^3.7.3",
-        "@types/estree": "^0.0.51",
-        "@webassemblyjs/ast": "1.11.1",
-        "@webassemblyjs/wasm-edit": "1.11.1",
-        "@webassemblyjs/wasm-parser": "1.11.1",
+        "@types/estree": "^1.0.0",
+        "@webassemblyjs/ast": "^1.11.5",
+        "@webassemblyjs/wasm-edit": "^1.11.5",
+        "@webassemblyjs/wasm-parser": "^1.11.5",
         "acorn": "^8.7.1",
-        "acorn-import-assertions": "^1.7.6",
+        "acorn-import-assertions": "^1.9.0",
         "browserslist": "^4.14.5",
         "chrome-trace-event": "^1.0.2",
-        "enhanced-resolve": "^5.10.0",
-        "es-module-lexer": "^0.9.0",
+        "enhanced-resolve": "^5.15.0",
+        "es-module-lexer": "^1.2.1",
         "eslint-scope": "5.1.1",
         "events": "^3.2.0",
         "glob-to-regexp": "^0.4.1",
@@ -16909,9 +17002,9 @@
         "loader-runner": "^4.2.0",
         "mime-types": "^2.1.27",
         "neo-async": "^2.6.2",
-        "schema-utils": "^3.1.0",
+        "schema-utils": "^3.2.0",
         "tapable": "^2.1.1",
-        "terser-webpack-plugin": "^5.1.3",
+        "terser-webpack-plugin": "^5.3.7",
         "watchpack": "^2.4.0",
         "webpack-sources": "^3.2.3"
       },
@@ -17101,27 +17194,6 @@
         "url": "https://opencollective.com/webpack"
       }
     },
-    "node_modules/webpack-dev-server/node_modules/ws": {
-      "version": "8.11.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
-      "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
-      "dev": true,
-      "engines": {
-        "node": ">=10.0.0"
-      },
-      "peerDependencies": {
-        "bufferutil": "^4.0.1",
-        "utf-8-validate": "^5.0.2"
-      },
-      "peerDependenciesMeta": {
-        "bufferutil": {
-          "optional": true
-        },
-        "utf-8-validate": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/webpack-merge": {
       "version": "5.8.0",
       "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz",
@@ -17163,6 +17235,142 @@
         }
       }
     },
+    "node_modules/webpack/node_modules/@types/estree": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
+      "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA=="
+    },
+    "node_modules/webpack/node_modules/@webassemblyjs/ast": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
+      "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==",
+      "dependencies": {
+        "@webassemblyjs/helper-numbers": "1.11.6",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.6"
+      }
+    },
+    "node_modules/webpack/node_modules/@webassemblyjs/floating-point-hex-parser": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
+      "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw=="
+    },
+    "node_modules/webpack/node_modules/@webassemblyjs/helper-api-error": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
+      "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="
+    },
+    "node_modules/webpack/node_modules/@webassemblyjs/helper-buffer": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz",
+      "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA=="
+    },
+    "node_modules/webpack/node_modules/@webassemblyjs/helper-numbers": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
+      "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
+      "dependencies": {
+        "@webassemblyjs/floating-point-hex-parser": "1.11.6",
+        "@webassemblyjs/helper-api-error": "1.11.6",
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/webpack/node_modules/@webassemblyjs/helper-wasm-bytecode": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
+      "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="
+    },
+    "node_modules/webpack/node_modules/@webassemblyjs/helper-wasm-section": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz",
+      "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/helper-buffer": "1.11.6",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+        "@webassemblyjs/wasm-gen": "1.11.6"
+      }
+    },
+    "node_modules/webpack/node_modules/@webassemblyjs/ieee754": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
+      "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
+      "dependencies": {
+        "@xtuc/ieee754": "^1.2.0"
+      }
+    },
+    "node_modules/webpack/node_modules/@webassemblyjs/leb128": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
+      "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
+      "dependencies": {
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/webpack/node_modules/@webassemblyjs/utf8": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
+      "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="
+    },
+    "node_modules/webpack/node_modules/@webassemblyjs/wasm-edit": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz",
+      "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/helper-buffer": "1.11.6",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+        "@webassemblyjs/helper-wasm-section": "1.11.6",
+        "@webassemblyjs/wasm-gen": "1.11.6",
+        "@webassemblyjs/wasm-opt": "1.11.6",
+        "@webassemblyjs/wasm-parser": "1.11.6",
+        "@webassemblyjs/wast-printer": "1.11.6"
+      }
+    },
+    "node_modules/webpack/node_modules/@webassemblyjs/wasm-gen": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz",
+      "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+        "@webassemblyjs/ieee754": "1.11.6",
+        "@webassemblyjs/leb128": "1.11.6",
+        "@webassemblyjs/utf8": "1.11.6"
+      }
+    },
+    "node_modules/webpack/node_modules/@webassemblyjs/wasm-opt": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz",
+      "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/helper-buffer": "1.11.6",
+        "@webassemblyjs/wasm-gen": "1.11.6",
+        "@webassemblyjs/wasm-parser": "1.11.6"
+      }
+    },
+    "node_modules/webpack/node_modules/@webassemblyjs/wasm-parser": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz",
+      "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/helper-api-error": "1.11.6",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+        "@webassemblyjs/ieee754": "1.11.6",
+        "@webassemblyjs/leb128": "1.11.6",
+        "@webassemblyjs/utf8": "1.11.6"
+      }
+    },
+    "node_modules/webpack/node_modules/@webassemblyjs/wast-printer": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz",
+      "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.6",
+        "@xtuc/long": "4.2.2"
+      }
+    },
     "node_modules/webpack/node_modules/ajv": {
       "version": "6.12.6",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -17186,15 +17394,20 @@
         "ajv": "^6.9.1"
       }
     },
+    "node_modules/webpack/node_modules/es-module-lexer": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz",
+      "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA=="
+    },
     "node_modules/webpack/node_modules/json-schema-traverse": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
       "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
     },
     "node_modules/webpack/node_modules/schema-utils": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
-      "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
+      "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
       "dependencies": {
         "@types/json-schema": "^7.0.8",
         "ajv": "^6.12.5",
@@ -17331,9 +17544,9 @@
       "dev": true
     },
     "node_modules/ws": {
-      "version": "8.2.3",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
-      "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
+      "version": "8.11.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
+      "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
       "dev": true,
       "engines": {
         "node": ">=10.0.0"
@@ -17446,6 +17659,12 @@
     }
   },
   "dependencies": {
+    "@aashutoshrathi/word-wrap": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+      "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+      "dev": true
+    },
     "@adobe/css-tools": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz",
@@ -17560,6 +17779,13 @@
         "webpack-subresource-integrity": "5.1.0"
       },
       "dependencies": {
+        "@ngtools/webpack": {
+          "version": "14.2.10",
+          "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.10.tgz",
+          "integrity": "sha512-sLHapZLVub6mEz5b19tf1VfIV1w3tYfg7FNPLeni79aldxu1FbP1v2WmiFAnMzrswqyK0bhTtxrl+Z/CLKqyoQ==",
+          "dev": true,
+          "requires": {}
+        },
         "ajv": {
           "version": "6.12.6",
           "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -18024,9 +18250,9 @@
       }
     },
     "@babel/compat-data": {
-      "version": "7.20.10",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.10.tgz",
-      "integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==",
+      "version": "7.22.6",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz",
+      "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==",
       "dev": true
     },
     "@babel/core": {
@@ -18104,24 +18330,16 @@
       }
     },
     "@babel/helper-compilation-targets": {
-      "version": "7.20.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz",
-      "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==",
+      "version": "7.22.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz",
+      "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==",
       "dev": true,
       "requires": {
-        "@babel/compat-data": "^7.20.5",
-        "@babel/helper-validator-option": "^7.18.6",
-        "browserslist": "^4.21.3",
-        "lru-cache": "^5.1.1",
-        "semver": "^6.3.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-          "dev": true
-        }
+        "@babel/compat-data": "^7.22.6",
+        "@babel/helper-validator-option": "^7.22.5",
+        "@nicolo-ribaudo/semver-v6": "^6.3.3",
+        "browserslist": "^4.21.9",
+        "lru-cache": "^5.1.1"
       }
     },
     "@babel/helper-create-class-features-plugin": {
@@ -18346,9 +18564,9 @@
       "dev": true
     },
     "@babel/helper-validator-option": {
-      "version": "7.18.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
-      "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==",
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz",
+      "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==",
       "dev": true
     },
     "@babel/helper-wrap-function": {
@@ -19575,9 +19793,9 @@
       "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="
     },
     "@jridgewell/source-map": {
-      "version": "0.3.2",
-      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
-      "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
+      "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
       "requires": {
         "@jridgewell/gen-mapping": "^0.3.0",
         "@jridgewell/trace-mapping": "^0.3.9"
@@ -19631,13 +19849,6 @@
         "tslib": "^2.0.0"
       }
     },
-    "@ngtools/webpack": {
-      "version": "14.2.10",
-      "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.10.tgz",
-      "integrity": "sha512-sLHapZLVub6mEz5b19tf1VfIV1w3tYfg7FNPLeni79aldxu1FbP1v2WmiFAnMzrswqyK0bhTtxrl+Z/CLKqyoQ==",
-      "dev": true,
-      "requires": {}
-    },
     "@ngxs/devtools-plugin": {
       "version": "3.7.6",
       "resolved": "https://registry.npmjs.org/@ngxs/devtools-plugin/-/devtools-plugin-3.7.6.tgz",
@@ -19670,6 +19881,12 @@
         }
       }
     },
+    "@nicolo-ribaudo/semver-v6": {
+      "version": "6.3.3",
+      "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz",
+      "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==",
+      "dev": true
+    },
     "@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -20023,6 +20240,12 @@
       "integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==",
       "dev": true
     },
+    "@types/gtag.js": {
+      "version": "0.0.13",
+      "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.13.tgz",
+      "integrity": "sha512-yOXFkfnt1DQr1v9B4ERulJOGnbdVqnPHV8NG4nkQhnu4qbrJecQ06DlaKmSjI3nzIwBj5U9/X61LY4sTc2KbaQ==",
+      "dev": true
+    },
     "@types/har-format": {
       "version": "1.2.10",
       "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.10.tgz",
@@ -20032,7 +20255,8 @@
     "@types/html-minifier-terser": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
-      "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg=="
+      "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==",
+      "dev": true
     },
     "@types/http-proxy": {
       "version": "1.17.9",
@@ -20303,6 +20527,7 @@
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
       "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==",
+      "dev": true,
       "requires": {
         "@webassemblyjs/helper-numbers": "1.11.1",
         "@webassemblyjs/helper-wasm-bytecode": "1.11.1"
@@ -20311,22 +20536,26 @@
     "@webassemblyjs/floating-point-hex-parser": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz",
-      "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ=="
+      "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==",
+      "dev": true
     },
     "@webassemblyjs/helper-api-error": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz",
-      "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg=="
+      "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==",
+      "dev": true
     },
     "@webassemblyjs/helper-buffer": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz",
-      "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA=="
+      "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==",
+      "dev": true
     },
     "@webassemblyjs/helper-numbers": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz",
       "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==",
+      "dev": true,
       "requires": {
         "@webassemblyjs/floating-point-hex-parser": "1.11.1",
         "@webassemblyjs/helper-api-error": "1.11.1",
@@ -20336,12 +20565,14 @@
     "@webassemblyjs/helper-wasm-bytecode": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz",
-      "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q=="
+      "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==",
+      "dev": true
     },
     "@webassemblyjs/helper-wasm-section": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz",
       "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==",
+      "dev": true,
       "requires": {
         "@webassemblyjs/ast": "1.11.1",
         "@webassemblyjs/helper-buffer": "1.11.1",
@@ -20353,6 +20584,7 @@
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz",
       "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==",
+      "dev": true,
       "requires": {
         "@xtuc/ieee754": "^1.2.0"
       }
@@ -20361,6 +20593,7 @@
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz",
       "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==",
+      "dev": true,
       "requires": {
         "@xtuc/long": "4.2.2"
       }
@@ -20368,12 +20601,14 @@
     "@webassemblyjs/utf8": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz",
-      "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ=="
+      "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==",
+      "dev": true
     },
     "@webassemblyjs/wasm-edit": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz",
       "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==",
+      "dev": true,
       "requires": {
         "@webassemblyjs/ast": "1.11.1",
         "@webassemblyjs/helper-buffer": "1.11.1",
@@ -20389,6 +20624,7 @@
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz",
       "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==",
+      "dev": true,
       "requires": {
         "@webassemblyjs/ast": "1.11.1",
         "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
@@ -20401,6 +20637,7 @@
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz",
       "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==",
+      "dev": true,
       "requires": {
         "@webassemblyjs/ast": "1.11.1",
         "@webassemblyjs/helper-buffer": "1.11.1",
@@ -20412,6 +20649,7 @@
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz",
       "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==",
+      "dev": true,
       "requires": {
         "@webassemblyjs/ast": "1.11.1",
         "@webassemblyjs/helper-api-error": "1.11.1",
@@ -20425,6 +20663,7 @@
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz",
       "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==",
+      "dev": true,
       "requires": {
         "@webassemblyjs/ast": "1.11.1",
         "@xtuc/long": "4.2.2"
@@ -20494,14 +20733,14 @@
       }
     },
     "acorn": {
-      "version": "8.8.1",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
-      "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA=="
+      "version": "8.10.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
+      "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw=="
     },
     "acorn-import-assertions": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
-      "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
+      "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
       "requires": {}
     },
     "acorn-jsx": {
@@ -20522,9 +20761,9 @@
       }
     },
     "adm-zip": {
-      "version": "0.4.16",
-      "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz",
-      "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==",
+      "version": "0.5.10",
+      "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
+      "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
       "dev": true
     },
     "agent-base": {
@@ -20660,7 +20899,8 @@
     "ansi-regex": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
-      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true
     },
     "ansi-styles": {
       "version": "3.2.1",
@@ -21008,7 +21248,8 @@
     "boolbase": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
-      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
+      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+      "dev": true
     },
     "brace-expansion": {
       "version": "2.0.1",
@@ -21036,14 +21277,14 @@
       }
     },
     "browserslist": {
-      "version": "4.21.4",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
-      "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==",
+      "version": "4.21.9",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
+      "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
       "requires": {
-        "caniuse-lite": "^1.0.30001400",
-        "electron-to-chromium": "^1.4.251",
-        "node-releases": "^2.0.6",
-        "update-browserslist-db": "^1.0.9"
+        "caniuse-lite": "^1.0.30001503",
+        "electron-to-chromium": "^1.4.431",
+        "node-releases": "^2.0.12",
+        "update-browserslist-db": "^1.0.11"
       }
     },
     "browserstack": {
@@ -21191,9 +21432,9 @@
       "dev": true
     },
     "caniuse-lite": {
-      "version": "1.0.30001441",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz",
-      "integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg=="
+      "version": "1.0.30001513",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001513.tgz",
+      "integrity": "sha512-pnjGJo7SOOjAGytZZ203Em95MRM8Cr6jhCXNF/FAXTpCTRTECnqQWLpiTRqrFtdYcth8hf4WECUpkezuYsMVww=="
     },
     "caseless": {
       "version": "0.12.0",
@@ -21571,9 +21812,9 @@
       "dev": true
     },
     "cookiejar": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz",
-      "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ=="
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
+      "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
     },
     "copy-anything": {
       "version": "2.0.6",
@@ -21817,6 +22058,7 @@
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
       "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
+      "dev": true,
       "requires": {
         "boolbase": "^1.0.0",
         "css-what": "^6.0.1",
@@ -21828,7 +22070,8 @@
     "css-what": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
-      "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="
+      "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
+      "dev": true
     },
     "cssdb": {
       "version": "7.2.0",
@@ -22258,6 +22501,7 @@
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
       "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==",
+      "dev": true,
       "requires": {
         "utila": "~0.4"
       }
@@ -22278,6 +22522,7 @@
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
       "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
+      "dev": true,
       "requires": {
         "domelementtype": "^2.0.1",
         "domhandler": "^4.2.0",
@@ -22287,12 +22532,14 @@
     "domelementtype": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
-      "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
+      "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+      "dev": true
     },
     "domhandler": {
       "version": "4.3.1",
       "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
       "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
+      "dev": true,
       "requires": {
         "domelementtype": "^2.2.0"
       }
@@ -22301,6 +22548,7 @@
       "version": "2.8.0",
       "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
       "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
+      "dev": true,
       "requires": {
         "dom-serializer": "^1.0.1",
         "domelementtype": "^2.2.0",
@@ -22349,9 +22597,9 @@
       "dev": true
     },
     "electron-to-chromium": {
-      "version": "1.4.284",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz",
-      "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA=="
+      "version": "1.4.453",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.453.tgz",
+      "integrity": "sha512-BU8UtQz6CB3T7RIGhId4BjmjJVXQDujb0+amGL8jpcluFJr6lwspBOvkUbnttfpZCm4zFMHmjrX1QrdPWBBMjQ=="
     },
     "emoji-regex": {
       "version": "8.0.0",
@@ -22394,9 +22642,9 @@
       }
     },
     "engine.io": {
-      "version": "6.2.1",
-      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz",
-      "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==",
+      "version": "6.5.1",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.1.tgz",
+      "integrity": "sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==",
       "dev": true,
       "requires": {
         "@types/cookie": "^0.4.1",
@@ -22407,20 +22655,20 @@
         "cookie": "~0.4.1",
         "cors": "~2.8.5",
         "debug": "~4.3.1",
-        "engine.io-parser": "~5.0.3",
-        "ws": "~8.2.3"
+        "engine.io-parser": "~5.1.0",
+        "ws": "~8.11.0"
       }
     },
     "engine.io-parser": {
-      "version": "5.0.4",
-      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
-      "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz",
+      "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==",
       "dev": true
     },
     "enhanced-resolve": {
-      "version": "5.12.0",
-      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
-      "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
+      "version": "5.15.0",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
+      "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
       "requires": {
         "graceful-fs": "^4.2.4",
         "tapable": "^2.2.0"
@@ -22435,7 +22683,8 @@
     "entities": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
-      "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
+      "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
+      "dev": true
     },
     "env-paths": {
       "version": "2.2.1",
@@ -22481,7 +22730,8 @@
     "es-module-lexer": {
       "version": "0.9.3",
       "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz",
-      "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ=="
+      "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
+      "dev": true
     },
     "es6-promise": {
       "version": "4.2.8",
@@ -22688,7 +22938,8 @@
     "escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="
+      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "dev": true
     },
     "escodegen": {
       "version": "2.0.0",
@@ -23919,6 +24170,7 @@
       "version": "1.0.0-beta.2",
       "resolved": "https://registry.npmjs.org/html-webpack-inline-source-plugin/-/html-webpack-inline-source-plugin-1.0.0-beta.2.tgz",
       "integrity": "sha512-ydsEKdp0tnbmnqRAH2WSSMXerCNYhjes5b79uvP2BU3p6cyk+6ucNMsw5b5xD1QxphgvBBA3QqVmdcpu8QLlRQ==",
+      "dev": true,
       "requires": {
         "escape-string-regexp": "^1.0.5",
         "slash": "^1.0.0",
@@ -23928,7 +24180,8 @@
         "slash": {
           "version": "1.0.0",
           "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
-          "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg=="
+          "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==",
+          "dev": true
         }
       }
     },
@@ -23936,6 +24189,7 @@
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz",
       "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==",
+      "dev": true,
       "requires": {
         "@types/html-minifier-terser": "^6.0.0",
         "html-minifier-terser": "^6.0.2",
@@ -23957,6 +24211,7 @@
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
       "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
+      "dev": true,
       "requires": {
         "domelementtype": "^2.0.1",
         "domhandler": "^4.0.0",
@@ -23965,9 +24220,9 @@
       }
     },
     "http-cache-semantics": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
-      "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
+      "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
       "dev": true
     },
     "http-deceiver": {
@@ -24754,9 +25009,9 @@
           }
         },
         "semver": {
-          "version": "7.3.8",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
-          "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+          "version": "7.5.3",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
+          "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
           "requires": {
             "lru-cache": "^6.0.0"
           }
@@ -26088,9 +26343,9 @@
       "optional": true
     },
     "node-releases": {
-      "version": "2.0.8",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
-      "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
+      "version": "2.0.13",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
+      "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
     },
     "node-source-walk": {
       "version": "4.3.0",
@@ -26322,6 +26577,7 @@
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
       "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+      "dev": true,
       "requires": {
         "boolbase": "^1.0.0"
       }
@@ -26394,17 +26650,17 @@
       }
     },
     "optionator": {
-      "version": "0.9.1",
-      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
-      "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+      "version": "0.9.3",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+      "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
       "dev": true,
       "requires": {
+        "@aashutoshrathi/word-wrap": "^1.2.3",
         "deep-is": "^0.1.3",
         "fast-levenshtein": "^2.0.6",
         "levn": "^0.4.1",
         "prelude-ls": "^1.2.1",
-        "type-check": "^0.4.0",
-        "word-wrap": "^1.2.3"
+        "type-check": "^0.4.0"
       }
     },
     "ora": {
@@ -27271,6 +27527,7 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
       "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==",
+      "dev": true,
       "requires": {
         "lodash": "^4.17.20",
         "renderkid": "^3.0.0"
@@ -27876,6 +28133,7 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
       "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==",
+      "dev": true,
       "requires": {
         "css-select": "^4.1.3",
         "dom-converter": "^0.2.0",
@@ -28441,9 +28699,9 @@
       }
     },
     "serialize-javascript": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
-      "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
+      "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
       "requires": {
         "randombytes": "^2.1.0"
       }
@@ -28598,29 +28856,33 @@
       }
     },
     "socket.io": {
-      "version": "4.5.4",
-      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz",
-      "integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==",
+      "version": "4.7.1",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.1.tgz",
+      "integrity": "sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw==",
       "dev": true,
       "requires": {
         "accepts": "~1.3.4",
         "base64id": "~2.0.0",
+        "cors": "~2.8.5",
         "debug": "~4.3.2",
-        "engine.io": "~6.2.1",
-        "socket.io-adapter": "~2.4.0",
-        "socket.io-parser": "~4.2.1"
+        "engine.io": "~6.5.0",
+        "socket.io-adapter": "~2.5.2",
+        "socket.io-parser": "~4.2.4"
       }
     },
     "socket.io-adapter": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz",
-      "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==",
-      "dev": true
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz",
+      "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==",
+      "dev": true,
+      "requires": {
+        "ws": "~8.11.0"
+      }
     },
     "socket.io-parser": {
-      "version": "4.2.1",
-      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz",
-      "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==",
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+      "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
       "dev": true,
       "requires": {
         "@socket.io/component-emitter": "~3.1.0",
@@ -28712,7 +28974,8 @@
     "source-map-url": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
-      "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw=="
+      "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
+      "dev": true
     },
     "sourcemap-codec": {
       "version": "1.4.8",
@@ -28870,6 +29133,7 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
       "requires": {
         "ansi-regex": "^5.0.1"
       }
@@ -29090,15 +29354,15 @@
       }
     },
     "terser-webpack-plugin": {
-      "version": "5.3.6",
-      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz",
-      "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==",
+      "version": "5.3.9",
+      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz",
+      "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==",
       "requires": {
-        "@jridgewell/trace-mapping": "^0.3.14",
+        "@jridgewell/trace-mapping": "^0.3.17",
         "jest-worker": "^27.4.5",
         "schema-utils": "^3.1.1",
-        "serialize-javascript": "^6.0.0",
-        "terser": "^5.14.1"
+        "serialize-javascript": "^6.0.1",
+        "terser": "^5.16.8"
       },
       "dependencies": {
         "ajv": {
@@ -29118,6 +29382,11 @@
           "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
           "requires": {}
         },
+        "commander": {
+          "version": "2.20.3",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+          "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+        },
         "json-schema-traverse": {
           "version": "0.4.1",
           "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -29132,6 +29401,17 @@
             "ajv": "^6.12.5",
             "ajv-keywords": "^3.5.2"
           }
+        },
+        "terser": {
+          "version": "5.18.2",
+          "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.2.tgz",
+          "integrity": "sha512-Ah19JS86ypbJzTzvUCX7KOsEIhDaRONungA4aYBjEP3JZRf4ocuDzTg4QWZnPn9DEMiMYGJPiSOy7aykoCc70w==",
+          "requires": {
+            "@jridgewell/source-map": "^0.3.3",
+            "acorn": "^8.8.2",
+            "commander": "^2.20.0",
+            "source-map-support": "~0.5.20"
+          }
         }
       }
     },
@@ -29335,9 +29615,9 @@
       },
       "dependencies": {
         "json5": {
-          "version": "1.0.1",
-          "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
-          "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+          "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
           "dev": true,
           "requires": {
             "minimist": "^1.2.0"
@@ -29506,14 +29786,14 @@
       "dev": true
     },
     "typescript": {
-      "version": "4.7.4",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
-      "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ=="
+      "version": "4.8.4",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
+      "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ=="
     },
     "ua-parser-js": {
-      "version": "0.7.32",
-      "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.32.tgz",
-      "integrity": "sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==",
+      "version": "0.7.35",
+      "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz",
+      "integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==",
       "dev": true
     },
     "unfetch": {
@@ -29586,9 +29866,9 @@
       "dev": true
     },
     "update-browserslist-db": {
-      "version": "1.0.10",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
-      "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
+      "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
       "requires": {
         "escalade": "^3.1.1",
         "picocolors": "^1.0.0"
@@ -29623,7 +29903,8 @@
     "utila": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
-      "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA=="
+      "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==",
+      "dev": true
     },
     "utils-merge": {
       "version": "1.0.1",
@@ -29731,12 +30012,12 @@
       }
     },
     "webdriver-manager": {
-      "version": "12.1.8",
-      "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.8.tgz",
-      "integrity": "sha512-qJR36SXG2VwKugPcdwhaqcLQOD7r8P2Xiv9sfNbfZrKBnX243iAkOueX1yAmeNgIKhJ3YAT/F2gq6IiEZzahsg==",
+      "version": "12.1.9",
+      "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.9.tgz",
+      "integrity": "sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ==",
       "dev": true,
       "requires": {
-        "adm-zip": "^0.4.9",
+        "adm-zip": "^0.5.2",
         "chalk": "^1.1.1",
         "del": "^2.2.0",
         "glob": "^7.0.3",
@@ -29856,21 +30137,21 @@
       "integrity": "sha512-cmO2aPd6gR6bK/ttdk8ZIypJfZMOcTvsvXv/LxXZjAFu5TC6vXqFrZYudlPuKxVsA34Pc8Fysq2rCnflu+wuuA=="
     },
     "webpack": {
-      "version": "5.75.0",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz",
-      "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==",
+      "version": "5.88.1",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.1.tgz",
+      "integrity": "sha512-FROX3TxQnC/ox4N+3xQoWZzvGXSuscxR32rbzjpXgEzWudJFEJBpdlkkob2ylrv5yzzufD1zph1OoFsLtm6stQ==",
       "requires": {
         "@types/eslint-scope": "^3.7.3",
-        "@types/estree": "^0.0.51",
-        "@webassemblyjs/ast": "1.11.1",
-        "@webassemblyjs/wasm-edit": "1.11.1",
-        "@webassemblyjs/wasm-parser": "1.11.1",
+        "@types/estree": "^1.0.0",
+        "@webassemblyjs/ast": "^1.11.5",
+        "@webassemblyjs/wasm-edit": "^1.11.5",
+        "@webassemblyjs/wasm-parser": "^1.11.5",
         "acorn": "^8.7.1",
-        "acorn-import-assertions": "^1.7.6",
+        "acorn-import-assertions": "^1.9.0",
         "browserslist": "^4.14.5",
         "chrome-trace-event": "^1.0.2",
-        "enhanced-resolve": "^5.10.0",
-        "es-module-lexer": "^0.9.0",
+        "enhanced-resolve": "^5.15.0",
+        "es-module-lexer": "^1.2.1",
         "eslint-scope": "5.1.1",
         "events": "^3.2.0",
         "glob-to-regexp": "^0.4.1",
@@ -29879,13 +30160,149 @@
         "loader-runner": "^4.2.0",
         "mime-types": "^2.1.27",
         "neo-async": "^2.6.2",
-        "schema-utils": "^3.1.0",
+        "schema-utils": "^3.2.0",
         "tapable": "^2.1.1",
-        "terser-webpack-plugin": "^5.1.3",
+        "terser-webpack-plugin": "^5.3.7",
         "watchpack": "^2.4.0",
         "webpack-sources": "^3.2.3"
       },
       "dependencies": {
+        "@types/estree": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
+          "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA=="
+        },
+        "@webassemblyjs/ast": {
+          "version": "1.11.6",
+          "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
+          "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==",
+          "requires": {
+            "@webassemblyjs/helper-numbers": "1.11.6",
+            "@webassemblyjs/helper-wasm-bytecode": "1.11.6"
+          }
+        },
+        "@webassemblyjs/floating-point-hex-parser": {
+          "version": "1.11.6",
+          "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
+          "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw=="
+        },
+        "@webassemblyjs/helper-api-error": {
+          "version": "1.11.6",
+          "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
+          "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="
+        },
+        "@webassemblyjs/helper-buffer": {
+          "version": "1.11.6",
+          "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz",
+          "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA=="
+        },
+        "@webassemblyjs/helper-numbers": {
+          "version": "1.11.6",
+          "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
+          "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
+          "requires": {
+            "@webassemblyjs/floating-point-hex-parser": "1.11.6",
+            "@webassemblyjs/helper-api-error": "1.11.6",
+            "@xtuc/long": "4.2.2"
+          }
+        },
+        "@webassemblyjs/helper-wasm-bytecode": {
+          "version": "1.11.6",
+          "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
+          "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="
+        },
+        "@webassemblyjs/helper-wasm-section": {
+          "version": "1.11.6",
+          "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz",
+          "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==",
+          "requires": {
+            "@webassemblyjs/ast": "1.11.6",
+            "@webassemblyjs/helper-buffer": "1.11.6",
+            "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+            "@webassemblyjs/wasm-gen": "1.11.6"
+          }
+        },
+        "@webassemblyjs/ieee754": {
+          "version": "1.11.6",
+          "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
+          "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
+          "requires": {
+            "@xtuc/ieee754": "^1.2.0"
+          }
+        },
+        "@webassemblyjs/leb128": {
+          "version": "1.11.6",
+          "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
+          "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
+          "requires": {
+            "@xtuc/long": "4.2.2"
+          }
+        },
+        "@webassemblyjs/utf8": {
+          "version": "1.11.6",
+          "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
+          "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="
+        },
+        "@webassemblyjs/wasm-edit": {
+          "version": "1.11.6",
+          "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz",
+          "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==",
+          "requires": {
+            "@webassemblyjs/ast": "1.11.6",
+            "@webassemblyjs/helper-buffer": "1.11.6",
+            "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+            "@webassemblyjs/helper-wasm-section": "1.11.6",
+            "@webassemblyjs/wasm-gen": "1.11.6",
+            "@webassemblyjs/wasm-opt": "1.11.6",
+            "@webassemblyjs/wasm-parser": "1.11.6",
+            "@webassemblyjs/wast-printer": "1.11.6"
+          }
+        },
+        "@webassemblyjs/wasm-gen": {
+          "version": "1.11.6",
+          "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz",
+          "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==",
+          "requires": {
+            "@webassemblyjs/ast": "1.11.6",
+            "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+            "@webassemblyjs/ieee754": "1.11.6",
+            "@webassemblyjs/leb128": "1.11.6",
+            "@webassemblyjs/utf8": "1.11.6"
+          }
+        },
+        "@webassemblyjs/wasm-opt": {
+          "version": "1.11.6",
+          "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz",
+          "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==",
+          "requires": {
+            "@webassemblyjs/ast": "1.11.6",
+            "@webassemblyjs/helper-buffer": "1.11.6",
+            "@webassemblyjs/wasm-gen": "1.11.6",
+            "@webassemblyjs/wasm-parser": "1.11.6"
+          }
+        },
+        "@webassemblyjs/wasm-parser": {
+          "version": "1.11.6",
+          "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz",
+          "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==",
+          "requires": {
+            "@webassemblyjs/ast": "1.11.6",
+            "@webassemblyjs/helper-api-error": "1.11.6",
+            "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+            "@webassemblyjs/ieee754": "1.11.6",
+            "@webassemblyjs/leb128": "1.11.6",
+            "@webassemblyjs/utf8": "1.11.6"
+          }
+        },
+        "@webassemblyjs/wast-printer": {
+          "version": "1.11.6",
+          "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz",
+          "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==",
+          "requires": {
+            "@webassemblyjs/ast": "1.11.6",
+            "@xtuc/long": "4.2.2"
+          }
+        },
         "ajv": {
           "version": "6.12.6",
           "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -29903,15 +30320,20 @@
           "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
           "requires": {}
         },
+        "es-module-lexer": {
+          "version": "1.3.0",
+          "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz",
+          "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA=="
+        },
         "json-schema-traverse": {
           "version": "0.4.1",
           "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
           "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
         },
         "schema-utils": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
-          "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
+          "version": "3.3.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
+          "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
           "requires": {
             "@types/json-schema": "^7.0.8",
             "ajv": "^6.12.5",
@@ -30021,13 +30443,6 @@
             "ajv-formats": "^2.1.1",
             "ajv-keywords": "^5.0.0"
           }
-        },
-        "ws": {
-          "version": "8.11.0",
-          "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
-          "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
-          "dev": true,
-          "requires": {}
         }
       }
     },
@@ -30149,9 +30564,9 @@
       "dev": true
     },
     "ws": {
-      "version": "8.2.3",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
-      "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
+      "version": "8.11.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
+      "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
       "dev": true,
       "requires": {}
     },
diff --git a/tools/winscope/package.json b/tools/winscope/package.json
index ef27d79..70d8432 100644
--- a/tools/winscope/package.json
+++ b/tools/winscope/package.json
@@ -6,21 +6,22 @@
     "format:fix": "(ls *.js .*.js && find src/ -regextype egrep -regex '^.*(\\.ts|\\.js)$') |xargs npx prettier --write",
     "eslint:check": "(ls *.js .*.js && find src/ -regextype egrep -regex '^.*(\\.ts|\\.js)$') |xargs npx eslint --format=unix",
     "eslint:fix": "(ls *.js .*.js && find src/ -regextype egrep -regex '^.*(\\.ts|\\.js)$') |xargs npx eslint --format=unix --fix",
-    "tslint:check": "find src/ -regextype egrep -regex '^.*\\.ts$' |xargs npx tslint -c google.tslint.json",
-    "tslint:fix": "find src/ -regextype egrep -regex '^.*\\.ts$' |xargs npx tslint -c google.tslint.json --fix",
-    "deps_graph:check_cycles": "count=$(npx madge --extensions ts,js src/ --circular 2>&1 | awk '/Found.*circular dependencies/ {print $3}'); test ${count:-0} -le 10",
+    "tslint:check": "find src/ -regextype egrep -regex '^.*\\.ts$' |xargs npx tslint -c google.tslint.json --exclude 'src/trace_processor/*'",
+    "tslint:fix": "find src/ -regextype egrep -regex '^.*\\.ts$' |xargs npx tslint -c google.tslint.json --exclude 'src/trace_processor/*' --fix",
+    "deps_graph:check_cycles": "npx madge --extensions ts,js src/ --circular",
     "start": "webpack serve --config webpack.config.dev.js --open --hot --port 8080",
     "start:remote_tool_mock": "webpack serve --config src/test/remote_tool_mock/webpack.config.js --open --hot --port 8081",
-    "build:kotlin_legacy": "rm -rf kotlin_build && JAVA_OPTS='-Xmx2g -Xms1g' npx kotlinc-js -source-map -source-map-embed-sources always -module-kind commonjs -output kotlin_build/flicker.js ../../../platform_testing/libraries/flicker/src/android/tools/common",
-    "build:kotlin": "rm -rf kotlin_build && mkdir kotlin_build && JAVA_OPTS='-Xmx2g -Xms1g' npx kotlinc-js -Xir-produce-js -Xir-only -Xir-module-name=flicker -Xtyped-arrays -source-map -source-map-embed-sources always -module-kind commonjs -target v8 -libraries ./node_modules/kotlin-compiler/lib/kotlin-stdlib-js.jar -output kotlin_build/flicker.js  ../../../platform_testing/libraries/flicker/src/android/tools/common/",
-    "build:prod": "webpack --config webpack.config.prod.js --progress",
-    "build:remote_tool_mock": "webpack --config src/test/remote_tool_mock/webpack.config.js --progress",
-    "build:all": "npm run build:kotlin && npm run build:prod && npm run build:remote_tool_mock",
-    "test:unit": "webpack --config webpack.config.unit_test.js && jasmine dist/unit_test/bundle.js",
-    "test:component": "npx karma start",
-    "test:e2e": "rm -rf dist/e2e_test && npx tsc -p ./src/test/e2e && npx protractor protractor.config.js",
-    "test:presubmit": "npm run test:unit && npm run test:component && npm run format:check && npm run tslint:check && npm run eslint:check && npm run deps_graph:check_cycles",
-    "test:all": "npm run test:unit && npm run test:component && npm run test:e2e && npm run format:check && npm run tslint:check && npm run eslint:check && npm run deps_graph:check_cycles"
+    "build:kotlin_legacy": "rm -rf deps_build/flickerlib && JAVA_OPTS='-Xmx2g -Xms1g' npx kotlinc-js -Xuse-deprecated-legacy-compiler -source-map -source-map-embed-sources always -module-kind commonjs -output deps_build/flickerlib/flicker.js ../../../platform_testing/libraries/flicker/src/android/tools/common",
+    "build:kotlin": "rm -rf deps_build/flickerlib && mkdir -p deps_build/flickerlib && JAVA_OPTS='-Xmx2g -Xms1g' npx kotlinc-js -Xir-produce-js -Xir-only -Xir-module-name=flicker -Xtyped-arrays -source-map -source-map-embed-sources always -module-kind commonjs -target v8 -libraries ./node_modules/kotlin-compiler/lib/kotlin-stdlib-js.jar -output deps_build/flickerlib/flicker.js  ../../../platform_testing/libraries/flicker/utils/src/android/tools/common/",
+    "build:trace_processor": "PERFETTO_TOP=../../../external/perfetto; (cd $PERFETTO_TOP && tools/install-build-deps --ui && ui/node ui/build.js --out trace_processor_build) && rm -rf deps_build/trace_processor && mkdir -p deps_build/trace_processor && rsync -ar $PERFETTO_TOP/trace_processor_build/ deps_build/trace_processor && mkdir deps_build/trace_processor/to_be_served && cp deps_build/trace_processor/ui/dist_version/engine_bundle.js deps_build/trace_processor/to_be_served/ && cp deps_build/trace_processor/wasm/trace_processor.wasm deps_build/trace_processor/to_be_served/",
+    "build:prod": "npm run build:trace_processor && npm run build:kotlin && webpack --config webpack.config.prod.js --progress && cp deps_build/trace_processor/to_be_served/* dist/prod/",
+    "install:chromedriver": "chrome_version=$(google-chrome --version |cut -d' ' -f3); rm -rf deps_build/chromedriver-linux64 && mkdir -p deps_build/chromedriver-linux64 && (cd deps_build/chromedriver-linux64 && wget https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${chrome_version}/linux64/chromedriver-linux64.zip && cd .. && unzip chromedriver-linux64/chromedriver-linux64.zip)",
+    "test:unit:ci": "npx karma start karma.config.ci.js",
+    "test:unit:dev": "npx karma start karma.config.dev.js",
+    "test:e2e": "npm run install:chromedriver && rm -rf dist/e2e_test && npx tsc -p ./src/test/e2e && npx protractor protractor.config.js",
+    "test:presubmit:quiet": "npm run test:unit:ci && npm run format:check && npm run tslint:check && npm run eslint:check && npm run deps_graph:check_cycles",
+    "test:presubmit": "(npm run test:presubmit:quiet && printf '\\033[1m\\033[32mALL GREEN! \\U1F49A (Kean loves you)\\n') || (printf '\\033[1m\\033[31mFAILING! \\U1F92F (Kean is upset)\\n' && false)",
+    "test:all": "npm run test:unit:ci && npm run test:e2e && npm run format:check && npm run tslint:check && npm run eslint:check && npm run deps_graph:check_cycles"
   },
   "private": true,
   "dependencies": {
@@ -43,8 +44,6 @@
     "dateformat": "^5.0.3",
     "gl-matrix": "^3.4.3",
     "html-loader": "^3.1.0",
-    "html-webpack-inline-source-plugin": "^1.0.0-beta.2",
-    "html-webpack-plugin": "^5.5.0",
     "html2canvas": "^1.4.1",
     "jsbn": "^1.1.0",
     "jsbn-rsa": "^1.0.4",
@@ -56,7 +55,7 @@
     "three": "^0.143.0",
     "ts-loader": "^9.3.0",
     "tslib": "^2.3.0",
-    "typescript": "~4.7.2",
+    "typescript": "^4.8.0",
     "webgl-utils": "^1.0.1",
     "webgl-utils.js": "^1.1.0",
     "webpack-cli": "^4.10.0",
@@ -69,6 +68,7 @@
     "@ngxs/devtools-plugin": "^3.7.4",
     "@types/chrome": "^0.0.204",
     "@types/dateformat": "^5.0.0",
+    "@types/gtag.js": "^0.0.13",
     "@types/jasmine": "~4.3.1",
     "@types/jquery": "^3.5.14",
     "@types/jsbn": "^1.2.30",
@@ -78,9 +78,12 @@
     "@typescript-eslint/eslint-plugin": "^5.30.6",
     "@typescript-eslint/parser": "^5.30.6",
     "angular2-template-loader": "^0.6.2",
+    "copy-webpack-plugin": "^11.0.0",
     "eslint": "^8.19.0",
     "eslint-config-prettier": "^8.5.0",
     "eslint-plugin-prettier": "^4.2.1",
+    "html-webpack-inline-source-plugin": "^1.0.0-beta.2",
+    "html-webpack-plugin": "^5.5.0",
     "jasmine": "~4.3.0",
     "jasmine-core": "~4.1.0",
     "karma": "~6.3.0",
diff --git a/tools/winscope/protos/udc/surfaceflinger/common.proto b/tools/winscope/protos/udc/surfaceflinger/common.proto
new file mode 100644
index 0000000..a6d8d61
--- /dev/null
+++ b/tools/winscope/protos/udc/surfaceflinger/common.proto
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+
+syntax = "proto3";
+option optimize_for = LITE_RUNTIME;
+package android.surfaceflinger;
+
+message RegionProto {
+    reserved 1; // Previously: uint64 id
+    repeated RectProto rect = 2;
+}
+
+message RectProto {
+  int32 left   = 1;
+  int32 top    = 2;
+  int32 right  = 3;
+  int32 bottom = 4;
+}
+
+message SizeProto {
+  int32 w = 1;
+  int32 h = 2;
+}
+
+message TransformProto {
+  float dsdx = 1;
+  float dtdx = 2;
+  float dsdy = 3;
+  float dtdy = 4;
+  int32 type = 5;
+}
+
+message ColorProto {
+    float r = 1;
+    float g = 2;
+    float b = 3;
+    float a = 4;
+}
+
+message InputWindowInfoProto {
+    uint32 layout_params_flags = 1;
+    int32 layout_params_type = 2;
+    RectProto frame = 3;
+    RegionProto touchable_region = 4;
+
+    int32 surface_inset = 5;
+    bool visible = 6;
+    bool can_receive_keys = 7 [deprecated = true];
+    bool focusable = 8;
+    bool has_wallpaper = 9;
+
+    float global_scale_factor = 10;
+    float window_x_scale = 11 [deprecated = true];
+    float window_y_scale = 12 [deprecated = true];
+
+    int32 crop_layer_id = 13;
+    bool replace_touchable_region_with_crop = 14;
+    RectProto touchable_region_crop = 15;
+    TransformProto transform = 16;
+}
+
+message BlurRegion {
+    uint32 blur_radius = 1;
+    uint32 corner_radius_tl = 2;
+    uint32 corner_radius_tr = 3;
+    uint32 corner_radius_bl = 4;
+    float corner_radius_br = 5;
+    float alpha = 6;
+    int32 left = 7;
+    int32 top = 8;
+    int32 right = 9;
+    int32 bottom = 10;
+}
+
+message ColorTransformProto {
+    // This will be a 4x4 matrix of float values
+    repeated float val = 1;
+}
diff --git a/tools/winscope/protos/udc/surfaceflinger/display.proto b/tools/winscope/protos/udc/surfaceflinger/display.proto
new file mode 100644
index 0000000..18e43ed
--- /dev/null
+++ b/tools/winscope/protos/udc/surfaceflinger/display.proto
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+syntax = "proto3";
+option optimize_for = LITE_RUNTIME;
+
+import "protos/udc/surfaceflinger/common.proto";
+
+package android.surfaceflinger;
+
+message DisplayProto {
+    uint64 id = 1;
+
+    string name = 2;
+
+    uint32 layer_stack = 3;
+
+    SizeProto size = 4;
+
+    RectProto layer_stack_space_rect = 5;
+
+    TransformProto transform = 6;
+
+    bool is_virtual = 7;
+
+    double dpi_x = 8;
+    double dpi_y = 9;
+}
diff --git a/tools/winscope/protos/udc/surfaceflinger/layers.proto b/tools/winscope/protos/udc/surfaceflinger/layers.proto
new file mode 100644
index 0000000..8ee2757
--- /dev/null
+++ b/tools/winscope/protos/udc/surfaceflinger/layers.proto
@@ -0,0 +1,171 @@
+// Definitions for SurfaceFlinger layers.
+
+syntax = "proto3";
+option optimize_for = LITE_RUNTIME;
+
+import "protos/udc/surfaceflinger/common.proto";
+
+package android.surfaceflinger;
+
+// Contains a list of all layers.
+message LayersProto {
+  repeated LayerProto layers = 1;
+}
+
+// Must match definition in the IComposerClient HAL
+enum HwcCompositionType {
+    // Invalid composition type
+    INVALID = 0;
+    // Layer was composited by the client into the client target buffer
+    CLIENT = 1;
+    // Layer was composited by the device through hardware overlays
+    DEVICE = 2;
+    // Layer was composited by the device using a color
+    SOLID_COLOR = 3;
+    // Similar to DEVICE, but the layer position may have been asynchronously set
+    // through setCursorPosition
+    CURSOR = 4;
+    // Layer was composited by the device via a sideband stream.
+    SIDEBAND = 5;
+}
+
+// Information about each layer.
+message LayerProto {
+  // unique id per layer.
+  int32 id = 1;
+  // unique name per layer.
+  string name = 2;
+  // list of children this layer may have. May be empty.
+  repeated int32 children = 3;
+  // list of layers that are z order relative to this layer.
+  repeated int32 relatives = 4;
+  // The type of layer, ex Color, Layer
+  string type = 5;
+  RegionProto transparent_region = 6;
+  RegionProto visible_region = 7;
+  RegionProto damage_region = 8;
+  uint32 layer_stack = 9;
+  // The layer's z order. Can be z order in layer stack, relative to parent,
+  // or relative to another layer specified in zOrderRelative.
+  int32 z = 10;
+  // The layer's position on the display.
+  PositionProto position = 11;
+  // The layer's requested position.
+  PositionProto requested_position = 12;
+  // The layer's size.
+  SizeProto size = 13;
+  // The layer's crop in it's own bounds.
+  RectProto crop = 14;
+  // The layer's crop in it's parent's bounds.
+  RectProto final_crop = 15 [deprecated=true];
+  bool is_opaque = 16;
+  bool invalidate = 17;
+  string dataspace = 18;
+  string pixel_format = 19;
+  // The layer's actual color.
+  ColorProto color = 20;
+  // The layer's requested color.
+  ColorProto requested_color = 21;
+  // Can be any combination of
+  //    hidden = 0x01
+  //    opaque = 0x02,
+  //    secure = 0x80,
+  uint32 flags = 22;
+  // The layer's actual transform
+  TransformProto transform = 23;
+  // The layer's requested transform.
+  TransformProto requested_transform = 24;
+  // The parent layer. This value can be null if there is no parent.
+  int32 parent = 25;
+  // The layer that this layer has a z order relative to. This value can be null.
+  int32 z_order_relative_of = 26;
+  // This value can be null if there's nothing to draw.
+  ActiveBufferProto active_buffer = 27;
+  // The number of frames available.
+  int32 queued_frames = 28;
+  bool refresh_pending = 29;
+  // The layer's composer backend destination frame
+  RectProto hwc_frame = 30;
+  // The layer's composer backend source crop
+  FloatRectProto hwc_crop = 31;
+  // The layer's composer backend transform
+  int32 hwc_transform = 32;
+  int32 window_type = 33 [deprecated=true];
+  int32 app_id = 34 [deprecated=true];
+  // The layer's composition type
+  HwcCompositionType hwc_composition_type = 35;
+  // If it's a buffer layer, indicate if the content is protected
+  bool is_protected = 36;
+  // Current frame number being rendered.
+  uint64 curr_frame = 37;
+  // A list of barriers that the layer is waiting to update state.
+  repeated BarrierLayerProto barrier_layer = 38;
+  // If active_buffer is not null, record its transform.
+  TransformProto buffer_transform = 39;
+  int32 effective_scaling_mode = 40;
+  // Layer's corner radius.
+  float corner_radius = 41;
+  // Metadata map. May be empty.
+  map<int32, bytes> metadata = 42;
+
+  TransformProto effective_transform = 43;
+  FloatRectProto source_bounds = 44;
+  FloatRectProto bounds = 45;
+  FloatRectProto screen_bounds = 46;
+
+  InputWindowInfoProto input_window_info = 47;
+
+  // Crop used to draw the rounded corner.
+  FloatRectProto corner_radius_crop = 48;
+
+  // length of the shadow to draw around the layer, it may be set on the
+  // layer or set by a parent layer.
+  float shadow_radius = 49;
+  ColorTransformProto color_transform = 50;
+
+  bool is_relative_of = 51;
+  // Layer's background blur radius in pixels.
+  int32 background_blur_radius = 52;
+
+  uint32 owner_uid = 53;
+
+  // Regions of a layer, where blur should be applied.
+  repeated BlurRegion blur_regions = 54;
+
+  bool is_trusted_overlay = 55;
+
+  // Corner radius explicitly set on layer rather than inherited
+  float requested_corner_radius = 56;
+
+  RectProto destination_frame = 57;
+
+  uint32 original_id = 58;
+}
+
+message PositionProto {
+  float x = 1;
+  float y = 2;
+}
+
+message FloatRectProto {
+  float left = 1;
+  float top = 2;
+  float right = 3;
+  float bottom = 4;
+}
+
+message ActiveBufferProto {
+  uint32 width = 1;
+  uint32 height = 2;
+  uint32 stride = 3;
+  int32 format = 4;
+  uint64 usage = 5;
+}
+
+message BarrierLayerProto {
+  // layer id the barrier is waiting on.
+  int32 id = 1;
+  // frame number the barrier is waiting on.
+  uint64 frame_number = 2;
+}
+
diff --git a/tools/winscope/protos/udc/surfaceflinger/layerstrace.proto b/tools/winscope/protos/udc/surfaceflinger/layerstrace.proto
new file mode 100644
index 0000000..9702f76
--- /dev/null
+++ b/tools/winscope/protos/udc/surfaceflinger/layerstrace.proto
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+syntax = "proto2";
+option optimize_for = LITE_RUNTIME;
+
+import "protos/udc/surfaceflinger/layers.proto";
+import "protos/udc/surfaceflinger/display.proto";
+
+package android.surfaceflinger;
+
+/* represents a file full of surface flinger trace entries.
+   Encoded, it should start with 0x4c 0x59 0x52 0x54 0x52 0x41 0x43 0x45 (.LYRTRACE), such
+   that they can be easily identified. */
+message LayersTraceFileProto {
+
+    /* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 | MagicNumber.MAGIC_NUMBER_L
+       (this is needed because enums have to be 32 bits and there's no nice way to put 64bit
+        constants into .proto files. */
+    enum MagicNumber {
+        INVALID = 0;
+        MAGIC_NUMBER_L = 0x5452594c;  /* LYRT (little-endian ASCII) */
+        MAGIC_NUMBER_H = 0x45434152;  /* RACE (little-endian ASCII) */
+    }
+
+    optional fixed64 magic_number = 1;  /* Must be the first field, set to value in MagicNumber */
+    repeated LayersTraceProto entry = 2;
+
+    /* offset between real-time clock and elapsed time clock in nanoseconds.
+       Calculated as: systemTime(SYSTEM_TIME_REALTIME) - systemTime(SYSTEM_TIME_MONOTONIC) */
+    optional fixed64 real_to_elapsed_time_offset_nanos = 3;
+}
+
+/* one layers trace entry. */
+message LayersTraceProto {
+    /* required: elapsed realtime in nanos since boot of when this entry was logged */
+    optional sfixed64 elapsed_realtime_nanos = 1;
+
+    /* where the trace originated */
+    optional string where = 2;
+
+    optional LayersProto layers = 3;
+
+    // Blob for the current HWC information for all layers, reported by dumpsys.
+    optional string hwc_blob = 4;
+
+    /* Includes state sent during composition like visible region and composition type. */
+    optional bool excludes_composition_state = 5;
+
+    /* Number of missed entries since the last entry was recorded. */
+    optional uint32 missed_entries = 6;
+
+    repeated DisplayProto displays = 7;
+
+    optional int64 vsync_id = 8;
+}
diff --git a/tools/winscope/protos/udc/surfaceflinger/transactions.proto b/tools/winscope/protos/udc/surfaceflinger/transactions.proto
new file mode 100644
index 0000000..fd8de63
--- /dev/null
+++ b/tools/winscope/protos/udc/surfaceflinger/transactions.proto
@@ -0,0 +1,309 @@
+/*
+ * 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.
+ */
+
+syntax = "proto3";
+option optimize_for = LITE_RUNTIME;
+
+import "protos/udc/surfaceflinger/common.proto";
+
+package android.surfaceflinger.proto;
+
+/* Represents a file full of surface flinger transactions.
+   Encoded, it should start with 0x54 0x4E 0x58 0x54 0x52 0x41 0x43 0x45 (.TNXTRACE), such
+   that they can be easily identified. */
+message TransactionTraceFile {
+    /* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 | MagicNumber.MAGIC_NUMBER_L
+       (this is needed because enums have to be 32 bits and there's no nice way to put 64bit
+        constants into .proto files. */
+    enum MagicNumber {
+        INVALID = 0;
+        MAGIC_NUMBER_L = 0x54584E54; /* TNXT (little-endian ASCII) */
+        MAGIC_NUMBER_H = 0x45434152; /* RACE (little-endian ASCII) */
+    }
+
+    fixed64 magic_number = 1; /* Must be the first field, set to value in MagicNumber */
+    repeated TransactionTraceEntry entry = 2;
+
+    /* offset between real-time clock and elapsed time clock in nanoseconds.
+       Calculated as: systemTime(SYSTEM_TIME_REALTIME) - systemTime(SYSTEM_TIME_MONOTONIC) */
+    fixed64 real_to_elapsed_time_offset_nanos = 3;
+    uint32 version = 4;
+}
+
+message TransactionTraceEntry {
+    int64 elapsed_realtime_nanos = 1;
+    int64 vsync_id = 2;
+    repeated TransactionState transactions = 3;
+    repeated LayerCreationArgs added_layers = 4;
+    repeated uint32 destroyed_layers = 5;
+    repeated DisplayState added_displays = 6;
+    repeated int32 removed_displays = 7;
+    repeated uint32 destroyed_layer_handles = 8;
+    bool displays_changed = 9;
+    repeated DisplayInfo displays = 10;
+}
+
+message DisplayInfo {
+    uint32 layer_stack = 1;
+    int32 display_id = 2;
+    int32 logical_width = 3;
+    int32 logical_height = 4;
+    Transform transform_inverse = 5;
+    Transform transform = 6;
+    bool receives_input = 7;
+    bool is_secure = 8;
+    bool is_primary = 9;
+    bool is_virtual = 10;
+    int32 rotation_flags = 11;
+    int32 transform_hint = 12;
+
+}
+
+message LayerCreationArgs {
+    uint32 layer_id = 1;
+    string name = 2;
+    uint32 flags = 3;
+    uint32 parent_id = 4;
+    uint32 mirror_from_id = 5;
+    bool add_to_root = 6;
+    uint32 layer_stack_to_mirror = 7;
+}
+
+message Transform {
+    float dsdx = 1;
+    float dtdx = 2;
+    float dtdy = 3;
+    float dsdy = 4;
+    float tx = 5;
+    float ty = 6;
+}
+
+message TransactionState {
+    int32 pid = 1;
+    int32 uid = 2;
+    int64 vsync_id = 3;
+    int32 input_event_id = 4;
+    int64 post_time = 5;
+    uint64 transaction_id = 6;
+    repeated LayerState layer_changes = 7;
+    repeated DisplayState display_changes = 8;
+    repeated uint64 merged_transaction_ids = 9;
+}
+
+// Keep insync with layer_state_t
+message LayerState {
+    uint32 layer_id = 1;
+    // Changes are split into ChangesLsb and ChangesMsb. First 32 bits are in ChangesLsb
+    // and the next 32 bits are in ChangesMsb. This is needed because enums have to be
+    // 32 bits and there's no nice way to put 64bit constants into .proto files.
+    enum ChangesLsb {
+        eChangesLsbNone = 0;
+        ePositionChanged = 0x00000001;
+        eLayerChanged = 0x00000002;
+        // unused = 0x00000004;
+        eAlphaChanged = 0x00000008;
+
+        eMatrixChanged = 0x00000010;
+        eTransparentRegionChanged = 0x00000020;
+        eFlagsChanged = 0x00000040;
+        eLayerStackChanged = 0x00000080;
+
+        eReleaseBufferListenerChanged = 0x00000400;
+        eShadowRadiusChanged = 0x00000800;
+
+        eBufferCropChanged = 0x00002000;
+        eRelativeLayerChanged = 0x00004000;
+        eReparent = 0x00008000;
+
+        eColorChanged = 0x00010000;
+        eBufferTransformChanged = 0x00040000;
+        eTransformToDisplayInverseChanged = 0x00080000;
+
+        eCropChanged = 0x00100000;
+        eBufferChanged = 0x00200000;
+        eAcquireFenceChanged = 0x00400000;
+        eDataspaceChanged = 0x00800000;
+
+        eHdrMetadataChanged = 0x01000000;
+        eSurfaceDamageRegionChanged = 0x02000000;
+        eApiChanged = 0x04000000;
+        eSidebandStreamChanged = 0x08000000;
+
+        eColorTransformChanged = 0x10000000;
+        eHasListenerCallbacksChanged = 0x20000000;
+        eInputInfoChanged = 0x40000000;
+        eCornerRadiusChanged = -2147483648; // 0x80000000; (proto stores enums as signed int)
+    };
+    enum ChangesMsb {
+        eChangesMsbNone = 0;
+        eDestinationFrameChanged = 0x1;
+        eCachedBufferChanged = 0x2;
+        eBackgroundColorChanged = 0x4;
+        eMetadataChanged = 0x8;
+        eColorSpaceAgnosticChanged = 0x10;
+        eFrameRateSelectionPriority = 0x20;
+        eFrameRateChanged = 0x40;
+        eBackgroundBlurRadiusChanged = 0x80;
+        eProducerDisconnect = 0x100;
+        eFixedTransformHintChanged = 0x200;
+        eFrameNumberChanged = 0x400;
+        eBlurRegionsChanged = 0x800;
+        eAutoRefreshChanged = 0x1000;
+        eStretchChanged = 0x2000;
+        eTrustedOverlayChanged = 0x4000;
+        eDropInputModeChanged = 0x8000;
+    };
+    uint64 what = 2;
+    float x = 3;
+    float y = 4;
+    int32 z = 5;
+    uint32 w = 6;
+    uint32 h = 7;
+    uint32 layer_stack = 8;
+
+    enum Flags {
+        eFlagsNone = 0;
+        eLayerHidden = 0x01;
+        eLayerOpaque = 0x02;
+        eLayerSkipScreenshot = 0x40;
+        eLayerSecure = 0x80;
+        eEnableBackpressure = 0x100;
+        eLayerIsDisplayDecoration = 0x200;
+    };
+    uint32 flags = 9;
+    uint32 mask = 10;
+
+    message Matrix22 {
+        float dsdx = 1;
+        float dtdx = 2;
+        float dtdy = 3;
+        float dsdy = 4;
+    };
+    Matrix22 matrix = 11;
+    float corner_radius = 12;
+    uint32 background_blur_radius = 13;
+    uint32 parent_id = 14;
+    uint32 relative_parent_id = 15;
+
+    float alpha = 16;
+    message Color3 {
+        float r = 1;
+        float g = 2;
+        float b = 3;
+    }
+    Color3 color = 17;
+    RegionProto transparent_region = 18;
+    uint32 transform = 19;
+    bool transform_to_display_inverse = 20;
+    RectProto crop = 21;
+
+    message BufferData {
+        uint64 buffer_id = 1;
+        uint32 width = 2;
+        uint32 height = 3;
+        uint64 frame_number = 4;
+
+        enum BufferDataChange {
+            BufferDataChangeNone = 0;
+            fenceChanged = 0x01;
+            frameNumberChanged = 0x02;
+            cachedBufferChanged = 0x04;
+        }
+        uint32 flags = 5;
+        uint64 cached_buffer_id = 6;
+
+        enum PixelFormat {
+            PIXEL_FORMAT_UNKNOWN = 0;
+            PIXEL_FORMAT_CUSTOM = -4;
+            PIXEL_FORMAT_TRANSLUCENT = -3;
+            PIXEL_FORMAT_TRANSPARENT = -2;
+            PIXEL_FORMAT_OPAQUE = -1;
+            PIXEL_FORMAT_RGBA_8888 = 1;
+            PIXEL_FORMAT_RGBX_8888 = 2;
+            PIXEL_FORMAT_RGB_888 = 3;
+            PIXEL_FORMAT_RGB_565 = 4;
+            PIXEL_FORMAT_BGRA_8888 = 5;
+            PIXEL_FORMAT_RGBA_5551 = 6;
+            PIXEL_FORMAT_RGBA_4444 = 7;
+            PIXEL_FORMAT_RGBA_FP16 = 22;
+            PIXEL_FORMAT_RGBA_1010102 = 43;
+            PIXEL_FORMAT_R_8 = 0x38;
+        }
+        PixelFormat pixel_format = 7;
+        uint64 usage = 8;
+    }
+    BufferData buffer_data = 22;
+    int32 api = 23;
+    bool has_sideband_stream = 24;
+    ColorTransformProto color_transform = 25;
+    repeated BlurRegion blur_regions = 26;
+
+    message WindowInfo {
+        uint32 layout_params_flags = 1;
+        int32 layout_params_type = 2;
+        RegionProto touchable_region = 3;
+        int32 surface_inset = 4;
+        bool focusable = 5;
+        bool has_wallpaper = 6;
+        float global_scale_factor = 7;
+        uint32 crop_layer_id = 8;
+        bool replace_touchable_region_with_crop = 9;
+        RectProto touchable_region_crop = 10;
+        Transform transform = 11;
+    }
+    WindowInfo window_info_handle = 27;
+    float bg_color_alpha = 28;
+    int32 bg_color_dataspace = 29;
+    bool color_space_agnostic = 30;
+    float shadow_radius = 31;
+    int32 frame_rate_selection_priority = 32;
+    float frame_rate = 33;
+    int32 frame_rate_compatibility = 34;
+    int32 change_frame_rate_strategy = 35;
+    uint32 fixed_transform_hint = 36;
+    uint64 frame_number = 37;
+    bool auto_refresh = 38;
+    bool is_trusted_overlay = 39;
+    RectProto buffer_crop = 40;
+    RectProto destination_frame = 41;
+
+    enum DropInputMode {
+        NONE = 0;
+        ALL = 1;
+        OBSCURED = 2;
+    };
+    DropInputMode drop_input_mode = 42;
+}
+
+message DisplayState {
+    enum Changes {
+        eChangesNone = 0;
+        eSurfaceChanged = 0x01;
+        eLayerStackChanged = 0x02;
+        eDisplayProjectionChanged = 0x04;
+        eDisplaySizeChanged = 0x08;
+        eFlagsChanged = 0x10;
+    };
+    int32 id = 1;
+    uint32 what = 2;
+    uint32 flags = 3;
+    uint32 layer_stack = 4;
+    uint32 orientation = 5;
+    RectProto layer_stack_space_rect = 6;
+    RectProto oriented_display_space_rect = 7;
+    uint32 width = 8;
+    uint32 height = 9;
+}
diff --git a/tools/winscope/protractor.config.js b/tools/winscope/protractor.config.js
index 875c38f..708f555 100644
--- a/tools/winscope/protractor.config.js
+++ b/tools/winscope/protractor.config.js
@@ -21,7 +21,7 @@
 // and change the hardcoded version here
 
 exports.config = {
-  specs: ['dist/e2e_test/e2e/*_test.js'],
+  specs: ['dist/e2e_test/*_test.js'],
 
   directConnect: true,
   capabilities: {
@@ -30,7 +30,7 @@
       args: ['--headless', '--disable-gpu', '--window-size=1280x1024'],
     },
   },
-  chromeDriver: './node_modules/webdriver-manager/selenium/chromedriver_114.0.5735.90',
+  chromeDriver: './deps_build/chromedriver-linux64/chromedriver',
 
   allScriptsTimeout: 10000,
   getPageTimeout: 10000,
diff --git a/tools/winscope/src/adb/winscope_proxy.py b/tools/winscope/src/adb/winscope_proxy.py
index 15f606c..e5addf0 100644
--- a/tools/winscope/src/adb/winscope_proxy.py
+++ b/tools/winscope/src/adb/winscope_proxy.py
@@ -47,7 +47,32 @@
 PORT = 5544
 
 # Keep in sync with ProxyClient#VERSION in Winscope
-VERSION = '1.0'
+VERSION = '1.1'
+
+PERFETTO_TRACE_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-trace.conf'
+PERFETTO_DUMP_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-dump.conf'
+PERFETTO_SF_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy.surfaceflinger.conf'
+PERFETTO_TRACE_FILE = '/data/misc/perfetto-traces/winscope-proxy-trace.perfetto-trace'
+PERFETTO_DUMP_FILE = '/data/misc/perfetto-traces/winscope-proxy-dump.perfetto-trace'
+PERFETTO_UTILS = """
+function is_perfetto_data_source_available {
+    local data_source_name=$1
+    if perfetto --query | grep $data_source_name 2>&1 >/dev/null; then
+        return 0
+    else
+        return 1
+    fi
+}
+
+function is_any_perfetto_data_source_available {
+    if is_perfetto_data_source_available android.surfaceflinger.layers || \
+       is_perfetto_data_source_available android.surfaceflinger.transactions; then
+        return 0
+    else
+        return 1
+    fi
+}
+"""
 
 WINSCOPE_VERSION_HEADER = "Winscope-Proxy-Version"
 WINSCOPE_TOKEN_HEADER = "Winscope-Token"
@@ -132,6 +157,7 @@
         self.trace_start = trace_start
         self.trace_stop = trace_stop
 
+
 # Order of files matters as they will be expected in that order and decoded in that order
 TRACE_TARGETS = {
     "window_trace": TraceTarget(
@@ -146,9 +172,22 @@
     ),
     "layers_trace": TraceTarget(
         WinscopeFileMatcher(WINSCOPE_DIR, "layers_trace", "layers_trace"),
-        'su root service call SurfaceFlinger 1025 i32 1\necho "SF trace started."',
-        'su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1'
-    ),
+        f"""
+if is_perfetto_data_source_available android.surfaceflinger.layers; then
+    cat {PERFETTO_SF_CONFIG_FILE} >> {PERFETTO_TRACE_CONFIG_FILE}
+    echo 'SF trace (perfetto) configured to start along the other perfetto traces'
+else
+    su root service call SurfaceFlinger 1025 i32 1
+    echo 'SF layers trace (legacy) started'
+fi
+        """,
+        """
+if ! is_perfetto_data_source_available android.surfaceflinger.layers; then
+    su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1
+    echo 'SF layers trace (legacy) stopped.'
+fi
+"""
+),
     "screen_recording": TraceTarget(
         File(f'/data/local/tmp/screen.mp4', "screen_recording"),
         f'screenrecord --bit-rate 8M /data/local/tmp/screen.mp4 >/dev/null 2>&1 &\necho "ScreenRecorder started."',
@@ -156,8 +195,29 @@
     ),
     "transactions": TraceTarget(
         WinscopeFileMatcher(WINSCOPE_DIR, "transactions_trace", "transactions"),
-        'su root service call SurfaceFlinger 1041 i32 1\necho "SF transactions recording started."',
-        'su root service call SurfaceFlinger 1041 i32 0 >/dev/null 2>&1'
+        f"""
+if is_perfetto_data_source_available android.surfaceflinger.transaction; then
+    cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
+data_sources: {{
+    config {{
+        name: "android.surfaceflinger.transactions"
+        surfaceflinger_transactions_config: {{
+            mode: MODE_ACTIVE
+        }}
+    }}
+}}
+EOF
+    echo 'SF transactions trace (perfetto) configured to start along the other perfetto traces'
+else
+    su root service call SurfaceFlinger 1041 i32 1
+    echo 'SF transactions trace (legacy) started'
+fi
+""",
+        """
+if ! is_perfetto_data_source_available android.surfaceflinger.transaction; then
+    su root service call SurfaceFlinger 1041 i32 0 >/dev/null 2>&1
+fi
+"""
     ),
     "transactions_legacy": TraceTarget(
         [
@@ -203,6 +263,33 @@
         'su root cmd window shell tracing start && su root dumpsys activity service SystemUIService WMShell transitions tracing start\necho "Transition traces started."',
         'su root cmd window shell tracing stop && su root dumpsys activity service SystemUIService WMShell transitions tracing stop >/dev/null 2>&1'
     ),
+    "perfetto_trace": TraceTarget(
+        File(PERFETTO_TRACE_FILE, "trace.perfetto-trace"),
+        f"""
+if is_any_perfetto_data_source_available; then
+    cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
+buffers: {{
+    size_kb: 100000
+    fill_policy: RING_BUFFER
+}}
+duration_ms: 0
+flush_period_ms: 1000
+write_into_file: true
+max_file_size_bytes: 1000000000
+EOF
+
+    rm -f {PERFETTO_TRACE_FILE}
+    perfetto --out {PERFETTO_TRACE_FILE} --txt --config {PERFETTO_TRACE_CONFIG_FILE} --detach=WINSCOPE-PROXY-TRACING-SESSION
+    echo 'Started perfetto trace'
+fi
+""",
+        """
+if is_any_perfetto_data_source_available; then
+    perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop
+    echo 'Stopped perfetto trace'
+fi
+""",
+    )
 }
 
 
@@ -211,16 +298,43 @@
     """
 
     def __init__(self) -> None:
-        self.flags = 0
+        self.flags = []
+        self.perfetto_flags = []
 
     def add(self, config: str) -> None:
-        self.flags |= CONFIG_FLAG[config]
+        self.flags.append(config)
 
     def is_valid(self, config: str) -> bool:
-        return config in CONFIG_FLAG
+        return config in SF_LEGACY_FLAGS_MAP
 
     def command(self) -> str:
-        return f'su root service call SurfaceFlinger 1033 i32 {self.flags}'
+        legacy_flags = 0
+        for flag in self.flags:
+            legacy_flags |= SF_LEGACY_FLAGS_MAP[flag]
+
+        perfetto_flags = "\n".join([f"""trace_flags: {SF_PERFETTO_FLAGS_MAP[flag]}""" for flag in self.flags])
+
+        return f"""
+{PERFETTO_UTILS}
+
+if is_perfetto_data_source_available android.surfaceflinger.layers; then
+    cat << EOF > {PERFETTO_SF_CONFIG_FILE}
+data_sources: {{
+    config {{
+        name: "android.surfaceflinger.layers"
+        surfaceflinger_layers_config: {{
+            mode: MODE_ACTIVE
+            {perfetto_flags}
+        }}
+    }}
+}}
+EOF
+    echo 'SF trace (perfetto) configured.'
+else
+    su root service call SurfaceFlinger 1033 i32 {legacy_flags}
+    echo 'SF trace (legacy) configured'
+fi
+"""
 
 class SurfaceFlingerTraceSelectedConfig:
     """Handles optional selected configuration for surfaceflinger traces.
@@ -269,7 +383,7 @@
         return f'su root cmd window tracing {self.selectedConfigs["tracingtype"]}'
 
 
-CONFIG_FLAG = {
+SF_LEGACY_FLAGS_MAP = {
     "input": 1 << 1,
     "composition": 1 << 2,
     "metadata": 1 << 3,
@@ -278,6 +392,15 @@
     "virtualdisplays": 1 << 6
 }
 
+SF_PERFETTO_FLAGS_MAP = {
+    "input": "TRACE_FLAG_INPUT",
+    "composition": "TRACE_FLAG_COMPOSITION",
+    "metadata": "TRACE_FLAG_EXTRA",
+    "hwc": "TRACE_FLAG_HWC",
+    "tracebuffers": "TRACE_FLAG_BUFFERS",
+    "virtualdisplays": "TRACE_FLAG_VIRTUAL_DISPLAYS",
+}
+
 #Keep up to date with options in DataAdb.vue
 CONFIG_SF_SELECTION = [
     "sfbuffersize",
@@ -310,9 +433,50 @@
         File(f'/data/local/tmp/wm_dump{WINSCOPE_EXT}', "window_dump"),
         f'su root dumpsys window --proto > /data/local/tmp/wm_dump{WINSCOPE_EXT}'
     ),
+
     "layers_dump": DumpTarget(
         File(f'/data/local/tmp/sf_dump{WINSCOPE_EXT}', "layers_dump"),
-        f'su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT}'
+        f"""
+if is_perfetto_data_source_available android.surfaceflinger.layers; then
+    cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE}
+data_sources: {{
+    config {{
+        name: "android.surfaceflinger.layers"
+        surfaceflinger_layers_config: {{
+            mode: MODE_DUMP
+            trace_flags: TRACE_FLAG_INPUT
+            trace_flags: TRACE_FLAG_COMPOSITION
+            trace_flags: TRACE_FLAG_HWC
+            trace_flags: TRACE_FLAG_BUFFERS
+            trace_flags: TRACE_FLAG_VIRTUAL_DISPLAYS
+        }}
+    }}
+}}
+EOF
+    echo 'SF transactions trace (perfetto) configured to start along the other perfetto traces'
+else
+    su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT}
+fi
+"""
+    ),
+
+    "perfetto_dump": DumpTarget(
+        File(PERFETTO_DUMP_FILE, "dump.perfetto-trace"),
+        f"""
+if is_any_perfetto_data_source_available; then
+    cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE}
+buffers: {{
+    size_kb: 100000
+    fill_policy: RING_BUFFER
+}}
+duration_ms: 1
+EOF
+
+    rm -f {PERFETTO_DUMP_FILE}
+    perfetto --out {PERFETTO_DUMP_FILE} --txt --config {PERFETTO_DUMP_CONFIG_FILE}
+    echo 'Recorded perfetto dump'
+fi
+        """
     )
 }
 
@@ -516,6 +680,18 @@
             raise BadRequest("Content length unreadable\n" + str(err))
         return json.loads(server.rfile.read(length).decode("utf-8"))
 
+    def move_perfetto_target_to_end_of_list(self, targets):
+        # Make sure a perfetto target (if present) comes last in the list of targets, i.e. will
+        # be processed last.
+        # A perfetto target must be processed last, so that perfetto tracing is started only after
+        # the other targets have been processed and have configured the perfetto config file.
+        def is_perfetto_target(target):
+            return target == TRACE_TARGETS["perfetto_trace"] or target == DUMP_TARGETS["perfetto_dump"]
+        non_perfetto_targets = [t for t in targets if not is_perfetto_target(t)]
+        perfetto_targets = [t for t in targets if is_perfetto_target(t)]
+        return non_perfetto_targets + perfetto_targets
+
+
 
 class FetchFilesEndpoint(DeviceRequestEndpoint):
     def process_with_device(self, server, path, device_id):
@@ -541,7 +717,7 @@
                     call_adb_outfile('exec-out su root cat ' +
                                      file_path, tmp, device_id)
                     log.debug(f"Deleting file {file_path} from device")
-                    call_adb('shell su root rm ' + file_path, device_id)
+                    call_adb('shell su root rm -f ' + file_path, device_id)
                     log.debug(f"Uploading file {tmp.name}")
                     if file_type not in file_buffers:
                         file_buffers[file_type] = []
@@ -642,6 +818,8 @@
     TRACE_COMMAND = """
 set -e
 
+{perfetto_utils}
+
 echo "Starting trace..."
 echo "TRACE_START" > /data/local/tmp/winscope_status
 
@@ -655,14 +833,17 @@
 
   set -x
   trap - EXIT HUP INT
-  {}
+  {stop_commands}
   echo "TRACE_OK" > /data/local/tmp/winscope_status
 }}
 
 trap stop_trace EXIT HUP INT
 echo "Signal handler registered."
 
-{}
+# Clear perfetto config file. The start commands below are going to populate it.
+rm -f {perfetto_config_file}
+
+{start_commands}
 
 # ADB shell does not handle hung up well and does not call HUP handler when a child is active in foreground,
 # as a workaround we sleep for short intervals in a loop so the handler is called after a sleep interval.
@@ -674,6 +855,7 @@
             requested_types = self.get_request(server)
             log.debug(f"Clienting requested trace types {requested_types}")
             requested_traces = [TRACE_TARGETS[t] for t in requested_types]
+            requested_traces = self.move_perfetto_target_to_end_of_list(requested_traces)
         except KeyError as err:
             raise BadRequest("Unsupported trace target\n" + str(err))
         if device_id in TRACE_THREADS:
@@ -684,8 +866,10 @@
                 "Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'".format(
                     device_id))
         command = StartTrace.TRACE_COMMAND.format(
-            '\n'.join([t.trace_stop for t in requested_traces]),
-            '\n'.join([t.trace_start for t in requested_traces]))
+            perfetto_utils=PERFETTO_UTILS,
+            stop_commands='\n'.join([t.trace_stop for t in requested_traces]),
+            perfetto_config_file=PERFETTO_TRACE_CONFIG_FILE,
+            start_commands='\n'.join([t.trace_start for t in requested_traces]))
         log.debug("Trace requested for {} with targets {}".format(
             device_id, ','.join(requested_types)))
         log.debug(f"Executing command \"{command}\" on {device_id}...")
@@ -799,9 +983,11 @@
         setTracingLevel = config.setTracingLevel()
         shell = ['adb', '-s', device_id, 'shell']
         log.debug(f"Starting shell {' '.join(shell)}")
-        execute_command(server, device_id, shell, "wm buffer size", setBufferSize)
         execute_command(server, device_id, shell, "tracing type", setTracingType)
         execute_command(server, device_id, shell, "tracing level", setTracingLevel)
+        # /!\ buffer size must be configured last
+        # otherwise the other configurations might override it
+        execute_command(server, device_id, shell, "wm buffer size", setBufferSize)
 
 
 class StatusEndpoint(DeviceRequestEndpoint):
@@ -818,6 +1004,7 @@
         try:
             requested_types = self.get_request(server)
             requested_traces = [DUMP_TARGETS[t] for t in requested_types]
+            requested_traces = self.move_perfetto_target_to_end_of_list(requested_traces)
         except KeyError as err:
             raise BadRequest("Unsupported trace target\n" + str(err))
         if device_id in TRACE_THREADS:
@@ -826,7 +1013,15 @@
             raise AdbError(
                 "Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'"
                 .format(device_id))
-        command = '\n'.join(t.dump_command for t in requested_traces)
+        dump_commands = '\n'.join(t.dump_command for t in requested_traces)
+        command = f"""
+{PERFETTO_UTILS}
+
+# Clear perfetto config file. The commands below are going to populate it.
+rm -f {PERFETTO_DUMP_CONFIG_FILE}
+
+{dump_commands}
+"""
         shell = ['adb', '-s', device_id, 'shell']
         log.debug("Starting dump shell {}".format(' '.join(shell)))
         process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
diff --git a/tools/winscope/src/app/app_event.ts b/tools/winscope/src/app/app_event.ts
new file mode 100644
index 0000000..97a68ea
--- /dev/null
+++ b/tools/winscope/src/app/app_event.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 {Timestamp} from 'common/time';
+import {TraceEntry} from 'trace/trace';
+import {TracePosition} from 'trace/trace_position';
+import {TraceType} from 'trace/trace_type';
+import {View} from 'viewers/viewer';
+
+export enum AppEventType {
+  TABBED_VIEW_SWITCHED = 'TABBED_VIEW_SWITCHED',
+  TABBED_VIEW_SWITCH_REQUEST = 'TABBED_VIEW_SWITCH_REQUEST',
+  TRACE_POSITION_UPDATE = 'TRACE_POSITION_UPDATE',
+}
+
+interface TypeMap {
+  [AppEventType.TABBED_VIEW_SWITCHED]: TabbedViewSwitched;
+  [AppEventType.TABBED_VIEW_SWITCH_REQUEST]: TabbedViewSwitchRequest;
+  [AppEventType.TRACE_POSITION_UPDATE]: TracePositionUpdate;
+}
+
+export abstract class AppEvent {
+  abstract readonly type: AppEventType;
+
+  async visit<T extends AppEventType>(type: T, callback: (event: TypeMap[T]) => Promise<void>) {
+    if (this.type === type) {
+      const event = this as unknown as TypeMap[T];
+      await callback(event);
+    }
+  }
+}
+
+export class TabbedViewSwitched extends AppEvent {
+  override readonly type = AppEventType.TABBED_VIEW_SWITCHED;
+  readonly newFocusedView: View;
+
+  constructor(view: View) {
+    super();
+    this.newFocusedView = view;
+  }
+}
+
+export class TabbedViewSwitchRequest extends AppEvent {
+  override readonly type = AppEventType.TABBED_VIEW_SWITCH_REQUEST;
+
+  //TODO(b/263779536): use proper view/viewer ID, instead of abusing trace type.
+  readonly newFocusedViewId: TraceType;
+
+  constructor(newFocusedViewId: TraceType) {
+    super();
+    this.newFocusedViewId = newFocusedViewId;
+  }
+}
+
+export class TracePositionUpdate extends AppEvent {
+  override readonly type = AppEventType.TRACE_POSITION_UPDATE;
+  readonly position: TracePosition;
+
+  constructor(position: TracePosition) {
+    super();
+    this.position = position;
+  }
+
+  static fromTimestamp(timestamp: Timestamp): TracePositionUpdate {
+    const position = TracePosition.fromTimestamp(timestamp);
+    return new TracePositionUpdate(position);
+  }
+
+  static fromTraceEntry(entry: TraceEntry<object>): TracePositionUpdate {
+    const position = TracePosition.fromTraceEntry(entry);
+    return new TracePositionUpdate(position);
+  }
+}
diff --git a/tools/winscope/src/app/app_module.ts b/tools/winscope/src/app/app_module.ts
index 93957ad..3246834 100644
--- a/tools/winscope/src/app/app_module.ts
+++ b/tools/winscope/src/app/app_module.ts
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+import {ClipboardModule} from '@angular/cdk/clipboard';
 import {DragDropModule} from '@angular/cdk/drag-drop';
 import {ScrollingModule} from '@angular/cdk/scrolling';
 import {CommonModule} from '@angular/common';
@@ -45,14 +46,15 @@
 import {ImeAdditionalPropertiesComponent} from 'viewers/components/ime_additional_properties_component';
 import {PropertiesComponent} from 'viewers/components/properties_component';
 import {PropertiesTableComponent} from 'viewers/components/properties_table_component';
-import {PropertyGroupsComponent} from 'viewers/components/property_groups_component';
 import {RectsComponent} from 'viewers/components/rects/rects_component';
+import {SurfaceFlingerPropertyGroupsComponent} from 'viewers/components/surface_flinger_property_groups_component';
 import {TransformMatrixComponent} from 'viewers/components/transform_matrix_component';
 import {TreeComponent} from 'viewers/components/tree_component';
 import {TreeNodeComponent} from 'viewers/components/tree_node_component';
 import {TreeNodeDataViewComponent} from 'viewers/components/tree_node_data_view_component';
 import {TreeNodePropertiesDataViewComponent} from 'viewers/components/tree_node_properties_data_view_component';
 import {ViewerInputMethodComponent} from 'viewers/components/viewer_input_method_component';
+import {ViewCapturePropertyGroupsComponent} from 'viewers/components/view_capture_property_groups_component';
 import {ViewerProtologComponent} from 'viewers/viewer_protolog/viewer_protolog_component';
 import {ViewerScreenRecordingComponent} from 'viewers/viewer_screen_recording/viewer_screen_recording_component';
 import {ViewerSurfaceFlingerComponent} from 'viewers/viewer_surface_flinger/viewer_surface_flinger_component';
@@ -70,9 +72,11 @@
 import {CollectTracesComponent} from './components/collect_traces_component';
 import {LoadProgressComponent} from './components/load_progress_component';
 import {SnackBarComponent} from './components/snack_bar_component';
-import {ExpandedTimelineComponent} from './components/timeline/expanded_timeline_component';
-import {MiniTimelineComponent} from './components/timeline/mini_timeline_component';
-import {SingleTimelineComponent} from './components/timeline/single_timeline_component';
+import {DefaultTimelineRowComponent} from './components/timeline/expanded-timeline/default_timeline_row_component';
+import {ExpandedTimelineComponent} from './components/timeline/expanded-timeline/expanded_timeline_component';
+import {TransitionTimelineComponent} from './components/timeline/expanded-timeline/transition_timeline_component';
+import {MiniTimelineComponent} from './components/timeline/mini-timeline/mini_timeline_component';
+import {SliderComponent} from './components/timeline/mini-timeline/slider_component';
 import {TimelineComponent} from './components/timeline/timeline_component';
 import {TraceConfigComponent} from './components/trace_config_component';
 import {TraceViewComponent} from './components/trace_view_component';
@@ -103,7 +107,7 @@
     TreeNodeComponent,
     TreeNodeDataViewComponent,
     TreeNodePropertiesDataViewComponent,
-    PropertyGroupsComponent,
+    SurfaceFlingerPropertyGroupsComponent,
     TransformMatrixComponent,
     PropertiesTableComponent,
     ImeAdditionalPropertiesComponent,
@@ -111,12 +115,15 @@
     TimelineComponent,
     MiniTimelineComponent,
     ExpandedTimelineComponent,
-    SingleTimelineComponent,
+    DefaultTimelineRowComponent,
+    TransitionTimelineComponent,
     SnackBarComponent,
     MatDrawer,
     MatDrawerContent,
     MatDrawerContainer,
     LoadProgressComponent,
+    SliderComponent,
+    ViewCapturePropertyGroupsComponent,
   ],
   imports: [
     BrowserModule,
@@ -145,6 +152,7 @@
     MatSnackBarModule,
     ScrollingModule,
     DragDropModule,
+    ClipboardModule,
     ReactiveFormsModule,
   ],
   schemas: [CUSTOM_ELEMENTS_SCHEMA],
diff --git a/tools/winscope/src/app/components/adb_proxy_component.ts b/tools/winscope/src/app/components/adb_proxy_component.ts
index 3f0c0a3..9e900be 100644
--- a/tools/winscope/src/app/components/adb_proxy_component.ts
+++ b/tools/winscope/src/app/components/adb_proxy_component.ts
@@ -25,13 +25,17 @@
           <p class="mat-body-1">
             Launch the Winscope ADB Connect proxy to capture traces directly from your browser.
           </p>
-          <p class="mat-body-1">Python 3.5+ and ADB are required.</p>
-          <p class="mat-body-1">
-            Run:
-            <code>
-              python3 $ANDROID_BUILD_TOP/development/tools/winscope/src/adb/winscope_proxy.py
-            </code>
-          </p>
+          <p class="mat-body-1">Python 3.5+ and ADB are required. Run this command:</p>
+          <mat-form-field class="proxy-command-form" appearance="outline">
+            <input matInput readonly [value]="proxyCommand" />
+            <button
+              mat-icon-button
+              matSuffix
+              [cdkCopyToClipboard]="proxyCommand"
+              matTooltip="Copy command">
+              <mat-icon>content_copy</mat-icon>
+            </button>
+          </mat-form-field>
           <p class="mat-body-1">Or get it from the AOSP repository.</p>
         </div>
 
@@ -51,13 +55,19 @@
             <mat-icon class="adb-icon">update</mat-icon>
             <span class="adb-info">Your local proxy version is incompatible with Winscope.</span>
           </p>
-          <p class="mat-body-1">Please update the proxy to version {{ proxyVersion }}.</p>
           <p class="mat-body-1">
-            Run:
-            <code>
-              python3 $ANDROID_BUILD_TOP/development/tools/winscope/src/adb/winscope_proxy.py
-            </code>
+            Please update the proxy to version {{ proxyVersion }}. Run this command:
           </p>
+          <mat-form-field class="proxy-command-container" appearance="outline">
+            <input matInput readonly [value]="proxyCommand" />
+            <button
+              mat-icon-button
+              matSuffix
+              [cdkCopyToClipboard]="proxyCommand"
+              matTooltip="Copy command">
+              <mat-icon>content_copy</mat-icon>
+            </button>
+          </mat-form-field>
           <p class="mat-body-1">Or get it from the AOSP repository.</p>
         </div>
 
@@ -116,6 +126,14 @@
         flex-wrap: wrap;
         gap: 10px;
       }
+      /* TODO(b/300063426): remove after migration to angular 15, replace with subscriptSizing */
+      ::ng-deep .proxy-command-form .mat-form-field-wrapper {
+        padding: 0;
+      }
+      .proxy-command-text {
+        user-select: all;
+        overflow: auto;
+      }
       .adb-info {
         margin-left: 5px;
       }
@@ -137,6 +155,8 @@
   readonly proxyVersion = this.proxy.VERSION;
   readonly downloadProxyUrl: string =
     'https://android.googlesource.com/platform/development/+/master/tools/winscope/adb_proxy/winscope_proxy.py';
+  readonly proxyCommand: string =
+    'python3 $ANDROID_BUILD_TOP/development/tools/winscope/src/adb/winscope_proxy.py';
 
   restart() {
     this.addKey.emit(this.proxyKeyItem);
diff --git a/tools/winscope/src/app/components/app_component.ts b/tools/winscope/src/app/components/app_component.ts
index deb1112..88000b8 100644
--- a/tools/winscope/src/app/components/app_component.ts
+++ b/tools/winscope/src/app/components/app_component.ts
@@ -24,15 +24,17 @@
 } from '@angular/core';
 import {createCustomElement} from '@angular/elements';
 import {AbtChromeExtensionProtocol} from 'abt_chrome_extension/abt_chrome_extension_protocol';
+import {AppEvent, AppEventType} from 'app/app_event';
 import {Mediator} from 'app/mediator';
 import {TimelineData} from 'app/timeline_data';
 import {TRACE_INFO} from 'app/trace_info';
 import {TracePipeline} from 'app/trace_pipeline';
-import {FileUtils} from 'common/file_utils';
 import {PersistentStore} from 'common/persistent_store';
+import {Timestamp} from 'common/time';
 import {CrossToolProtocol} from 'cross_tool/cross_tool_protocol';
+import {CrossPlatform, NoCache} from 'flickerlib/common';
+import {AppEventListener} from 'interfaces/app_event_listener';
 import {TraceDataListener} from 'interfaces/trace_data_listener';
-import {Timestamp} from 'trace/timestamp';
 import {Trace} from 'trace/trace';
 import {TraceType} from 'trace/trace_type';
 import {proxyClient, ProxyState} from 'trace_collection/proxy_client';
@@ -48,6 +50,7 @@
 import {CollectTracesComponent} from './collect_traces_component';
 import {SnackBarOpener} from './snack_bar_opener';
 import {TimelineComponent} from './timeline/timeline_component';
+import {TraceViewComponent} from './trace_view_component';
 import {UploadTracesComponent} from './upload_traces_component';
 
 @Component({
@@ -60,7 +63,7 @@
         <button color="primary" mat-button>Open legacy Winscope</button>
       </a>
 
-      <div class="spacer">
+      <div class="trace-descriptor">
         <mat-icon
           *ngIf="dataLoaded && activeTrace"
           class="icon"
@@ -100,15 +103,14 @@
 
     <mat-divider></mat-divider>
 
-    <mat-drawer-container class="example-container" autosize disableClose autoFocus>
+    <mat-drawer-container autosize disableClose autoFocus>
       <mat-drawer-content>
         <ng-container *ngIf="dataLoaded; else noLoadedTracesBlock">
           <trace-view
             class="viewers"
             [viewers]="viewers"
             [store]="store"
-            (downloadTracesButtonClick)="onDownloadTracesButtonClick()"
-            (activeViewChanged)="onActiveViewChanged($event)"></trace-view>
+            (downloadTracesButtonClick)="onDownloadTracesButtonClick()"></trace-view>
 
           <mat-divider></mat-divider>
         </ng-container>
@@ -163,12 +165,20 @@
         overflow: auto;
         height: 820px;
       }
-      .spacer {
+      .trace-descriptor {
         flex: 1;
         text-align: center;
         display: flex;
         align-items: center;
         justify-content: center;
+        overflow-x: hidden;
+      }
+      .trace-descriptor .icon {
+        overflow: unset;
+      }
+      .trace-descriptor .active-trace-file-info {
+        text-overflow: ellipsis;
+        overflow-x: hidden;
       }
       .viewers {
         height: 0;
@@ -198,11 +208,11 @@
   ],
   encapsulation: ViewEncapsulation.None,
 })
-export class AppComponent implements TraceDataListener {
+export class AppComponent implements AppEventListener, TraceDataListener {
   title = 'winscope';
   changeDetectorRef: ChangeDetectorRef;
   snackbarOpener: SnackBarOpener;
-  tracePipeline = new TracePipeline();
+  tracePipeline: TracePipeline;
   timelineData = new TimelineData();
   abtChromeExtensionProtocol = new AbtChromeExtensionProtocol();
   crossToolProtocol = new CrossToolProtocol();
@@ -219,6 +229,7 @@
   collapsedTimelineHeight = 0;
   @ViewChild(UploadTracesComponent) uploadTracesComponent?: UploadTracesComponent;
   @ViewChild(CollectTracesComponent) collectTracesComponent?: UploadTracesComponent;
+  @ViewChild(TraceViewComponent) traceViewComponent?: TraceViewComponent;
   @ViewChild(TimelineComponent) timelineComponent?: TimelineComponent;
   TRACE_INFO = TRACE_INFO;
 
@@ -227,8 +238,11 @@
     @Inject(ChangeDetectorRef) changeDetectorRef: ChangeDetectorRef,
     @Inject(SnackBarOpener) snackBar: SnackBarOpener
   ) {
+    CrossPlatform.setCache(new NoCache());
+
     this.changeDetectorRef = changeDetectorRef;
     this.snackbarOpener = snackBar;
+    this.tracePipeline = new TracePipeline(this.snackbarOpener);
     this.mediator = new Mediator(
       this.tracePipeline,
       this.timelineData,
@@ -300,6 +314,7 @@
   ngAfterViewChecked() {
     this.mediator.setUploadTracesComponent(this.uploadTracesComponent);
     this.mediator.setCollectTracesComponent(this.collectTracesComponent);
+    this.mediator.setTraceViewComponent(this.traceViewComponent);
     this.mediator.setTimelineComponent(this.timelineComponent);
   }
 
@@ -331,25 +346,25 @@
   }
 
   async onDownloadTracesButtonClick() {
-    const traceFiles = await this.makeTraceFilesForDownload();
-    const zipFileBlob = await FileUtils.createZipArchive(traceFiles);
-    const zipFileName = 'winscope.zip';
+    const archiveBlob = await this.tracePipeline.makeZipArchiveWithLoadedTraceFiles();
+    const archiveFilename = 'winscope.zip';
 
     const a = document.createElement('a');
     document.body.appendChild(a);
-    const url = window.URL.createObjectURL(zipFileBlob);
+    const url = window.URL.createObjectURL(archiveBlob);
     a.href = url;
-    a.download = zipFileName;
+    a.download = archiveFilename;
     a.click();
     window.URL.revokeObjectURL(url);
     document.body.removeChild(a);
   }
 
-  async onActiveViewChanged(view: View) {
-    this.activeView = view;
-    this.activeTrace = this.getActiveTrace(view);
-    this.activeTraceFileInfo = this.makeActiveTraceFileInfo(view);
-    await this.mediator.onWinscopeActiveViewChanged(view);
+  async onAppEvent(event: AppEvent) {
+    await event.visit(AppEventType.TABBED_VIEW_SWITCHED, async (event) => {
+      this.activeView = event.newFocusedView;
+      this.activeTrace = this.getActiveTrace(event.newFocusedView);
+      this.activeTraceFileInfo = this.makeActiveTraceFileInfo(event.newFocusedView);
+    });
   }
 
   goToLink(url: string) {
@@ -375,15 +390,4 @@
     });
     return activeTrace;
   }
-
-  private async makeTraceFilesForDownload(): Promise<File[]> {
-    const loadedFiles = this.tracePipeline.getLoadedFiles();
-    return [...loadedFiles.keys()].map((traceType) => {
-      const file = loadedFiles.get(traceType)!;
-      const path = TRACE_INFO[traceType].downloadArchiveDir;
-
-      const newName = path + '/' + FileUtils.removeDirFromFileName(file.file.name);
-      return new File([file.file], newName);
-    });
-  }
 }
diff --git a/tools/winscope/src/app/components/app_component_stub.ts b/tools/winscope/src/app/components/app_component_stub.ts
index 2e84e8d..5b91211 100644
--- a/tools/winscope/src/app/components/app_component_stub.ts
+++ b/tools/winscope/src/app/components/app_component_stub.ts
@@ -14,10 +14,16 @@
  * limitations under the License.
  */
 
+import {AppEvent} from 'app/app_event';
+import {AppEventListener} from 'interfaces/app_event_listener';
 import {TraceDataListener} from 'interfaces/trace_data_listener';
 import {Viewer} from 'viewers/viewer';
 
-export class AppComponentStub implements TraceDataListener {
+export class AppComponentStub implements AppEventListener, TraceDataListener {
+  async onAppEvent(event: AppEvent) {
+    // do nothing
+  }
+
   onTraceDataLoaded(viewers: Viewer[]) {
     // do nothing
   }
diff --git a/tools/winscope/src/app/components/app_component_test.ts b/tools/winscope/src/app/components/app_component_test.ts
index 937bf1c..7d09176 100644
--- a/tools/winscope/src/app/components/app_component_test.ts
+++ b/tools/winscope/src/app/components/app_component_test.ts
@@ -33,7 +33,7 @@
 import {AppComponent} from './app_component';
 import {MatDrawer, MatDrawerContainer, MatDrawerContent} from './bottomnav/bottom_drawer_component';
 import {CollectTracesComponent} from './collect_traces_component';
-import {MiniTimelineComponent} from './timeline/mini_timeline_component';
+import {MiniTimelineComponent} from './timeline/mini-timeline/mini_timeline_component';
 import {TimelineComponent} from './timeline/timeline_component';
 import {TraceConfigComponent} from './trace_config_component';
 import {TraceViewComponent} from './trace_view_component';
diff --git a/tools/winscope/src/app/components/collect_traces_component.ts b/tools/winscope/src/app/components/collect_traces_component.ts
index edcc2e4..3ef6370 100644
--- a/tools/winscope/src/app/components/collect_traces_component.ts
+++ b/tools/winscope/src/app/components/collect_traces_component.ts
@@ -482,7 +482,7 @@
   private requestedTraces() {
     const tracesFromCollection: string[] = [];
     const tracingConfig = this.tracingConfig.getTraceConfig();
-    const req = Object.keys(tracingConfig).filter((traceKey: string) => {
+    const requested = Object.keys(tracingConfig).filter((traceKey: string) => {
       const traceConfig = tracingConfig[traceKey];
       if (traceConfig.isTraceCollection) {
         traceConfig.config?.enableConfigs.forEach((innerTrace: EnableConfiguration) => {
@@ -494,14 +494,18 @@
       }
       return traceConfig.run;
     });
-    return req.concat(tracesFromCollection);
+    requested.push(...tracesFromCollection);
+    requested.push('perfetto_trace'); // always start/stop/fetch perfetto trace
+    return requested;
   }
 
   private requestedDumps() {
     const dumpConfig = this.tracingConfig.getDumpConfig();
-    return Object.keys(dumpConfig).filter((dumpKey: string) => {
+    const requested = Object.keys(dumpConfig).filter((dumpKey: string) => {
       return dumpConfig[dumpKey].run;
     });
+    requested.push('perfetto_dump'); // always dump/fetch perfetto dump
+    return requested;
   }
 
   private requestedEnableConfig(): string[] {
diff --git a/tools/winscope/src/app/components/snack_bar_opener.ts b/tools/winscope/src/app/components/snack_bar_opener.ts
index d9c35c6..40feb6f 100644
--- a/tools/winscope/src/app/components/snack_bar_opener.ts
+++ b/tools/winscope/src/app/components/snack_bar_opener.ts
@@ -68,16 +68,18 @@
 
   private convertErrorToMessage(error: ParserError): string {
     const fileName = error.trace !== undefined ? error.trace : '<no file name>';
-    const traceTypeName =
-      error.traceType !== undefined ? TRACE_INFO[error.traceType].name : '<unknown>';
+    const traceTypeInfo =
+      error.traceType !== undefined ? ` of type ${TRACE_INFO[error.traceType].name}` : '';
 
     switch (error.type) {
+      case ParserErrorType.CORRUPTED_ARCHIVE:
+        return `${fileName}: corrupted archive`;
       case ParserErrorType.NO_INPUT_FILES:
-        return 'No input files';
+        return `Input doesn't contain trace files`;
       case ParserErrorType.UNSUPPORTED_FORMAT:
         return `${fileName}: unsupported file format`;
       case ParserErrorType.OVERRIDE: {
-        return `${fileName}: overridden by another trace of type ${traceTypeName}`;
+        return `${fileName}: overridden by another trace${traceTypeInfo}`;
       }
       default:
         return `${fileName}: unknown error occurred`;
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/abstract_timeline_row_component.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/abstract_timeline_row_component.ts
new file mode 100644
index 0000000..1172559
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/abstract_timeline_row_component.ts
@@ -0,0 +1,143 @@
+/*
+ * 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 {ElementRef, EventEmitter, SimpleChanges} from '@angular/core';
+import {TraceEntry} from 'trace/trace';
+import {TracePosition} from 'trace/trace_position';
+import {CanvasDrawer} from './canvas_drawer';
+
+export abstract class AbstractTimelineRowComponent<T extends {}> {
+  abstract selectedEntry: TraceEntry<T> | undefined;
+  abstract onTracePositionUpdate: EventEmitter<TracePosition>;
+  abstract wrapperRef: ElementRef;
+  abstract canvasRef: ElementRef;
+
+  canvasDrawer: CanvasDrawer = new CanvasDrawer();
+
+  getCanvas(): HTMLCanvasElement {
+    return this.canvasRef.nativeElement;
+  }
+
+  private _observer = new ResizeObserver(() => this.initializeCanvas());
+  async ngAfterViewInit() {
+    this._observer.observe(this.wrapperRef.nativeElement);
+    await this.initializeCanvas();
+  }
+
+  ngOnDestroy() {
+    this._observer.disconnect();
+  }
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (this.viewInitialized) {
+      this.redraw();
+    }
+  }
+
+  protected viewInitialized = false;
+
+  async initializeCanvas() {
+    const canvas = this.getCanvas();
+
+    // Reset any size before computing new size to avoid it interfering with size computations
+    canvas.width = 0;
+    canvas.height = 0;
+    canvas.style.width = 'auto';
+    canvas.style.height = 'auto';
+
+    const computedStyle = getComputedStyle(this.wrapperRef.nativeElement);
+    const width = this.wrapperRef.nativeElement.offsetWidth;
+    const height =
+      this.wrapperRef.nativeElement.offsetHeight -
+      // tslint:disable-next-line:ban
+      parseFloat(computedStyle.paddingTop) -
+      // tslint:disable-next-line:ban
+      parseFloat(computedStyle.paddingBottom);
+
+    const HiPPIwidth = window.devicePixelRatio * width;
+    const HiPPIheight = window.devicePixelRatio * height;
+
+    canvas.width = HiPPIwidth;
+    canvas.height = HiPPIheight;
+    canvas.style.width = width + 'px';
+    canvas.style.height = height + 'px';
+
+    // ensure all drawing operations are scaled
+    if (window.devicePixelRatio !== 1) {
+      const context = canvas.getContext('2d')!;
+      context.scale(window.devicePixelRatio, window.devicePixelRatio);
+    }
+
+    this.canvasDrawer.setCanvas(this.getCanvas());
+    await this.redraw();
+
+    canvas.addEventListener('mousemove', (event: MouseEvent) => {
+      this.handleMouseMove(event);
+    });
+    canvas.addEventListener('mousedown', (event: MouseEvent) => {
+      this.handleMouseDown(event);
+    });
+    canvas.addEventListener('mouseout', (event: MouseEvent) => {
+      this.handleMouseOut(event);
+    });
+
+    this.viewInitialized = true;
+  }
+
+  async handleMouseDown(e: MouseEvent) {
+    e.preventDefault();
+    e.stopPropagation();
+    const mouseX = e.offsetX * this.canvasDrawer.getXScale();
+    const mouseY = e.offsetY * this.canvasDrawer.getYScale();
+
+    const transitionEntry = await this.getEntryAt(mouseX, mouseY);
+    // TODO: This can probably get made better by getting the transition and checking both the end and start timestamps match
+    if (transitionEntry && transitionEntry !== this.selectedEntry) {
+      this.redraw();
+      this.selectedEntry = transitionEntry;
+      this.onTracePositionUpdate.emit(TracePosition.fromTraceEntry(transitionEntry));
+    }
+  }
+
+  handleMouseMove(e: MouseEvent) {
+    e.preventDefault();
+    e.stopPropagation();
+    const mouseX = e.offsetX * this.canvasDrawer.getXScale();
+    const mouseY = e.offsetY * this.canvasDrawer.getYScale();
+
+    this.updateCursor(mouseX, mouseY);
+    this.onHover(mouseX, mouseY);
+  }
+
+  protected async updateCursor(mouseX: number, mouseY: number) {
+    if (this.getEntryAt(mouseX, mouseY) !== undefined) {
+      this.getCanvas().style.cursor = 'pointer';
+    } else {
+      this.getCanvas().style.cursor = 'auto';
+    }
+  }
+
+  protected abstract getEntryAt(mouseX: number, mouseY: number): Promise<TraceEntry<T> | undefined>;
+  protected abstract onHover(mouseX: number, mouseY: number): void;
+  protected abstract handleMouseOut(e: MouseEvent): void;
+
+  protected async redraw() {
+    this.canvasDrawer.clear();
+    await this.drawTimeline();
+  }
+
+  abstract drawTimeline(): Promise<void>;
+}
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer.ts
new file mode 100644
index 0000000..7100d43
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer.ts
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+export class CanvasDrawer {
+  private canvas!: HTMLCanvasElement;
+  private ctx!: CanvasRenderingContext2D;
+
+  setCanvas(canvas: HTMLCanvasElement) {
+    this.canvas = canvas;
+    const ctx = this.canvas.getContext('2d');
+    if (ctx === null) {
+      throw new Error("Couldn't get context from canvas");
+    }
+    this.ctx = ctx;
+  }
+
+  drawRect(drawParams: {x: number; y: number; w: number; h: number; color: string; alpha: number}) {
+    const {x, y, w, h, color, alpha} = drawParams;
+    const rgbColor = this.hexToRgb(color);
+    if (rgbColor === undefined) {
+      throw new Error('Failed to parse provided hex color');
+    }
+    const {r, g, b} = rgbColor;
+
+    this.defineRectPath(x, y, w, h);
+    this.ctx.fillStyle = `rgba(${r},${g},${b},${alpha})`;
+    this.ctx.fill();
+
+    this.ctx.restore();
+  }
+
+  drawRectBorder(x: number, y: number, w: number, h: number) {
+    this.defineRectPath(x, y, w, h);
+    this.highlightPath();
+    this.ctx.restore();
+  }
+
+  clear() {
+    this.ctx.clearRect(0, 0, this.getScaledCanvasWidth(), this.getScaledCanvasHeight());
+  }
+
+  getScaledCanvasWidth() {
+    return Math.floor(this.canvas.width / this.getXScale());
+  }
+
+  getScaledCanvasHeight() {
+    return Math.floor(this.canvas.height / this.getYScale());
+  }
+
+  getXScale(): number {
+    return this.ctx.getTransform().m11;
+  }
+
+  getYScale(): number {
+    return this.ctx.getTransform().m22;
+  }
+
+  private highlightPath() {
+    this.ctx.globalAlpha = 1.0;
+    this.ctx.lineWidth = 2;
+    this.ctx.save();
+    this.ctx.clip();
+    this.ctx.lineWidth *= 2;
+    this.ctx.stroke();
+    this.ctx.restore();
+    this.ctx.stroke();
+  }
+
+  private defineRectPath(x: number, y: number, w: number, h: number) {
+    this.ctx.beginPath();
+    this.ctx.moveTo(x, y);
+    this.ctx.lineTo(x + w, y);
+    this.ctx.lineTo(x + w, y + h);
+    this.ctx.lineTo(x, y + h);
+    this.ctx.lineTo(x, y);
+    this.ctx.closePath();
+  }
+
+  private hexToRgb(hex: string): {r: number; g: number; b: number} | undefined {
+    // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
+    const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
+    hex = hex.replace(shorthandRegex, (m, r, g, b) => {
+      return r + r + g + g + b + b;
+    });
+
+    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+    return result
+      ? {
+          // tslint:disable-next-line:ban
+          r: parseInt(result[1], 16),
+          // tslint:disable-next-line:ban
+          g: parseInt(result[2], 16),
+          // tslint:disable-next-line:ban
+          b: parseInt(result[3], 16),
+        }
+      : undefined;
+  }
+}
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer_test.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer_test.ts
new file mode 100644
index 0000000..61ecafe
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer_test.ts
@@ -0,0 +1,140 @@
+/*
+ * 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 {CanvasDrawer} from './canvas_drawer';
+
+describe('CanvasDrawer', () => {
+  it('erases the canvas', async () => {
+    const actualCanvas = createCanvas(100, 100);
+    const expectedCanvas = createCanvas(100, 100);
+
+    const canvasDrawer = new CanvasDrawer();
+    canvasDrawer.setCanvas(actualCanvas);
+    canvasDrawer.drawRect({x: 10, y: 10, w: 10, h: 10, color: '#333333', alpha: 1.0});
+
+    expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeFalse();
+
+    canvasDrawer.clear();
+
+    expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue();
+  });
+
+  it('can draw opaque rect', () => {
+    const actualCanvas = createCanvas(100, 100);
+    const expectedCanvas = createCanvas(100, 100);
+
+    const canvasDrawer = new CanvasDrawer();
+    canvasDrawer.setCanvas(actualCanvas);
+    canvasDrawer.drawRect({x: 10, y: 10, w: 10, h: 10, color: '#333333', alpha: 1.0});
+
+    const expectedCtx = assertDefined(expectedCanvas.getContext('2d'));
+    expectedCtx.fillStyle = '#333333';
+    expectedCtx.rect(10, 10, 10, 10);
+    expectedCtx.fill();
+
+    expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue();
+  });
+
+  it('can draw translucent rect', () => {
+    const actualCanvas = createCanvas(100, 100);
+    const expectedCanvas = createCanvas(100, 100);
+
+    const canvasDrawer = new CanvasDrawer();
+    canvasDrawer.setCanvas(actualCanvas);
+    canvasDrawer.drawRect({x: 10, y: 10, w: 10, h: 10, color: '#333333', alpha: 0.5});
+
+    const expectedCtx = assertDefined(expectedCanvas.getContext('2d'));
+    expectedCtx.fillStyle = 'rgba(51,51,51,0.5)';
+    expectedCtx.rect(10, 10, 10, 10);
+    expectedCtx.fill();
+
+    expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue();
+  });
+
+  it('can draw rect border', () => {
+    const actualCanvas = createCanvas(100, 100);
+    const expectedCanvas = createCanvas(100, 100);
+
+    const canvasDrawer = new CanvasDrawer();
+    canvasDrawer.setCanvas(actualCanvas);
+    canvasDrawer.drawRectBorder(10, 10, 10, 10);
+
+    const expectedCtx = assertDefined(expectedCanvas.getContext('2d'));
+
+    expectedCtx.rect(9, 9, 12, 3);
+    expectedCtx.fill();
+    expectedCtx.rect(9, 9, 3, 12);
+    expectedCtx.fill();
+    expectedCtx.rect(9, 18, 12, 3);
+    expectedCtx.fill();
+    expectedCtx.rect(18, 9, 3, 12);
+    expectedCtx.fill();
+
+    expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue();
+  });
+
+  it('can draw rect outside bounds', () => {
+    const actualCanvas = createCanvas(100, 100);
+    const expectedCanvas = createCanvas(100, 100);
+
+    const canvasDrawer = new CanvasDrawer();
+    canvasDrawer.setCanvas(actualCanvas);
+    canvasDrawer.drawRect({x: 200, y: 200, w: 10, h: 10, color: '#333333', alpha: 1.0});
+    canvasDrawer.drawRect({x: 95, y: 95, w: 50, h: 50, color: '#333333', alpha: 1.0});
+
+    const expectedCtx = assertDefined(expectedCanvas.getContext('2d'));
+    expectedCtx.fillStyle = '#333333';
+    expectedCtx.rect(95, 95, 5, 5);
+    expectedCtx.fill();
+
+    expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue();
+  });
+});
+
+function createCanvas(width: number, height: number): HTMLCanvasElement {
+  const canvas = document.createElement('canvas') as HTMLCanvasElement;
+  canvas.width = width;
+  canvas.height = height;
+  return canvas;
+}
+
+function pixelsAllMatch(canvasA: HTMLCanvasElement, canvasB: HTMLCanvasElement): boolean {
+  if (canvasA.width !== canvasB.width || canvasA.height !== canvasB.height) {
+    return false;
+  }
+
+  const imgA = assertDefined(canvasA.getContext('2d')).getImageData(
+    0,
+    0,
+    canvasA.width,
+    canvasA.height
+  ).data;
+  const imgB = assertDefined(canvasB.getContext('2d')).getImageData(
+    0,
+    0,
+    canvasB.width,
+    canvasB.height
+  ).data;
+
+  for (let i = 0; i < imgA.length; i++) {
+    if (imgA[i] !== imgB[i]) {
+      return false;
+    }
+  }
+
+  return true;
+}
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component.ts
new file mode 100644
index 0000000..97d0747
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component.ts
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2022 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 {Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
+import {isPointInRect} from 'common/geometry_utils';
+import {TimeRange, Timestamp} from 'common/time';
+import {Trace, TraceEntry} from 'trace/trace';
+import {TracePosition} from 'trace/trace_position';
+import {AbstractTimelineRowComponent} from './abstract_timeline_row_component';
+
+@Component({
+  selector: 'single-timeline',
+  template: `
+    <div class="single-timeline" #wrapper>
+      <canvas #canvas></canvas>
+    </div>
+  `,
+  styles: [
+    `
+      .single-timeline {
+        height: 2rem;
+        padding: 1rem 0;
+      }
+    `,
+  ],
+})
+export class DefaultTimelineRowComponent extends AbstractTimelineRowComponent<{}> {
+  @Input() color = '#AF5CF7';
+  @Input() trace!: Trace<{}>;
+  @Input() selectedEntry: TraceEntry<{}> | undefined = undefined;
+  @Input() selectionRange!: TimeRange;
+
+  @Output() onTracePositionUpdate = new EventEmitter<TracePosition>();
+
+  @ViewChild('canvas', {static: false}) canvasRef!: ElementRef;
+  @ViewChild('wrapper', {static: false}) wrapperRef!: ElementRef;
+
+  hoveringEntry?: Timestamp;
+  hoveringSegment?: TimeRange;
+
+  ngOnInit() {
+    if (!this.trace || !this.selectionRange) {
+      throw Error('Not all required inputs have been set');
+    }
+  }
+
+  override onHover(mouseX: number, mouseY: number) {
+    this.drawEntryHover(mouseX, mouseY);
+  }
+
+  override handleMouseOut(e: MouseEvent) {
+    if (this.hoveringEntry || this.hoveringSegment) {
+      // If undefined there is no current hover effect so no need to clear
+      this.redraw();
+    }
+    this.hoveringEntry = undefined;
+    this.hoveringSegment = undefined;
+  }
+
+  private async drawEntryHover(mouseX: number, mouseY: number) {
+    const currentHoverEntry = (await this.getEntryAt(mouseX, mouseY))?.getTimestamp();
+
+    if (this.hoveringEntry === currentHoverEntry) {
+      return;
+    }
+
+    if (this.hoveringEntry) {
+      // If null there is no current hover effect so no need to clear
+      this.canvasDrawer.clear();
+      this.drawTimeline();
+    }
+
+    this.hoveringEntry = currentHoverEntry;
+
+    if (!this.hoveringEntry) {
+      return;
+    }
+
+    const {x, y, w, h} = this.entryRect(this.hoveringEntry);
+
+    this.canvasDrawer.drawRect({x, y, w, h, color: this.color, alpha: 1.0});
+    this.canvasDrawer.drawRectBorder(x, y, w, h);
+  }
+
+  protected override async getEntryAt(
+    mouseX: number,
+    mouseY: number
+  ): Promise<TraceEntry<{}> | undefined> {
+    const timestampOfClick = this.getTimestampOf(mouseX);
+    const candidateEntry = this.trace.findLastLowerOrEqualEntry(timestampOfClick);
+
+    if (candidateEntry !== undefined) {
+      const timestamp = candidateEntry.getTimestamp();
+      const {x, y, w, h} = this.entryRect(timestamp);
+      if (isPointInRect({x: mouseX, y: mouseY}, {x, y, w, h})) {
+        return candidateEntry;
+      }
+    }
+
+    return undefined;
+  }
+
+  get entryWidth() {
+    return this.canvasDrawer.getScaledCanvasHeight();
+  }
+
+  get availableWidth() {
+    return Math.floor(this.canvasDrawer.getScaledCanvasWidth() - this.entryWidth);
+  }
+
+  private entryRect(entry: Timestamp, padding = 0) {
+    const xPos = this.getXPosOf(entry);
+
+    return {
+      x: xPos + padding,
+      y: padding,
+      w: this.entryWidth - 2 * padding,
+      h: this.entryWidth - 2 * padding,
+    };
+  }
+
+  private getXPosOf(entry: Timestamp): number {
+    const start = this.selectionRange.from.getValueNs();
+    const end = this.selectionRange.to.getValueNs();
+
+    return Number((BigInt(this.availableWidth) * (entry.getValueNs() - start)) / (end - start));
+  }
+
+  private getTimestampOf(x: number): Timestamp {
+    const start = this.selectionRange.from.getValueNs();
+    const end = this.selectionRange.to.getValueNs();
+
+    const ts = (BigInt(x) * (end - start)) / BigInt(this.availableWidth) + start;
+    return new Timestamp(this.selectionRange.from.getType(), ts);
+  }
+
+  override async drawTimeline() {
+    this.trace
+      .sliceTime(this.selectionRange.from, this.selectionRange.to)
+      .forEachTimestamp((entry) => {
+        this.drawEntry(entry);
+      });
+    this.drawSelectedEntry();
+  }
+
+  private drawEntry(entry: Timestamp) {
+    const {x, y, w, h} = this.entryRect(entry);
+
+    this.canvasDrawer.drawRect({x, y, w, h, color: this.color, alpha: 0.2});
+  }
+
+  private drawSelectedEntry() {
+    if (this.selectedEntry === undefined) {
+      return;
+    }
+
+    const {x, y, w, h} = this.entryRect(this.selectedEntry.getTimestamp(), 1);
+    this.canvasDrawer.drawRect({x, y, w, h, color: this.color, alpha: 1.0});
+    this.canvasDrawer.drawRectBorder(x, y, w, h);
+  }
+}
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component_test.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component_test.ts
new file mode 100644
index 0000000..a7b5238
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component_test.ts
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2022 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 {DragDropModule} from '@angular/cdk/drag-drop';
+import {ChangeDetectionStrategy} from '@angular/core';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {MatButtonModule} from '@angular/material/button';
+import {MatFormFieldModule} from '@angular/material/form-field';
+import {MatIconModule} from '@angular/material/icon';
+import {MatInputModule} from '@angular/material/input';
+import {MatSelectModule} from '@angular/material/select';
+import {MatTooltipModule} from '@angular/material/tooltip';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {RealTimestamp} from 'common/time';
+import {TraceBuilder} from 'test/unit/trace_builder';
+import {waitToBeCalled} from 'test/utils';
+import {TraceType} from 'trace/trace_type';
+import {DefaultTimelineRowComponent} from './default_timeline_row_component';
+
+describe('DefaultTimelineRowComponent', () => {
+  let fixture: ComponentFixture<DefaultTimelineRowComponent>;
+  let component: DefaultTimelineRowComponent;
+  let htmlElement: HTMLElement;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [
+        FormsModule,
+        MatButtonModule,
+        MatFormFieldModule,
+        MatInputModule,
+        MatIconModule,
+        MatSelectModule,
+        MatTooltipModule,
+        ReactiveFormsModule,
+        BrowserAnimationsModule,
+        DragDropModule,
+      ],
+      declarations: [DefaultTimelineRowComponent],
+    })
+      .overrideComponent(DefaultTimelineRowComponent, {
+        set: {changeDetection: ChangeDetectionStrategy.Default},
+      })
+      .compileComponents();
+    fixture = TestBed.createComponent(DefaultTimelineRowComponent);
+    component = fixture.componentInstance;
+    htmlElement = fixture.nativeElement;
+  });
+
+  it('can be created', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('can draw entries', async () => {
+    component.trace = new TraceBuilder<{}>()
+      .setType(TraceType.TRANSITION)
+      .setEntries([{}, {}, {}, {}])
+      .setTimestamps([
+        new RealTimestamp(10n),
+        new RealTimestamp(12n),
+        new RealTimestamp(15n),
+        new RealTimestamp(70n),
+      ])
+      .build();
+    component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+
+    const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect').and.callThrough();
+
+    fixture.detectChanges();
+    await fixture.whenRenderingDone();
+    drawRectSpy.calls.reset();
+    await waitToBeCalled(drawRectSpy, 4);
+
+    const width = 32;
+    const height = width;
+    const alpha = 0.2;
+
+    const canvasWidth = component.canvasDrawer.getScaledCanvasWidth() - width;
+
+    expect(drawRectSpy).toHaveBeenCalledTimes(4);
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: 0,
+      y: 0,
+      w: width,
+      h: height,
+      color: component.color,
+      alpha,
+    });
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: Math.floor((canvasWidth * 2) / 100),
+      y: 0,
+      w: width,
+      h: height,
+      color: component.color,
+      alpha,
+    });
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: Math.floor((canvasWidth * 5) / 100),
+      y: 0,
+      w: width,
+      h: height,
+      color: component.color,
+      alpha,
+    });
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: Math.floor((canvasWidth * 60) / 100),
+      y: 0,
+      w: width,
+      h: height,
+      color: component.color,
+      alpha,
+    });
+  });
+
+  it('can draw entries zoomed in', async () => {
+    component.trace = new TraceBuilder<{}>()
+      .setType(TraceType.TRANSITION)
+      .setEntries([{}, {}, {}, {}])
+      .setTimestamps([
+        new RealTimestamp(10n),
+        new RealTimestamp(12n),
+        new RealTimestamp(15n),
+        new RealTimestamp(70n),
+      ])
+      .build();
+    component.selectionRange = {from: new RealTimestamp(60n), to: new RealTimestamp(85n)};
+
+    const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+
+    fixture.detectChanges();
+    await fixture.whenRenderingDone();
+    drawRectSpy.calls.reset();
+    await waitToBeCalled(drawRectSpy, 1);
+
+    const width = 32;
+    const height = width;
+    const alpha = 0.2;
+
+    const canvasWidth = component.canvasDrawer.getScaledCanvasWidth() - width;
+
+    expect(drawRectSpy).toHaveBeenCalledTimes(1);
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: Math.floor((canvasWidth * 10) / 25),
+      y: 0,
+      w: width,
+      h: height,
+      color: component.color,
+      alpha,
+    });
+  });
+
+  it('can draw hovering entry', async () => {
+    component.trace = new TraceBuilder<{}>()
+      .setType(TraceType.TRANSITION)
+      .setEntries([{}, {}, {}, {}])
+      .setTimestamps([
+        new RealTimestamp(10n),
+        new RealTimestamp(12n),
+        new RealTimestamp(15n),
+        new RealTimestamp(70n),
+      ])
+      .build();
+    component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+
+    fixture.detectChanges();
+    await fixture.whenRenderingDone();
+
+    const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect').and.callThrough();
+    const drawRectBorderSpy = spyOn(component.canvasDrawer, 'drawRectBorder').and.callThrough();
+
+    const waitPromises = [waitToBeCalled(drawRectBorderSpy, 1), waitToBeCalled(drawRectSpy, 1)];
+
+    component.handleMouseMove({
+      offsetX: 5,
+      offsetY: component.canvasDrawer.getScaledCanvasHeight() / 2,
+      preventDefault: () => {},
+      stopPropagation: () => {},
+    } as MouseEvent);
+
+    fixture.detectChanges();
+    await fixture.whenRenderingDone();
+
+    await Promise.all(waitPromises);
+
+    expect(drawRectSpy).toHaveBeenCalledTimes(1);
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: 0,
+      y: 0,
+      w: 32,
+      h: 32,
+      color: component.color,
+      alpha: 1.0,
+    });
+
+    expect(drawRectBorderSpy).toHaveBeenCalledTimes(1);
+    expect(drawRectBorderSpy).toHaveBeenCalledWith(0, 0, 32, 32);
+  });
+
+  it('can draw selected entry', async () => {
+    component.trace = new TraceBuilder<{}>()
+      .setType(TraceType.TRANSITION)
+      .setEntries([{}, {}, {}, {}])
+      .setTimestamps([
+        new RealTimestamp(10n),
+        new RealTimestamp(12n),
+        new RealTimestamp(15n),
+        new RealTimestamp(70n),
+      ])
+      .build();
+    component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+    component.selectedEntry = component.trace.getEntry(0);
+
+    const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+    const drawRectBorderSpy = spyOn(component.canvasDrawer, 'drawRectBorder');
+
+    const waitPromises = [waitToBeCalled(drawRectSpy, 1), waitToBeCalled(drawRectBorderSpy, 1)];
+
+    fixture.detectChanges();
+    await fixture.whenRenderingDone();
+
+    await Promise.all(waitPromises);
+
+    expect(drawRectSpy).toHaveBeenCalledTimes(1 + 4); // 1 for selected entry + 4 for redraw
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: 1,
+      y: 1,
+      w: 30,
+      h: 30,
+      color: component.color,
+      alpha: 1.0,
+    });
+
+    expect(drawRectBorderSpy).toHaveBeenCalledTimes(1);
+    expect(drawRectBorderSpy).toHaveBeenCalledWith(1, 1, 30, 30);
+  });
+});
diff --git a/tools/winscope/src/app/components/timeline/expanded_timeline_component.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component.ts
similarity index 73%
rename from tools/winscope/src/app/components/timeline/expanded_timeline_component.ts
rename to tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component.ts
index c392d85..244ce06 100644
--- a/tools/winscope/src/app/components/timeline/expanded_timeline_component.ts
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component.ts
@@ -27,10 +27,13 @@
 } from '@angular/core';
 import {TimelineData} from 'app/timeline_data';
 import {TRACE_INFO} from 'app/trace_info';
-import {Timestamp} from 'trace/timestamp';
+import {Timestamp} from 'common/time';
 import {Trace} from 'trace/trace';
 import {TracePosition} from 'trace/trace_position';
-import {SingleTimelineComponent} from './single_timeline_component';
+import {TraceType} from 'trace/trace_type';
+import {AbstractTimelineRowComponent} from './abstract_timeline_row_component';
+import {DefaultTimelineRowComponent} from './default_timeline_row_component';
+import {TransitionTimelineComponent} from './transition_timeline_component';
 
 @Component({
   selector: 'expanded-timeline',
@@ -38,7 +41,7 @@
     <div id="expanded-timeline-wrapper" #expandedTimelineWrapper>
       <div
         *ngFor="let trace of this.timelineData.getTraces(); trackBy: trackTraceBySelectedTimestamp"
-        class="timeline">
+        class="timeline row">
         <div class="icon-wrapper">
           <mat-icon
             class="icon"
@@ -47,13 +50,25 @@
             {{ TRACE_INFO[trace.type].icon }}
           </mat-icon>
         </div>
-        <single-timeline
+        <transition-timeline
+          *ngIf="trace.type === TraceType.TRANSITION"
           [color]="TRACE_INFO[trace.type].color"
           [trace]="trace"
           [selectedEntry]="timelineData.findCurrentEntryFor(trace.type)"
           [selectionRange]="timelineData.getSelectionTimeRange()"
           (onTracePositionUpdate)="onTracePositionUpdate.emit($event)"
-          class="single-timeline"></single-timeline>
+          class="single-timeline">
+        </transition-timeline>
+        <single-timeline
+          *ngIf="trace.type !== TraceType.TRANSITION"
+          [color]="TRACE_INFO[trace.type].color"
+          [trace]="trace"
+          [selectedEntry]="timelineData.findCurrentEntryFor(trace.type)"
+          [selectionRange]="timelineData.getSelectionTimeRange()"
+          (onTracePositionUpdate)="onTracePositionUpdate.emit($event)"
+          class="single-timeline">
+        </single-timeline>
+
         <div class="icon-wrapper">
           <mat-icon class="icon placeholder-icon"></mat-icon>
         </div>
@@ -144,9 +159,15 @@
 
   @ViewChild('canvas', {static: false}) canvasRef!: ElementRef<HTMLCanvasElement>;
   @ViewChild('expandedTimelineWrapper', {static: false}) warpperRef!: ElementRef;
-  @ViewChildren(SingleTimelineComponent) singleTimelines!: QueryList<SingleTimelineComponent>;
+
+  @ViewChildren(DefaultTimelineRowComponent)
+  singleTimelines: QueryList<DefaultTimelineRowComponent> | undefined;
+
+  @ViewChildren(TransitionTimelineComponent)
+  transitionTimelines: QueryList<TransitionTimelineComponent> | undefined;
 
   TRACE_INFO = TRACE_INFO;
+  TraceType = TraceType;
 
   @HostListener('window:resize', ['$event'])
   onResize(event: Event) {
@@ -161,18 +182,22 @@
     // Reset any size before computing new size to avoid it interfering with size computations.
     // Needs to be done together because otherwise the sizes of each timeline will interfere with
     // each other, since if one timeline is still too big the container will stretch to that size.
-    for (const timeline of this.singleTimelines) {
-      timeline.canvas.width = 0;
-      timeline.canvas.height = 0;
-      timeline.canvas.style.width = 'auto';
-      timeline.canvas.style.height = 'auto';
+    const timelines = [
+      ...(this.transitionTimelines as QueryList<AbstractTimelineRowComponent<{}>>),
+      ...(this.singleTimelines as QueryList<AbstractTimelineRowComponent<{}>>),
+    ];
+    for (const timeline of timelines) {
+      timeline.getCanvas().width = 0;
+      timeline.getCanvas().height = 0;
+      timeline.getCanvas().style.width = 'auto';
+      timeline.getCanvas().style.height = 'auto';
     }
 
-    for (const timeline of this.singleTimelines) {
+    for (const timeline of timelines) {
       timeline.initializeCanvas();
-      timeline.canvas.height = 0;
-      timeline.canvas.style.width = 'auto';
-      timeline.canvas.style.height = 'auto';
+      timeline.getCanvas().height = 0;
+      timeline.getCanvas().style.width = 'auto';
+      timeline.getCanvas().style.height = 'auto';
     }
   }
 }
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component_test.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component_test.ts
new file mode 100644
index 0000000..7981b07
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component_test.ts
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2022 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 {DragDropModule} from '@angular/cdk/drag-drop';
+import {ChangeDetectionStrategy} from '@angular/core';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {MatButtonModule} from '@angular/material/button';
+import {MatFormFieldModule} from '@angular/material/form-field';
+import {MatIconModule} from '@angular/material/icon';
+import {MatInputModule} from '@angular/material/input';
+import {MatSelectModule} from '@angular/material/select';
+import {MatTooltipModule} from '@angular/material/tooltip';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {TimelineData} from 'app/timeline_data';
+import {RealTimestamp} from 'common/time';
+import {Transition} from 'flickerlib/common';
+import {TracesBuilder} from 'test/unit/traces_builder';
+import {TraceType} from 'trace/trace_type';
+import {DefaultTimelineRowComponent} from './default_timeline_row_component';
+import {ExpandedTimelineComponent} from './expanded_timeline_component';
+import {TransitionTimelineComponent} from './transition_timeline_component';
+
+describe('ExpandedTimelineComponent', () => {
+  let fixture: ComponentFixture<ExpandedTimelineComponent>;
+  let component: ExpandedTimelineComponent;
+  let htmlElement: HTMLElement;
+  let timelineData: TimelineData;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [
+        FormsModule,
+        MatButtonModule,
+        MatFormFieldModule,
+        MatInputModule,
+        MatIconModule,
+        MatSelectModule,
+        MatTooltipModule,
+        ReactiveFormsModule,
+        BrowserAnimationsModule,
+        DragDropModule,
+      ],
+      declarations: [
+        ExpandedTimelineComponent,
+        TransitionTimelineComponent,
+        DefaultTimelineRowComponent,
+      ],
+    })
+      .overrideComponent(ExpandedTimelineComponent, {
+        set: {changeDetection: ChangeDetectionStrategy.Default},
+      })
+      .compileComponents();
+    fixture = TestBed.createComponent(ExpandedTimelineComponent);
+    component = fixture.componentInstance;
+    htmlElement = fixture.nativeElement;
+    timelineData = new TimelineData();
+    const traces = new TracesBuilder()
+      .setEntries(TraceType.SURFACE_FLINGER, [{}])
+      .setTimestamps(TraceType.SURFACE_FLINGER, [new RealTimestamp(10n)])
+      .setEntries(TraceType.WINDOW_MANAGER, [{}])
+      .setTimestamps(TraceType.WINDOW_MANAGER, [new RealTimestamp(11n)])
+      .setEntries(TraceType.TRANSACTIONS, [{}])
+      .setTimestamps(TraceType.TRANSACTIONS, [new RealTimestamp(12n)])
+      .setEntries(TraceType.TRANSITION, [
+        {
+          createTime: {unixNanos: 10n},
+          finishTime: {unixNanos: 30n},
+        } as Transition,
+        {
+          createTime: {unixNanos: 60n},
+          finishTime: {unixNanos: 110n},
+        } as Transition,
+      ])
+      .setTimestamps(TraceType.TRANSITION, [new RealTimestamp(10n), new RealTimestamp(60n)])
+      .build();
+    timelineData.initialize(traces, undefined);
+    component.timelineData = timelineData;
+  });
+
+  it('can be created', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('renders all timelines', () => {
+    fixture.detectChanges();
+
+    const timelines = htmlElement.querySelectorAll('.timeline.row');
+    expect(timelines.length).toEqual(4);
+  });
+});
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component.ts
new file mode 100644
index 0000000..53c4c98
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component.ts
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2022 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 {Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
+import {isPointInRect} from 'common/geometry_utils';
+import {ElapsedTimestamp, RealTimestamp, TimeRange, Timestamp, TimestampType} from 'common/time';
+import {Transition} from 'flickerlib/common';
+import {Trace, TraceEntry} from 'trace/trace';
+import {TracePosition} from 'trace/trace_position';
+import {TraceType} from 'trace/trace_type';
+import {AbstractTimelineRowComponent} from './abstract_timeline_row_component';
+
+@Component({
+  selector: 'transition-timeline',
+  template: `
+    <div class="transition-timeline" #wrapper>
+      <canvas #canvas></canvas>
+    </div>
+  `,
+  styles: [
+    `
+      .transition-timeline {
+        height: 4rem;
+      }
+    `,
+  ],
+})
+export class TransitionTimelineComponent extends AbstractTimelineRowComponent<Transition> {
+  @Input() color = '#AF5CF7';
+  @Input() trace!: Trace<Transition>;
+  @Input() selectedEntry: TraceEntry<Transition> | undefined = undefined;
+  @Input() selectionRange!: TimeRange;
+
+  @Output() onTracePositionUpdate = new EventEmitter<TracePosition>();
+
+  @ViewChild('canvas', {static: false}) override canvasRef!: ElementRef;
+  @ViewChild('wrapper', {static: false}) override wrapperRef!: ElementRef;
+
+  hoveringEntry?: TraceEntry<Transition>;
+
+  rowsToUse = new Map<number, number>();
+  maxRowsRequires = 0;
+
+  ngOnInit() {
+    if (!this.trace || !this.selectionRange) {
+      throw Error('Not all required inputs have been set');
+    }
+    this.computeRowsForEntries();
+  }
+
+  private async computeRowsForEntries(): Promise<void> {
+    const rowAvailableFrom: Array<bigint | undefined> = [];
+    await Promise.all(
+      (this.trace as Trace<Transition>).mapEntry(async (entry) => {
+        const transition = await entry.getValue();
+        let rowToUse = 0;
+        while (
+          (rowAvailableFrom[rowToUse] ?? 0n) > BigInt(transition.createTime.unixNanos.toString())
+        ) {
+          rowToUse++;
+        }
+
+        rowAvailableFrom[rowToUse] = BigInt(transition.finishTime.unixNanos.toString());
+
+        if (rowToUse + 1 > this.maxRowsRequires) {
+          this.maxRowsRequires = rowToUse + 1;
+        }
+        this.rowsToUse.set(entry.getIndex(), rowToUse);
+      })
+    );
+  }
+
+  private getRowToUseFor(entry: TraceEntry<Transition>): number {
+    const rowToUse = this.rowsToUse.get(entry.getIndex());
+    if (rowToUse === undefined) {
+      console.error('Failed to find', entry, 'in', this.rowsToUse);
+      throw new Error(`Could not find entry in rowsToUse`);
+    }
+    return rowToUse;
+  }
+
+  override onHover(mouseX: number, mouseY: number) {
+    this.drawSegmentHover(mouseX, mouseY);
+  }
+
+  override handleMouseOut(e: MouseEvent) {
+    if (this.hoveringEntry) {
+      // If undefined there is no current hover effect so no need to clear
+      this.redraw();
+    }
+    this.hoveringEntry = undefined;
+  }
+
+  private async drawSegmentHover(mouseX: number, mouseY: number) {
+    const currentHoverEntry = await this.getEntryAt(mouseX, mouseY);
+
+    if (this.hoveringEntry) {
+      this.canvasDrawer.clear();
+      this.drawTimeline();
+    }
+
+    this.hoveringEntry = currentHoverEntry;
+
+    if (!this.hoveringEntry) {
+      return;
+    }
+
+    const hoveringSegment = await this.getSegmentForTransition(this.hoveringEntry);
+    const rowToUse = this.getRowToUseFor(this.hoveringEntry);
+    const {x, y, w, h} = this.getSegmentRect(hoveringSegment.from, hoveringSegment.to, rowToUse);
+    this.canvasDrawer.drawRectBorder(x, y, w, h);
+  }
+
+  private async getSegmentAt(mouseX: number, mouseY: number): Promise<TimeRange | undefined> {
+    const transitionEntry = await this.getEntryAt(mouseX, mouseY);
+    if (transitionEntry) {
+      return this.getSegmentForTransition(transitionEntry);
+    }
+    return undefined;
+  }
+
+  protected override async getEntryAt(
+    mouseX: number,
+    mouseY: number
+  ): Promise<TraceEntry<Transition> | undefined> {
+    if (this.trace.type !== TraceType.TRANSITION) {
+      return undefined;
+    }
+
+    const transitionEntries: Array<Promise<TraceEntry<Transition> | undefined>> = [];
+    this.trace.forEachEntry((entry) => {
+      transitionEntries.push(
+        (async () => {
+          const transitionSegment = await this.getSegmentForTransition(entry);
+          const rowToUse = this.getRowToUseFor(entry);
+          const {x, y, w, h} = this.getSegmentRect(
+            transitionSegment.from,
+            transitionSegment.to,
+            rowToUse
+          );
+          if (isPointInRect({x: mouseX, y: mouseY}, {x, y, w, h})) {
+            return entry;
+          }
+          return undefined;
+        })()
+      );
+    });
+
+    for (const entryPromise of transitionEntries) {
+      const entry = await entryPromise;
+      if (entry) {
+        return entry;
+      }
+    }
+
+    return undefined;
+  }
+
+  get entryWidth() {
+    return this.canvasDrawer.getScaledCanvasHeight();
+  }
+
+  get availableWidth() {
+    return this.canvasDrawer.getScaledCanvasWidth();
+  }
+
+  private getXPosOf(entry: Timestamp): number {
+    const start = this.selectionRange.from.getValueNs();
+    const end = this.selectionRange.to.getValueNs();
+
+    return Number((BigInt(this.availableWidth) * (entry.getValueNs() - start)) / (end - start));
+  }
+
+  private getSegmentRect(start: Timestamp, end: Timestamp, rowToUse: number) {
+    const xPosStart = this.getXPosOf(start);
+    const selectionStart = this.selectionRange.from.getValueNs();
+    const selectionEnd = this.selectionRange.to.getValueNs();
+
+    const width = Number(
+      (BigInt(this.availableWidth) * (end.getValueNs() - start.getValueNs())) /
+        (selectionEnd - selectionStart)
+    );
+
+    const borderPadding = 5;
+    let totalRowHeight =
+      (this.canvasDrawer.getScaledCanvasHeight() - 2 * borderPadding) / this.maxRowsRequires;
+    if (totalRowHeight < 10) {
+      totalRowHeight = 10;
+    }
+    if (this.maxRowsRequires === 1) {
+      totalRowHeight = 30;
+    }
+
+    const padding = 5;
+    const rowHeight = totalRowHeight - padding;
+
+    return {x: xPosStart, y: borderPadding + rowToUse * totalRowHeight, w: width, h: rowHeight};
+  }
+
+  override async drawTimeline() {
+    await Promise.all(
+      (this.trace as Trace<Transition>).mapEntry(async (entry) => {
+        const transitionSegment = await this.getSegmentForTransition(entry);
+        const rowToUse = this.getRowToUseFor(entry);
+        const aborted = (await entry.getValue()).aborted;
+        this.drawSegment(transitionSegment.from, transitionSegment.to, rowToUse, aborted);
+      })
+    );
+    this.drawSelectedTransitionEntry();
+  }
+
+  private async getSegmentForTransition(entry: TraceEntry<Transition>): Promise<TimeRange> {
+    const transition = await entry.getValue();
+
+    let createTime: Timestamp;
+    let finishTime: Timestamp;
+    if (entry.getTimestamp().getType() === TimestampType.REAL) {
+      createTime = new RealTimestamp(BigInt(transition.createTime.unixNanos.toString()));
+      finishTime = new RealTimestamp(BigInt(transition.finishTime.unixNanos.toString()));
+    } else if (entry.getTimestamp().getType() === TimestampType.ELAPSED) {
+      createTime = new ElapsedTimestamp(BigInt(transition.createTime.elapsedNanos.toString()));
+      finishTime = new ElapsedTimestamp(BigInt(transition.finishTime.elapsedNanos.toString()));
+    } else {
+      throw new Error('Unspported timestamp type');
+    }
+
+    return {from: createTime, to: finishTime};
+  }
+
+  private drawSegment(start: Timestamp, end: Timestamp, rowToUse: number, aborted: boolean) {
+    const {x, y, w, h} = this.getSegmentRect(start, end, rowToUse);
+    const alpha = aborted ? 0.25 : 1.0;
+    this.canvasDrawer.drawRect({x, y, w, h, color: this.color, alpha});
+  }
+
+  private async drawSelectedTransitionEntry() {
+    if (this.selectedEntry === undefined) {
+      return;
+    }
+
+    const transitionSegment = await this.getSegmentForTransition(this.selectedEntry);
+
+    const transition = await this.selectedEntry.getValue();
+    const rowIndex = this.getRowToUseFor(this.selectedEntry);
+    const {x, y, w, h} = this.getSegmentRect(
+      transitionSegment.from,
+      transitionSegment.to,
+      rowIndex
+    );
+    const alpha = transition.aborted ? 0.25 : 1.0;
+    this.canvasDrawer.drawRect({x, y, w, h, color: this.color, alpha});
+    this.canvasDrawer.drawRectBorder(x, y, w, h);
+  }
+}
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component_test.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component_test.ts
new file mode 100644
index 0000000..9b94c52
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component_test.ts
@@ -0,0 +1,395 @@
+/*
+ * 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 {DragDropModule} from '@angular/cdk/drag-drop';
+import {ChangeDetectionStrategy} from '@angular/core';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {MatButtonModule} from '@angular/material/button';
+import {MatFormFieldModule} from '@angular/material/form-field';
+import {MatIconModule} from '@angular/material/icon';
+import {MatInputModule} from '@angular/material/input';
+import {MatSelectModule} from '@angular/material/select';
+import {MatTooltipModule} from '@angular/material/tooltip';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {RealTimestamp} from 'common/time';
+import {Transition} from 'flickerlib/common';
+import {TraceBuilder} from 'test/unit/trace_builder';
+import {waitToBeCalled} from 'test/utils';
+import {TraceType} from 'trace/trace_type';
+import {TransitionTimelineComponent} from './transition_timeline_component';
+
+describe('TransitionTimelineComponent', () => {
+  let fixture: ComponentFixture<TransitionTimelineComponent>;
+  let component: TransitionTimelineComponent;
+  let htmlElement: HTMLElement;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [
+        FormsModule,
+        MatButtonModule,
+        MatFormFieldModule,
+        MatInputModule,
+        MatIconModule,
+        MatSelectModule,
+        MatTooltipModule,
+        ReactiveFormsModule,
+        BrowserAnimationsModule,
+        DragDropModule,
+      ],
+      declarations: [TransitionTimelineComponent],
+    })
+      .overrideComponent(TransitionTimelineComponent, {
+        set: {changeDetection: ChangeDetectionStrategy.Default},
+      })
+      .compileComponents();
+    fixture = TestBed.createComponent(TransitionTimelineComponent);
+    component = fixture.componentInstance;
+    htmlElement = fixture.nativeElement;
+  });
+
+  it('can be created', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('can draw non-overlapping transitions', async () => {
+    component.trace = new TraceBuilder()
+      .setType(TraceType.TRANSITION)
+      .setEntries([
+        {
+          createTime: {unixNanos: 10n},
+          finishTime: {unixNanos: 30n},
+        } as Transition,
+        {
+          createTime: {unixNanos: 60n},
+          finishTime: {unixNanos: 110n},
+        } as Transition,
+      ])
+      .setTimestamps([new RealTimestamp(10n), new RealTimestamp(60n)])
+      .build();
+    component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+
+    const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+
+    fixture.detectChanges();
+    await fixture.whenRenderingDone();
+    await waitToBeCalled(drawRectSpy, 2);
+
+    const padding = 5;
+    const oneRowTotalHeight = 30;
+    const oneRowHeight = oneRowTotalHeight - padding;
+    const width = component.canvasDrawer.getScaledCanvasWidth();
+
+    expect(drawRectSpy).toHaveBeenCalledTimes(2);
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: 0,
+      y: padding,
+      w: Math.floor(width / 5),
+      h: oneRowHeight,
+      color: component.color,
+      alpha: 1,
+    });
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: Math.floor(width / 2),
+      y: padding,
+      w: Math.floor(width / 2),
+      h: oneRowHeight,
+      color: component.color,
+      alpha: 1,
+    });
+  });
+
+  it('can draw transitions zoomed in', async () => {
+    component.trace = new TraceBuilder()
+      .setType(TraceType.TRANSITION)
+      .setEntries([
+        {
+          createTime: {unixNanos: 0n},
+          finishTime: {unixNanos: 20n},
+        } as Transition,
+        {
+          createTime: {unixNanos: 60n},
+          finishTime: {unixNanos: 160n},
+        } as Transition,
+      ])
+      .setTimestamps([new RealTimestamp(10n), new RealTimestamp(60n)])
+      .build();
+    component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+
+    const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+
+    fixture.detectChanges();
+    await fixture.whenRenderingDone();
+    await waitToBeCalled(drawRectSpy, 2);
+
+    const padding = 5;
+    const oneRowTotalHeight = 30;
+    const oneRowHeight = oneRowTotalHeight - padding;
+    const width = component.canvasDrawer.getScaledCanvasWidth();
+
+    expect(drawRectSpy).toHaveBeenCalledTimes(2);
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: -Math.floor(width / 10),
+      y: padding,
+      w: Math.floor(width / 5),
+      h: oneRowHeight,
+      color: component.color,
+      alpha: 1,
+    });
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: Math.floor(width / 2),
+      y: padding,
+      w: Math.floor(width),
+      h: oneRowHeight,
+      color: component.color,
+      alpha: 1,
+    });
+  });
+
+  it('can draw selected entry', async () => {
+    const transition = {
+      createTime: {unixNanos: 35n},
+      finishTime: {unixNanos: 85n},
+      aborted: true,
+    } as Transition;
+    component.trace = new TraceBuilder()
+      .setType(TraceType.TRANSITION)
+      .setEntries([transition])
+      .setTimestamps([new RealTimestamp(35n)])
+      .build();
+    component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+    component.selectedEntry = component.trace.getEntry(0);
+
+    const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+    const drawRectBorderSpy = spyOn(component.canvasDrawer, 'drawRectBorder');
+
+    const waitPromises = [waitToBeCalled(drawRectSpy, 1), waitToBeCalled(drawRectBorderSpy, 1)];
+
+    fixture.detectChanges();
+    await fixture.whenRenderingDone();
+
+    await Promise.all(waitPromises);
+
+    const padding = 5;
+    const oneRowTotalHeight = 30;
+    const oneRowHeight = oneRowTotalHeight - padding;
+    const width = component.canvasDrawer.getScaledCanvasWidth();
+
+    expect(drawRectSpy).toHaveBeenCalledTimes(2); // once drawn as a normal entry another time with rect border
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: Math.floor((width * 1) / 4),
+      y: padding,
+      w: Math.floor(width / 2),
+      h: oneRowHeight,
+      color: component.color,
+      alpha: 0.25,
+    });
+
+    expect(drawRectBorderSpy).toHaveBeenCalledTimes(1);
+    expect(drawRectBorderSpy).toHaveBeenCalledWith(
+      Math.floor((width * 1) / 4),
+      padding,
+      Math.floor(width / 2),
+      oneRowHeight
+    );
+  });
+
+  it('can draw hovering entry', async () => {
+    const transition = {
+      createTime: {unixNanos: 35n},
+      finishTime: {unixNanos: 85n},
+      aborted: true,
+    } as Transition;
+    component.trace = new TraceBuilder()
+      .setType(TraceType.TRANSITION)
+      .setEntries([transition])
+      .setTimestamps([new RealTimestamp(35n)])
+      .build();
+    component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+
+    const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+    const drawRectBorderSpy = spyOn(component.canvasDrawer, 'drawRectBorder');
+
+    fixture.detectChanges();
+    await fixture.whenRenderingDone();
+
+    const padding = 5;
+    const oneRowTotalHeight = 30;
+    const oneRowHeight = oneRowTotalHeight - padding;
+    const width = component.canvasDrawer.getScaledCanvasWidth();
+
+    component.handleMouseMove({
+      offsetX: Math.floor(width / 2),
+      offsetY: oneRowTotalHeight / 2,
+      preventDefault: () => {},
+      stopPropagation: () => {},
+    } as MouseEvent);
+
+    await waitToBeCalled(drawRectSpy, 1);
+    await waitToBeCalled(drawRectBorderSpy, 1);
+
+    expect(drawRectSpy).toHaveBeenCalledTimes(1);
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: Math.floor((width * 1) / 4),
+      y: padding,
+      w: Math.floor(width / 2),
+      h: oneRowHeight,
+      color: component.color,
+      alpha: 0.25,
+    });
+
+    expect(drawRectBorderSpy).toHaveBeenCalledTimes(1);
+    expect(drawRectBorderSpy).toHaveBeenCalledWith(
+      Math.floor((width * 1) / 4),
+      padding,
+      Math.floor(width / 2),
+      oneRowHeight
+    );
+  });
+
+  it('can draw overlapping transitions (default)', async () => {
+    component.trace = new TraceBuilder()
+      .setType(TraceType.TRANSITION)
+      .setEntries([
+        {
+          createTime: {unixNanos: 10n},
+          finishTime: {unixNanos: 85n},
+        } as Transition,
+        {
+          createTime: {unixNanos: 60n},
+          finishTime: {unixNanos: 110n},
+        } as Transition,
+      ])
+      .setTimestamps([new RealTimestamp(10n), new RealTimestamp(60n)])
+      .build();
+    component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+
+    const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+
+    fixture.detectChanges();
+    await fixture.whenRenderingDone();
+    await waitToBeCalled(drawRectSpy, 2);
+
+    const padding = 5;
+    const rows = 2;
+    const oneRowTotalHeight = (component.canvasDrawer.getScaledCanvasHeight() - 2 * padding) / rows;
+    const oneRowHeight = oneRowTotalHeight - padding;
+    const width = component.canvasDrawer.getScaledCanvasWidth();
+
+    expect(drawRectSpy).toHaveBeenCalledTimes(2);
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: 0,
+      y: padding,
+      w: Math.floor((width * 3) / 4),
+      h: oneRowHeight,
+      color: component.color,
+      alpha: 1,
+    });
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: Math.floor(width / 2),
+      y: padding + oneRowTotalHeight,
+      w: Math.floor(width / 2),
+      h: oneRowHeight,
+      color: component.color,
+      alpha: 1,
+    });
+  });
+
+  it('can draw overlapping transitions (contained)', async () => {
+    component.trace = new TraceBuilder()
+      .setType(TraceType.TRANSITION)
+      .setEntries([
+        {
+          createTime: {unixNanos: 10n},
+          finishTime: {unixNanos: 85n},
+        } as Transition,
+        {
+          createTime: {unixNanos: 35n},
+          finishTime: {unixNanos: 60n},
+        } as Transition,
+      ])
+      .setTimestamps([new RealTimestamp(10n), new RealTimestamp(35n)])
+      .build();
+    component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+
+    const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+
+    fixture.detectChanges();
+    await fixture.whenRenderingDone();
+    await waitToBeCalled(drawRectSpy, 2);
+
+    const padding = 5;
+    const rows = 2;
+    const oneRowTotalHeight = (component.canvasDrawer.getScaledCanvasHeight() - 2 * padding) / rows;
+    const oneRowHeight = oneRowTotalHeight - padding;
+    const width = component.canvasDrawer.getScaledCanvasWidth();
+
+    expect(drawRectSpy).toHaveBeenCalledTimes(2);
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: 0,
+      y: padding,
+      w: Math.floor((width * 3) / 4),
+      h: oneRowHeight,
+      color: component.color,
+      alpha: 1,
+    });
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: Math.floor(width / 4),
+      y: padding + oneRowTotalHeight,
+      w: Math.floor(width / 4),
+      h: oneRowHeight,
+      color: component.color,
+      alpha: 1,
+    });
+  });
+
+  it('can draw aborted transitions', async () => {
+    component.trace = new TraceBuilder()
+      .setType(TraceType.TRANSITION)
+      .setEntries([
+        {
+          createTime: {unixNanos: 35n},
+          finishTime: {unixNanos: 85n},
+          aborted: true,
+        } as Transition,
+      ])
+      .setTimestamps([new RealTimestamp(35n)])
+      .build();
+    component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+
+    const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+
+    fixture.detectChanges();
+    await fixture.whenRenderingDone();
+    await waitToBeCalled(drawRectSpy, 1);
+
+    const padding = 5;
+    const oneRowTotalHeight = 30;
+    const oneRowHeight = oneRowTotalHeight - padding;
+    const width = component.canvasDrawer.getScaledCanvasWidth();
+
+    expect(drawRectSpy).toHaveBeenCalledTimes(1);
+    expect(drawRectSpy).toHaveBeenCalledWith({
+      x: Math.floor((width * 1) / 4),
+      y: padding,
+      w: Math.floor(width / 2),
+      h: oneRowHeight,
+      color: component.color,
+      alpha: 0.25,
+    });
+  });
+});
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler.ts
new file mode 100644
index 0000000..d66484a
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 {DraggableCanvasObject} from './draggable_canvas_object';
+
+export type DragListener = (x: number, y: number) => void;
+export type DropListener = DragListener;
+
+export interface CanvasMouseHandler {
+  registerDraggableObject(
+    draggableObject: DraggableCanvasObject,
+    onDrag: DragListener,
+    onDrop: DropListener
+  ): void;
+
+  notifyDrawnOnTop(draggableObject: DraggableCanvasObject): void;
+}
diff --git a/tools/winscope/src/app/components/canvas/canvas_mouse_handler.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler_impl.ts
similarity index 88%
rename from tools/winscope/src/app/components/canvas/canvas_mouse_handler.ts
rename to tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler_impl.ts
index 4eb3bea..d641f92 100644
--- a/tools/winscope/src/app/components/canvas/canvas_mouse_handler.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler_impl.ts
@@ -14,13 +14,12 @@
  * limitations under the License.
  */
 
-import {CanvasDrawer} from './canvas_drawer';
+import {assertDefined} from 'common/assert_utils';
+import {CanvasMouseHandler, DragListener, DropListener} from './canvas_mouse_handler';
 import {DraggableCanvasObject} from './draggable_canvas_object';
+import {MiniTimelineDrawer} from './mini_timeline_drawer';
 
-export type DragListener = (x: number, y: number) => void;
-export type DropListener = DragListener;
-
-export class CanvasMouseHandler {
+export class CanvasMouseHandlerImpl implements CanvasMouseHandler {
   // Ordered top most element to bottom most
   private draggableObjects: DraggableCanvasObject[] = [];
   private draggingObject: DraggableCanvasObject | undefined = undefined;
@@ -29,7 +28,7 @@
   private onDrop = new Map<DraggableCanvasObject, DropListener>();
 
   constructor(
-    private drawer: CanvasDrawer,
+    private drawer: MiniTimelineDrawer,
     private defaultCursor: string = 'auto',
     private onUnhandledMouseDown: (x: number, y: number) => void = (x, y) => {}
   ) {
@@ -139,13 +138,9 @@
 
   private objectAt(mouseX: number, mouseY: number): DraggableCanvasObject | undefined {
     for (const object of this.draggableObjects) {
-      object.definePath(this.drawer.ctx);
-      if (
-        this.drawer.ctx.isPointInPath(
-          mouseX * this.drawer.getXScale(),
-          mouseY * this.drawer.getYScale()
-        )
-      ) {
+      const ctx = assertDefined(this.drawer.canvas.getContext('2d'));
+      object.definePath(ctx);
+      if (ctx.isPointInPath(mouseX * this.drawer.getXScale(), mouseY * this.drawer.getYScale())) {
         return object;
       }
     }
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/draggable_canvas_object.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/draggable_canvas_object.ts
new file mode 100644
index 0000000..7125ea3
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/draggable_canvas_object.ts
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+export interface DraggableCanvasObject {
+  draw(ctx: CanvasRenderingContext2D): void;
+  definePath(ctx: CanvasRenderingContext2D): void;
+}
diff --git a/tools/winscope/src/app/components/canvas/draggable_canvas_object.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/draggable_canvas_object_impl.ts
similarity index 88%
rename from tools/winscope/src/app/components/canvas/draggable_canvas_object.ts
rename to tools/winscope/src/app/components/timeline/mini-timeline/drawer/draggable_canvas_object_impl.ts
index 65b1032..38acbe2 100644
--- a/tools/winscope/src/app/components/canvas/draggable_canvas_object.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/draggable_canvas_object_impl.ts
@@ -15,19 +15,20 @@
  */
 
 import {MathUtils} from 'three/src/Three';
-import {Segment} from '../timeline/utils';
-import {CanvasDrawer} from './canvas_drawer';
+import {Segment} from '../../utils';
+import {DraggableCanvasObject} from './draggable_canvas_object';
+import {MiniTimelineDrawer} from './mini_timeline_drawer';
 
 export interface DrawConfig {
   fillStyle: string;
   fill: boolean;
 }
 
-export class DraggableCanvasObject {
+export class DraggableCanvasObjectImpl implements DraggableCanvasObject {
   private draggingPosition: number | undefined;
 
   constructor(
-    private drawer: CanvasDrawer,
+    private drawer: MiniTimelineDrawer,
     private positionGetter: () => number,
     private definePathFunc: (ctx: CanvasRenderingContext2D, position: number) => void,
     private drawConfig: DrawConfig,
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_canvas_drawer_data.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_canvas_drawer_data.ts
new file mode 100644
index 0000000..10d66b1
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_canvas_drawer_data.ts
@@ -0,0 +1,39 @@
+import {TraceType} from 'trace/trace_type';
+import {Segment} from '../../utils';
+import {Transformer} from '../transformer';
+import {MiniTimelineDrawerOutput} from './mini_timeline_drawer_output';
+
+export type TimelineEntries = Map<
+  TraceType,
+  {
+    points: number[];
+    segments: Segment[];
+    activePoint: number | undefined;
+    activeSegment: Segment | undefined;
+  }
+>;
+
+export class MiniCanvasDrawerData {
+  constructor(
+    public selectedPosition: number,
+    public selection: Segment,
+    private timelineEntriesGetter: () => Promise<TimelineEntries>,
+    public transformer: Transformer
+  ) {}
+
+  private entries: TimelineEntries | undefined = undefined;
+
+  async getTimelineEntries(): Promise<TimelineEntries> {
+    if (this.entries === undefined) {
+      this.entries = await this.timelineEntriesGetter();
+    }
+    return this.entries;
+  }
+
+  toOutput(): MiniTimelineDrawerOutput {
+    return new MiniTimelineDrawerOutput(this.transformer.untransform(this.selectedPosition), {
+      from: this.transformer.untransform(this.selection.from),
+      to: this.transformer.untransform(this.selection.to),
+    });
+  }
+}
diff --git a/tools/winscope/src/app/components/canvas/canvas_drawer.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer.ts
similarity index 74%
rename from tools/winscope/src/app/components/canvas/canvas_drawer.ts
rename to tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer.ts
index 03e8e83..0670aa4 100644
--- a/tools/winscope/src/app/components/canvas/canvas_drawer.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer.ts
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -14,23 +14,16 @@
  * limitations under the License.
  */
 
+import {Padding} from 'common/padding';
 import {CanvasMouseHandler} from './canvas_mouse_handler';
 
-export interface Padding {
-  left: number;
-  top: number;
-  right: number;
-  bottom: number;
-}
-
-export interface CanvasDrawer {
-  draw(): void;
-  handler: CanvasMouseHandler;
-  canvas: HTMLCanvasElement;
-  ctx: CanvasRenderingContext2D;
-  padding: Padding;
+export interface MiniTimelineDrawer {
+  draw(): Promise<void>;
   getXScale(): number;
   getYScale(): number;
   getWidth(): number;
-  getHeight(): number;
+  canvas: HTMLCanvasElement;
+  handler: CanvasMouseHandler;
+  padding: Padding;
+  usableRange: {from: number; to: number};
 }
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_impl.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_impl.ts
new file mode 100644
index 0000000..fda8db7
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_impl.ts
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2022 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 {Color} from 'app/colors';
+import {TRACE_INFO} from 'app/trace_info';
+import {Padding} from 'common/padding';
+import {Timestamp} from 'common/time';
+import {CanvasMouseHandler} from './canvas_mouse_handler';
+import {CanvasMouseHandlerImpl} from './canvas_mouse_handler_impl';
+import {DraggableCanvasObject} from './draggable_canvas_object';
+import {DraggableCanvasObjectImpl} from './draggable_canvas_object_impl';
+import {MiniCanvasDrawerData, TimelineEntries} from './mini_canvas_drawer_data';
+import {MiniTimelineDrawer} from './mini_timeline_drawer';
+import {MiniTimelineDrawerInput} from './mini_timeline_drawer_input';
+
+export class MiniTimelineDrawerImpl implements MiniTimelineDrawer {
+  ctx: CanvasRenderingContext2D;
+  handler: CanvasMouseHandler;
+
+  private activePointer: DraggableCanvasObject;
+
+  private get pointerWidth() {
+    return this.getHeight() / 6;
+  }
+
+  getXScale() {
+    return this.ctx.getTransform().m11;
+  }
+
+  getYScale() {
+    return this.ctx.getTransform().m22;
+  }
+
+  getWidth() {
+    return this.canvas.width / this.getXScale();
+  }
+
+  getHeight() {
+    return this.canvas.height / this.getYScale();
+  }
+
+  get usableRange() {
+    return {
+      from: this.padding.left,
+      to: this.getWidth() - this.padding.left - this.padding.right,
+    };
+  }
+
+  get input(): MiniCanvasDrawerData {
+    return this.inputGetter().transform(this.usableRange);
+  }
+
+  constructor(
+    public canvas: HTMLCanvasElement,
+    private inputGetter: () => MiniTimelineDrawerInput,
+    private onPointerPositionDragging: (pos: Timestamp) => void,
+    private onPointerPositionChanged: (pos: Timestamp) => void,
+    private onUnhandledClick: (pos: Timestamp) => void
+  ) {
+    const ctx = canvas.getContext('2d');
+
+    if (ctx === null) {
+      throw Error('MiniTimeline canvas context was null!');
+    }
+
+    this.ctx = ctx;
+
+    const onUnhandledClickInternal = async (x: number, y: number) => {
+      this.onUnhandledClick(this.input.transformer.untransform(x));
+    };
+    this.handler = new CanvasMouseHandlerImpl(this, 'pointer', onUnhandledClickInternal);
+
+    this.activePointer = new DraggableCanvasObjectImpl(
+      this,
+      () => this.selectedPosition,
+      (ctx: CanvasRenderingContext2D, position: number) => {
+        const barWidth = 3;
+        const triangleHeight = this.pointerWidth / 2;
+
+        ctx.beginPath();
+        ctx.moveTo(position - triangleHeight, 0);
+        ctx.lineTo(position + triangleHeight, 0);
+        ctx.lineTo(position + barWidth / 2, triangleHeight);
+        ctx.lineTo(position + barWidth / 2, this.getHeight());
+        ctx.lineTo(position - barWidth / 2, this.getHeight());
+        ctx.lineTo(position - barWidth / 2, triangleHeight);
+        ctx.closePath();
+      },
+      {
+        fillStyle: Color.ACTIVE_POINTER,
+        fill: true,
+      },
+      (x) => {
+        this.input.selectedPosition = x;
+        this.onPointerPositionDragging(this.input.transformer.untransform(x));
+      },
+      (x) => {
+        this.input.selectedPosition = x;
+        this.onPointerPositionChanged(this.input.transformer.untransform(x));
+      },
+      () => this.usableRange
+    );
+  }
+
+  get selectedPosition() {
+    return this.input.selectedPosition;
+  }
+
+  get selection() {
+    return this.input.selection;
+  }
+
+  async getTimelineEntries(): Promise<TimelineEntries> {
+    return await this.input.getTimelineEntries();
+  }
+
+  get padding(): Padding {
+    return {
+      top: Math.ceil(this.getHeight() / 5),
+      bottom: Math.ceil(this.getHeight() / 5),
+      left: Math.ceil(this.pointerWidth / 2),
+      right: Math.ceil(this.pointerWidth / 2),
+    };
+  }
+
+  get innerHeight() {
+    return this.getHeight() - this.padding.top - this.padding.bottom;
+  }
+
+  async draw() {
+    this.ctx.clearRect(0, 0, this.getWidth(), this.getHeight());
+
+    await this.drawTraceLines();
+
+    this.drawTimelineGuides();
+
+    this.activePointer.draw(this.ctx);
+  }
+
+  private async drawTraceLines() {
+    const lineHeight = this.innerHeight / 8;
+
+    let fromTop = this.padding.top + (this.innerHeight * 2) / 3 - lineHeight;
+
+    (await this.getTimelineEntries()).forEach((entries, traceType) => {
+      this.ctx.globalAlpha = 0.7;
+      this.ctx.fillStyle = TRACE_INFO[traceType].color;
+      this.ctx.strokeStyle = 'blue';
+
+      for (const entry of entries.points) {
+        const width = 5;
+        this.ctx.fillRect(entry - width / 2, fromTop, width, lineHeight);
+      }
+
+      for (const entry of entries.segments) {
+        const width = Math.max(entry.to - entry.from, 3);
+        this.ctx.fillRect(entry.from, fromTop, width, lineHeight);
+      }
+
+      this.ctx.fillStyle = Color.ACTIVE_POINTER;
+      if (entries.activePoint) {
+        const entry = entries.activePoint;
+        const width = 5;
+        this.ctx.fillRect(entry - width / 2, fromTop, width, lineHeight);
+      }
+
+      if (entries.activeSegment) {
+        const entry = entries.activeSegment;
+        const width = Math.max(entry.to - entry.from, 3);
+        this.ctx.fillRect(entry.from, fromTop, width, lineHeight);
+      }
+
+      this.ctx.globalAlpha = 1.0;
+
+      fromTop -= (lineHeight * 4) / 3;
+    });
+  }
+
+  private drawTimelineGuides() {
+    const edgeBarHeight = (this.innerHeight * 1) / 2;
+    const edgeBarWidth = 4;
+
+    const boldBarHeight = (this.innerHeight * 1) / 5;
+    const boldBarWidth = edgeBarWidth;
+
+    const lightBarHeight = (this.innerHeight * 1) / 6;
+    const lightBarWidth = 2;
+
+    const minSpacing = lightBarWidth * 7;
+    const barsInSetWidth = 9 * lightBarWidth + boldBarWidth;
+    const barSets = Math.floor(
+      (this.getWidth() - edgeBarWidth * 2 - minSpacing) / (barsInSetWidth + 10 * minSpacing)
+    );
+    const bars = barSets * 10;
+    const spacing = (this.getWidth() - barSets * barsInSetWidth - edgeBarWidth) / bars;
+    let start = edgeBarWidth + spacing;
+    for (let i = 1; i < bars; i++) {
+      if (i % 10 === 0) {
+        // Draw boldbar
+        this.ctx.fillStyle = Color.GUIDE_BAR;
+        this.ctx.fillRect(
+          start,
+          this.padding.top + this.innerHeight - boldBarHeight,
+          boldBarWidth,
+          boldBarHeight
+        );
+        start += boldBarWidth; // TODO: Shift a bit
+      } else {
+        // Draw lightbar
+        this.ctx.fillStyle = Color.GUIDE_BAR_LIGHT;
+        this.ctx.fillRect(
+          start,
+          this.padding.top + this.innerHeight - lightBarHeight,
+          lightBarWidth,
+          lightBarHeight
+        );
+        start += lightBarWidth;
+      }
+      start += spacing;
+    }
+  }
+}
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_input.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_input.ts
new file mode 100644
index 0000000..9201bc4
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_input.ts
@@ -0,0 +1,125 @@
+import {TimelineData} from 'app/timeline_data';
+import {ElapsedTimestamp, RealTimestamp, TimeRange, Timestamp, TimestampType} from 'common/time';
+import {Transition} from 'flickerlib/common';
+import {Trace, TraceEntry} from 'trace/trace';
+import {Traces} from 'trace/traces';
+import {TraceType} from 'trace/trace_type';
+import {Segment} from '../../utils';
+import {Transformer} from '../transformer';
+import {MiniCanvasDrawerData, TimelineEntries} from './mini_canvas_drawer_data';
+
+export class MiniTimelineDrawerInput {
+  constructor(
+    public fullRange: TimeRange,
+    public selectedPosition: Timestamp,
+    public selection: TimeRange,
+    public zoomRange: TimeRange,
+    public traces: Traces,
+    public timelineData: TimelineData
+  ) {}
+
+  transform(mapToRange: Segment): MiniCanvasDrawerData {
+    const transformer = new Transformer(this.zoomRange, mapToRange);
+
+    return new MiniCanvasDrawerData(
+      transformer.transform(this.selectedPosition),
+      {
+        from: transformer.transform(this.selection.from),
+        to: transformer.transform(this.selection.to),
+      },
+      () => {
+        return this.transformTracesTimestamps(transformer);
+      },
+      transformer
+    );
+  }
+
+  private async transformTracesTimestamps(transformer: Transformer): Promise<TimelineEntries> {
+    const transformedTraceSegments = new Map<
+      TraceType,
+      {
+        points: number[];
+        segments: Segment[];
+        activePoint: number | undefined;
+        activeSegment: Segment | undefined;
+      }
+    >();
+
+    await Promise.all(
+      this.traces.mapTrace(async (trace, type) => {
+        const activeEntry = this.timelineData.findCurrentEntryFor(trace.type);
+
+        if (type === TraceType.TRANSITION) {
+          // Transition trace is a special case, with entries with time ranges
+          const transitionTrace = this.traces.getTrace(type)!;
+          transformedTraceSegments.set(trace.type, {
+            points: [],
+            activePoint: undefined,
+            segments: await this.transformTransitionTraceTimestamps(transformer, transitionTrace),
+            activeSegment: activeEntry
+              ? await this.transformTransitionEntry(transformer, activeEntry)
+              : undefined,
+          });
+        } else {
+          transformedTraceSegments.set(trace.type, {
+            points: this.transformTraceTimestamps(transformer, trace),
+            activePoint: activeEntry
+              ? transformer.transform(activeEntry.getTimestamp())
+              : undefined,
+            segments: [],
+            activeSegment: undefined,
+          });
+        }
+      })
+    );
+
+    return transformedTraceSegments;
+  }
+
+  private async transformTransitionTraceTimestamps(
+    transformer: Transformer,
+    trace: Trace<Transition>
+  ): Promise<Segment[]> {
+    const promises: Array<Promise<Segment | undefined>> = [];
+    trace.forEachEntry((entry) => {
+      promises.push(this.transformTransitionEntry(transformer, entry));
+    });
+
+    return (await Promise.all(promises)).filter((it) => it !== undefined) as Segment[];
+  }
+
+  private async transformTransitionEntry(
+    transformer: Transformer,
+    entry: TraceEntry<Transition>
+  ): Promise<Segment | undefined> {
+    const transition = await entry.getValue();
+    let createTime: Timestamp;
+    let finishTime: Timestamp;
+
+    if (transition.createTime.isMin || transition.finishTime.isMax) {
+      return undefined;
+    }
+
+    if (entry.getTimestamp().getType() === TimestampType.REAL) {
+      createTime = new RealTimestamp(BigInt(transition.createTime.unixNanos.toString()));
+      finishTime = new RealTimestamp(BigInt(transition.finishTime.unixNanos.toString()));
+    } else if (entry.getTimestamp().getType() === TimestampType.ELAPSED) {
+      createTime = new ElapsedTimestamp(BigInt(transition.createTime.elapsedNanos.toString()));
+      finishTime = new ElapsedTimestamp(BigInt(transition.finishTime.elapsedNanos.toString()));
+    } else {
+      throw new Error('Unspported timestamp type');
+    }
+
+    return {from: transformer.transform(createTime), to: transformer.transform(finishTime)};
+  }
+
+  private transformTraceTimestamps(transformer: Transformer, trace: Trace<{}>): number[] {
+    const result: number[] = [];
+
+    trace.forEachTimestamp((timestamp) => {
+      result.push(transformer.transform(timestamp));
+    });
+
+    return result;
+  }
+}
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_output.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_output.ts
new file mode 100644
index 0000000..7a496dc
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_output.ts
@@ -0,0 +1,5 @@
+import {TimeRange, Timestamp} from 'common/time';
+
+export class MiniTimelineDrawerOutput {
+  constructor(public selectedPosition: Timestamp, public selection: TimeRange) {}
+}
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component.ts b/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component.ts
new file mode 100644
index 0000000..d14c060
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component.ts
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2022 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 {
+  Component,
+  ElementRef,
+  EventEmitter,
+  HostListener,
+  Input,
+  Output,
+  SimpleChanges,
+  ViewChild,
+} from '@angular/core';
+import {TimelineData} from 'app/timeline_data';
+import {assertDefined} from 'common/assert_utils';
+import {TimeRange, Timestamp} from 'common/time';
+import {TimeUtils} from 'common/time_utils';
+import {Traces} from 'trace/traces';
+import {TracePosition} from 'trace/trace_position';
+import {TraceType} from 'trace/trace_type';
+import {MiniTimelineDrawer} from './drawer/mini_timeline_drawer';
+import {MiniTimelineDrawerImpl} from './drawer/mini_timeline_drawer_impl';
+import {MiniTimelineDrawerInput} from './drawer/mini_timeline_drawer_input';
+import {Transformer} from './transformer';
+
+@Component({
+  selector: 'mini-timeline',
+  template: `
+    <div id="mini-timeline-wrapper" #miniTimelineWrapper>
+      <canvas #canvas id="mini-timeline-canvas"></canvas>
+      <div class="zoom-control-wrapper">
+        <div class="zoom-control">
+          <div class="zoom-buttons">
+            <button mat-icon-button id="reset-zoom-btn" (click)="resetZoom()">
+              <mat-icon>refresh</mat-icon>
+            </button>
+            <button mat-icon-button id="zoom-in-btn" (click)="zoomIn()">
+              <mat-icon>zoom_in</mat-icon>
+            </button>
+            <button mat-icon-button id="zoom-out-btn" (click)="zoomOut()">
+              <mat-icon>zoom_out</mat-icon>
+            </button>
+          </div>
+          <slider
+            [fullRange]="timelineData.getFullTimeRange()"
+            [zoomRange]="timelineData.getZoomRange()"
+            [currentPosition]="timelineData.getCurrentPosition()"
+            (onZoomChanged)="onZoomChanged($event)"></slider>
+        </div>
+      </div>
+    </div>
+  `,
+  styles: [
+    `
+      #mini-timeline-wrapper {
+        width: 100%;
+        min-height: 5em;
+        height: 100%;
+      }
+      .zoom-control-wrapper {
+        margin-top: -25px;
+        margin-left: -60px;
+        padding-right: 30px;
+      }
+      .zoom-control {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        justify-items: center;
+        gap: 23px;
+      }
+      .zoom-control slider {
+        flex-grow: 1;
+      }
+    `,
+  ],
+})
+export class MiniTimelineComponent {
+  @Input() timelineData!: TimelineData;
+  @Input() currentTracePosition!: TracePosition;
+  @Input() selectedTraces!: TraceType[];
+
+  @Output() onTracePositionUpdate = new EventEmitter<TracePosition>();
+  @Output() onSeekTimestampUpdate = new EventEmitter<Timestamp | undefined>();
+
+  @ViewChild('miniTimelineWrapper', {static: false}) miniTimelineWrapper!: ElementRef;
+  @ViewChild('canvas', {static: false}) canvasRef!: ElementRef;
+  get canvas(): HTMLCanvasElement {
+    return this.canvasRef.nativeElement;
+  }
+
+  drawer: MiniTimelineDrawer | undefined = undefined;
+
+  ngAfterViewInit(): void {
+    this.makeHiPPICanvas();
+
+    const updateTimestampCallback = (timestamp: Timestamp) => {
+      this.onSeekTimestampUpdate.emit(undefined);
+      this.onTracePositionUpdate.emit(this.timelineData.makePositionFromActiveTrace(timestamp));
+    };
+
+    this.drawer = new MiniTimelineDrawerImpl(
+      this.canvas,
+      () => this.getMiniCanvasDrawerInput(),
+      (position) => this.onSeekTimestampUpdate.emit(position),
+      updateTimestampCallback,
+      updateTimestampCallback
+    );
+    this.drawer.draw();
+  }
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (this.drawer !== undefined) {
+      this.drawer.draw();
+    }
+  }
+
+  isZoomed(): boolean {
+    const fullRange = this.timelineData.getFullTimeRange();
+    const zoomRange = this.timelineData.getZoomRange();
+    return fullRange.from !== zoomRange.from || fullRange.to !== zoomRange.to;
+  }
+
+  private getMiniCanvasDrawerInput() {
+    return new MiniTimelineDrawerInput(
+      this.timelineData.getFullTimeRange(),
+      this.currentTracePosition.timestamp,
+      this.timelineData.getSelectionTimeRange(),
+      this.timelineData.getZoomRange(),
+      this.getTracesToShow(),
+      this.timelineData
+    );
+  }
+
+  getTracesToShow(): Traces {
+    const traces = new Traces();
+    this.selectedTraces
+      .filter((type) => this.timelineData.getTraces().getTrace(type) !== undefined)
+      .forEach((type) => {
+        traces.setTrace(type, assertDefined(this.timelineData.getTraces().getTrace(type)));
+      });
+    return traces;
+  }
+
+  private makeHiPPICanvas() {
+    // Reset any size before computing new size to avoid it interfering with size computations
+    this.canvas.width = 0;
+    this.canvas.height = 0;
+    this.canvas.style.width = 'auto';
+    this.canvas.style.height = 'auto';
+
+    const width = this.miniTimelineWrapper.nativeElement.offsetWidth;
+    const height = this.miniTimelineWrapper.nativeElement.offsetHeight;
+
+    const HiPPIwidth = window.devicePixelRatio * width;
+    const HiPPIheight = window.devicePixelRatio * height;
+
+    this.canvas.width = HiPPIwidth;
+    this.canvas.height = HiPPIheight;
+    this.canvas.style.width = width + 'px';
+    this.canvas.style.height = height + 'px';
+
+    // ensure all drawing operations are scaled
+    if (window.devicePixelRatio !== 1) {
+      const context = this.canvas.getContext('2d')!;
+      context.scale(window.devicePixelRatio, window.devicePixelRatio);
+    }
+  }
+
+  @HostListener('window:resize', ['$event'])
+  onResize(event: Event) {
+    this.makeHiPPICanvas();
+    this.drawer?.draw();
+  }
+
+  onZoomChanged(zoom: TimeRange) {
+    this.timelineData.setZoom(zoom);
+    this.timelineData.setSelectionTimeRange(zoom);
+    this.drawer?.draw();
+  }
+
+  resetZoom() {
+    this.onZoomChanged(this.timelineData.getFullTimeRange());
+  }
+
+  zoomIn(zoomOn: Timestamp | undefined = undefined) {
+    this.zoom({nominator: 3n, denominator: 4n}, zoomOn);
+  }
+
+  zoomOut(zoomOn: Timestamp | undefined = undefined) {
+    this.zoom({nominator: 5n, denominator: 4n}, zoomOn);
+  }
+
+  zoom(
+    zoomRatio: {nominator: bigint; denominator: bigint},
+    zoomOn: Timestamp | undefined = undefined
+  ) {
+    const fullRange = this.timelineData.getFullTimeRange();
+    const currentZoomRange = this.timelineData.getZoomRange();
+    const currentZoomWidth = currentZoomRange.to.minus(currentZoomRange.from);
+    const zoomToWidth = currentZoomWidth.times(zoomRatio.nominator).div(zoomRatio.denominator);
+
+    const cursorPosition = this.timelineData.getCurrentPosition()?.timestamp;
+    const currentMiddle = currentZoomRange.from.plus(currentZoomRange.to).div(2n);
+
+    let newFrom: Timestamp;
+    let newTo: Timestamp;
+    if (zoomOn === undefined) {
+      let zoomTowards = currentMiddle;
+      if (cursorPosition !== undefined && cursorPosition.in(currentZoomRange)) {
+        zoomTowards = cursorPosition;
+      }
+
+      let leftAdjustment;
+      let rightAdjustment;
+      if (zoomTowards.getValueNs() < currentMiddle.getValueNs()) {
+        leftAdjustment = currentZoomWidth.times(0n);
+        rightAdjustment = currentZoomWidth
+          .times(zoomRatio.denominator - zoomRatio.nominator)
+          .div(zoomRatio.denominator);
+      } else {
+        leftAdjustment = currentZoomWidth
+          .times(zoomRatio.denominator - zoomRatio.nominator)
+          .div(zoomRatio.denominator);
+        rightAdjustment = currentZoomWidth.times(0n);
+      }
+
+      newFrom = currentZoomRange.from.plus(leftAdjustment);
+      newTo = currentZoomRange.to.minus(rightAdjustment);
+      const newMiddle = newFrom.plus(newTo).div(2n);
+
+      if (
+        (zoomTowards.getValueNs() <= currentMiddle.getValueNs() &&
+          newMiddle.getValueNs() < zoomTowards.getValueNs()) ||
+        (zoomTowards.getValueNs() >= currentMiddle.getValueNs() &&
+          newMiddle.getValueNs() > zoomTowards.getValueNs())
+      ) {
+        // Moved past middle, so ensure cursor is in the middle
+        newFrom = zoomTowards.minus(zoomToWidth.div(2n));
+        newTo = zoomTowards.plus(zoomToWidth.div(2n));
+      }
+    } else {
+      newFrom = zoomOn.minus(zoomToWidth.div(2n));
+      newTo = zoomOn.plus(zoomToWidth.div(2n));
+    }
+
+    if (newFrom.getValueNs() < fullRange.from.getValueNs()) {
+      newTo = TimeUtils.min(fullRange.to, newTo.plus(fullRange.from.minus(newFrom)));
+      newFrom = fullRange.from;
+    }
+
+    if (newTo.getValueNs() > fullRange.to.getValueNs()) {
+      newFrom = TimeUtils.max(fullRange.from, newFrom.minus(newTo.minus(fullRange.to)));
+      newTo = fullRange.to;
+    }
+
+    this.onZoomChanged({
+      from: newFrom,
+      to: newTo,
+    });
+  }
+
+  // -1 for x direction, 1 for y direction
+  private lastMoves: WheelEvent[] = [];
+  @HostListener('wheel', ['$event'])
+  onScroll(event: WheelEvent) {
+    this.lastMoves.push(event);
+    setTimeout(() => this.lastMoves.shift(), 1000);
+
+    const xMoveAmount = this.lastMoves.reduce((accumulator, it) => accumulator + it.deltaX, 0);
+    const yMoveAmount = this.lastMoves.reduce((accumulator, it) => accumulator + it.deltaY, 0);
+
+    let moveDirection: 'x' | 'y';
+    if (Math.abs(yMoveAmount) > Math.abs(xMoveAmount)) {
+      moveDirection = 'y';
+    } else {
+      moveDirection = 'x';
+    }
+
+    if (
+      (event.target as any)?.id === 'mini-timeline-canvas' &&
+      event.deltaY !== 0 &&
+      moveDirection === 'y'
+    ) {
+      // Zooming
+      const canvas = event.target as HTMLCanvasElement;
+      const xPosInCanvas = event.x - canvas.offsetLeft;
+      const zoomRange = this.timelineData.getZoomRange();
+
+      const zoomTo = new Transformer(zoomRange, assertDefined(this.drawer).usableRange).untransform(
+        xPosInCanvas
+      );
+
+      if (event.deltaY < 0) {
+        this.zoomIn(zoomTo);
+      } else {
+        this.zoomOut(zoomTo);
+      }
+    }
+
+    if (event.deltaX !== 0 && moveDirection === 'x') {
+      // Horizontal scrolling
+      const scrollAmount = event.deltaX;
+      const fullRange = this.timelineData.getFullTimeRange();
+      const zoomRange = this.timelineData.getZoomRange();
+
+      const usableRange = assertDefined(this.drawer).usableRange;
+      const transformer = new Transformer(zoomRange, usableRange);
+      const shiftAmount = transformer
+        .untransform(usableRange.from + scrollAmount)
+        .minus(zoomRange.from);
+      let newFrom = zoomRange.from.plus(shiftAmount);
+      let newTo = zoomRange.to.plus(shiftAmount);
+
+      if (newFrom.getValueNs() < fullRange.from.getValueNs()) {
+        newTo = newTo.plus(fullRange.from.minus(newFrom));
+        newFrom = fullRange.from;
+      }
+
+      if (newTo.getValueNs() > fullRange.to.getValueNs()) {
+        newFrom = newFrom.minus(newTo.minus(fullRange.to));
+        newTo = fullRange.to;
+      }
+
+      this.onZoomChanged({
+        from: newFrom,
+        to: newTo,
+      });
+    }
+  }
+}
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component_test.ts b/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component_test.ts
new file mode 100644
index 0000000..3859a62
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component_test.ts
@@ -0,0 +1,437 @@
+/*
+ * 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 {DragDropModule} from '@angular/cdk/drag-drop';
+import {ChangeDetectionStrategy} from '@angular/core';
+import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {MatButtonModule} from '@angular/material/button';
+import {MatFormFieldModule} from '@angular/material/form-field';
+import {MatIconModule} from '@angular/material/icon';
+import {MatInputModule} from '@angular/material/input';
+import {MatSelectModule} from '@angular/material/select';
+import {MatTooltipModule} from '@angular/material/tooltip';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {TimelineData} from 'app/timeline_data';
+import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp} from 'common/time';
+import {TracesBuilder} from 'test/unit/traces_builder';
+import {dragElement} from 'test/utils';
+import {TracePosition} from 'trace/trace_position';
+import {TraceType} from 'trace/trace_type';
+import {MiniTimelineComponent} from './mini_timeline_component';
+import {SliderComponent} from './slider_component';
+
+describe('MiniTimelineComponent', () => {
+  let fixture: ComponentFixture<MiniTimelineComponent>;
+  let component: MiniTimelineComponent;
+  let htmlElement: HTMLElement;
+  let timelineData: TimelineData;
+
+  const timestamp10 = new RealTimestamp(10n);
+  const timestamp20 = new RealTimestamp(20n);
+  const timestamp1000 = new RealTimestamp(1000n);
+
+  const position800 = TracePosition.fromTimestamp(new RealTimestamp(800n));
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [
+        FormsModule,
+        MatButtonModule,
+        MatFormFieldModule,
+        MatInputModule,
+        MatIconModule,
+        MatSelectModule,
+        MatTooltipModule,
+        ReactiveFormsModule,
+        BrowserAnimationsModule,
+        DragDropModule,
+      ],
+      declarations: [MiniTimelineComponent, SliderComponent],
+    })
+      .overrideComponent(MiniTimelineComponent, {
+        set: {changeDetection: ChangeDetectionStrategy.Default},
+      })
+      .compileComponents();
+    fixture = TestBed.createComponent(MiniTimelineComponent);
+    component = fixture.componentInstance;
+    htmlElement = fixture.nativeElement;
+
+    timelineData = new TimelineData();
+    const traces = new TracesBuilder()
+      .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
+      .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp20])
+      .build();
+    timelineData.initialize(traces, undefined);
+    component.timelineData = timelineData;
+    expect(timelineData.getCurrentPosition()).toBeTruthy();
+    component.currentTracePosition = timelineData.getCurrentPosition()!;
+    component.selectedTraces = [TraceType.SURFACE_FLINGER];
+
+    fixture.detectChanges();
+  });
+
+  it('can be created', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('redraws on resize', () => {
+    const spy = spyOn(assertDefined(component.drawer), 'draw');
+    expect(spy).not.toHaveBeenCalled();
+
+    component.onResize({} as Event);
+
+    expect(spy).toHaveBeenCalled();
+  });
+
+  it('resets zoom on reset zoom button click', () => {
+    const expectedZoomRange = {
+      from: new RealTimestamp(15n),
+      to: new RealTimestamp(16n),
+    };
+    timelineData.setZoom(expectedZoomRange);
+
+    let zoomRange = timelineData.getZoomRange();
+    let fullRange = timelineData.getFullTimeRange();
+    expect(zoomRange).toBe(expectedZoomRange);
+    expect(fullRange.from).toBe(timestamp10);
+    expect(fullRange.to).toBe(timestamp20);
+
+    fixture.detectChanges();
+
+    const zoomButton = htmlElement.querySelector('button#reset-zoom-btn') as HTMLButtonElement;
+    expect(zoomButton).toBeTruthy();
+    assertDefined(zoomButton).click();
+
+    zoomRange = timelineData.getZoomRange();
+    fullRange = timelineData.getFullTimeRange();
+    expect(zoomRange).toBe(fullRange);
+  });
+
+  it('show zoom controls when zoomed out', () => {
+    const zoomControlDiv = htmlElement.querySelector('.zoom-control');
+    expect(zoomControlDiv).toBeTruthy();
+    expect(window.getComputedStyle(assertDefined(zoomControlDiv)).visibility).toBe('visible');
+
+    const zoomButton = htmlElement.querySelector('button#reset-zoom-btn') as HTMLButtonElement;
+    expect(zoomButton).toBeTruthy();
+    expect(window.getComputedStyle(assertDefined(zoomButton)).visibility).toBe('visible');
+  });
+
+  it('shows zoom controls when zoomed in', () => {
+    const zoom = {
+      from: new RealTimestamp(15n),
+      to: new RealTimestamp(16n),
+    };
+    timelineData.setZoom(zoom);
+
+    fixture.detectChanges();
+
+    const zoomControlDiv = htmlElement.querySelector('.zoom-control');
+    expect(zoomControlDiv).toBeTruthy();
+    expect(window.getComputedStyle(assertDefined(zoomControlDiv)).visibility).toBe('visible');
+
+    const zoomButton = htmlElement.querySelector('button#reset-zoom-btn') as HTMLButtonElement;
+    expect(zoomButton).toBeTruthy();
+    expect(window.getComputedStyle(assertDefined(zoomButton)).visibility).toBe('visible');
+  });
+
+  it('loads zoomed out', () => {
+    expect(component.isZoomed()).toBeFalse();
+  });
+
+  it('updates timelinedata on zoom changed', () => {
+    const zoom = {
+      from: new RealTimestamp(15n),
+      to: new RealTimestamp(16n),
+    };
+    component.onZoomChanged(zoom);
+    expect(timelineData.getZoomRange()).toBe(zoom);
+  });
+
+  it('creates an appropriately sized canvas', () => {
+    expect(component.canvas.width).toBeGreaterThan(100);
+    expect(component.canvas.height).toBeGreaterThan(10);
+  });
+
+  it('getTracesToShow returns traces targeted by selectedTraces', () => {
+    const traces = component.getTracesToShow();
+    const types: TraceType[] = [];
+    traces.forEachTrace((trace) => {
+      types.push(trace.type);
+    });
+    expect(types).toHaveSize(component.selectedTraces.length);
+    for (const type of component.selectedTraces) {
+      expect(types).toContain(type);
+    }
+  });
+
+  it('moving slider around updates zoom', fakeAsync(async () => {
+    const initialZoom = {
+      from: new RealTimestamp(15n),
+      to: new RealTimestamp(16n),
+    };
+    component.onZoomChanged(initialZoom);
+
+    fixture.detectChanges();
+
+    const slider = htmlElement.querySelector('.slider .handle');
+    expect(slider).toBeTruthy();
+    expect(window.getComputedStyle(assertDefined(slider)).visibility).toBe('visible');
+
+    dragElement(fixture, assertDefined(slider), 100, 8);
+
+    const finalZoom = timelineData.getZoomRange();
+    expect(finalZoom).not.toBe(initialZoom);
+  }));
+
+  it('zoom button zooms onto cursor', () => {
+    const traces = new TracesBuilder()
+      .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
+      .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
+      .build();
+
+    component.timelineData.initialize(traces, undefined);
+
+    let initialZoom = {
+      from: timestamp10,
+      to: timestamp1000,
+    };
+    component.onZoomChanged(initialZoom);
+
+    component.timelineData.setPosition(position800);
+    const cursorPos = position800.timestamp.getValueNs();
+
+    fixture.detectChanges();
+
+    const zoomButton = htmlElement.querySelector('#zoom-in-btn') as HTMLButtonElement;
+    expect(zoomButton).toBeTruthy();
+
+    for (let i = 0; i < 10; i++) {
+      zoomButton.click();
+      fixture.detectChanges();
+      const finalZoom = timelineData.getZoomRange();
+      expect(finalZoom).not.toBe(initialZoom);
+      expect(finalZoom.from.getValueNs()).toBeGreaterThanOrEqual(
+        Number(initialZoom.from.getValueNs())
+      );
+      expect(finalZoom.to.getValueNs()).toBeLessThanOrEqual(Number(initialZoom.to.getValueNs()));
+      expect(finalZoom.to.minus(finalZoom.from).getValueNs()).toBeLessThan(
+        Number(initialZoom.to.minus(initialZoom.from).getValueNs())
+      );
+
+      // center to get closer to cursor or stay on cursor
+      const curCenter = finalZoom.from.plus(finalZoom.to).div(2n).getValueNs();
+      const prevCenter = initialZoom.from.plus(initialZoom.to).div(2n).getValueNs();
+
+      if (prevCenter === position800.timestamp.getValueNs()) {
+        expect(curCenter).toBe(prevCenter);
+      } else {
+        expect(Math.abs(Number(curCenter - cursorPos))).toBeLessThan(
+          Math.abs(Number(prevCenter - cursorPos))
+        );
+      }
+
+      initialZoom = finalZoom;
+    }
+  });
+
+  it('can zoom out with the buttons', () => {
+    const traces = new TracesBuilder()
+      .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
+      .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
+      .build();
+
+    component.timelineData.initialize(traces, undefined);
+
+    let initialZoom = {
+      from: new RealTimestamp(700n),
+      to: new RealTimestamp(810n),
+    };
+    component.onZoomChanged(initialZoom);
+
+    component.timelineData.setPosition(position800);
+    const cursorPos = position800.timestamp.getValueNs();
+
+    fixture.detectChanges();
+
+    const zoomButton = htmlElement.querySelector('#zoom-out-btn') as HTMLButtonElement;
+    expect(zoomButton).toBeTruthy();
+
+    for (let i = 0; i < 10; i++) {
+      zoomButton.click();
+      fixture.detectChanges();
+      const finalZoom = timelineData.getZoomRange();
+      expect(finalZoom).not.toBe(initialZoom);
+      expect(finalZoom.from.getValueNs()).toBeLessThanOrEqual(
+        Number(initialZoom.from.getValueNs())
+      );
+      expect(finalZoom.to.getValueNs()).toBeGreaterThanOrEqual(Number(initialZoom.to.getValueNs()));
+      expect(finalZoom.to.minus(finalZoom.from).getValueNs()).toBeGreaterThan(
+        Number(initialZoom.to.minus(initialZoom.from).getValueNs())
+      );
+
+      // center to get closer to cursor or stay on cursor unless we reach the edge
+      const curCenter = finalZoom.from.plus(finalZoom.to).div(2n).getValueNs();
+      const prevCenter = initialZoom.from.plus(initialZoom.to).div(2n).getValueNs();
+
+      if (
+        finalZoom.from.getValueNs() === timestamp10.getValueNs() ||
+        finalZoom.to.getValueNs() === timestamp1000.getValueNs()
+      ) {
+        // No checks as cursor will stop being more centered
+      } else if (prevCenter === cursorPos) {
+        expect(curCenter).toBe(prevCenter);
+      } else {
+        expect(Math.abs(Number(curCenter - cursorPos))).toBeGreaterThan(
+          Math.abs(Number(prevCenter - cursorPos))
+        );
+      }
+
+      initialZoom = finalZoom;
+    }
+  });
+
+  it('can not zoom out past full range', () => {
+    const traces = new TracesBuilder()
+      .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
+      .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
+      .build();
+
+    component.timelineData.initialize(traces, undefined);
+
+    const initialZoom = {
+      from: timestamp10,
+      to: timestamp1000,
+    };
+    component.onZoomChanged(initialZoom);
+
+    component.timelineData.setPosition(position800);
+    const cursorPos = position800.timestamp.getValueNs();
+
+    fixture.detectChanges();
+
+    const zoomButton = htmlElement.querySelector('#zoom-out-btn') as HTMLButtonElement;
+    expect(zoomButton).toBeTruthy();
+
+    zoomButton.click();
+    fixture.detectChanges();
+    const finalZoom = timelineData.getZoomRange();
+
+    expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs());
+    expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs());
+  });
+
+  it('zooms in with scroll wheel', () => {
+    const traces = new TracesBuilder()
+      .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
+      .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
+      .build();
+
+    component.timelineData.initialize(traces, undefined);
+
+    let initialZoom = {
+      from: timestamp10,
+      to: timestamp1000,
+    };
+    component.onZoomChanged(initialZoom);
+
+    fixture.detectChanges();
+
+    for (let i = 0; i < 10; i++) {
+      component.onScroll({
+        deltaY: -200,
+        deltaX: 0,
+        x: 10, // scrolling on pos
+        target: {id: 'mini-timeline-canvas', offsetLeft: 0},
+      } as any as WheelEvent);
+
+      fixture.detectChanges();
+      const finalZoom = timelineData.getZoomRange();
+      expect(finalZoom).not.toBe(initialZoom);
+      expect(finalZoom.to.minus(finalZoom.from).getValueNs()).toBeLessThan(
+        Number(initialZoom.to.minus(initialZoom.from).getValueNs())
+      );
+
+      initialZoom = finalZoom;
+    }
+  });
+
+  it('zooms out with scroll wheel', () => {
+    const traces = new TracesBuilder()
+      .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
+      .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
+      .build();
+
+    component.timelineData.initialize(traces, undefined);
+
+    let initialZoom = {
+      from: new RealTimestamp(700n),
+      to: new RealTimestamp(810n),
+    };
+    component.onZoomChanged(initialZoom);
+
+    fixture.detectChanges();
+
+    for (let i = 0; i < 10; i++) {
+      component.onScroll({
+        deltaY: 200,
+        deltaX: 0,
+        x: 10, // scrolling on pos
+        target: {id: 'mini-timeline-canvas', offsetLeft: 0},
+      } as any as WheelEvent);
+
+      fixture.detectChanges();
+      const finalZoom = timelineData.getZoomRange();
+      expect(finalZoom).not.toBe(initialZoom);
+      expect(finalZoom.to.minus(finalZoom.from).getValueNs()).toBeGreaterThan(
+        Number(initialZoom.to.minus(initialZoom.from).getValueNs())
+      );
+
+      initialZoom = finalZoom;
+    }
+  });
+
+  it('cannot zoom out past full range', () => {
+    const traces = new TracesBuilder()
+      .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
+      .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
+      .build();
+
+    component.timelineData.initialize(traces, undefined);
+
+    const initialZoom = {
+      from: timestamp10,
+      to: timestamp1000,
+    };
+    component.onZoomChanged(initialZoom);
+
+    component.onScroll({
+      deltaY: 1000,
+      deltaX: 0,
+      x: 10, // scrolling on pos
+      target: {id: 'mini-timeline-canvas', offsetLeft: 0},
+    } as any as WheelEvent);
+
+    fixture.detectChanges();
+
+    const finalZoom = timelineData.getZoomRange();
+
+    expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs());
+    expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs());
+  });
+});
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/slider_component.ts b/tools/winscope/src/app/components/timeline/mini-timeline/slider_component.ts
new file mode 100644
index 0000000..82dc303
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/slider_component.ts
@@ -0,0 +1,293 @@
+/*
+ * 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 {
+  ChangeDetectorRef,
+  Component,
+  ElementRef,
+  EventEmitter,
+  HostListener,
+  Inject,
+  Input,
+  Output,
+  SimpleChanges,
+  ViewChild,
+} from '@angular/core';
+import {Color} from 'app/colors';
+import {assertDefined} from 'common/assert_utils';
+import {TimeRange, Timestamp} from 'common/time';
+import {TracePosition} from 'trace/trace_position';
+import {Transformer} from './transformer';
+
+@Component({
+  selector: 'slider',
+  template: `
+    <div id="timeline-slider-box" #sliderBox>
+      <div class="background line"></div>
+      <div
+        class="slider"
+        cdkDragLockAxis="x"
+        cdkDragBoundary="#timeline-slider-box"
+        cdkDrag
+        (cdkDragMoved)="onSliderMove($event)"
+        (cdkDragStarted)="onSlideStart($event)"
+        (cdkDragEnded)="onSlideEnd($event)"
+        [cdkDragFreeDragPosition]="dragPosition"
+        [style]="{width: sliderWidth + 'px'}">
+        <div class="left cropper" (mousedown)="startMoveLeft($event)"></div>
+        <div class="handle" cdkDragHandle></div>
+        <div class="right cropper" (mousedown)="startMoveRight($event)"></div>
+      </div>
+      <div class="cursor" [style]="{left: cursorOffset + 'px'}"></div>
+    </div>
+  `,
+  styles: [
+    `
+      #timeline-slider-box {
+        position: relative;
+        margin: 5px 0;
+      }
+
+      #timeline-slider-box,
+      .slider {
+        height: 10px;
+      }
+
+      .line {
+        height: 3px;
+        position: absolute;
+        margin: auto;
+        top: 0;
+        bottom: 0;
+        margin: auto 0;
+      }
+
+      .background.line {
+        width: 100%;
+        background: ${Color.GUIDE_BAR_LIGHT};
+      }
+
+      .selection.line {
+        background: ${Color.SELECTOR_COLOR};
+      }
+
+      .slider {
+        display: flex;
+        justify-content: space-between;
+        cursor: grab;
+        position: absolute;
+      }
+
+      .handle {
+        flex-grow: 1;
+        background: ${Color.SELECTION_BACKGROUND};
+        cursor: grab;
+      }
+
+      .cropper {
+        width: 5px;
+        background: ${Color.SELECTOR_COLOR};
+      }
+
+      .cropper.left,
+      .cropper.right {
+        cursor: ew-resize;
+      }
+
+      .cursor {
+        width: 2px;
+        height: 100%;
+        position: absolute;
+        pointer-events: none;
+        background: ${Color.ACTIVE_POINTER};
+      }
+    `,
+  ],
+})
+export class SliderComponent {
+  @Input() fullRange: TimeRange | undefined;
+  @Input() zoomRange: TimeRange | undefined;
+  @Input() currentPosition: TracePosition | undefined;
+
+  @Output() onZoomChanged = new EventEmitter<TimeRange>();
+
+  dragging = false;
+  sliderWidth = 0;
+  dragPosition = {x: 0, y: 0};
+  viewInitialized = false;
+  cursorOffset = 0;
+
+  @ViewChild('sliderBox', {static: false}) sliderBox!: ElementRef;
+
+  constructor(@Inject(ChangeDetectorRef) private cdr: ChangeDetectorRef) {}
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (changes['zoomRange'] !== undefined && !this.dragging) {
+      const zoomRange = changes['zoomRange'].currentValue as TimeRange;
+      this.syncDragPositionTo(zoomRange);
+    }
+
+    if (changes['currentPosition']) {
+      const currentPosition = changes['currentPosition'].currentValue as TracePosition;
+      this.syncCursosPositionTo(currentPosition.timestamp);
+    }
+  }
+
+  syncDragPositionTo(zoomRange: TimeRange) {
+    this.sliderWidth = this.computeSliderWidth();
+    const middleOfZoomRange = zoomRange.from.plus(zoomRange.to.minus(zoomRange.from).div(2n));
+
+    this.dragPosition = {
+      // Calculation to account for there being a min width of the slider
+      x: this.getTransformer().transform(middleOfZoomRange) - this.sliderWidth / 2,
+      y: 0,
+    };
+  }
+
+  syncCursosPositionTo(timestamp: Timestamp) {
+    this.cursorOffset = this.getTransformer().transform(timestamp);
+  }
+
+  getTransformer(): Transformer {
+    const width = this.viewInitialized ? this.sliderBox.nativeElement.offsetWidth : 0;
+    return new Transformer(assertDefined(this.fullRange), {from: 0, to: width});
+  }
+
+  ngAfterViewInit(): void {
+    this.viewInitialized = true;
+  }
+
+  ngAfterViewChecked() {
+    assertDefined(this.fullRange);
+    const zoomRange = assertDefined(this.zoomRange);
+    this.syncDragPositionTo(zoomRange);
+    this.cdr.detectChanges();
+  }
+
+  @HostListener('window:resize', ['$event'])
+  onResize(event: Event) {
+    this.syncDragPositionTo(assertDefined(this.zoomRange));
+    this.syncCursosPositionTo(assertDefined(this.currentPosition).timestamp);
+  }
+
+  computeSliderWidth() {
+    const transformer = this.getTransformer();
+    let width =
+      transformer.transform(assertDefined(this.zoomRange).to) -
+      transformer.transform(assertDefined(this.zoomRange).from);
+    if (width < MIN_SLIDER_WIDTH) {
+      width = MIN_SLIDER_WIDTH;
+    }
+
+    return width;
+  }
+
+  slideStartX: number | undefined = undefined;
+  onSlideStart(e: any) {
+    this.dragging = true;
+    this.slideStartX = e.source.freeDragPosition.x;
+    document.body.classList.add('inheritCursors');
+    document.body.style.cursor = 'grabbing';
+  }
+
+  onSlideEnd(e: any) {
+    this.dragging = false;
+    this.slideStartX = undefined;
+    this.syncDragPositionTo(assertDefined(this.zoomRange));
+    document.body.classList.remove('inheritCursors');
+    document.body.style.cursor = 'unset';
+  }
+
+  onSliderMove(e: any) {
+    const zoomRange = assertDefined(this.zoomRange);
+    let newX = this.slideStartX + e.distance.x;
+    if (newX < 0) {
+      newX = 0;
+    }
+
+    // Calculation to adjust for min width slider
+    const from = this.getTransformer()
+      .untransform(newX + this.sliderWidth / 2)
+      .minus(zoomRange.to.minus(zoomRange.from).div(2n));
+
+    const to = new Timestamp(
+      assertDefined(this.zoomRange).to.getType(),
+      from.getValueNs() +
+        (assertDefined(this.zoomRange).to.getValueNs() -
+          assertDefined(this.zoomRange).from.getValueNs())
+    );
+
+    this.onZoomChanged.emit({from, to});
+  }
+
+  startMoveLeft(e: any) {
+    e.preventDefault();
+
+    const startPos = e.pageX;
+    const startOffset = this.getTransformer().transform(assertDefined(this.zoomRange).from);
+
+    const listener = (event: any) => {
+      const movedX = event.pageX - startPos;
+      let from = this.getTransformer().untransform(startOffset + movedX);
+      if (from.getValueNs() < assertDefined(this.fullRange).from.getValueNs()) {
+        from = assertDefined(this.fullRange).from;
+      }
+      if (from.getValueNs() > assertDefined(this.zoomRange).to.getValueNs()) {
+        from = assertDefined(this.zoomRange).to;
+      }
+      const to = assertDefined(this.zoomRange).to;
+
+      this.onZoomChanged.emit({from, to});
+    };
+    addEventListener('mousemove', listener);
+
+    const mouseUpListener = () => {
+      removeEventListener('mousemove', listener);
+      removeEventListener('mouseup', mouseUpListener);
+    };
+    addEventListener('mouseup', mouseUpListener);
+  }
+
+  startMoveRight(e: any) {
+    e.preventDefault();
+
+    const startPos = e.pageX;
+    const startOffset = this.getTransformer().transform(assertDefined(this.zoomRange).to);
+
+    const listener = (event: any) => {
+      const movedX = event.pageX - startPos;
+      const from = assertDefined(this.zoomRange).from;
+      let to = this.getTransformer().untransform(startOffset + movedX);
+      if (to.getValueNs() > assertDefined(this.fullRange).to.getValueNs()) {
+        to = assertDefined(this.fullRange).to;
+      }
+      if (to.getValueNs() < assertDefined(this.zoomRange).from.getValueNs()) {
+        to = assertDefined(this.zoomRange).from;
+      }
+
+      this.onZoomChanged.emit({from, to});
+    };
+    addEventListener('mousemove', listener);
+
+    const mouseUpListener = () => {
+      removeEventListener('mousemove', listener);
+      removeEventListener('mouseup', mouseUpListener);
+    };
+    addEventListener('mouseup', mouseUpListener);
+  }
+}
+
+export const MIN_SLIDER_WIDTH = 50;
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/slider_component_test.ts b/tools/winscope/src/app/components/timeline/mini-timeline/slider_component_test.ts
new file mode 100644
index 0000000..69ffd29
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/slider_component_test.ts
@@ -0,0 +1,360 @@
+/*
+ * 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 {DragDropModule} from '@angular/cdk/drag-drop';
+import {ChangeDetectionStrategy} from '@angular/core';
+import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {MatButtonModule} from '@angular/material/button';
+import {MatFormFieldModule} from '@angular/material/form-field';
+import {MatIconModule} from '@angular/material/icon';
+import {MatInputModule} from '@angular/material/input';
+import {MatSelectModule} from '@angular/material/select';
+import {MatTooltipModule} from '@angular/material/tooltip';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp, TimeRange} from 'common/time';
+import {dragElement} from 'test/utils';
+import {TracePosition} from 'trace/trace_position';
+import {MIN_SLIDER_WIDTH, SliderComponent} from './slider_component';
+
+describe('SliderComponent', () => {
+  let fixture: ComponentFixture<SliderComponent>;
+  let component: SliderComponent;
+  let htmlElement: HTMLElement;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [
+        FormsModule,
+        MatButtonModule,
+        MatFormFieldModule,
+        MatInputModule,
+        MatIconModule,
+        MatSelectModule,
+        MatTooltipModule,
+        ReactiveFormsModule,
+        BrowserAnimationsModule,
+        DragDropModule,
+      ],
+      declarations: [SliderComponent],
+    })
+      .overrideComponent(SliderComponent, {
+        set: {changeDetection: ChangeDetectionStrategy.Default},
+      })
+      .compileComponents();
+    fixture = TestBed.createComponent(SliderComponent);
+    component = fixture.componentInstance;
+    htmlElement = fixture.nativeElement;
+
+    component.fullRange = {
+      from: new RealTimestamp(100n),
+      to: new RealTimestamp(200n),
+    };
+    component.zoomRange = {
+      from: new RealTimestamp(125n),
+      to: new RealTimestamp(175n),
+    };
+    component.currentPosition = TracePosition.fromTimestamp(new RealTimestamp(150n));
+
+    fixture.detectChanges();
+  });
+
+  it('can be created', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('reposition properly on zoom', () => {
+    fixture.detectChanges();
+    component.ngOnChanges({
+      zoomRange: {
+        firstChange: true,
+        isFirstChange: () => true,
+        previousValue: undefined,
+        currentValue: component.zoomRange,
+      },
+    });
+    fixture.detectChanges();
+
+    const sliderWitdth = component.sliderBox.nativeElement.offsetWidth;
+    expect(component.sliderWidth).toBe(sliderWitdth / 2);
+    expect(component.dragPosition.x).toBe(sliderWitdth / 4);
+  });
+
+  it('has min width', () => {
+    component.fullRange = {
+      from: new RealTimestamp(100n),
+      to: new RealTimestamp(200n),
+    };
+    component.zoomRange = {
+      from: new RealTimestamp(125n),
+      to: new RealTimestamp(126n),
+    };
+
+    fixture.detectChanges();
+    component.ngOnChanges({
+      zoomRange: {
+        firstChange: true,
+        isFirstChange: () => true,
+        previousValue: undefined,
+        currentValue: component.zoomRange,
+      },
+    });
+    fixture.detectChanges();
+
+    const sliderWidth = component.sliderBox.nativeElement.offsetWidth;
+    expect(component.sliderWidth).toBe(MIN_SLIDER_WIDTH);
+    expect(component.dragPosition.x).toBe(sliderWidth / 4 - MIN_SLIDER_WIDTH / 2);
+  });
+
+  it('repositions slider on resize', () => {
+    const slider = assertDefined(htmlElement.querySelector('.slider'));
+    const cursor = assertDefined(htmlElement.querySelector('.cursor'));
+
+    fixture.detectChanges();
+
+    const initialSliderXPos = slider.getBoundingClientRect().left;
+    const initialCursorXPos = cursor.getBoundingClientRect().left;
+
+    spyOnProperty(component.sliderBox.nativeElement, 'offsetWidth', 'get').and.returnValue(100);
+    expect(component.sliderBox.nativeElement.offsetWidth).toBe(100);
+
+    htmlElement.style.width = '587px';
+    component.onResize({} as Event);
+    fixture.detectChanges();
+
+    expect(initialSliderXPos).not.toBe(slider.getBoundingClientRect().left);
+    expect(initialCursorXPos).not.toBe(cursor.getBoundingClientRect().left);
+  });
+
+  it('draws current position cursor', () => {
+    fixture.detectChanges();
+    component.ngOnChanges({
+      currentPosition: {
+        firstChange: true,
+        isFirstChange: () => true,
+        previousValue: undefined,
+        currentValue: component.currentPosition,
+      },
+    });
+    fixture.detectChanges();
+
+    const sliderBox = assertDefined(htmlElement.querySelector('#timeline-slider-box'));
+    const cursor = assertDefined(htmlElement.querySelector('.cursor'));
+    const sliderBoxRect = sliderBox.getBoundingClientRect();
+    expect(cursor.getBoundingClientRect().left).toBe(
+      (sliderBoxRect.left + sliderBoxRect.right) / 2
+    );
+  });
+
+  it('moving slider around updates zoom', fakeAsync(async () => {
+    fixture.detectChanges();
+
+    const initialZoom = assertDefined(component.zoomRange);
+
+    let lastZoomUpdate: TimeRange | undefined = undefined;
+    const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+      lastZoomUpdate = zoom;
+    });
+
+    const slider = htmlElement.querySelector('.slider .handle');
+    expect(slider).toBeTruthy();
+    expect(window.getComputedStyle(assertDefined(slider)).visibility).toBe('visible');
+
+    dragElement(fixture, assertDefined(slider), 100, 8);
+
+    expect(zoomChangedSpy).toHaveBeenCalled();
+
+    const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+    expect(finalZoom.from).not.toBe(initialZoom.from);
+    expect(finalZoom.to).not.toBe(initialZoom.to);
+    expect(finalZoom.to.minus(finalZoom.from).getValueNs()).toBe(
+      initialZoom.to.minus(initialZoom.from).getValueNs()
+    );
+  }));
+
+  it('moving slider left pointer around updates zoom', fakeAsync(async () => {
+    fixture.detectChanges();
+
+    const initialZoom = assertDefined(component.zoomRange);
+
+    let lastZoomUpdate: TimeRange | undefined = undefined;
+    const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+      lastZoomUpdate = zoom;
+    });
+
+    const leftCropper = htmlElement.querySelector('.slider .cropper.left');
+    expect(leftCropper).toBeTruthy();
+    expect(window.getComputedStyle(assertDefined(leftCropper)).visibility).toBe('visible');
+
+    dragElement(fixture, assertDefined(leftCropper), 5, 0);
+
+    expect(zoomChangedSpy).toHaveBeenCalled();
+
+    const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+    expect(finalZoom.from).not.toBe(initialZoom.from);
+    expect(finalZoom.to).toBe(initialZoom.to);
+  }));
+
+  it('moving slider right pointer around updates zoom', fakeAsync(async () => {
+    fixture.detectChanges();
+
+    const initialZoom = assertDefined(component.zoomRange);
+
+    let lastZoomUpdate: TimeRange | undefined = undefined;
+    const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+      lastZoomUpdate = zoom;
+    });
+
+    const rightCropper = htmlElement.querySelector('.slider .cropper.right');
+    expect(rightCropper).toBeTruthy();
+    expect(window.getComputedStyle(assertDefined(rightCropper)).visibility).toBe('visible');
+
+    dragElement(fixture, assertDefined(rightCropper), 5, 0);
+
+    expect(zoomChangedSpy).toHaveBeenCalled();
+
+    const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+    expect(finalZoom.from).toBe(initialZoom.from);
+    expect(finalZoom.to).not.toBe(initialZoom.to);
+  }));
+
+  it('cannot slide left cropper past edges', fakeAsync(() => {
+    component.zoomRange = component.fullRange;
+    fixture.detectChanges();
+
+    const initialZoom = assertDefined(component.zoomRange);
+
+    let lastZoomUpdate: TimeRange | undefined = undefined;
+    const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+      lastZoomUpdate = zoom;
+    });
+
+    const leftCropper = htmlElement.querySelector('.slider .cropper.left');
+    expect(leftCropper).toBeTruthy();
+    expect(window.getComputedStyle(assertDefined(leftCropper)).visibility).toBe('visible');
+
+    dragElement(fixture, assertDefined(leftCropper), -5, 0);
+
+    expect(zoomChangedSpy).toHaveBeenCalled();
+
+    const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+    expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs());
+    expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs());
+  }));
+
+  it('cannot slide right cropper past edges', fakeAsync(() => {
+    component.zoomRange = component.fullRange;
+    fixture.detectChanges();
+
+    const initialZoom = assertDefined(component.zoomRange);
+
+    let lastZoomUpdate: TimeRange | undefined = undefined;
+    const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+      lastZoomUpdate = zoom;
+    });
+
+    const rightCropper = htmlElement.querySelector('.slider .cropper.right');
+    expect(rightCropper).toBeTruthy();
+    expect(window.getComputedStyle(assertDefined(rightCropper)).visibility).toBe('visible');
+
+    dragElement(fixture, assertDefined(rightCropper), 5, 0);
+
+    expect(zoomChangedSpy).toHaveBeenCalled();
+
+    const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+    expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs());
+    expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs());
+  }));
+
+  it('cannot slide left cropper past right cropper', fakeAsync(() => {
+    component.zoomRange = {
+      from: new RealTimestamp(125n),
+      to: new RealTimestamp(125n),
+    };
+    fixture.detectChanges();
+
+    const initialZoom = assertDefined(component.zoomRange);
+
+    let lastZoomUpdate: TimeRange | undefined = undefined;
+    const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+      lastZoomUpdate = zoom;
+    });
+
+    const leftCropper = htmlElement.querySelector('.slider .cropper.left');
+    expect(leftCropper).toBeTruthy();
+    expect(window.getComputedStyle(assertDefined(leftCropper)).visibility).toBe('visible');
+
+    dragElement(fixture, assertDefined(leftCropper), 100, 0);
+
+    expect(zoomChangedSpy).toHaveBeenCalled();
+
+    const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+    expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs());
+    expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs());
+  }));
+
+  it('cannot slide right cropper past left cropper', fakeAsync(() => {
+    component.zoomRange = {
+      from: new RealTimestamp(125n),
+      to: new RealTimestamp(125n),
+    };
+    fixture.detectChanges();
+
+    const initialZoom = assertDefined(component.zoomRange);
+
+    let lastZoomUpdate: TimeRange | undefined = undefined;
+    const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+      lastZoomUpdate = zoom;
+    });
+
+    const rightCropper = htmlElement.querySelector('.slider .cropper.right');
+    expect(rightCropper).toBeTruthy();
+    expect(window.getComputedStyle(assertDefined(rightCropper)).visibility).toBe('visible');
+
+    dragElement(fixture, assertDefined(rightCropper), -100, 0);
+
+    expect(zoomChangedSpy).toHaveBeenCalled();
+
+    const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+    expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs());
+    expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs());
+  }));
+
+  it('cannot move slider past edges', fakeAsync(() => {
+    component.zoomRange = component.fullRange;
+    fixture.detectChanges();
+
+    const initialZoom = assertDefined(component.zoomRange);
+
+    let lastZoomUpdate: TimeRange | undefined = undefined;
+    const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+      lastZoomUpdate = zoom;
+    });
+
+    const slider = htmlElement.querySelector('.slider .handle');
+    expect(slider).toBeTruthy();
+    expect(window.getComputedStyle(assertDefined(slider)).visibility).toBe('visible');
+
+    dragElement(fixture, assertDefined(slider), 100, 8);
+
+    expect(zoomChangedSpy).toHaveBeenCalled();
+
+    const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+    expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs());
+    expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs());
+  }));
+});
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/transformer.ts b/tools/winscope/src/app/components/timeline/mini-timeline/transformer.ts
new file mode 100644
index 0000000..dd5a71c
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/transformer.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 {TimeRange, Timestamp, TimestampType} from 'common/time';
+import {Segment} from '../utils';
+
+export class Transformer {
+  private timestampType: TimestampType;
+
+  private fromWidth: bigint;
+  private targetWidth: number;
+
+  private fromOffset: bigint;
+  private toOffset: number;
+
+  constructor(private fromRange: TimeRange, private toRange: Segment) {
+    this.timestampType = fromRange.from.getType();
+
+    this.fromWidth = this.fromRange.to.getValueNs() - this.fromRange.from.getValueNs();
+    // Needs to be a whole number to be compatible with bigints
+    this.targetWidth = Math.round(this.toRange.to - this.toRange.from);
+
+    this.fromOffset = this.fromRange.from.getValueNs();
+    // Needs to be a whole number to be compatible with bigints
+    this.toOffset = this.toRange.from;
+  }
+
+  transform(x: Timestamp): number {
+    return (
+      this.toOffset +
+      (this.targetWidth * Number(x.getValueNs() - this.fromOffset)) / Number(this.fromWidth)
+    );
+  }
+
+  untransform(x: number): Timestamp {
+    x = Math.round(x);
+    const valueNs =
+      this.fromOffset + (BigInt(x - this.toOffset) * this.fromWidth) / BigInt(this.targetWidth);
+    return new Timestamp(this.timestampType, valueNs);
+  }
+}
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/transformer_test.ts b/tools/winscope/src/app/components/timeline/mini-timeline/transformer_test.ts
new file mode 100644
index 0000000..604560f
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/transformer_test.ts
@@ -0,0 +1,92 @@
+/*
+ * 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 {RealTimestamp} from 'common/time';
+import {Transformer} from './transformer';
+
+describe('Transformer', () => {
+  it('can transform', () => {
+    const fromRange = {
+      from: new RealTimestamp(1689763211000000000n),
+      to: new RealTimestamp(1689763571000000000n),
+    };
+    const toRange = {
+      from: 100,
+      to: 1100,
+    };
+    const transformer = new Transformer(fromRange, toRange);
+
+    const rangeStart = fromRange.from.getValueNs();
+    const rangeEnd = fromRange.to.getValueNs();
+    const range = fromRange.to.getValueNs() - fromRange.from.getValueNs();
+
+    expect(transformer.transform(fromRange.from)).toBe(toRange.from);
+    expect(transformer.transform(fromRange.to)).toBe(toRange.to);
+
+    expect(transformer.transform(new RealTimestamp(rangeStart + range / 2n))).toBe(
+      toRange.from + (toRange.to - toRange.from) / 2
+    );
+    expect(transformer.transform(new RealTimestamp(rangeStart + range / 4n))).toBe(
+      toRange.from + (toRange.to - toRange.from) / 4
+    );
+    expect(transformer.transform(new RealTimestamp(rangeStart + range / 20n))).toBe(
+      toRange.from + (toRange.to - toRange.from) / 20
+    );
+
+    expect(transformer.transform(new RealTimestamp(rangeStart - range / 2n))).toBe(
+      toRange.from - (toRange.to - toRange.from) / 2
+    );
+    expect(transformer.transform(new RealTimestamp(rangeEnd + range / 2n))).toBe(
+      toRange.to + (toRange.to - toRange.from) / 2
+    );
+  });
+
+  it('can untransform', () => {
+    const fromRange = {
+      from: new RealTimestamp(1689763211000000000n),
+      to: new RealTimestamp(1689763571000000000n),
+    };
+    const toRange = {
+      from: 100,
+      to: 1100,
+    };
+    const transformer = new Transformer(fromRange, toRange);
+
+    const rangeStart = fromRange.from.getValueNs();
+    const rangeEnd = fromRange.to.getValueNs();
+    const range = fromRange.to.getValueNs() - fromRange.from.getValueNs();
+
+    expect(transformer.untransform(toRange.from).getValueNs()).toBe(fromRange.from.getValueNs());
+    expect(transformer.untransform(toRange.to).getValueNs()).toBe(fromRange.to.getValueNs());
+
+    expect(
+      transformer.untransform(toRange.from + (toRange.to - toRange.from) / 2).getValueNs()
+    ).toBe(rangeStart + range / 2n);
+    expect(
+      transformer.untransform(toRange.from + (toRange.to - toRange.from) / 4).getValueNs()
+    ).toBe(rangeStart + range / 4n);
+    expect(
+      transformer.untransform(toRange.from + (toRange.to - toRange.from) / 20).getValueNs()
+    ).toBe(rangeStart + range / 20n);
+
+    expect(
+      transformer.untransform(toRange.from - (toRange.to - toRange.from) / 2).getValueNs()
+    ).toBe(rangeStart - range / 2n);
+    expect(
+      transformer.untransform(toRange.from + (toRange.to - toRange.from) / 2).getValueNs()
+    ).toBe(rangeStart + range / 2n);
+  });
+});
diff --git a/tools/winscope/src/app/components/timeline/mini_canvas_drawer.ts b/tools/winscope/src/app/components/timeline/mini_canvas_drawer.ts
deleted file mode 100644
index 14c6b77..0000000
--- a/tools/winscope/src/app/components/timeline/mini_canvas_drawer.ts
+++ /dev/null
@@ -1,430 +0,0 @@
-/*
- * Copyright (C) 2022 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 {TimeRange} from 'app/timeline_data';
-import {TRACE_INFO} from 'app/trace_info';
-import {Timestamp, TimestampType} from 'trace/timestamp';
-import {Trace} from 'trace/trace';
-import {Traces} from 'trace/traces';
-import {TraceType} from 'trace/trace_type';
-import {Color} from '../../colors';
-import {CanvasDrawer} from '../canvas/canvas_drawer';
-import {CanvasMouseHandler} from '../canvas/canvas_mouse_handler';
-import {DraggableCanvasObject} from '../canvas/draggable_canvas_object';
-import {Segment} from './utils';
-
-export class MiniCanvasDrawerInput {
-  constructor(
-    public fullRange: TimeRange,
-    public selectedPosition: Timestamp,
-    public selection: TimeRange,
-    public traces: Traces
-  ) {}
-
-  transform(mapToRange: Segment): MiniCanvasDrawerData {
-    const transformer = new Transformer(this.fullRange, mapToRange);
-    return new MiniCanvasDrawerData(
-      transformer.transform(this.selectedPosition),
-      {
-        from: transformer.transform(this.selection.from),
-        to: transformer.transform(this.selection.to),
-      },
-      this.transformTracesTimestamps(transformer),
-      transformer
-    );
-  }
-
-  private transformTracesTimestamps(transformer: Transformer): Map<TraceType, number[]> {
-    const transformedTraceSegments = new Map<TraceType, number[]>();
-
-    this.traces.forEachTrace((trace) => {
-      transformedTraceSegments.set(trace.type, this.transformTraceTimestamps(transformer, trace));
-    });
-
-    return transformedTraceSegments;
-  }
-
-  private transformTraceTimestamps(transformer: Transformer, trace: Trace<{}>): number[] {
-    const result: number[] = [];
-
-    trace.forEachTimestamp((timestamp) => {
-      result.push(transformer.transform(timestamp));
-    });
-
-    return result;
-  }
-}
-
-export class Transformer {
-  private timestampType: TimestampType;
-
-  private fromWidth: bigint;
-  private targetWidth: number;
-
-  private fromOffset: bigint;
-  private toOffset: number;
-
-  constructor(private fromRange: TimeRange, private toRange: Segment) {
-    this.timestampType = fromRange.from.getType();
-
-    this.fromWidth = this.fromRange.to.getValueNs() - this.fromRange.from.getValueNs();
-    // Needs to be a whole number to be compatible with bigints
-    this.targetWidth = Math.round(this.toRange.to - this.toRange.from);
-
-    this.fromOffset = this.fromRange.from.getValueNs();
-    // Needs to be a whole number to be compatible with bigints
-    this.toOffset = Math.round(this.toRange.from);
-  }
-
-  transform(x: Timestamp): number {
-    return (
-      this.toOffset +
-      (this.targetWidth * Number(x.getValueNs() - this.fromOffset)) / Number(this.fromWidth)
-    );
-  }
-
-  untransform(x: number): Timestamp {
-    x = Math.round(x);
-    const valueNs =
-      this.fromOffset + (BigInt(x - this.toOffset) * this.fromWidth) / BigInt(this.targetWidth);
-    return new Timestamp(this.timestampType, valueNs);
-  }
-}
-
-class MiniCanvasDrawerOutput {
-  constructor(public selectedPosition: Timestamp, public selection: TimeRange) {}
-}
-
-class MiniCanvasDrawerData {
-  constructor(
-    public selectedPosition: number,
-    public selection: Segment,
-    public timelineEntries: Map<TraceType, number[]>,
-    public transformer: Transformer
-  ) {}
-
-  toOutput(): MiniCanvasDrawerOutput {
-    return new MiniCanvasDrawerOutput(this.transformer.untransform(this.selectedPosition), {
-      from: this.transformer.untransform(this.selection.from),
-      to: this.transformer.untransform(this.selection.to),
-    });
-  }
-}
-
-export class MiniCanvasDrawer implements CanvasDrawer {
-  ctx: CanvasRenderingContext2D;
-  handler: CanvasMouseHandler;
-
-  private activePointer: DraggableCanvasObject;
-  private leftFocusSectionSelector: DraggableCanvasObject;
-  private rightFocusSectionSelector: DraggableCanvasObject;
-
-  private get pointerWidth() {
-    return this.getHeight() / 6;
-  }
-
-  getXScale() {
-    return this.ctx.getTransform().m11;
-  }
-
-  getYScale() {
-    return this.ctx.getTransform().m22;
-  }
-
-  getWidth() {
-    return this.canvas.width / this.getXScale();
-  }
-
-  getHeight() {
-    return this.canvas.height / this.getYScale();
-  }
-
-  get usableRange() {
-    return {
-      from: this.padding.left,
-      to: this.getWidth() - this.padding.left - this.padding.right,
-    };
-  }
-
-  get input() {
-    return this.inputGetter().transform(this.usableRange);
-  }
-
-  constructor(
-    public canvas: HTMLCanvasElement,
-    private inputGetter: () => MiniCanvasDrawerInput,
-    private onPointerPositionDragging: (pos: Timestamp) => void,
-    private onPointerPositionChanged: (pos: Timestamp) => void,
-    private onSelectionChanged: (selection: TimeRange) => void,
-    private onUnhandledClick: (pos: Timestamp) => void
-  ) {
-    const ctx = canvas.getContext('2d');
-
-    if (ctx === null) {
-      throw Error('MiniTimeline canvas context was null!');
-    }
-
-    this.ctx = ctx;
-
-    const onUnhandledClickInternal = (x: number, y: number) => {
-      this.onUnhandledClick(this.input.transformer.untransform(x));
-    };
-    this.handler = new CanvasMouseHandler(this, 'pointer', onUnhandledClickInternal);
-
-    this.activePointer = new DraggableCanvasObject(
-      this,
-      () => this.selectedPosition,
-      (ctx: CanvasRenderingContext2D, position: number) => {
-        const barWidth = 3;
-        const triangleHeight = this.pointerWidth / 2;
-
-        ctx.beginPath();
-        ctx.moveTo(position - triangleHeight, 0);
-        ctx.lineTo(position + triangleHeight, 0);
-        ctx.lineTo(position + barWidth / 2, triangleHeight);
-        ctx.lineTo(position + barWidth / 2, this.getHeight());
-        ctx.lineTo(position - barWidth / 2, this.getHeight());
-        ctx.lineTo(position - barWidth / 2, triangleHeight);
-        ctx.closePath();
-      },
-      {
-        fillStyle: Color.ACTIVE_POINTER,
-        fill: true,
-      },
-      (x) => {
-        this.input.selectedPosition = x;
-        this.onPointerPositionDragging(this.input.transformer.untransform(x));
-      },
-      (x) => {
-        this.input.selectedPosition = x;
-        this.onPointerPositionChanged(this.input.transformer.untransform(x));
-      },
-      () => this.usableRange
-    );
-
-    const focusSelectorDrawConfig = {
-      fillStyle: Color.SELECTOR_COLOR,
-      fill: true,
-    };
-
-    const onLeftSelectionChanged = (x: number) => {
-      this.selection.from = x;
-      this.onSelectionChanged({
-        from: this.input.transformer.untransform(x),
-        to: this.input.transformer.untransform(this.selection.to),
-      });
-    };
-    const onRightSelectionChanged = (x: number) => {
-      this.selection.to = x;
-      this.onSelectionChanged({
-        from: this.input.transformer.untransform(this.selection.from),
-        to: this.input.transformer.untransform(x),
-      });
-    };
-
-    const barWidth = 6;
-    const selectorArrowWidth = this.innerHeight / 12;
-    const selectorArrowHeight = selectorArrowWidth * 2;
-
-    this.leftFocusSectionSelector = new DraggableCanvasObject(
-      this,
-      () => this.selection.from,
-      (ctx: CanvasRenderingContext2D, position: number) => {
-        ctx.beginPath();
-        ctx.moveTo(position - barWidth, this.padding.top);
-        ctx.lineTo(position, this.padding.top);
-        ctx.lineTo(position + selectorArrowWidth, this.padding.top + selectorArrowWidth);
-        ctx.lineTo(position, this.padding.top + selectorArrowHeight);
-        ctx.lineTo(position, this.padding.top + this.innerHeight);
-        ctx.lineTo(position - barWidth, this.padding.top + this.innerHeight);
-        ctx.lineTo(position - barWidth, this.padding.top);
-        ctx.closePath();
-      },
-      focusSelectorDrawConfig,
-      onLeftSelectionChanged,
-      onLeftSelectionChanged,
-      () => {
-        return {
-          from: this.usableRange.from,
-          to: this.rightFocusSectionSelector.position - selectorArrowWidth - barWidth,
-        };
-      }
-    );
-
-    this.rightFocusSectionSelector = new DraggableCanvasObject(
-      this,
-      () => this.selection.to,
-      (ctx: CanvasRenderingContext2D, position: number) => {
-        ctx.beginPath();
-        ctx.moveTo(position + barWidth, this.padding.top);
-        ctx.lineTo(position, this.padding.top);
-        ctx.lineTo(position - selectorArrowWidth, this.padding.top + selectorArrowWidth);
-        ctx.lineTo(position, this.padding.top + selectorArrowHeight);
-        ctx.lineTo(position, this.padding.top + this.innerHeight);
-        ctx.lineTo(position + barWidth, this.padding.top + this.innerHeight);
-        ctx.closePath();
-      },
-      focusSelectorDrawConfig,
-      onRightSelectionChanged,
-      onRightSelectionChanged,
-      () => {
-        return {
-          from: this.leftFocusSectionSelector.position + selectorArrowWidth + barWidth,
-          to: this.usableRange.to,
-        };
-      }
-    );
-  }
-
-  get selectedPosition() {
-    return this.input.selectedPosition;
-  }
-
-  get selection() {
-    return this.input.selection;
-  }
-
-  get timelineEntries() {
-    return this.input.timelineEntries;
-  }
-
-  get padding() {
-    return {
-      top: Math.ceil(this.getHeight() / 5),
-      bottom: Math.ceil(this.getHeight() / 5),
-      left: Math.ceil(this.pointerWidth / 2),
-      right: Math.ceil(this.pointerWidth / 2),
-    };
-  }
-
-  get innerHeight() {
-    return this.getHeight() - this.padding.top - this.padding.bottom;
-  }
-
-  draw() {
-    this.ctx.clearRect(0, 0, this.getWidth(), this.getHeight());
-
-    this.drawSelectionBackground();
-
-    this.drawTraceLines();
-
-    this.drawTimelineGuides();
-
-    this.leftFocusSectionSelector.draw(this.ctx);
-    this.rightFocusSectionSelector.draw(this.ctx);
-
-    this.activePointer.draw(this.ctx);
-  }
-
-  private drawSelectionBackground() {
-    const triangleHeight = this.innerHeight / 6;
-
-    // Selection background
-    this.ctx.globalAlpha = 0.8;
-    this.ctx.fillStyle = Color.SELECTION_BACKGROUND;
-    const width = this.selection.to - this.selection.from;
-    this.ctx.fillRect(
-      this.selection.from,
-      this.padding.top + triangleHeight / 2,
-      width,
-      this.innerHeight - triangleHeight / 2
-    );
-    this.ctx.restore();
-  }
-
-  private drawTraceLines() {
-    const lineHeight = this.innerHeight / 8;
-
-    let fromTop = this.padding.top + (this.innerHeight * 2) / 3 - lineHeight;
-
-    this.timelineEntries.forEach((entries, traceType) => {
-      // TODO: Only if active or a selected trace
-      for (const entry of entries) {
-        this.ctx.globalAlpha = 0.7;
-        this.ctx.fillStyle = TRACE_INFO[traceType].color;
-
-        const width = 5;
-        this.ctx.fillRect(entry - width / 2, fromTop, width, lineHeight);
-        this.ctx.globalAlpha = 1.0;
-      }
-
-      fromTop -= (lineHeight * 4) / 3;
-    });
-  }
-
-  private drawTimelineGuides() {
-    const edgeBarHeight = (this.innerHeight * 1) / 2;
-    const edgeBarWidth = 4;
-
-    const boldBarHeight = (this.innerHeight * 1) / 5;
-    const boldBarWidth = edgeBarWidth;
-
-    const lightBarHeight = (this.innerHeight * 1) / 6;
-    const lightBarWidth = 2;
-
-    const minSpacing = lightBarWidth * 7;
-    const barsInSetWidth = 9 * lightBarWidth + boldBarWidth;
-    const barSets = Math.floor(
-      (this.getWidth() - edgeBarWidth * 2 - minSpacing) / (barsInSetWidth + 10 * minSpacing)
-    );
-    const bars = barSets * 10;
-
-    // Draw start bar
-    this.ctx.fillStyle = Color.GUIDE_BAR;
-    this.ctx.fillRect(
-      0,
-      this.padding.top + this.innerHeight - edgeBarHeight,
-      edgeBarWidth,
-      edgeBarHeight
-    );
-
-    // Draw end bar
-    this.ctx.fillStyle = Color.GUIDE_BAR;
-    this.ctx.fillRect(
-      this.getWidth() - edgeBarWidth,
-      this.padding.top + this.innerHeight - edgeBarHeight,
-      edgeBarWidth,
-      edgeBarHeight
-    );
-
-    const spacing = (this.getWidth() - barSets * barsInSetWidth - edgeBarWidth) / bars;
-    let start = edgeBarWidth + spacing;
-    for (let i = 1; i < bars; i++) {
-      if (i % 10 === 0) {
-        // Draw boldbar
-        this.ctx.fillStyle = Color.GUIDE_BAR;
-        this.ctx.fillRect(
-          start,
-          this.padding.top + this.innerHeight - boldBarHeight,
-          boldBarWidth,
-          boldBarHeight
-        );
-        start += boldBarWidth; // TODO: Shift a bit
-      } else {
-        // Draw lightbar
-        this.ctx.fillStyle = Color.GUIDE_BAR_LIGHT;
-        this.ctx.fillRect(
-          start,
-          this.padding.top + this.innerHeight - lightBarHeight,
-          lightBarWidth,
-          lightBarHeight
-        );
-        start += lightBarWidth;
-      }
-      start += spacing;
-    }
-  }
-}
diff --git a/tools/winscope/src/app/components/timeline/mini_timeline_component.ts b/tools/winscope/src/app/components/timeline/mini_timeline_component.ts
deleted file mode 100644
index 2f11e1e..0000000
--- a/tools/winscope/src/app/components/timeline/mini_timeline_component.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (C) 2022 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 {
-  Component,
-  ElementRef,
-  EventEmitter,
-  HostListener,
-  Input,
-  Output,
-  SimpleChanges,
-  ViewChild,
-} from '@angular/core';
-import {TimelineData} from 'app/timeline_data';
-import {assertDefined} from 'common/assert_utils';
-import {Timestamp} from 'trace/timestamp';
-import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
-import {TraceType} from 'trace/trace_type';
-import {MiniCanvasDrawer, MiniCanvasDrawerInput} from './mini_canvas_drawer';
-
-@Component({
-  selector: 'mini-timeline',
-  template: `
-    <div id="mini-timeline-wrapper" #miniTimelineWrapper>
-      <canvas #canvas></canvas>
-    </div>
-  `,
-  styles: [
-    `
-      #mini-timeline-wrapper {
-        width: 100%;
-        min-height: 5em;
-        height: 100%;
-      }
-    `,
-  ],
-})
-export class MiniTimelineComponent {
-  @Input() timelineData!: TimelineData;
-  @Input() currentTracePosition!: TracePosition;
-  @Input() selectedTraces!: TraceType[];
-
-  @Output() onTracePositionUpdate = new EventEmitter<TracePosition>();
-  @Output() onSeekTimestampUpdate = new EventEmitter<Timestamp | undefined>();
-
-  @ViewChild('miniTimelineWrapper', {static: false}) miniTimelineWrapper!: ElementRef;
-  @ViewChild('canvas', {static: false}) canvasRef!: ElementRef;
-  get canvas(): HTMLCanvasElement {
-    return this.canvasRef.nativeElement;
-  }
-
-  private drawer: MiniCanvasDrawer | undefined = undefined;
-
-  ngAfterViewInit(): void {
-    this.makeHiPPICanvas();
-
-    const updateTimestampCallback = (timestamp: Timestamp) => {
-      this.onSeekTimestampUpdate.emit(undefined);
-      this.onTracePositionUpdate.emit(TracePosition.fromTimestamp(timestamp));
-    };
-
-    this.drawer = new MiniCanvasDrawer(
-      this.canvas,
-      () => this.getMiniCanvasDrawerInput(),
-      (position) => {
-        const timestampType = this.timelineData.getTimestampType()!;
-        this.onSeekTimestampUpdate.emit(position);
-      },
-      updateTimestampCallback,
-      (selection) => {
-        const timestampType = this.timelineData.getTimestampType()!;
-        this.timelineData.setSelectionTimeRange(selection);
-      },
-      updateTimestampCallback
-    );
-    this.drawer.draw();
-  }
-
-  ngOnChanges(changes: SimpleChanges) {
-    if (this.drawer !== undefined) {
-      this.drawer.draw();
-    }
-  }
-
-  private getMiniCanvasDrawerInput() {
-    return new MiniCanvasDrawerInput(
-      this.timelineData.getFullTimeRange(),
-      this.currentTracePosition.timestamp,
-      this.timelineData.getSelectionTimeRange(),
-      this.getTracesToShow()
-    );
-  }
-
-  private getTracesToShow(): Traces {
-    const traces = new Traces();
-    this.selectedTraces
-      .filter((type) => this.timelineData.getTraces().getTrace(type) !== undefined)
-      .forEach((type) => {
-        traces.setTrace(type, assertDefined(this.timelineData.getTraces().getTrace(type)));
-      });
-    return traces;
-  }
-
-  private makeHiPPICanvas() {
-    // Reset any size before computing new size to avoid it interfering with size computations
-    this.canvas.width = 0;
-    this.canvas.height = 0;
-    this.canvas.style.width = 'auto';
-    this.canvas.style.height = 'auto';
-
-    const width = this.miniTimelineWrapper.nativeElement.offsetWidth;
-    const height = this.miniTimelineWrapper.nativeElement.offsetHeight;
-
-    const HiPPIwidth = window.devicePixelRatio * width;
-    const HiPPIheight = window.devicePixelRatio * height;
-
-    this.canvas.width = HiPPIwidth;
-    this.canvas.height = HiPPIheight;
-    this.canvas.style.width = width + 'px';
-    this.canvas.style.height = height + 'px';
-
-    // ensure all drawing operations are scaled
-    if (window.devicePixelRatio !== 1) {
-      const context = this.canvas.getContext('2d')!;
-      context.scale(window.devicePixelRatio, window.devicePixelRatio);
-    }
-  }
-
-  @HostListener('window:resize', ['$event'])
-  onResize(event: Event) {
-    this.makeHiPPICanvas();
-    this.drawer?.draw();
-  }
-}
diff --git a/tools/winscope/src/app/components/timeline/single_timeline_component.ts b/tools/winscope/src/app/components/timeline/single_timeline_component.ts
deleted file mode 100644
index 290d246..0000000
--- a/tools/winscope/src/app/components/timeline/single_timeline_component.ts
+++ /dev/null
@@ -1,326 +0,0 @@
-/*
- * Copyright (C) 2022 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 {
-  Component,
-  ElementRef,
-  EventEmitter,
-  Input,
-  Output,
-  SimpleChanges,
-  ViewChild,
-} from '@angular/core';
-import {Color} from 'app/colors';
-import {TimeRange} from 'app/timeline_data';
-import {Timestamp} from 'trace/timestamp';
-import {Trace, TraceEntry} from 'trace/trace';
-import {TracePosition} from 'trace/trace_position';
-
-@Component({
-  selector: 'single-timeline',
-  template: `
-    <div class="single-timeline" #wrapper>
-      <canvas #canvas></canvas>
-    </div>
-  `,
-  styles: [
-    `
-      .single-timeline {
-        height: 2rem;
-        padding: 1rem 0;
-      }
-    `,
-  ],
-})
-export class SingleTimelineComponent {
-  @Input() color = '#AF5CF7';
-  @Input() trace!: Trace<{}>;
-  @Input() selectedEntry: TraceEntry<{}> | undefined = undefined;
-  @Input() selectionRange!: TimeRange;
-
-  @Output() onTracePositionUpdate = new EventEmitter<TracePosition>();
-
-  @ViewChild('canvas', {static: false}) canvasRef!: ElementRef;
-  @ViewChild('wrapper', {static: false}) wrapperRef!: ElementRef;
-
-  hoveringEntry?: Timestamp;
-
-  private viewInitialized = false;
-
-  get canvas() {
-    return this.canvasRef.nativeElement;
-  }
-
-  get ctx(): CanvasRenderingContext2D {
-    const ctx = this.canvas.getContext('2d');
-
-    if (ctx == null) {
-      throw Error('Failed to get canvas context!');
-    }
-
-    return ctx;
-  }
-
-  ngOnInit() {
-    if (!this.trace || !this.selectionRange) {
-      throw Error('Not all required inputs have been set');
-    }
-  }
-
-  ngAfterViewInit() {
-    // TODO: Clear observer at some point
-    new ResizeObserver(() => this.initializeCanvas()).observe(this.wrapperRef.nativeElement);
-    this.initializeCanvas();
-  }
-
-  initializeCanvas() {
-    // Reset any size before computing new size to avoid it interfering with size computations
-    this.canvas.width = 0;
-    this.canvas.height = 0;
-    this.canvas.style.width = 'auto';
-    this.canvas.style.height = 'auto';
-
-    const computedStyle = getComputedStyle(this.wrapperRef.nativeElement);
-    const width = this.wrapperRef.nativeElement.offsetWidth;
-    const height =
-      this.wrapperRef.nativeElement.offsetHeight -
-      // tslint:disable-next-line:ban
-      parseFloat(computedStyle.paddingTop) -
-      // tslint:disable-next-line:ban
-      parseFloat(computedStyle.paddingBottom);
-
-    const HiPPIwidth = window.devicePixelRatio * width;
-    const HiPPIheight = window.devicePixelRatio * height;
-
-    this.canvas.width = HiPPIwidth;
-    this.canvas.height = HiPPIheight;
-    this.canvas.style.width = width + 'px';
-    this.canvas.style.height = height + 'px';
-
-    // ensure all drawing operations are scaled
-    if (window.devicePixelRatio !== 1) {
-      const context = this.canvas.getContext('2d')!;
-      context.scale(window.devicePixelRatio, window.devicePixelRatio);
-    }
-
-    this.redraw();
-
-    this.canvas.addEventListener('mousemove', (event: MouseEvent) => {
-      this.handleMouseMove(event);
-    });
-    this.canvas.addEventListener('mousedown', (event: MouseEvent) => {
-      this.handleMouseDown(event);
-    });
-    this.canvas.addEventListener('mouseout', (event: MouseEvent) => {
-      this.handleMouseOut(event);
-    });
-
-    this.viewInitialized = true;
-  }
-
-  ngOnChanges(changes: SimpleChanges) {
-    if (this.viewInitialized) {
-      this.redraw();
-    }
-  }
-
-  private handleMouseOut(e: MouseEvent) {
-    if (this.hoveringEntry) {
-      // If undefined there is no current hover effect so no need to clear
-      this.redraw();
-    }
-    this.hoveringEntry = undefined;
-  }
-
-  getXScale(): number {
-    return this.ctx.getTransform().m11;
-  }
-
-  getYScale(): number {
-    return this.ctx.getTransform().m22;
-  }
-
-  private handleMouseMove(e: MouseEvent) {
-    e.preventDefault();
-    e.stopPropagation();
-    const mouseX = e.offsetX * this.getXScale();
-    const mouseY = e.offsetY * this.getYScale();
-
-    this.updateCursor(mouseX, mouseY);
-    this.drawEntryHover(mouseX, mouseY);
-  }
-
-  private drawEntryHover(mouseX: number, mouseY: number) {
-    const currentHoverEntry = this.getEntryAt(mouseX, mouseY);
-    if (this.hoveringEntry === currentHoverEntry) {
-      return;
-    }
-
-    if (this.hoveringEntry) {
-      // If null there is no current hover effect so no need to clear
-      this.clearCanvas();
-      this.drawTimeline();
-    }
-
-    this.hoveringEntry = currentHoverEntry;
-
-    if (!this.hoveringEntry) {
-      return;
-    }
-
-    this.defineEntryPath(this.hoveringEntry);
-
-    this.ctx.globalAlpha = 1.0;
-    this.ctx.strokeStyle = Color.ACTIVE_BORDER;
-    this.ctx.lineWidth = 2;
-    this.ctx.save();
-    this.ctx.clip();
-    this.ctx.lineWidth *= 2;
-    this.ctx.fill();
-    this.ctx.stroke();
-    this.ctx.restore();
-    this.ctx.stroke();
-
-    this.ctx.restore();
-  }
-
-  private clearCanvas() {
-    // Clear canvas
-    this.ctx.clearRect(0, 0, this.getScaledCanvasWidth(), this.getScaledCanvasHeight());
-  }
-
-  private getEntryAt(mouseX: number, mouseY: number): Timestamp | undefined {
-    // TODO: This can be optimized if it's laggy
-    for (let i = 0; i < this.trace.lengthEntries; ++i) {
-      const timestamp = this.trace.getEntry(i).getTimestamp();
-      this.defineEntryPath(timestamp);
-      if (this.ctx.isPointInPath(mouseX, mouseY)) {
-        this.canvas.style.cursor = 'pointer';
-        return timestamp;
-      }
-    }
-    return undefined;
-  }
-
-  private updateCursor(mouseX: number, mouseY: number) {
-    if (this.getEntryAt(mouseX, mouseY)) {
-      this.canvas.style.cursor = 'pointer';
-    }
-    this.canvas.style.cursor = 'auto';
-  }
-
-  private handleMouseDown(e: MouseEvent) {
-    e.preventDefault();
-    e.stopPropagation();
-    const mouseX = e.offsetX * this.getXScale();
-    const mouseY = e.offsetY * this.getYScale();
-
-    const clickedTimestamp = this.getEntryAt(mouseX, mouseY);
-
-    if (
-      clickedTimestamp &&
-      clickedTimestamp.getValueNs() !== this.selectedEntry?.getTimestamp().getValueNs()
-    ) {
-      this.redraw();
-      const entry = this.trace.findClosestEntry(clickedTimestamp);
-      if (entry) {
-        this.selectedEntry = entry;
-        this.onTracePositionUpdate.emit(TracePosition.fromTraceEntry(entry));
-      }
-    }
-  }
-
-  getScaledCanvasWidth() {
-    return Math.floor(this.canvas.width / this.getXScale());
-  }
-
-  getScaledCanvasHeight() {
-    return Math.floor(this.canvas.height / this.getYScale());
-  }
-
-  get entryWidth() {
-    return this.getScaledCanvasHeight();
-  }
-
-  get availableWidth() {
-    return Math.floor(this.getScaledCanvasWidth() - this.entryWidth);
-  }
-
-  private defineEntryPath(entry: Timestamp, padding = 0) {
-    const start = this.selectionRange.from.getValueNs();
-    const end = this.selectionRange.to.getValueNs();
-
-    const xPos = Number(
-      (BigInt(this.availableWidth) * (entry.getValueNs() - start)) / (end - start)
-    );
-
-    rect(
-      this.ctx,
-      xPos + padding,
-      padding,
-      this.entryWidth - 2 * padding,
-      this.entryWidth - 2 * padding
-    );
-  }
-
-  private redraw() {
-    this.clearCanvas();
-    this.drawTimeline();
-  }
-
-  private drawTimeline() {
-    this.trace.forEachTimestamp((entry) => {
-      this.drawEntry(entry);
-    });
-    this.drawSelectedEntry();
-  }
-
-  private drawEntry(entry: Timestamp) {
-    this.ctx.globalAlpha = 0.2;
-
-    this.defineEntryPath(entry);
-    this.ctx.fillStyle = this.color;
-    this.ctx.fill();
-
-    this.ctx.restore();
-  }
-
-  private drawSelectedEntry() {
-    if (this.selectedEntry === undefined) {
-      return;
-    }
-
-    this.ctx.globalAlpha = 1.0;
-    this.defineEntryPath(this.selectedEntry.getTimestamp(), 1);
-    this.ctx.fillStyle = this.color;
-    this.ctx.strokeStyle = Color.ACTIVE_BORDER;
-    this.ctx.lineWidth = 3;
-    this.ctx.stroke();
-    this.ctx.fill();
-
-    this.ctx.restore();
-  }
-}
-
-function rect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number) {
-  ctx.beginPath();
-  ctx.moveTo(x, y);
-  ctx.lineTo(x + w, y);
-  ctx.lineTo(x + w, y + h);
-  ctx.lineTo(x, y + h);
-  ctx.lineTo(x, y);
-  ctx.closePath();
-}
diff --git a/tools/winscope/src/app/components/timeline/timeline_component.ts b/tools/winscope/src/app/components/timeline/timeline_component.ts
index 1650d75..85bff5f 100644
--- a/tools/winscope/src/app/components/timeline/timeline_component.ts
+++ b/tools/winscope/src/app/components/timeline/timeline_component.ts
@@ -33,16 +33,16 @@
 import {assertDefined} from 'common/assert_utils';
 import {FunctionUtils} from 'common/function_utils';
 import {StringUtils} from 'common/string_utils';
+import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'common/time';
 import {TimeUtils} from 'common/time_utils';
 import {
   OnTracePositionUpdate,
   TracePositionUpdateEmitter,
 } from 'interfaces/trace_position_update_emitter';
 import {TracePositionUpdateListener} from 'interfaces/trace_position_update_listener';
-import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
 import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
-import {MiniTimelineComponent} from './mini_timeline_component';
+import {MiniTimelineComponent} from './mini-timeline/mini_timeline_component';
 
 @Component({
   selector: 'timeline',
@@ -76,11 +76,15 @@
             [disabled]="!hasPrevEntry()">
             <mat-icon>chevron_left</mat-icon>
           </button>
-          <form [formGroup]="timestampForm" class="time-selector-form">
+          <form
+            [formGroup]="timestampForm"
+            class="time-selector-form"
+            (focusin)="onInputFocusChange()"
+            (focusout)="onInputFocusChange()">
             <mat-form-field
               class="time-input"
               appearance="fill"
-              (change)="humanElapsedTimeInputChange($event)"
+              (change)="onHumanElapsedTimeInputChange($event)"
               *ngIf="!usingRealtime()">
               <input
                 matInput
@@ -90,7 +94,7 @@
             <mat-form-field
               class="time-input"
               appearance="fill"
-              (change)="humanRealTimeInputChanged($event)"
+              (change)="onHumanRealTimeInputChange($event)"
               *ngIf="usingRealtime()">
               <input
                 matInput
@@ -100,7 +104,7 @@
             <mat-form-field
               class="time-input"
               appearance="fill"
-              (change)="nanosecondsInputTimeChange($event)">
+              (change)="onNanosecondsInputTimeChange($event)">
               <input matInput name="nsTimeInput" [formControl]="selectedNsFormControl" />
             </mat-form-field>
           </form>
@@ -364,6 +368,8 @@
 
   TRACE_INFO = TRACE_INFO;
 
+  isTimestampFormFocused = false;
+
   private onTracePositionUpdateCallback: OnTracePositionUpdate = FunctionUtils.DO_NOTHING_ASYNC;
 
   constructor(
@@ -434,7 +440,7 @@
 
   updateSeekTimestamp(timestamp: Timestamp | undefined) {
     if (timestamp) {
-      this.seekTracePosition = TracePosition.fromTimestamp(timestamp);
+      this.seekTracePosition = this.timelineData.makePositionFromActiveTrace(timestamp);
     } else {
       this.seekTracePosition = undefined;
     }
@@ -480,8 +486,15 @@
     this.selectedTraces = this.selectedTracesFormControl.value;
   }
 
+  onInputFocusChange() {
+    this.isTimestampFormFocused = !this.isTimestampFormFocused;
+  }
+
   @HostListener('document:keydown', ['$event'])
   async handleKeyboardEvent(event: KeyboardEvent) {
+    if (this.isTimestampFormFocused) {
+      return;
+    }
     if (event.key === 'ArrowLeft') {
       await this.moveToPreviousEntry();
     } else if (event.key === 'ArrowRight') {
@@ -525,29 +538,29 @@
     await this.onTracePositionUpdateCallback(assertDefined(this.timelineData.getCurrentPosition()));
   }
 
-  async humanElapsedTimeInputChange(event: Event) {
-    if (event.type !== 'change') {
+  async onHumanElapsedTimeInputChange(event: Event) {
+    if (event.type !== 'change' || !this.selectedElapsedTimeFormControl.valid) {
       return;
     }
     const target = event.target as HTMLInputElement;
     const timestamp = TimeUtils.parseHumanElapsed(target.value);
-    await this.updatePosition(TracePosition.fromTimestamp(timestamp));
+    await this.updatePosition(this.timelineData.makePositionFromActiveTrace(timestamp));
     this.updateTimeInputValuesToCurrentTimestamp();
   }
 
-  async humanRealTimeInputChanged(event: Event) {
-    if (event.type !== 'change') {
+  async onHumanRealTimeInputChange(event: Event) {
+    if (event.type !== 'change' || !this.selectedRealTimeFormControl.valid) {
       return;
     }
     const target = event.target as HTMLInputElement;
 
     const timestamp = TimeUtils.parseHumanReal(target.value);
-    await this.updatePosition(TracePosition.fromTimestamp(timestamp));
+    await this.updatePosition(this.timelineData.makePositionFromActiveTrace(timestamp));
     this.updateTimeInputValuesToCurrentTimestamp();
   }
 
-  async nanosecondsInputTimeChange(event: Event) {
-    if (event.type !== 'change') {
+  async onNanosecondsInputTimeChange(event: Event) {
+    if (event.type !== 'change' || !this.selectedNsFormControl.valid) {
       return;
     }
     const target = event.target as HTMLInputElement;
@@ -556,7 +569,7 @@
       this.timelineData.getTimestampType()!,
       StringUtils.parseBigIntStrippingUnit(target.value)
     );
-    await this.updatePosition(TracePosition.fromTimestamp(timestamp));
+    await this.updatePosition(this.timelineData.makePositionFromActiveTrace(timestamp));
     this.updateTimeInputValuesToCurrentTimestamp();
   }
 }
diff --git a/tools/winscope/src/app/components/timeline/timeline_component_test.ts b/tools/winscope/src/app/components/timeline/timeline_component_test.ts
index 7720b09..4fdc10c 100644
--- a/tools/winscope/src/app/components/timeline/timeline_component_test.ts
+++ b/tools/winscope/src/app/components/timeline/timeline_component_test.ts
@@ -14,7 +14,8 @@
  * limitations under the License.
  */
 
-import {ChangeDetectionStrategy} from '@angular/core';
+import {DragDropModule} from '@angular/cdk/drag-drop';
+import {ChangeDetectionStrategy, DebugElement} from '@angular/core';
 import {ComponentFixture, TestBed} from '@angular/core/testing';
 import {FormsModule, ReactiveFormsModule} from '@angular/forms';
 import {MatButtonModule} from '@angular/material/button';
@@ -31,25 +32,28 @@
   MatDrawerContent,
 } from 'app/components/bottomnav/bottom_drawer_component';
 import {TimelineData} from 'app/timeline_data';
+import {RealTimestamp} from 'common/time';
 import {TracesBuilder} from 'test/unit/traces_builder';
-import {RealTimestamp} from 'trace/timestamp';
 import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
-import {ExpandedTimelineComponent} from './expanded_timeline_component';
-import {MiniTimelineComponent} from './mini_timeline_component';
-import {SingleTimelineComponent} from './single_timeline_component';
+import {DefaultTimelineRowComponent} from './expanded-timeline/default_timeline_row_component';
+import {ExpandedTimelineComponent} from './expanded-timeline/expanded_timeline_component';
+import {TransitionTimelineComponent} from './expanded-timeline/transition_timeline_component';
+import {MiniTimelineComponent} from './mini-timeline/mini_timeline_component';
+import {SliderComponent} from './mini-timeline/slider_component';
 import {TimelineComponent} from './timeline_component';
 
 describe('TimelineComponent', () => {
   const time90 = new RealTimestamp(90n);
   const time100 = new RealTimestamp(100n);
   const time101 = new RealTimestamp(101n);
+  const time105 = new RealTimestamp(105n);
   const time110 = new RealTimestamp(110n);
   const time112 = new RealTimestamp(112n);
 
   const position90 = TracePosition.fromTimestamp(time90);
   const position100 = TracePosition.fromTimestamp(time100);
-  const position105 = TracePosition.fromTimestamp(new RealTimestamp(105n));
+  const position105 = TracePosition.fromTimestamp(time105);
   const position110 = TracePosition.fromTimestamp(time110);
   const position112 = TracePosition.fromTimestamp(time112);
 
@@ -69,15 +73,18 @@
         MatTooltipModule,
         ReactiveFormsModule,
         BrowserAnimationsModule,
+        DragDropModule,
       ],
       declarations: [
         ExpandedTimelineComponent,
-        SingleTimelineComponent,
+        DefaultTimelineRowComponent,
         MatDrawer,
         MatDrawerContainer,
         MatDrawerContent,
         MiniTimelineComponent,
         TimelineComponent,
+        SliderComponent,
+        TransitionTimelineComponent,
       ],
     })
       .overrideComponent(TimelineComponent, {
@@ -194,14 +201,7 @@
   });
 
   it('next button disabled if no next entry', () => {
-    const traces = new TracesBuilder()
-      .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110])
-      .setTimestamps(TraceType.WINDOW_MANAGER, [time90, time101, time110, time112])
-      .build();
-    component.timelineData.initialize(traces, undefined);
-    component.activeViewTraceTypes = [TraceType.SURFACE_FLINGER];
-    component.timelineData.setPosition(position100);
-    fixture.detectChanges();
+    loadTraces();
 
     expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
 
@@ -223,14 +223,7 @@
   });
 
   it('prev button disabled if no prev entry', () => {
-    const builder = new TracesBuilder()
-      .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110])
-      .setTimestamps(TraceType.WINDOW_MANAGER, [time90, time101, time110, time112]);
-    const traces = builder.build();
-    component.timelineData.initialize(traces, undefined);
-    component.activeViewTraceTypes = [TraceType.SURFACE_FLINGER];
-    component.timelineData.setPosition(position100);
-    fixture.detectChanges();
+    loadTraces();
 
     expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
     const prevEntryButton = fixture.debugElement.query(By.css('#prev_entry_button'));
@@ -251,87 +244,93 @@
   });
 
   it('changes timestamp on next entry button press', () => {
-    const traces = new TracesBuilder()
-      .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110])
-      .setTimestamps(TraceType.WINDOW_MANAGER, [time90, time101, time110, time112])
-      .build();
-    component.timelineData.initialize(traces, undefined);
-    component.activeViewTraceTypes = [TraceType.SURFACE_FLINGER];
-    component.timelineData.setPosition(position100);
-    fixture.detectChanges();
+    loadTraces();
+
     expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
     const nextEntryButton = fixture.debugElement.query(By.css('#next_entry_button'));
     expect(nextEntryButton).toBeTruthy();
 
-    component.timelineData.setPosition(position105);
-    fixture.detectChanges();
-    nextEntryButton.nativeElement.click();
-    expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(110n);
+    testCurrentTimestampOnButtonClick(nextEntryButton, position105, 110n);
 
-    component.timelineData.setPosition(position100);
-    fixture.detectChanges();
-    nextEntryButton.nativeElement.click();
-    expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(110n);
+    testCurrentTimestampOnButtonClick(nextEntryButton, position100, 110n);
 
-    component.timelineData.setPosition(position90);
-    fixture.detectChanges();
-    nextEntryButton.nativeElement.click();
-    expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
+    testCurrentTimestampOnButtonClick(nextEntryButton, position90, 100n);
 
     // No change when we are already on the last timestamp of the active trace
-    component.timelineData.setPosition(position110);
-    fixture.detectChanges();
-    nextEntryButton.nativeElement.click();
-    expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(110n);
+    testCurrentTimestampOnButtonClick(nextEntryButton, position110, 110n);
 
     // No change when we are after the last entry of the active trace
-    component.timelineData.setPosition(position112);
-    fixture.detectChanges();
-    nextEntryButton.nativeElement.click();
-    expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(112n);
+    testCurrentTimestampOnButtonClick(nextEntryButton, position112, 112n);
   });
 
   it('changes timestamp on previous entry button press', () => {
-    const traces = new TracesBuilder()
-      .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110])
-      .setTimestamps(TraceType.WINDOW_MANAGER, [time90, time101, time110, time112])
-      .build();
-    component.timelineData.initialize(traces, undefined);
-    component.activeViewTraceTypes = [TraceType.SURFACE_FLINGER];
-    component.timelineData.setPosition(position100);
-    fixture.detectChanges();
+    loadTraces();
+
     expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
     const prevEntryButton = fixture.debugElement.query(By.css('#prev_entry_button'));
     expect(prevEntryButton).toBeTruthy();
 
     // In this state we are already on the first entry at timestamp 100, so
     // there is no entry to move to before and we just don't update the timestamp
-    component.timelineData.setPosition(position105);
-    fixture.detectChanges();
-    prevEntryButton.nativeElement.click();
-    expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(105n);
+    testCurrentTimestampOnButtonClick(prevEntryButton, position105, 105n);
 
-    component.timelineData.setPosition(position110);
-    fixture.detectChanges();
-    prevEntryButton.nativeElement.click();
-    expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
+    testCurrentTimestampOnButtonClick(prevEntryButton, position110, 100n);
 
     // Active entry here should be 110 so moving back means moving to 100.
-    component.timelineData.setPosition(position112);
-    fixture.detectChanges();
-    prevEntryButton.nativeElement.click();
-    expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
+    testCurrentTimestampOnButtonClick(prevEntryButton, position112, 100n);
 
     // No change when we are already on the first timestamp of the active trace
-    component.timelineData.setPosition(position100);
-    fixture.detectChanges();
-    prevEntryButton.nativeElement.click();
-    expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
+    testCurrentTimestampOnButtonClick(prevEntryButton, position100, 100n);
 
     // No change when we are before the first entry of the active trace
-    component.timelineData.setPosition(position90);
-    fixture.detectChanges();
-    prevEntryButton.nativeElement.click();
-    expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(90n);
+    testCurrentTimestampOnButtonClick(prevEntryButton, position90, 90n);
   });
+
+  it('performs expected action on arrow key press depending on input form focus', () => {
+    loadTraces();
+
+    const spyNextEntry = spyOn(component, 'moveToNextEntry');
+    const spyPrevEntry = spyOn(component, 'moveToPreviousEntry');
+
+    component.handleKeyboardEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'}));
+    fixture.detectChanges();
+    expect(spyNextEntry).toHaveBeenCalled();
+
+    const formElement = fixture.nativeElement.querySelector('.time-selector-form');
+    formElement.dispatchEvent(new Event('focusin'));
+    fixture.detectChanges();
+
+    component.handleKeyboardEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'}));
+    fixture.detectChanges();
+    expect(spyPrevEntry).not.toHaveBeenCalled();
+
+    formElement.dispatchEvent(new Event('focusout'));
+    fixture.detectChanges();
+
+    component.handleKeyboardEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'}));
+    fixture.detectChanges();
+    expect(spyPrevEntry).toHaveBeenCalled();
+  });
+
+  const loadTraces = () => {
+    const traces = new TracesBuilder()
+      .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110])
+      .setTimestamps(TraceType.WINDOW_MANAGER, [time90, time101, time110, time112])
+      .build();
+    component.timelineData.initialize(traces, undefined);
+    component.activeViewTraceTypes = [TraceType.SURFACE_FLINGER];
+    component.timelineData.setPosition(position100);
+    fixture.detectChanges();
+  };
+
+  const testCurrentTimestampOnButtonClick = (
+    button: DebugElement,
+    pos: TracePosition,
+    expectedNs: bigint
+  ) => {
+    component.timelineData.setPosition(pos);
+    fixture.detectChanges();
+    button.nativeElement.click();
+    expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(expectedNs);
+  };
 });
diff --git a/tools/winscope/src/app/components/trace_view_component.ts b/tools/winscope/src/app/components/trace_view_component.ts
index 7bd3bd8..4b658bf 100644
--- a/tools/winscope/src/app/components/trace_view_component.ts
+++ b/tools/winscope/src/app/components/trace_view_component.ts
@@ -15,8 +15,12 @@
  */
 
 import {Component, ElementRef, EventEmitter, Inject, Input, Output} from '@angular/core';
+import {AppEvent, AppEventType, TabbedViewSwitched} from 'app/app_event';
 import {TRACE_INFO} from 'app/trace_info';
+import {FunctionUtils} from 'common/function_utils';
 import {PersistentStore} from 'common/persistent_store';
+import {AppEventEmitter, EmitAppEvent} from 'interfaces/app_event_emitter';
+import {AppEventListener} from 'interfaces/app_event_listener';
 import {View, Viewer, ViewType} from 'viewers/viewer';
 
 interface Tab extends View {
@@ -102,11 +106,10 @@
     `,
   ],
 })
-export class TraceViewComponent {
+export class TraceViewComponent implements AppEventEmitter, AppEventListener {
   @Input() viewers!: Viewer[];
   @Input() store!: PersistentStore;
   @Output() downloadTracesButtonClick = new EventEmitter<void>();
-  @Output() activeViewChanged = new EventEmitter<View>();
 
   TRACE_INFO = TRACE_INFO;
 
@@ -114,6 +117,7 @@
 
   tabs: Tab[] = [];
   private currentActiveTab: undefined | Tab;
+  private emitAppEvent: EmitAppEvent = FunctionUtils.DO_NOTHING_ASYNC;
 
   constructor(@Inject(ElementRef) elementRef: ElementRef) {
     this.elementRef = elementRef;
@@ -124,8 +128,21 @@
     this.renderViewsOverlay();
   }
 
-  onTabClick(tab: Tab) {
-    this.showTab(tab);
+  async onTabClick(tab: Tab) {
+    await this.showTab(tab);
+  }
+
+  async onAppEvent(event: AppEvent) {
+    await event.visit(AppEventType.TABBED_VIEW_SWITCH_REQUEST, async (event) => {
+      const tab = this.tabs.find((tab) => tab.traceType === event.newFocusedViewId);
+      if (tab) {
+        await this.showTab(tab);
+      }
+    });
+  }
+
+  setEmitAppEvent(callback: EmitAppEvent) {
+    this.emitAppEvent = callback;
   }
 
   private renderViewsTab() {
@@ -179,7 +196,7 @@
     });
   }
 
-  private showTab(tab: Tab) {
+  private async showTab(tab: Tab) {
     if (this.currentActiveTab) {
       this.currentActiveTab.htmlElement.style.display = 'none';
     }
@@ -198,7 +215,8 @@
     }
 
     this.currentActiveTab = tab;
-    this.activeViewChanged.emit(tab);
+
+    await this.emitAppEvent(new TabbedViewSwitched(tab));
   }
 
   isCurrentActiveTab(tab: Tab) {
diff --git a/tools/winscope/src/app/components/trace_view_component_test.ts b/tools/winscope/src/app/components/trace_view_component_test.ts
index 37be18a..b88fdc8 100644
--- a/tools/winscope/src/app/components/trace_view_component_test.ts
+++ b/tools/winscope/src/app/components/trace_view_component_test.ts
@@ -18,6 +18,8 @@
 import {ComponentFixture, TestBed} from '@angular/core/testing';
 import {MatCardModule} from '@angular/material/card';
 import {MatDividerModule} from '@angular/material/divider';
+import {AppEvent, AppEventType, TabbedViewSwitchRequest} from 'app/app_event';
+import {TraceType} from 'trace/trace_type';
 import {ViewerStub} from 'viewers/viewer_stub';
 import {TraceViewComponent} from './trace_view_component';
 
@@ -36,8 +38,8 @@
     htmlElement = fixture.nativeElement;
     component = fixture.componentInstance;
     component.viewers = [
-      new ViewerStub('Title0', 'Content0'),
-      new ViewerStub('Title1', 'Content1'),
+      new ViewerStub('Title0', 'Content0', [TraceType.SURFACE_FLINGER]),
+      new ViewerStub('Title1', 'Content1', [TraceType.WINDOW_MANAGER]),
     ];
     component.ngOnChanges();
     fixture.detectChanges();
@@ -55,17 +57,7 @@
     expect(tabs.item(1)!.textContent).toContain('Title1');
   });
 
-  it('changes active view on click', () => {
-    const getVisibleTabContents = () => {
-      const contents: HTMLElement[] = [];
-      htmlElement.querySelectorAll('.trace-view-content div').forEach((content) => {
-        if ((content as HTMLElement).style.display !== 'none') {
-          contents.push(content as HTMLElement);
-        }
-      });
-      return contents;
-    };
-
+  it('switches view on click', () => {
     const tabButtons = htmlElement.querySelectorAll('.tab');
 
     // Initially tab 0
@@ -89,6 +81,54 @@
     expect(visibleTabContents[0].innerHTML).toEqual('Content0');
   });
 
+  it("emits 'view switched' events", () => {
+    const tabButtons = htmlElement.querySelectorAll('.tab');
+
+    const emitAppEvent = jasmine.createSpy();
+    component.setEmitAppEvent(emitAppEvent);
+
+    expect(emitAppEvent).not.toHaveBeenCalled();
+
+    tabButtons[1].dispatchEvent(new Event('click'));
+    expect(emitAppEvent).toHaveBeenCalledTimes(1);
+    expect(emitAppEvent).toHaveBeenCalledWith(
+      jasmine.objectContaining({
+        type: AppEventType.TABBED_VIEW_SWITCHED,
+      } as AppEvent)
+    );
+
+    tabButtons[0].dispatchEvent(new Event('click'));
+    expect(emitAppEvent).toHaveBeenCalledTimes(2);
+    expect(emitAppEvent).toHaveBeenCalledWith(
+      jasmine.objectContaining({
+        type: AppEventType.TABBED_VIEW_SWITCHED,
+      } as AppEvent)
+    );
+  });
+
+  it("handles 'view switch' requests", async () => {
+    const tabButtons = htmlElement.querySelectorAll('.tab');
+
+    // Initially tab 0
+    let visibleTabContents = getVisibleTabContents();
+    expect(visibleTabContents.length).toEqual(1);
+    expect(visibleTabContents[0].innerHTML).toEqual('Content0');
+
+    // Switch to tab 1
+    await component.onAppEvent(new TabbedViewSwitchRequest(TraceType.WINDOW_MANAGER));
+    fixture.detectChanges();
+    visibleTabContents = getVisibleTabContents();
+    expect(visibleTabContents.length).toEqual(1);
+    expect(visibleTabContents[0].innerHTML).toEqual('Content1');
+
+    // Switch to tab 0
+    await component.onAppEvent(new TabbedViewSwitchRequest(TraceType.SURFACE_FLINGER));
+    fixture.detectChanges();
+    visibleTabContents = getVisibleTabContents();
+    expect(visibleTabContents.length).toEqual(1);
+    expect(visibleTabContents[0].innerHTML).toEqual('Content0');
+  });
+
   it('emits event on download button click', () => {
     const spy = spyOn(component.downloadTracesButtonClick, 'emit');
 
@@ -103,4 +143,30 @@
     fixture.detectChanges();
     expect(spy).toHaveBeenCalledTimes(2);
   });
+
+  it('emits tab set onChanges', () => {
+    const emitAppEvent = jasmine.createSpy();
+    component.setEmitAppEvent(emitAppEvent);
+
+    expect(emitAppEvent).not.toHaveBeenCalled();
+
+    component.ngOnChanges();
+
+    expect(emitAppEvent).toHaveBeenCalledTimes(1);
+    expect(emitAppEvent).toHaveBeenCalledWith(
+      jasmine.objectContaining({
+        type: AppEventType.TABBED_VIEW_SWITCHED,
+      } as AppEvent)
+    );
+  });
+
+  const getVisibleTabContents = () => {
+    const contents: HTMLElement[] = [];
+    htmlElement.querySelectorAll('.trace-view-content div').forEach((content) => {
+      if ((content as HTMLElement).style.display !== 'none') {
+        contents.push(content as HTMLElement);
+      }
+    });
+    return contents;
+  };
 });
diff --git a/tools/winscope/src/app/components/upload_traces_component.ts b/tools/winscope/src/app/components/upload_traces_component.ts
index 1652ccc..ba3d992 100644
--- a/tools/winscope/src/app/components/upload_traces_component.ts
+++ b/tools/winscope/src/app/components/upload_traces_component.ts
@@ -13,15 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {
-  ChangeDetectorRef,
-  Component,
-  EventEmitter,
-  Inject,
-  Input,
-  NgZone,
-  Output,
-} from '@angular/core';
+import {ChangeDetectorRef, Component, EventEmitter, Inject, Input, Output} from '@angular/core';
 import {TRACE_INFO} from 'app/trace_info';
 import {TracePipeline} from 'app/trace_pipeline';
 import {ProgressListener} from 'interfaces/progress_listener';
@@ -178,10 +170,7 @@
   @Output() filesUploaded = new EventEmitter<File[]>();
   @Output() viewTracesButtonClick = new EventEmitter<void>();
 
-  constructor(
-    @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef,
-    @Inject(NgZone) private ngZone: NgZone
-  ) {}
+  constructor(@Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef) {}
 
   ngOnInit() {
     this.tracePipeline.clear();
diff --git a/tools/winscope/src/app/mediator.ts b/tools/winscope/src/app/mediator.ts
index 955dd6c..c0694bf 100644
--- a/tools/winscope/src/app/mediator.ts
+++ b/tools/winscope/src/app/mediator.ts
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-import {FileUtils, OnFile} from 'common/file_utils';
+import {Timestamp, TimestampType} from 'common/time';
+import {AppEventEmitter} from 'interfaces/app_event_emitter';
+import {AppEventListener} from 'interfaces/app_event_listener';
 import {BuganizerAttachmentsDownloadEmitter} from 'interfaces/buganizer_attachments_download_emitter';
 import {ProgressListener} from 'interfaces/progress_listener';
 import {RemoteBugreportReceiver} from 'interfaces/remote_bugreport_receiver';
@@ -25,12 +27,11 @@
 import {TracePositionUpdateEmitter} from 'interfaces/trace_position_update_emitter';
 import {TracePositionUpdateListener} from 'interfaces/trace_position_update_listener';
 import {UserNotificationListener} from 'interfaces/user_notification_listener';
-import {Timestamp, TimestampType} from 'trace/timestamp';
-import {TraceFile} from 'trace/trace_file';
+import {TraceEntry} from 'trace/trace';
 import {TracePosition} from 'trace/trace_position';
-import {TraceType} from 'trace/trace_type';
-import {View, Viewer} from 'viewers/viewer';
+import {Viewer} from 'viewers/viewer';
 import {ViewerFactory} from 'viewers/viewer_factory';
+import {AppEvent, AppEventType, TracePositionUpdate} from './app_event';
 import {TimelineData} from './timeline_data';
 import {TracePipeline} from './trace_pipeline';
 
@@ -45,15 +46,15 @@
   private crossToolProtocol: CrossToolProtocolInterface;
   private uploadTracesComponent?: ProgressListener;
   private collectTracesComponent?: ProgressListener;
+  private traceViewComponent?: AppEventListener & AppEventEmitter;
   private timelineComponent?: TimelineComponentInterface;
-  private appComponent: TraceDataListener;
+  private appComponent: AppEventListener & TraceDataListener;
   private userNotificationListener: UserNotificationListener;
   private storage: Storage;
 
   private tracePipeline: TracePipeline;
   private timelineData: TimelineData;
   private viewers: Viewer[] = [];
-  private isChangingCurrentTimestamp = false;
   private isTraceDataVisualized = false;
   private lastRemoteToolTimestampReceived: Timestamp | undefined;
   private currentProgressListener?: ProgressListener;
@@ -63,7 +64,7 @@
     timelineData: TimelineData,
     abtChromeExtensionProtocol: AbtChromeExtensionProtocolInterface,
     crossToolProtocol: CrossToolProtocolInterface,
-    appComponent: TraceDataListener,
+    appComponent: AppEventListener & TraceDataListener,
     userNotificationListener: UserNotificationListener,
     storage: Storage
   ) {
@@ -96,16 +97,23 @@
     );
   }
 
-  setUploadTracesComponent(uploadTracesComponent: ProgressListener | undefined) {
-    this.uploadTracesComponent = uploadTracesComponent;
+  setUploadTracesComponent(component: ProgressListener | undefined) {
+    this.uploadTracesComponent = component;
   }
 
-  setCollectTracesComponent(collectTracesComponent: ProgressListener | undefined) {
-    this.collectTracesComponent = collectTracesComponent;
+  setCollectTracesComponent(component: ProgressListener | undefined) {
+    this.collectTracesComponent = component;
   }
 
-  setTimelineComponent(timelineComponent: TimelineComponentInterface | undefined) {
-    this.timelineComponent = timelineComponent;
+  setTraceViewComponent(component: (AppEventListener & AppEventEmitter) | undefined) {
+    this.traceViewComponent = component;
+    this.traceViewComponent?.setEmitAppEvent(async (event) => {
+      await this.onTraceViewAppEvent(event);
+    });
+  }
+
+  setTimelineComponent(component: TimelineComponentInterface | undefined) {
+    this.timelineComponent = component;
     this.timelineComponent?.setOnTracePositionUpdate(async (position) => {
       await this.onTimelineTracePositionUpdate(position);
     });
@@ -121,12 +129,12 @@
 
   async onWinscopeFilesUploaded(files: File[]) {
     this.currentProgressListener = this.uploadTracesComponent;
-    await this.processFiles(files);
+    await this.tracePipeline.loadFiles(files, this.currentProgressListener);
   }
 
   async onWinscopeFilesCollected(files: File[]) {
     this.currentProgressListener = this.collectTracesComponent;
-    await this.processFiles(files);
+    await this.tracePipeline.loadFiles(files, this.currentProgressListener);
     await this.processLoadedTraceFiles();
   }
 
@@ -134,15 +142,24 @@
     await this.processLoadedTraceFiles();
   }
 
-  async onWinscopeActiveViewChanged(view: View) {
-    this.timelineData.setActiveViewTraceTypes(view.dependencies);
-    await this.propagateTracePosition(this.timelineData.getCurrentPosition());
-  }
-
   async onTimelineTracePositionUpdate(position: TracePosition) {
     await this.propagateTracePosition(position);
   }
 
+  async onTraceViewAppEvent(event: AppEvent) {
+    await event.visit(AppEventType.TABBED_VIEW_SWITCHED, async (event) => {
+      await this.appComponent.onAppEvent(event);
+      this.timelineData.setActiveViewTraceTypes(event.newFocusedView.dependencies);
+      await this.propagateTracePosition(this.timelineData.getCurrentPosition());
+    });
+  }
+
+  async onViewerAppEvent(event: AppEvent) {
+    await event.visit(AppEventType.TABBED_VIEW_SWITCH_REQUEST, async (event) => {
+      await this.traceViewComponent?.onAppEvent(event);
+    });
+  }
+
   private async propagateTracePosition(
     position?: TracePosition,
     omitCrossToolProtocol: boolean = false
@@ -152,8 +169,9 @@
     }
 
     //TODO (b/289478304): update only visible viewers (1 tab viewer + overlay viewers)
+    const event = new TracePositionUpdate(position);
     const promises = this.viewers.map((viewer) => {
-      return viewer.onTracePositionUpdate(position);
+      return viewer.onAppEvent(event);
     });
     await Promise.all(promises);
 
@@ -211,7 +229,7 @@
       return;
     }
 
-    const position = TracePosition.fromTimestamp(timestamp);
+    const position = this.timelineData.makePositionFromActiveTrace(timestamp);
     this.timelineData.setPosition(position);
 
     await this.propagateTracePosition(this.timelineData.getCurrentPosition(), true);
@@ -219,45 +237,36 @@
 
   private async processRemoteFilesReceived(files: File[]) {
     this.resetAppToInitialState();
-    await this.processFiles(files);
-  }
-
-  private async processFiles(files: File[]) {
-    let progressMessage = '';
-    const onProgressUpdate = (progressPercentage: number) => {
-      this.currentProgressListener?.onProgressUpdate(progressMessage, progressPercentage);
-    };
-
-    const traceFiles: TraceFile[] = [];
-    const onFile: OnFile = (file: File, parentArchive?: File) => {
-      traceFiles.push(new TraceFile(file, parentArchive));
-    };
-
-    progressMessage = 'Unzipping files...';
-    this.currentProgressListener?.onProgressUpdate(progressMessage, 0);
-    await FileUtils.unzipFilesIfNeeded(files, onFile, onProgressUpdate);
-
-    progressMessage = 'Parsing files...';
-    this.currentProgressListener?.onProgressUpdate(progressMessage, 0);
-    const parserErrors = await this.tracePipeline.loadTraceFiles(traceFiles, onProgressUpdate);
-    this.currentProgressListener?.onOperationFinished();
-    this.userNotificationListener?.onParserErrors(parserErrors);
+    await this.tracePipeline.loadFiles(files, this.currentProgressListener);
   }
 
   private async processLoadedTraceFiles() {
     this.currentProgressListener?.onProgressUpdate('Computing frame mapping...', undefined);
 
+    // TODO: move this into the ProgressListener
     // allow the UI to update before making the main thread very busy
     await new Promise<void>((resolve) => setTimeout(resolve, 10));
 
     await this.tracePipeline.buildTraces();
     this.currentProgressListener?.onOperationFinished();
 
+    this.currentProgressListener?.onProgressUpdate('Initializing UI...', undefined);
+
     this.timelineData.initialize(
       this.tracePipeline.getTraces(),
       await this.tracePipeline.getScreenRecordingVideo()
     );
-    await this.createViewers();
+
+    this.viewers = new ViewerFactory().createViewers(this.tracePipeline.getTraces(), this.storage);
+    this.viewers.forEach((viewer) =>
+      viewer.setEmitAppEvent(async (event) => {
+        await this.onViewerAppEvent(event);
+      })
+    );
+
+    // Set position to initialize viewers as soon as they are created
+    await this.propagateTracePosition(this.getTracePositionForViewersInitialization(), true);
+
     this.appComponent.onTraceDataLoaded(this.viewers);
     this.isTraceDataVisualized = true;
 
@@ -266,28 +275,32 @@
     }
   }
 
-  private async createViewers() {
-    const traces = this.tracePipeline.getTraces();
-    const traceTypes = new Set<TraceType>();
-    traces.forEachTrace((trace) => {
-      traceTypes.add(trace.type);
-    });
-    this.viewers = new ViewerFactory().createViewers(traceTypes, traces, this.storage);
-
-    // Set position as soon as the viewers are created
-    await this.propagateTracePosition(this.timelineData.getCurrentPosition(), true);
-  }
-
-  private async executeIgnoringRecursiveTimestampNotifications(op: () => Promise<void>) {
-    if (this.isChangingCurrentTimestamp) {
-      return;
+  private getTracePositionForViewersInitialization(): TracePosition | undefined {
+    const position = this.timelineData.getCurrentPosition();
+    if (position) {
+      return position;
     }
-    this.isChangingCurrentTimestamp = true;
-    try {
-      await op();
-    } finally {
-      this.isChangingCurrentTimestamp = false;
+
+    // TimelineData might not provide a TracePosition because all the loaded traces are
+    // dumps with invalid timestamps (value zero). In this case let's create a TracePosition
+    // out of any entry from the loaded traces (if available).
+    const firstEntries = this.tracePipeline
+      .getTraces()
+      .mapTrace((trace) => {
+        if (trace.lengthEntries > 0) {
+          return trace.getEntry(0);
+        }
+        return undefined;
+      })
+      .filter((entry) => {
+        return entry !== undefined;
+      }) as Array<TraceEntry<object>>;
+
+    if (firstEntries.length > 0) {
+      return TracePosition.fromTraceEntry(firstEntries[0]);
     }
+
+    return undefined;
   }
 
   private resetAppToInitialState() {
diff --git a/tools/winscope/src/app/mediator_test.ts b/tools/winscope/src/app/mediator_test.ts
index 6c681b4..8158684 100644
--- a/tools/winscope/src/app/mediator_test.ts
+++ b/tools/winscope/src/app/mediator_test.ts
@@ -15,15 +15,18 @@
  */
 
 import {AbtChromeExtensionProtocolStub} from 'abt_chrome_extension/abt_chrome_extension_protocol_stub';
+import {RealTimestamp} from 'common/time';
 import {CrossToolProtocolStub} from 'cross_tool/cross_tool_protocol_stub';
+import {AppEventListenerEmitterStub} from 'interfaces/app_event_listener_emitter_stub';
 import {ProgressListenerStub} from 'interfaces/progress_listener_stub';
+import {UserNotificationListenerStub} from 'interfaces/user_notification_listener_stub';
 import {MockStorage} from 'test/unit/mock_storage';
 import {UnitTestUtils} from 'test/unit/utils';
-import {RealTimestamp} from 'trace/timestamp';
-import {TraceFile} from 'trace/trace_file';
 import {TracePosition} from 'trace/trace_position';
+import {TraceType} from 'trace/trace_type';
 import {ViewerFactory} from 'viewers/viewer_factory';
 import {ViewerStub} from 'viewers/viewer_stub';
+import {AppEvent, AppEventType, TabbedViewSwitched, TabbedViewSwitchRequest} from './app_event';
 import {AppComponentStub} from './components/app_component_stub';
 import {SnackBarOpenerStub} from './components/snack_bar_opener_stub';
 import {TimelineComponentStub} from './components/timeline/timeline_component_stub';
@@ -34,6 +37,7 @@
 describe('Mediator', () => {
   const viewerStub = new ViewerStub('Title');
   let inputFiles: File[];
+  let userNotificationListener: UserNotificationListenerStub;
   let tracePipeline: TracePipeline;
   let timelineData: TimelineData;
   let abtChromeExtensionProtocol: AbtChromeExtensionProtocolStub;
@@ -42,11 +46,13 @@
   let timelineComponent: TimelineComponentStub;
   let uploadTracesComponent: ProgressListenerStub;
   let collectTracesComponent: ProgressListenerStub;
+  let traceViewComponent: AppEventListenerEmitterStub;
   let snackBarOpener: SnackBarOpenerStub;
   let mediator: Mediator;
 
   const TIMESTAMP_10 = new RealTimestamp(10n);
   const TIMESTAMP_11 = new RealTimestamp(11n);
+
   const POSITION_10 = TracePosition.fromTimestamp(TIMESTAMP_10);
   const POSITION_11 = TracePosition.fromTimestamp(TIMESTAMP_11);
 
@@ -61,7 +67,8 @@
   });
 
   beforeEach(async () => {
-    tracePipeline = new TracePipeline();
+    userNotificationListener = new UserNotificationListenerStub();
+    tracePipeline = new TracePipeline(userNotificationListener);
     timelineData = new TimelineData();
     abtChromeExtensionProtocol = new AbtChromeExtensionProtocolStub();
     crossToolProtocol = new CrossToolProtocolStub();
@@ -69,6 +76,7 @@
     timelineComponent = new TimelineComponentStub();
     uploadTracesComponent = new ProgressListenerStub();
     collectTracesComponent = new ProgressListenerStub();
+    traceViewComponent = new AppEventListenerEmitterStub();
     snackBarOpener = new SnackBarOpenerStub();
     mediator = new Mediator(
       tracePipeline,
@@ -82,6 +90,7 @@
     mediator.setTimelineComponent(timelineComponent);
     mediator.setUploadTracesComponent(uploadTracesComponent);
     mediator.setCollectTracesComponent(collectTracesComponent);
+    mediator.setTraceViewComponent(traceViewComponent);
 
     spyOn(ViewerFactory.prototype, 'createViewers').and.returnValue([viewerStub]);
   });
@@ -92,7 +101,7 @@
       spyOn(uploadTracesComponent, 'onOperationFinished'),
       spyOn(timelineData, 'initialize').and.callThrough(),
       spyOn(appComponent, 'onTraceDataLoaded'),
-      spyOn(viewerStub, 'onTracePositionUpdate'),
+      spyOn(viewerStub, 'onAppEvent'),
       spyOn(timelineComponent, 'onTracePositionUpdate'),
       spyOn(crossToolProtocol, 'sendTimestamp'),
     ];
@@ -103,7 +112,7 @@
     expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalled();
     expect(timelineData.initialize).not.toHaveBeenCalled();
     expect(appComponent.onTraceDataLoaded).not.toHaveBeenCalled();
-    expect(viewerStub.onTracePositionUpdate).not.toHaveBeenCalled();
+    expect(viewerStub.onAppEvent).not.toHaveBeenCalled();
 
     spies.forEach((spy) => {
       spy.calls.reset();
@@ -116,7 +125,11 @@
     expect(appComponent.onTraceDataLoaded).toHaveBeenCalledOnceWith([viewerStub]);
 
     // propagates trace position on viewers creation
-    expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
+    expect(viewerStub.onAppEvent).toHaveBeenCalledOnceWith(
+      jasmine.objectContaining({
+        type: AppEventType.TRACE_POSITION_UPDATE,
+      } as AppEvent)
+    );
     expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1);
     expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0);
   });
@@ -127,7 +140,7 @@
       spyOn(collectTracesComponent, 'onOperationFinished'),
       spyOn(timelineData, 'initialize').and.callThrough(),
       spyOn(appComponent, 'onTraceDataLoaded'),
-      spyOn(viewerStub, 'onTracePositionUpdate'),
+      spyOn(viewerStub, 'onAppEvent'),
       spyOn(timelineComponent, 'onTracePositionUpdate'),
       spyOn(crossToolProtocol, 'sendTimestamp'),
     ];
@@ -140,7 +153,11 @@
     expect(appComponent.onTraceDataLoaded).toHaveBeenCalledOnceWith([viewerStub]);
 
     // propagates trace position on viewers creation
-    expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
+    expect(viewerStub.onAppEvent).toHaveBeenCalledOnceWith(
+      jasmine.objectContaining({
+        type: AppEventType.TRACE_POSITION_UPDATE,
+      } as AppEvent)
+    );
     expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1);
     expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0);
   });
@@ -170,64 +187,100 @@
   });
 
   it('propagates trace position update from timeline component', async () => {
-    await loadTraceFiles();
+    await loadFiles();
     await mediator.onWinscopeViewTracesRequest();
 
-    spyOn(viewerStub, 'onTracePositionUpdate');
+    spyOn(viewerStub, 'onAppEvent');
     spyOn(timelineComponent, 'onTracePositionUpdate');
     spyOn(crossToolProtocol, 'sendTimestamp');
-    expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(0);
+    expect(viewerStub.onAppEvent).toHaveBeenCalledTimes(0);
     expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0);
     expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0);
 
     // notify position
     await mediator.onTimelineTracePositionUpdate(POSITION_10);
-    expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
+    expect(viewerStub.onAppEvent).toHaveBeenCalledOnceWith(
+      jasmine.objectContaining({
+        type: AppEventType.TRACE_POSITION_UPDATE,
+      } as AppEvent)
+    );
     expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1);
     expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(1);
 
     // notify position
     await mediator.onTimelineTracePositionUpdate(POSITION_11);
-    expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(2);
+    expect(viewerStub.onAppEvent).toHaveBeenCalledTimes(2);
+    expect(viewerStub.onAppEvent).toHaveBeenCalledWith(
+      jasmine.objectContaining({
+        type: AppEventType.TRACE_POSITION_UPDATE,
+      } as AppEvent)
+    );
     expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(2);
     expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(2);
   });
 
+  it("initializes viewers' trace position also when loaded traces have no valid timestamps", async () => {
+    spyOn(viewerStub, 'onAppEvent');
+
+    const dumpFile = await UnitTestUtils.getFixtureFile('traces/dump_WindowManager.pb');
+    await mediator.onWinscopeFilesUploaded([dumpFile]);
+    await mediator.onWinscopeViewTracesRequest();
+
+    expect(viewerStub.onAppEvent).toHaveBeenCalledOnceWith(
+      jasmine.objectContaining({
+        type: AppEventType.TRACE_POSITION_UPDATE,
+      } as AppEvent)
+    );
+  });
+
   describe('timestamp received from remote tool', () => {
     it('propagates trace position update', async () => {
-      await loadTraceFiles();
+      await loadFiles();
       await mediator.onWinscopeViewTracesRequest();
 
-      spyOn(viewerStub, 'onTracePositionUpdate');
+      spyOn(viewerStub, 'onAppEvent');
       spyOn(timelineComponent, 'onTracePositionUpdate');
-      expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(0);
+      expect(viewerStub.onAppEvent).toHaveBeenCalledTimes(0);
       expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0);
 
       // receive timestamp
       await crossToolProtocol.onTimestampReceived(TIMESTAMP_10);
-      expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
+      expect(viewerStub.onAppEvent).toHaveBeenCalledOnceWith(
+        jasmine.objectContaining({
+          type: AppEventType.TRACE_POSITION_UPDATE,
+        } as AppEvent)
+      );
       expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1);
 
       // receive timestamp
       await crossToolProtocol.onTimestampReceived(TIMESTAMP_11);
-      expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(2);
+      expect(viewerStub.onAppEvent).toHaveBeenCalledTimes(2);
+      expect(viewerStub.onAppEvent).toHaveBeenCalledWith(
+        jasmine.objectContaining({
+          type: AppEventType.TRACE_POSITION_UPDATE,
+        } as AppEvent)
+      );
       expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(2);
     });
 
     it("doesn't propagate timestamp back to remote tool", async () => {
-      await loadTraceFiles();
+      await loadFiles();
       await mediator.onWinscopeViewTracesRequest();
 
-      spyOn(viewerStub, 'onTracePositionUpdate');
+      spyOn(viewerStub, 'onAppEvent');
       spyOn(crossToolProtocol, 'sendTimestamp');
 
       // receive timestamp
       await crossToolProtocol.onTimestampReceived(TIMESTAMP_10);
-      expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
+      expect(viewerStub.onAppEvent).toHaveBeenCalledOnceWith(
+        jasmine.objectContaining({
+          type: AppEventType.TRACE_POSITION_UPDATE,
+        } as AppEvent)
+      );
       expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0);
     });
 
-    it('defers propagation till traces are loaded and visualized', async () => {
+    it('defers trace position propagation till traces are loaded and visualized', async () => {
       spyOn(timelineComponent, 'onTracePositionUpdate');
 
       // keep timestamp for later
@@ -239,15 +292,46 @@
       expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0);
 
       // apply timestamp
-      await loadTraceFiles();
+      await loadFiles();
       await mediator.onWinscopeViewTracesRequest();
       expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledWith(POSITION_11);
     });
   });
 
-  const loadTraceFiles = async () => {
-    const traceFiles = inputFiles.map((file) => new TraceFile(file));
-    const errors = await tracePipeline.loadTraceFiles(traceFiles);
-    expect(errors).toEqual([]);
+  it("forwards 'switched view' events", async () => {
+    //TODO (after integrating also the timeline with AppEvent):
+    // spyOn(timelineComponent, 'onAppEvent') + checks
+
+    spyOn(appComponent, 'onAppEvent');
+    expect(appComponent.onAppEvent).not.toHaveBeenCalled();
+
+    const event = new TabbedViewSwitched(viewerStub.getViews()[0]);
+    await mediator.onTraceViewAppEvent(event);
+    expect(appComponent.onAppEvent).toHaveBeenCalledOnceWith(
+      jasmine.objectContaining({
+        type: AppEventType.TABBED_VIEW_SWITCHED,
+      } as AppEvent)
+    );
+  });
+
+  it("forwards 'switch view' requests from viewers to trace view component", async () => {
+    await mediator.onWinscopeViewTracesRequest();
+
+    spyOn(traceViewComponent, 'onAppEvent');
+    expect(traceViewComponent.onAppEvent).not.toHaveBeenCalled();
+
+    await viewerStub.emitAppEventForTesting(new TabbedViewSwitchRequest(TraceType.VIEW_CAPTURE));
+
+    expect(traceViewComponent.onAppEvent).toHaveBeenCalledOnceWith(
+      jasmine.objectContaining({
+        type: AppEventType.TABBED_VIEW_SWITCH_REQUEST,
+      } as AppEvent)
+    );
+  });
+
+  const loadFiles = async () => {
+    const parserErrorsSpy = spyOn(userNotificationListener, 'onParserErrors');
+    await tracePipeline.loadFiles(inputFiles);
+    expect(parserErrorsSpy).not.toHaveBeenCalled();
   };
 });
diff --git a/tools/winscope/src/app/timeline_data.ts b/tools/winscope/src/app/timeline_data.ts
index 7910600..2e93477 100644
--- a/tools/winscope/src/app/timeline_data.ts
+++ b/tools/winscope/src/app/timeline_data.ts
@@ -15,20 +15,16 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
+import {TimeRange, Timestamp, TimestampType} from 'common/time';
 import {TimeUtils} from 'common/time_utils';
 import {ScreenRecordingUtils} from 'trace/screen_recording_utils';
-import {Timestamp, TimestampType} from 'trace/timestamp';
-import {TraceEntry} from 'trace/trace';
+import {Trace, TraceEntry} from 'trace/trace';
 import {Traces} from 'trace/traces';
 import {TraceEntryFinder} from 'trace/trace_entry_finder';
 import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 
-export interface TimeRange {
-  from: Timestamp;
-  to: Timestamp;
-}
-const INVALID_TIMESTAMP = 0n
+const INVALID_TIMESTAMP = 0n;
 
 export class TimelineData {
   private traces = new Traces();
@@ -38,6 +34,7 @@
   private lastEntry?: TraceEntry<{}>;
   private explicitlySetPosition?: TracePosition;
   private explicitlySetSelection?: TimeRange;
+  private explicitlySetZoomRange?: TimeRange;
   private activeViewTraceTypes: TraceType[] = []; // dependencies of current active view
 
   initialize(traces: Traces, screenRecordingVideo: Blob | undefined) {
@@ -45,14 +42,12 @@
 
     this.traces = new Traces();
     traces.forEachTrace((trace, type) => {
-      if (type === TraceType.WINDOW_MANAGER) {
-        // Filter out WindowManager dumps with no timestamp from timeline
-        if (
-          trace.lengthEntries === 1 &&
-          trace.getEntry(0).getTimestamp().getValueNs() === INVALID_TIMESTAMP
-        ) {
-          return;
-        }
+      // Filter out dumps with invalid timestamp (would mess up the timeline)
+      if (
+        trace.lengthEntries === 1 &&
+        trace.getEntry(0).getTimestamp().getValueNs() === INVALID_TIMESTAMP
+      ) {
+        return;
       }
 
       this.traces.setTrace(type, trace);
@@ -64,18 +59,31 @@
     this.timestampType = this.firstEntry?.getTimestamp().getType();
   }
 
+  private lastReturnedCurrentPosition?: TracePosition;
   getCurrentPosition(): TracePosition | undefined {
     if (this.explicitlySetPosition) {
       return this.explicitlySetPosition;
     }
+
+    let currentPosition: TracePosition | undefined = undefined;
+    if (this.firstEntry) {
+      currentPosition = TracePosition.fromTraceEntry(this.firstEntry);
+    }
+
     const firstActiveEntry = this.getFirstEntryOfActiveViewTraces();
     if (firstActiveEntry) {
-      return TracePosition.fromTraceEntry(firstActiveEntry);
+      currentPosition = TracePosition.fromTraceEntry(firstActiveEntry);
     }
-    if (this.firstEntry) {
-      return TracePosition.fromTraceEntry(this.firstEntry);
+
+    if (
+      this.lastReturnedCurrentPosition === undefined ||
+      currentPosition === undefined ||
+      !this.lastReturnedCurrentPosition.isEqual(currentPosition)
+    ) {
+      this.lastReturnedCurrentPosition = currentPosition;
     }
-    return undefined;
+
+    return this.lastReturnedCurrentPosition;
   }
 
   setPosition(position: TracePosition | undefined) {
@@ -96,6 +104,24 @@
     this.explicitlySetPosition = position;
   }
 
+  makePositionFromActiveTrace(timestamp: Timestamp): TracePosition {
+    let trace: Trace<object> | undefined;
+    if (this.activeViewTraceTypes.length > 0) {
+      trace = this.traces.getTrace(this.activeViewTraceTypes[0]);
+    }
+
+    if (!trace) {
+      return TracePosition.fromTimestamp(timestamp);
+    }
+
+    const entry = trace.findClosestEntry(timestamp);
+    if (!entry) {
+      return TracePosition.fromTimestamp(timestamp);
+    }
+
+    return TracePosition.fromTraceEntry(entry, timestamp);
+  }
+
   setActiveViewTraceTypes(types: TraceType[]) {
     this.activeViewTraceTypes = types;
   }
@@ -104,14 +130,26 @@
     return this.timestampType;
   }
 
+  private lastReturnedFullTimeRange?: TimeRange;
   getFullTimeRange(): TimeRange {
     if (!this.firstEntry || !this.lastEntry) {
       throw Error('Trying to get full time range when there are no timestamps');
     }
-    return {
+
+    const fullTimeRange = {
       from: this.firstEntry.getTimestamp(),
       to: this.lastEntry.getTimestamp(),
     };
+
+    if (
+      this.lastReturnedFullTimeRange === undefined ||
+      this.lastReturnedFullTimeRange.from.getValueNs() !== fullTimeRange.from.getValueNs() ||
+      this.lastReturnedFullTimeRange.to.getValueNs() !== fullTimeRange.to.getValueNs()
+    ) {
+      this.lastReturnedFullTimeRange = fullTimeRange;
+    }
+
+    return this.lastReturnedFullTimeRange;
   }
 
   getSelectionTimeRange(): TimeRange {
@@ -126,6 +164,18 @@
     this.explicitlySetSelection = selection;
   }
 
+  getZoomRange(): TimeRange {
+    if (this.explicitlySetZoomRange === undefined) {
+      return this.getFullTimeRange();
+    } else {
+      return this.explicitlySetZoomRange;
+    }
+  }
+
+  setZoom(zoomRange: TimeRange) {
+    this.explicitlySetZoomRange = zoomRange;
+  }
+
   getTraces(): Traces {
     return this.traces;
   }
@@ -192,15 +242,23 @@
     return trace.getEntry(currentIndex + 1);
   }
 
+  private lastReturnedCurrentEntries: Map<TraceType, TraceEntry<any> | undefined> = new Map();
   findCurrentEntryFor(type: TraceType): TraceEntry<{}> | undefined {
     const position = this.getCurrentPosition();
     if (!position) {
       return undefined;
     }
-    return TraceEntryFinder.findCorrespondingEntry(
+
+    const entry = TraceEntryFinder.findCorrespondingEntry(
       assertDefined(this.traces.getTrace(type)),
       position
     );
+
+    if (this.lastReturnedCurrentEntries.get(type)?.getIndex() !== entry?.getIndex()) {
+      this.lastReturnedCurrentEntries.set(type, entry);
+    }
+
+    return this.lastReturnedCurrentEntries.get(type);
   }
 
   moveToPreviousEntryFor(type: TraceType) {
diff --git a/tools/winscope/src/app/timeline_data_test.ts b/tools/winscope/src/app/timeline_data_test.ts
index 9d4d097..3fdde4a 100644
--- a/tools/winscope/src/app/timeline_data_test.ts
+++ b/tools/winscope/src/app/timeline_data_test.ts
@@ -14,16 +14,18 @@
  * limitations under the License.
  */
 
+import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp, Timestamp, TimestampType} from 'common/time';
 import {TracesBuilder} from 'test/unit/traces_builder';
-import {RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
 import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {TimelineData} from './timeline_data';
 
 describe('TimelineData', () => {
   let timelineData: TimelineData;
-  const timestamp10 = new Timestamp(TimestampType.REAL, 10n);
-  const timestamp11 = new Timestamp(TimestampType.REAL, 11n);
+
+  const timestamp10 = new RealTimestamp(10n);
+  const timestamp11 = new RealTimestamp(11n);
 
   const traces = new TracesBuilder()
     .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
@@ -31,11 +33,12 @@
     .build();
 
   const position10 = TracePosition.fromTraceEntry(
-    traces.getTrace(TraceType.SURFACE_FLINGER)!.getEntry(0)
+    assertDefined(traces.getTrace(TraceType.SURFACE_FLINGER)).getEntry(0)
   );
   const position11 = TracePosition.fromTraceEntry(
-    traces.getTrace(TraceType.WINDOW_MANAGER)!.getEntry(0)
+    assertDefined(traces.getTrace(TraceType.WINDOW_MANAGER)).getEntry(0)
   );
+  const position1000 = TracePosition.fromTimestamp(new RealTimestamp(1000n));
 
   beforeEach(() => {
     timelineData = new TimelineData();
@@ -71,15 +74,14 @@
     timelineData.initialize(traces, undefined);
     expect(timelineData.getCurrentPosition()).toEqual(position10);
 
-    const explicitPosition = TracePosition.fromTimestamp(new RealTimestamp(1000n));
-    timelineData.setPosition(explicitPosition);
-    expect(timelineData.getCurrentPosition()).toEqual(explicitPosition);
+    timelineData.setPosition(position1000);
+    expect(timelineData.getCurrentPosition()).toEqual(position1000);
 
     timelineData.setActiveViewTraceTypes([TraceType.SURFACE_FLINGER]);
-    expect(timelineData.getCurrentPosition()).toEqual(explicitPosition);
+    expect(timelineData.getCurrentPosition()).toEqual(position1000);
 
     timelineData.setActiveViewTraceTypes([TraceType.WINDOW_MANAGER]);
-    expect(timelineData.getCurrentPosition()).toEqual(explicitPosition);
+    expect(timelineData.getCurrentPosition()).toEqual(position1000);
   });
 
   it('sets active trace types and update current position accordingly', () => {
@@ -151,4 +153,72 @@
       expect(timelineData.hasMoreThanOneDistinctTimestamp()).toBeTrue();
     }
   });
+
+  it('getCurrentPosition() returns same object if no change to range', () => {
+    timelineData.initialize(traces, undefined);
+
+    expect(timelineData.getCurrentPosition()).toBe(timelineData.getCurrentPosition());
+
+    timelineData.setPosition(position11);
+
+    expect(timelineData.getCurrentPosition()).toBe(timelineData.getCurrentPosition());
+  });
+
+  it('makePositionFromActiveTrace()', () => {
+    timelineData.initialize(traces, undefined);
+    const time100 = new RealTimestamp(100n);
+
+    {
+      timelineData.setActiveViewTraceTypes([TraceType.SURFACE_FLINGER]);
+      const position = timelineData.makePositionFromActiveTrace(time100);
+      expect(position.timestamp).toEqual(time100);
+      expect(position.entry).toEqual(traces.getTrace(TraceType.SURFACE_FLINGER)?.getEntry(0));
+    }
+
+    {
+      timelineData.setActiveViewTraceTypes([TraceType.WINDOW_MANAGER]);
+      const position = timelineData.makePositionFromActiveTrace(time100);
+      expect(position.timestamp).toEqual(time100);
+      expect(position.entry).toEqual(traces.getTrace(TraceType.WINDOW_MANAGER)?.getEntry(0));
+    }
+  });
+
+  it('getFullTimeRange() returns same object if no change to range', () => {
+    timelineData.initialize(traces, undefined);
+
+    expect(timelineData.getFullTimeRange()).toBe(timelineData.getFullTimeRange());
+  });
+
+  it('getSelectionTimeRange() returns same object if no change to range', () => {
+    timelineData.initialize(traces, undefined);
+
+    expect(timelineData.getSelectionTimeRange()).toBe(timelineData.getSelectionTimeRange());
+
+    timelineData.setSelectionTimeRange({
+      from: new Timestamp(TimestampType.REAL, 0n),
+      to: new Timestamp(TimestampType.REAL, 5n),
+    });
+
+    expect(timelineData.getSelectionTimeRange()).toBe(timelineData.getSelectionTimeRange());
+  });
+
+  it('getZoomRange() returns same object if no change to range', () => {
+    timelineData.initialize(traces, undefined);
+
+    expect(timelineData.getZoomRange()).toBe(timelineData.getZoomRange());
+
+    timelineData.setZoom({
+      from: new Timestamp(TimestampType.REAL, 0n),
+      to: new Timestamp(TimestampType.REAL, 5n),
+    });
+
+    expect(timelineData.getZoomRange()).toBe(timelineData.getZoomRange());
+  });
+
+  it("getCurrentPosition() prioritizes active trace's first entry", () => {
+    timelineData.initialize(traces, undefined);
+    timelineData.setActiveViewTraceTypes([TraceType.WINDOW_MANAGER]);
+
+    expect(timelineData.getCurrentPosition()?.timestamp).toBe(timestamp11);
+  });
 });
diff --git a/tools/winscope/src/app/trace_file_filter.ts b/tools/winscope/src/app/trace_file_filter.ts
new file mode 100644
index 0000000..3fe8f33
--- /dev/null
+++ b/tools/winscope/src/app/trace_file_filter.ts
@@ -0,0 +1,113 @@
+/*
+ * 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 {ParserError, ParserErrorType} from 'parsers/parser_factory';
+import {TraceFile} from 'trace/trace_file';
+
+export interface FilterResult {
+  perfetto?: TraceFile;
+  legacy: TraceFile[];
+  errors: ParserError[];
+}
+
+export class TraceFileFilter {
+  private static readonly BUGREPORT_SYSTRACE_PATH =
+    'FS/data/misc/perfetto-traces/bugreport/systrace.pftrace';
+  private static readonly BUGREPORT_LEGACY_FILES_ALLOWLIST = [
+    'FS/data/misc/wmtrace/',
+    'FS/data/misc/perfetto-traces/',
+    'proto/window_CRITICAL.proto',
+  ];
+
+  async filter(files: TraceFile[]): Promise<FilterResult> {
+    const bugreportMainEntry = files.find((file) => file.file.name === 'main_entry.txt');
+    const perfettoFiles = files.filter((file) => this.isPerfettoFile(file));
+    const legacyFiles = files.filter((file) => !this.isPerfettoFile(file));
+
+    if (!(await this.isBugreport(bugreportMainEntry, files))) {
+      const errors: ParserError[] = [];
+      const perfettoFile = this.pickLargestFile(perfettoFiles, errors);
+      return {
+        perfetto: perfettoFile,
+        legacy: legacyFiles,
+        errors,
+      };
+    }
+
+    return this.filterBugreport(assertDefined(bugreportMainEntry), perfettoFiles, legacyFiles);
+  }
+
+  private async isBugreport(
+    bugreportMainEntry: TraceFile | undefined,
+    files: TraceFile[]
+  ): Promise<boolean> {
+    if (!bugreportMainEntry) {
+      return false;
+    }
+    const bugreportName = (await bugreportMainEntry.file.text()).trim();
+    return (
+      files.find(
+        (file) =>
+          file.parentArchive === bugreportMainEntry.parentArchive &&
+          file.file.name === bugreportName
+      ) !== undefined
+    );
+  }
+
+  private filterBugreport(
+    bugreportMainEntry: TraceFile,
+    perfettoFiles: TraceFile[],
+    legacyFiles: TraceFile[]
+  ): FilterResult {
+    const isFileAllowlisted = (file: TraceFile) => {
+      for (const traceDir of TraceFileFilter.BUGREPORT_LEGACY_FILES_ALLOWLIST) {
+        if (file.file.name.startsWith(traceDir)) {
+          return true;
+        }
+      }
+      return false;
+    };
+
+    const fileBelongsToBugreport = (file: TraceFile) =>
+      file.parentArchive === bugreportMainEntry.parentArchive;
+
+    legacyFiles = legacyFiles.filter((file) => {
+      return isFileAllowlisted(file) || !fileBelongsToBugreport(file);
+    });
+
+    const perfettoFile = perfettoFiles.find(
+      (file) => file.file.name === TraceFileFilter.BUGREPORT_SYSTRACE_PATH
+    );
+    return {perfetto: perfettoFile, legacy: legacyFiles, errors: []};
+  }
+
+  private isPerfettoFile(file: TraceFile): boolean {
+    return file.file.name.endsWith('.pftrace') || file.file.name.endsWith('.perfetto-trace');
+  }
+
+  private pickLargestFile(files: TraceFile[], errors: ParserError[]): TraceFile | undefined {
+    if (files.length === 0) {
+      return undefined;
+    }
+    return files.reduce((largestSoFar, file) => {
+      const [largest, overridden] =
+        largestSoFar.file.size > file.file.size ? [largestSoFar, file] : [file, largestSoFar];
+      errors.push(new ParserError(ParserErrorType.OVERRIDE, overridden.getDescriptor()));
+      return largest;
+    });
+  }
+}
diff --git a/tools/winscope/src/app/trace_file_filter_test.ts b/tools/winscope/src/app/trace_file_filter_test.ts
new file mode 100644
index 0000000..c62f5c0
--- /dev/null
+++ b/tools/winscope/src/app/trace_file_filter_test.ts
@@ -0,0 +1,148 @@
+/*
+ * 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 {ParserError, ParserErrorType} from 'parsers/parser_factory';
+import {UnitTestUtils} from 'test/unit/utils';
+import {TraceFile} from 'trace/trace_file';
+import {TraceFileFilter} from './trace_file_filter';
+
+describe('TraceFileFilter', () => {
+  const filter = new TraceFileFilter();
+
+  // Could be any file, we just need an instance of File to be used as a fake bugreport archive
+  const bugreportArchive = new File([new ArrayBuffer(0)], 'test_bugreport.zip') as unknown as File;
+
+  describe('bugreport (detects it is a bugreport)', () => {
+    it('ignores non-trace dirs', async () => {
+      const pickedBugreportFiles = [
+        makeTraceFile('FS/data/misc/wmtrace/surface_flinger.bp', bugreportArchive),
+        makeTraceFile('FS/data/misc/wmtrace/transactions.bp', bugreportArchive),
+        makeTraceFile('proto/window_CRITICAL.proto', bugreportArchive),
+      ];
+
+      const ignoredBugreportFile = makeTraceFile(
+        'FS/data/misc/ignored-dir/wm_transition_trace.bp',
+        bugreportArchive
+      );
+
+      const bugreportFiles = [
+        await makeBugreportMainEntryTraceFile(),
+        await makeBugreportCodenameTraceFile(),
+        ...pickedBugreportFiles,
+        ignoredBugreportFile,
+      ];
+
+      // Corner case:
+      // A plain trace file is loaded along the bugreport
+      //    -> trace file must not be ignored
+      //
+      // Note:
+      // The even weirder corner case where two bugreports are loaded at the same time is
+      // currently not properly handled.
+      const plainTraceFile = makeTraceFile(
+        'would-be-ignored-if-was-part-of-bugreport/input_method_clients.pb'
+      );
+
+      const result = await filter.filter([...bugreportFiles, plainTraceFile]);
+      expect(result.perfetto).toBeUndefined();
+
+      const expectedLegacy = new Set([...pickedBugreportFiles, plainTraceFile]);
+      const actualLegacy = new Set(result.legacy);
+      expect(actualLegacy).toEqual(expectedLegacy);
+    });
+
+    it('picks perfetto systrace.pftrace', async () => {
+      const perfettoSystemTrace = makeTraceFile(
+        'FS/data/misc/perfetto-traces/bugreport/systrace.pftrace',
+        bugreportArchive
+      );
+      const bugreportFiles = [
+        await makeBugreportMainEntryTraceFile(),
+        await makeBugreportCodenameTraceFile(),
+        perfettoSystemTrace,
+        makeTraceFile('FS/data/misc/perfetto-traces/other.perfetto-trace', bugreportArchive),
+        makeTraceFile('FS/data/misc/perfetto-traces/other.pftrace', bugreportArchive),
+      ];
+      const result = await filter.filter(bugreportFiles);
+      expect(result.perfetto).toEqual(perfettoSystemTrace);
+      expect(result.legacy).toEqual([]);
+      expect(result.errors).toEqual([]);
+    });
+
+    it('ignores perfetto traces other than systrace.pftrace', async () => {
+      const bugreportFiles = [
+        await makeBugreportMainEntryTraceFile(),
+        await makeBugreportCodenameTraceFile(),
+        makeTraceFile('FS/data/misc/perfetto-traces/other.perfetto-trace', bugreportArchive),
+        makeTraceFile('FS/data/misc/perfetto-traces/other.pftrace', bugreportArchive),
+      ];
+      const result = await filter.filter(bugreportFiles);
+      expect(result.perfetto).toBeUndefined();
+      expect(result.legacy).toEqual([]);
+      expect(result.errors).toEqual([]);
+    });
+  });
+
+  describe('plain input (no bugreport)', () => {
+    it('picks perfetto trace with .perfetto-trace extension', async () => {
+      const perfettoTrace = makeTraceFile('file.perfetto-trace');
+      const result = await filter.filter([perfettoTrace]);
+      expect(result.perfetto).toEqual(perfettoTrace);
+      expect(result.legacy).toEqual([]);
+      expect(result.errors).toEqual([]);
+    });
+
+    it('picks perfetto trace with .pftrace extension', async () => {
+      const pftrace = makeTraceFile('file.pftrace');
+      const result = await filter.filter([pftrace]);
+      expect(result.perfetto).toEqual(pftrace);
+      expect(result.legacy).toEqual([]);
+      expect(result.errors).toEqual([]);
+    });
+
+    it('picks largest perfetto trace', async () => {
+      const small = makeTraceFile('small.perfetto-trace', undefined, 10);
+      const medium = makeTraceFile('medium.perfetto-trace', undefined, 20);
+      const large = makeTraceFile('large.perfetto-trace', undefined, 30);
+      const result = await filter.filter([small, large, medium]);
+      expect(result.perfetto).toEqual(large);
+      expect(result.legacy).toEqual([]);
+      expect(result.errors).toEqual([
+        new ParserError(ParserErrorType.OVERRIDE, small.getDescriptor()),
+        new ParserError(ParserErrorType.OVERRIDE, medium.getDescriptor()),
+      ]);
+    });
+  });
+
+  const makeTraceFile = (filename: string, parentArchive?: File, size?: number) => {
+    size = size ?? 0;
+    const file = new File([new ArrayBuffer(size)], filename);
+    return new TraceFile(file as unknown as File, parentArchive);
+  };
+
+  const makeBugreportMainEntryTraceFile = async () => {
+    const file = await UnitTestUtils.getFixtureFile('bugreports/main_entry.txt', 'main_entry.txt');
+    return new TraceFile(file, bugreportArchive);
+  };
+
+  const makeBugreportCodenameTraceFile = async () => {
+    const file = await UnitTestUtils.getFixtureFile(
+      'bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
+      'bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt'
+    );
+    return new TraceFile(file, bugreportArchive);
+  };
+});
diff --git a/tools/winscope/src/app/trace_icons.ts b/tools/winscope/src/app/trace_icons.ts
index d7de8fa..1496a2c 100644
--- a/tools/winscope/src/app/trace_icons.ts
+++ b/tools/winscope/src/app/trace_icons.ts
@@ -7,7 +7,7 @@
 const WAYLAND_ICON = 'filter_none';
 const PROTO_LOG_ICON = 'notes';
 const SYSTEM_UI_ICON = 'filter_none';
-const LAUNCHER_ICON = 'filter_none';
+const VIEW_CAPTURE_ICON = 'filter_none';
 const IME_ICON = 'keyboard';
 const ACCESSIBILITY_ICON = 'filter_none';
 const TAG_ICON = 'details';
@@ -28,7 +28,9 @@
   [TraceType.WAYLAND_DUMP]: WAYLAND_ICON,
   [TraceType.PROTO_LOG]: PROTO_LOG_ICON,
   [TraceType.SYSTEM_UI]: SYSTEM_UI_ICON,
-  [TraceType.LAUNCHER]: LAUNCHER_ICON,
+  [TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY]: VIEW_CAPTURE_ICON,
+  [TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER]: VIEW_CAPTURE_ICON,
+  [TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER]: VIEW_CAPTURE_ICON,
   [TraceType.INPUT_METHOD_CLIENTS]: IME_ICON,
   [TraceType.INPUT_METHOD_SERVICE]: IME_ICON,
   [TraceType.INPUT_METHOD_MANAGER_SERVICE]: IME_ICON,
diff --git a/tools/winscope/src/app/trace_info.ts b/tools/winscope/src/app/trace_info.ts
index 75afe69..ce7fe09 100644
--- a/tools/winscope/src/app/trace_info.ts
+++ b/tools/winscope/src/app/trace_info.ts
@@ -23,7 +23,7 @@
 const WAYLAND_ICON = 'filter_none';
 const PROTO_LOG_ICON = 'notes';
 const SYSTEM_UI_ICON = 'filter_none';
-const LAUNCHER_ICON = 'filter_none';
+const VIEW_CAPTURE_ICON = 'filter_none';
 const IME_ICON = 'keyboard_alt';
 const ACCESSIBILITY_ICON = 'accessibility_new';
 const TAG_ICON = 'details';
@@ -102,18 +102,23 @@
     color: '#7A86FF',
     downloadArchiveDir: 'sysui',
   },
-  [TraceType.LAUNCHER]: {
-    name: 'Launcher',
-    icon: LAUNCHER_ICON,
+  [TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY]: {
+    name: 'View Capture - Nexuslauncher',
+    icon: VIEW_CAPTURE_ICON,
     color: '#137333',
-    downloadArchiveDir: 'launcher',
+    downloadArchiveDir: 'vc',
   },
-  // TODO: Choose ViewCapture icon, color, title name, and download archive directory
-  [TraceType.VIEW_CAPTURE]: {
-    name: 'View Capture',
-    icon: LAUNCHER_ICON,
+  [TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER]: {
+    name: 'View Capture - Taskbar',
+    icon: VIEW_CAPTURE_ICON,
     color: '#137333',
-    downloadArchiveDir: 'launcher',
+    downloadArchiveDir: 'vc',
+  },
+  [TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER]: {
+    name: 'View Capture - Taskbar Overlay',
+    icon: VIEW_CAPTURE_ICON,
+    color: '#137333',
+    downloadArchiveDir: 'vc',
   },
   [TraceType.INPUT_METHOD_CLIENTS]: {
     name: 'IME Clients',
diff --git a/tools/winscope/src/app/trace_pipeline.ts b/tools/winscope/src/app/trace_pipeline.ts
index 2c9d8bc..6ca6c68 100644
--- a/tools/winscope/src/app/trace_pipeline.ts
+++ b/tools/winscope/src/app/trace_pipeline.ts
@@ -14,36 +14,66 @@
  * limitations under the License.
  */
 
-import {FunctionUtils, OnProgressUpdateType} from 'common/function_utils';
-import {ParserError, ParserFactory} from 'parsers/parser_factory';
+import {FileUtils, OnFile} from 'common/file_utils';
+import {TimestampType} from 'common/time';
+import {ProgressListener} from 'interfaces/progress_listener';
+import {UserNotificationListener} from 'interfaces/user_notification_listener';
+import {ParserError, ParserErrorType, ParserFactory} from 'parsers/parser_factory';
+import {ParserFactory as PerfettoParserFactory} from 'parsers/perfetto/parser_factory';
 import {TracesParserFactory} from 'parsers/traces_parser_factory';
 import {FrameMapper} from 'trace/frame_mapper';
 import {Parser} from 'trace/parser';
-import {TimestampType} from 'trace/timestamp';
 import {Trace} from 'trace/trace';
 import {Traces} from 'trace/traces';
 import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
+import {TraceFileFilter} from './trace_file_filter';
+import {TRACE_INFO} from './trace_info';
 
-class TracePipeline {
+export class TracePipeline {
+  private userNotificationListener?: UserNotificationListener;
+  private traceFileFilter = new TraceFileFilter();
   private parserFactory = new ParserFactory();
   private tracesParserFactory = new TracesParserFactory();
   private parsers: Array<Parser<object>> = [];
-  private files = new Map<TraceType, TraceFile>();
+  private loadedPerfettoTraceFile?: TraceFile;
+  private loadedTraceFiles = new Map<TraceType, TraceFile>();
   private traces = new Traces();
   private commonTimestampType?: TimestampType;
 
-  async loadTraceFiles(
-    traceFiles: TraceFile[],
-    onLoadProgressUpdate: OnProgressUpdateType = FunctionUtils.DO_NOTHING
-  ): Promise<ParserError[]> {
-    traceFiles = await this.filterBugreportFilesIfNeeded(traceFiles);
-    const [fileAndParsers, parserErrors] = await this.parserFactory.createParsers(
-      traceFiles,
-      onLoadProgressUpdate
+  constructor(userNotificationListener?: UserNotificationListener) {
+    this.userNotificationListener = userNotificationListener;
+  }
+
+  async loadFiles(files: File[], progressListener?: ProgressListener) {
+    const [traceFiles, errors] = await this.unzipFiles(files, progressListener);
+
+    const filterResult = await this.traceFileFilter.filter(traceFiles);
+    if (!filterResult.perfetto && filterResult.legacy.length === 0) {
+      progressListener?.onOperationFinished();
+      errors.push(new ParserError(ParserErrorType.NO_INPUT_FILES));
+      this.userNotificationListener?.onParserErrors(errors);
+      return;
+    }
+
+    errors.push(...filterResult.errors);
+
+    if (filterResult.perfetto) {
+      const perfettoParsers = await new PerfettoParserFactory().createParsers(
+        filterResult.perfetto,
+        progressListener
+      );
+      this.loadedPerfettoTraceFile = perfettoParsers.length > 0 ? filterResult.perfetto : undefined;
+      this.parsers = this.parsers.concat(perfettoParsers);
+    }
+
+    const [fileAndParsers, legacyErrors] = await this.parserFactory.createParsers(
+      filterResult.legacy,
+      progressListener
     );
+    errors.push(...legacyErrors);
     for (const fileAndParser of fileAndParsers) {
-      this.files.set(fileAndParser.parser.getTraceType(), fileAndParser.file);
+      this.loadedTraceFiles.set(fileAndParser.parser.getTraceType(), fileAndParser.file);
     }
 
     const newParsers = fileAndParsers.map((it) => it.parser);
@@ -67,7 +97,11 @@
       this.traces.deleteTrace(TraceType.SHELL_TRANSITION);
     }
 
-    return parserErrors;
+    progressListener?.onOperationFinished();
+
+    if (errors.length > 0) {
+      this.userNotificationListener?.onParserErrors(errors);
+    }
   }
 
   removeTrace(trace: Trace<object>) {
@@ -75,8 +109,33 @@
     this.traces.deleteTrace(trace.type);
   }
 
-  getLoadedFiles(): Map<TraceType, TraceFile> {
-    return this.files;
+  async makeZipArchiveWithLoadedTraceFiles(): Promise<Blob> {
+    const archiveFiles: File[] = [];
+
+    if (this.loadedPerfettoTraceFile) {
+      const archiveFilename = FileUtils.removeDirFromFileName(
+        this.loadedPerfettoTraceFile.file.name
+      );
+      const archiveFile = new File([this.loadedPerfettoTraceFile.file], archiveFilename);
+      archiveFiles.push(archiveFile);
+    }
+
+    this.loadedTraceFiles.forEach((traceFile, traceType) => {
+      const archiveDir =
+        TRACE_INFO[traceType].downloadArchiveDir.length > 0
+          ? TRACE_INFO[traceType].downloadArchiveDir + '/'
+          : '';
+      const archiveFilename = archiveDir + FileUtils.removeDirFromFileName(traceFile.file.name);
+      const archiveFile = new File([traceFile.file], archiveFilename);
+      archiveFiles.push(archiveFile);
+    });
+
+    // Remove duplicates because some traces (e.g. view capture) could share the same file
+    const uniqueArchiveFiles = archiveFiles.filter(
+      (file, index, fileList) => fileList.indexOf(file) === index
+    );
+
+    return await FileUtils.createZipArchive(uniqueArchiveFiles);
   }
 
   async buildTraces() {
@@ -102,41 +161,56 @@
     this.parsers = [];
     this.traces = new Traces();
     this.commonTimestampType = undefined;
-    this.files = new Map<TraceType, TraceFile>();
+    this.loadedPerfettoTraceFile = undefined;
+    this.loadedTraceFiles = new Map<TraceType, TraceFile>();
   }
 
-  private async filterBugreportFilesIfNeeded(files: TraceFile[]): Promise<TraceFile[]> {
-    const bugreportMainEntry = files.find((file) => file.file.name === 'main_entry.txt');
-    if (!bugreportMainEntry) {
-      return files;
-    }
+  private async unzipFiles(
+    files: File[],
+    progressListener?: ProgressListener
+  ): Promise<[TraceFile[], ParserError[]]> {
+    const traceFiles: TraceFile[] = [];
+    const errors: ParserError[] = [];
+    const progressMessage = 'Unzipping files...';
 
-    const bugreportName = (await bugreportMainEntry.file.text()).trim();
-    const isBugreport = files.find((file) => file.file.name === bugreportName) !== undefined;
-    if (!isBugreport) {
-      return files;
-    }
-
-    const BUGREPORT_FILES_ALLOWLIST = [
-      'FS/data/misc/wmtrace/',
-      'FS/data/misc/perfetto-traces/',
-      'proto/window_CRITICAL.proto',
-    ];
-    const isFileAllowlisted = (file: TraceFile) => {
-      for (const traceDir of BUGREPORT_FILES_ALLOWLIST) {
-        if (file.file.name.startsWith(traceDir)) {
-          return true;
-        }
-      }
-      return false;
+    const onProgressUpdate = (progressPercentage: number) => {
+      progressListener?.onProgressUpdate(progressMessage, progressPercentage);
     };
 
-    const fileBelongsToBugreport = (file: TraceFile) =>
-      file.parentArchive === bugreportMainEntry.parentArchive;
+    const onFile: OnFile = (file: File, parentArchive?: File) => {
+      traceFiles.push(new TraceFile(file, parentArchive));
+    };
 
-    return files.filter((file) => {
-      return isFileAllowlisted(file) || !fileBelongsToBugreport(file);
-    });
+    progressListener?.onProgressUpdate(progressMessage, 0);
+
+    for (let i = 0; i < files.length; i++) {
+      const file = files[i];
+
+      const onSubProgressUpdate = (subPercentage: number) => {
+        const totalPercentage = (100 * i) / files.length + subPercentage / files.length;
+        progressListener?.onProgressUpdate(progressMessage, totalPercentage);
+      };
+
+      if (FileUtils.isZipFile(file)) {
+        try {
+          const subFiles = await FileUtils.unzipFile(file, onSubProgressUpdate);
+          const subTraceFiles = subFiles.map((subFile) => {
+            return new TraceFile(subFile, file);
+          });
+          traceFiles.push(...subTraceFiles);
+          onSubProgressUpdate(100);
+        } catch (e) {
+          errors.push(new ParserError(ParserErrorType.CORRUPTED_ARCHIVE, file.name));
+        }
+      } else {
+        traceFiles.push(new TraceFile(file, undefined));
+        onSubProgressUpdate(100);
+      }
+    }
+
+    progressListener?.onProgressUpdate(progressMessage, 100);
+
+    return [traceFiles, errors];
   }
 
   private getCommonTimestampType(): TimestampType {
@@ -155,5 +229,3 @@
     throw Error('Failed to find common timestamp type across all traces');
   }
 }
-
-export {TracePipeline};
diff --git a/tools/winscope/src/app/trace_pipeline_test.ts b/tools/winscope/src/app/trace_pipeline_test.ts
index 33f03f7..6eb336b 100644
--- a/tools/winscope/src/app/trace_pipeline_test.ts
+++ b/tools/winscope/src/app/trace_pipeline_test.ts
@@ -15,25 +15,31 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
+import {FileUtils} from 'common/file_utils';
+import {UserNotificationListenerStub} from 'interfaces/user_notification_listener_stub';
+import {ParserError, ParserErrorType} from 'parsers/parser_factory';
 import {TracesUtils} from 'test/unit/traces_utils';
 import {UnitTestUtils} from 'test/unit/utils';
-import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
 import {TracePipeline} from './trace_pipeline';
 
 describe('TracePipeline', () => {
-  let validSfTraceFile: TraceFile;
-  let validWmTraceFile: TraceFile;
+  let validSfFile: File;
+  let validWmFile: File;
+  let userNotificationListener: UserNotificationListenerStub;
+  let parserErrorsSpy: jasmine.Spy<(errors: ParserError[]) => void>;
   let tracePipeline: TracePipeline;
 
   beforeEach(async () => {
-    validSfTraceFile = new TraceFile(
-      await UnitTestUtils.getFixtureFile('traces/elapsed_and_real_timestamp/SurfaceFlinger.pb')
+    validSfFile = await UnitTestUtils.getFixtureFile(
+      'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb'
     );
-    validWmTraceFile = new TraceFile(
-      await UnitTestUtils.getFixtureFile('traces/elapsed_and_real_timestamp/WindowManager.pb')
+    validWmFile = await UnitTestUtils.getFixtureFile(
+      'traces/elapsed_and_real_timestamp/WindowManager.pb'
     );
-    tracePipeline = new TracePipeline();
+    userNotificationListener = new UserNotificationListenerStub();
+    parserErrorsSpy = spyOn(userNotificationListener, 'onParserErrors');
+    tracePipeline = new TracePipeline(userNotificationListener);
   });
 
   it('can load valid trace files', async () => {
@@ -51,82 +57,62 @@
   it('can load a new file without dropping already-loaded traces', async () => {
     expect(tracePipeline.getTraces().getSize()).toEqual(0);
 
-    await tracePipeline.loadTraceFiles([validSfTraceFile]);
+    await tracePipeline.loadFiles([validSfFile]);
     expect(tracePipeline.getTraces().getSize()).toEqual(1);
 
-    await tracePipeline.loadTraceFiles([validWmTraceFile]);
+    await tracePipeline.loadFiles([validWmFile]);
     expect(tracePipeline.getTraces().getSize()).toEqual(2);
 
-    await tracePipeline.loadTraceFiles([validWmTraceFile]); // ignored (duplicated)
+    await tracePipeline.loadFiles([validWmFile]); // ignored (duplicated)
     expect(tracePipeline.getTraces().getSize()).toEqual(2);
   });
 
   it('can load bugreport and ignores non-trace dirs', async () => {
     expect(tracePipeline.getTraces().getSize()).toEqual(0);
 
-    // Could be any file, we just need an instance of File to be used as a fake bugreport archive
-    const bugreportArchive = await UnitTestUtils.getFixtureFile(
-      'bugreports/bugreport_stripped.zip'
-    );
-
     const bugreportFiles = [
-      new TraceFile(
-        await UnitTestUtils.getFixtureFile('bugreports/main_entry.txt', 'main_entry.txt'),
-        bugreportArchive
+      await UnitTestUtils.getFixtureFile('bugreports/main_entry.txt', 'main_entry.txt'),
+      await UnitTestUtils.getFixtureFile(
+        'bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
+        'bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt'
       ),
-      new TraceFile(
-        await UnitTestUtils.getFixtureFile(
-          'bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
-          'bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt'
-        ),
-        bugreportArchive
+      await UnitTestUtils.getFixtureFile(
+        'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
+        'FS/data/misc/wmtrace/surface_flinger.bp'
       ),
-      new TraceFile(
-        await UnitTestUtils.getFixtureFile(
-          'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
-          'FS/data/misc/wmtrace/surface_flinger.bp'
-        ),
-        bugreportArchive
+      await UnitTestUtils.getFixtureFile(
+        'traces/elapsed_and_real_timestamp/Transactions.pb',
+        'FS/data/misc/wmtrace/transactions.bp'
       ),
-      new TraceFile(
-        await UnitTestUtils.getFixtureFile(
-          'traces/elapsed_and_real_timestamp/Transactions.pb',
-          'FS/data/misc/wmtrace/transactions.bp'
-        ),
-        bugreportArchive
+      await UnitTestUtils.getFixtureFile(
+        'traces/elapsed_and_real_timestamp/WindowManager.pb',
+        'proto/window_CRITICAL.proto'
       ),
-      new TraceFile(
-        await UnitTestUtils.getFixtureFile(
-          'traces/elapsed_and_real_timestamp/WindowManager.pb',
-          'proto/window_CRITICAL.proto'
-        ),
-        bugreportArchive
-      ),
-      new TraceFile(
-        await UnitTestUtils.getFixtureFile(
-          'traces/elapsed_and_real_timestamp/wm_transition_trace.pb',
-          'FS/data/misc/ignored-dir/wm_transition_trace.bp'
-        ),
-        bugreportArchive
+      await UnitTestUtils.getFixtureFile(
+        'traces/elapsed_and_real_timestamp/wm_transition_trace.pb',
+        'FS/data/misc/ignored-dir/wm_transition_trace.bp'
       ),
     ];
 
+    const bugreportArchive = new File(
+      [await FileUtils.createZipArchive(bugreportFiles)],
+      'bugreport.zip'
+    );
+
     // Corner case:
-    // A plain trace file is loaded along the bugreport -> trace file must not be ignored
+    // Another file is loaded along the bugreport -> the file must not be ignored
     //
     // Note:
     // The even weirder corner case where two bugreports are loaded at the same time is
     // currently not properly handled.
-    const plainTraceFile = new TraceFile(
-      await UnitTestUtils.getFixtureFile(
-        'traces/elapsed_and_real_timestamp/InputMethodClients.pb',
-        'would-be-ignored-if-was-part-of-bugreport/input_method_clients.pb'
-      )
+    const otherFile = await UnitTestUtils.getFixtureFile(
+      'traces/elapsed_and_real_timestamp/InputMethodClients.pb',
+      'would-be-ignored-if-was-in-bugreport-archive/input_method_clients.pb'
     );
 
-    const mergedFiles = bugreportFiles.concat([plainTraceFile]);
-    const errors = await tracePipeline.loadTraceFiles(mergedFiles);
-    expect(errors.length).toEqual(0);
+    await tracePipeline.loadFiles([bugreportArchive, otherFile]);
+    expect(parserErrorsSpy).not.toHaveBeenCalled();
+
     await tracePipeline.buildTraces();
     const traces = tracePipeline.getTraces();
 
@@ -137,39 +123,78 @@
     expect(traces.getTrace(TraceType.WINDOW_MANAGER)).toBeDefined();
   });
 
-  it('is robust to invalid trace files', async () => {
-    const invalidTraceFiles = [
-      new TraceFile(await UnitTestUtils.getFixtureFile('winscope_homepage.png')),
-    ];
+  it('is robust to corrupted archive', async () => {
+    const corruptedArchive = await UnitTestUtils.getFixtureFile('corrupted_archive.zip');
 
-    const errors = await tracePipeline.loadTraceFiles(invalidTraceFiles);
+    await tracePipeline.loadFiles([corruptedArchive]);
+    expect(parserErrorsSpy).toHaveBeenCalledOnceWith([
+      new ParserError(ParserErrorType.CORRUPTED_ARCHIVE, 'corrupted_archive.zip'),
+      new ParserError(ParserErrorType.NO_INPUT_FILES),
+    ]);
+
     await tracePipeline.buildTraces();
-    expect(errors.length).toEqual(1);
+    expect(tracePipeline.getTraces().getSize()).toEqual(0);
+  });
+
+  it('is robust to invalid trace files', async () => {
+    const invalidFiles = [await UnitTestUtils.getFixtureFile('winscope_homepage.png')];
+
+    await tracePipeline.loadFiles(invalidFiles);
+    expect(parserErrorsSpy).toHaveBeenCalledOnceWith([
+      new ParserError(ParserErrorType.UNSUPPORTED_FORMAT, 'winscope_homepage.png'),
+    ]);
+
+    await tracePipeline.buildTraces();
     expect(tracePipeline.getTraces().getSize()).toEqual(0);
   });
 
   it('is robust to mixed valid and invalid trace files', async () => {
     expect(tracePipeline.getTraces().getSize()).toEqual(0);
     const files = [
-      new TraceFile(await UnitTestUtils.getFixtureFile('winscope_homepage.png')),
-      new TraceFile(await UnitTestUtils.getFixtureFile('traces/dump_WindowManager.pb')),
+      await UnitTestUtils.getFixtureFile('winscope_homepage.png'),
+      await UnitTestUtils.getFixtureFile('traces/dump_WindowManager.pb'),
     ];
-    const errors = await tracePipeline.loadTraceFiles(files);
+
+    await tracePipeline.loadFiles(files);
+    expect(parserErrorsSpy).toHaveBeenCalledOnceWith([
+      new ParserError(ParserErrorType.UNSUPPORTED_FORMAT, 'winscope_homepage.png'),
+    ]);
+
     await tracePipeline.buildTraces();
     expect(tracePipeline.getTraces().getSize()).toEqual(1);
-    expect(errors.length).toEqual(1);
   });
 
   it('is robust to trace files with no entries', async () => {
     const traceFilesWithNoEntries = [
-      new TraceFile(await UnitTestUtils.getFixtureFile('traces/no_entries_InputMethodClients.pb')),
+      await UnitTestUtils.getFixtureFile('traces/no_entries_InputMethodClients.pb'),
     ];
 
-    const errors = await tracePipeline.loadTraceFiles(traceFilesWithNoEntries);
+    await tracePipeline.loadFiles(traceFilesWithNoEntries);
+    expect(parserErrorsSpy).not.toHaveBeenCalled();
+
     await tracePipeline.buildTraces();
+    expect(tracePipeline.getTraces().getSize()).toEqual(1);
+  });
 
-    expect(errors.length).toEqual(0);
+  it('is robust to multiple files of same trace type', async () => {
+    const filesOfSameTraceType = [
+      await UnitTestUtils.getFixtureFile(
+        'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
+        'file0.pb'
+      ),
+      await UnitTestUtils.getFixtureFile(
+        'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
+        'file1.pb'
+      ),
+    ];
 
+    // Expect one trace to be overridden/discarded
+    await tracePipeline.loadFiles(filesOfSameTraceType);
+    expect(parserErrorsSpy).toHaveBeenCalledOnceWith([
+      new ParserError(ParserErrorType.OVERRIDE, 'file1.pb', TraceType.SURFACE_FLINGER),
+    ]);
+
+    await tracePipeline.buildTraces();
     expect(tracePipeline.getTraces().getSize()).toEqual(1);
   });
 
@@ -212,14 +237,12 @@
   });
 
   it('gets screenrecording data', async () => {
-    const traceFiles = [
-      new TraceFile(
-        await UnitTestUtils.getFixtureFile(
-          'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4'
-        )
+    const files = [
+      await UnitTestUtils.getFixtureFile(
+        'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4'
       ),
     ];
-    await tracePipeline.loadTraceFiles(traceFiles);
+    await tracePipeline.loadFiles(files);
     await tracePipeline.buildTraces();
 
     const video = await tracePipeline.getScreenRecordingVideo();
@@ -227,6 +250,30 @@
     expect(video!.size).toBeGreaterThan(0);
   });
 
+  it('creates zip archive with loaded trace files', async () => {
+    const files = [
+      await UnitTestUtils.getFixtureFile(
+        'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4'
+      ),
+      await UnitTestUtils.getFixtureFile('traces/perfetto/transactions_trace.perfetto-trace'),
+    ];
+    await tracePipeline.loadFiles(files);
+    const archiveBlob = await tracePipeline.makeZipArchiveWithLoadedTraceFiles();
+    const actualFiles = await FileUtils.unzipFile(archiveBlob);
+    const actualFilenames = actualFiles
+      .map((file) => {
+        return file.name;
+      })
+      .sort();
+
+    const expectedFilenames = [
+      'screen_recording_metadata_v2.mp4',
+      'transactions_trace.perfetto-trace',
+    ];
+
+    expect(actualFilenames).toEqual(expectedFilenames);
+  });
+
   it('can be cleared', async () => {
     await loadValidSfWmTraces();
     expect(tracePipeline.getTraces().getSize()).toBeGreaterThan(0);
@@ -236,9 +283,9 @@
   });
 
   const loadValidSfWmTraces = async () => {
-    const traceFiles = [validSfTraceFile, validWmTraceFile];
-    const errors = await tracePipeline.loadTraceFiles(traceFiles);
-    expect(errors.length).toEqual(0);
+    const files = [validSfFile, validWmFile];
+    await tracePipeline.loadFiles(files);
+    expect(parserErrorsSpy).not.toHaveBeenCalled();
     await tracePipeline.buildTraces();
   };
 });
diff --git a/tools/winscope/src/common/assert_utils.ts b/tools/winscope/src/common/assert_utils.ts
index bc20385..f315429 100644
--- a/tools/winscope/src/common/assert_utils.ts
+++ b/tools/winscope/src/common/assert_utils.ts
@@ -20,3 +20,9 @@
   }
   return value;
 }
+
+export function assertTrue(value: boolean, lazyErrorMessage: () => string) {
+  if (!value) {
+    throw new Error(lazyErrorMessage());
+  }
+}
diff --git a/tools/winscope/src/common/file_utils.ts b/tools/winscope/src/common/file_utils.ts
index ac2b688..5541678 100644
--- a/tools/winscope/src/common/file_utils.ts
+++ b/tools/winscope/src/common/file_utils.ts
@@ -19,7 +19,7 @@
 export type OnFile = (file: File, parentArchive: File | undefined) => void;
 
 class FileUtils {
-  static getFileExtension(file: File) {
+  static getFileExtension(file: File): string | undefined {
     const split = file.name.split('.');
     if (split.length > 1) {
       return split.pop();
@@ -27,7 +27,7 @@
     return undefined;
   }
 
-  static removeDirFromFileName(name: string) {
+  static removeDirFromFileName(name: string): string {
     if (name.includes('/')) {
       const startIndex = name.lastIndexOf('/') + 1;
       return name.slice(startIndex);
@@ -47,7 +47,7 @@
   }
 
   static async unzipFile(
-    file: File,
+    file: Blob,
     onProgressUpdate: OnProgressUpdateType = FunctionUtils.DO_NOTHING
   ): Promise<File[]> {
     const unzippedFiles: File[] = [];
@@ -72,28 +72,6 @@
     return unzippedFiles;
   }
 
-  static async unzipFilesIfNeeded(
-    files: File[],
-    onFile: OnFile,
-    onProgressUpdate: OnProgressUpdateType = FunctionUtils.DO_NOTHING
-  ) {
-    for (let i = 0; i < files.length; i++) {
-      const file = files[i];
-
-      const onSubprogressUpdate = (subPercentage: number) => {
-        const percentage = (100 * i) / files.length + subPercentage / files.length;
-        onProgressUpdate(percentage);
-      };
-
-      if (FileUtils.isZipFile(file)) {
-        const unzippedFile = await FileUtils.unzipFile(file, onSubprogressUpdate);
-        unzippedFile.forEach((unzippedFile) => onFile(unzippedFile, file));
-      } else {
-        onFile(file, undefined);
-      }
-    }
-  }
-
   static isZipFile(file: File) {
     return FileUtils.getFileExtension(file) === 'zip';
   }
diff --git a/tools/winscope/src/common/geometry_utils.ts b/tools/winscope/src/common/geometry_utils.ts
new file mode 100644
index 0000000..2abeefc
--- /dev/null
+++ b/tools/winscope/src/common/geometry_utils.ts
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+export function isPointInRect(
+  point: {x: number; y: number},
+  rect: {
+    x: number;
+    y: number;
+    w: number;
+    h: number;
+  }
+): boolean {
+  return (
+    rect.x <= point.x &&
+    point.x <= rect.x + rect.w &&
+    rect.y <= point.y &&
+    point.y <= rect.y + rect.h
+  );
+}
diff --git a/tools/winscope/src/common/global_config.ts b/tools/winscope/src/common/global_config.ts
index 7325b34..bf6046b 100644
--- a/tools/winscope/src/common/global_config.ts
+++ b/tools/winscope/src/common/global_config.ts
@@ -17,7 +17,7 @@
 export type Schema = Omit<GlobalConfig, 'set'>;
 
 class GlobalConfig {
-  readonly MODE: 'DEV' | 'PROD' = 'PROD' as const;
+  readonly MODE: 'KARMA_TEST' | 'DEV' | 'PROD' = 'KARMA_TEST' as const;
 
   set(config: Schema) {
     Object.assign(this, config);
diff --git a/tools/winscope/src/common/object_utils.ts b/tools/winscope/src/common/object_utils.ts
new file mode 100644
index 0000000..24e913a
--- /dev/null
+++ b/tools/winscope/src/common/object_utils.ts
@@ -0,0 +1,98 @@
+/*
+ * 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, assertTrue} from './assert_utils';
+
+class Key {
+  constructor(public key: string, public index?: number) {}
+
+  isArrayKey(): boolean {
+    return this.index !== undefined;
+  }
+}
+
+export class ObjectUtils {
+  static readonly ARRAY_KEY_REGEX = new RegExp('(.+)\\[(\\d+)\\]');
+
+  static getProperty(obj: object, path: string): any {
+    const keys = ObjectUtils.parseKeys(path);
+    keys.forEach((key) => {
+      if (obj === undefined) {
+        return;
+      }
+
+      if (key.isArrayKey()) {
+        if ((obj as any)[key.key] === undefined) {
+          return;
+        }
+        assertTrue(Array.isArray((obj as any)[key.key]), () => 'Expected to be array');
+        obj = (obj as any)[key.key][assertDefined(key.index)];
+      } else {
+        obj = (obj as any)[key.key];
+      }
+    });
+    return obj;
+  }
+
+  static setProperty(obj: object, path: string, value: any) {
+    const keys = ObjectUtils.parseKeys(path);
+
+    keys.slice(0, -1).forEach((key) => {
+      if (key.isArrayKey()) {
+        ObjectUtils.initializePropertyArrayIfNeeded(obj, key);
+        obj = (obj as any)[key.key][assertDefined(key.index)];
+      } else {
+        ObjectUtils.initializePropertyIfNeeded(obj, key.key);
+        obj = (obj as any)[key.key];
+      }
+    });
+
+    const lastKey = assertDefined(keys.at(-1));
+    if (lastKey.isArrayKey()) {
+      ObjectUtils.initializePropertyArrayIfNeeded(obj, lastKey);
+      (obj as any)[lastKey.key][assertDefined(lastKey.index)] = value;
+    } else {
+      (obj as any)[lastKey.key] = value;
+    }
+  }
+
+  private static parseKeys(path: string): Key[] {
+    return path.split('.').map((rawKey) => {
+      const match = ObjectUtils.ARRAY_KEY_REGEX.exec(rawKey);
+      if (match) {
+        return new Key(match[1], Number(match[2]));
+      }
+      return new Key(rawKey);
+    });
+  }
+
+  private static initializePropertyIfNeeded(obj: object, key: string) {
+    if ((obj as any)[key] === undefined) {
+      (obj as any)[key] = {};
+    }
+    assertTrue(typeof (obj as any)[key] === 'object', () => 'Expected to be object');
+  }
+
+  private static initializePropertyArrayIfNeeded(obj: object, key: Key) {
+    if ((obj as any)[key.key] === undefined) {
+      (obj as any)[key.key] = [];
+    }
+    if ((obj as any)[key.key][assertDefined(key.index)] === undefined) {
+      (obj as any)[key.key][assertDefined(key.index)] = {};
+    }
+    assertTrue(Array.isArray((obj as any)[key.key]), () => 'Expected to be array');
+  }
+}
diff --git a/tools/winscope/src/common/object_utils_test.ts b/tools/winscope/src/common/object_utils_test.ts
new file mode 100644
index 0000000..c09e842
--- /dev/null
+++ b/tools/winscope/src/common/object_utils_test.ts
@@ -0,0 +1,55 @@
+/*
+ * 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 {ObjectUtils} from './object_utils';
+
+describe('ObjectUtils', () => {
+  it('getField()', () => {
+    const obj = {
+      child0: {
+        key0: 'value0',
+      },
+      child1: [{key1: 'value1'}, 10],
+    };
+
+    expect(ObjectUtils.getProperty(obj, 'child0')).toEqual({key0: 'value0'});
+    expect(ObjectUtils.getProperty(obj, 'child0.key0')).toEqual('value0');
+    expect(ObjectUtils.getProperty(obj, 'child1')).toEqual([{key1: 'value1'}, 10]);
+    expect(ObjectUtils.getProperty(obj, 'child1[0]')).toEqual({key1: 'value1'});
+    expect(ObjectUtils.getProperty(obj, 'child1[0].key1')).toEqual('value1');
+    expect(ObjectUtils.getProperty(obj, 'child1[1]')).toEqual(10);
+  });
+
+  it('setField()', () => {
+    const obj = {};
+
+    ObjectUtils.setProperty(obj, 'child0.key0', 'value0');
+    expect(obj).toEqual({
+      child0: {
+        key0: 'value0',
+      },
+    });
+
+    ObjectUtils.setProperty(obj, 'child1[0].key1', 'value1');
+    ObjectUtils.setProperty(obj, 'child1[1]', 10);
+    expect(obj).toEqual({
+      child0: {
+        key0: 'value0',
+      },
+      child1: [{key1: 'value1'}, 10],
+    });
+  });
+});
diff --git a/tools/winscope/src/app/components/canvas/canvas_drawer.ts b/tools/winscope/src/common/padding.ts
similarity index 63%
copy from tools/winscope/src/app/components/canvas/canvas_drawer.ts
copy to tools/winscope/src/common/padding.ts
index 03e8e83..1f6ef2a 100644
--- a/tools/winscope/src/app/components/canvas/canvas_drawer.ts
+++ b/tools/winscope/src/common/padding.ts
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -14,23 +14,9 @@
  * limitations under the License.
  */
 
-import {CanvasMouseHandler} from './canvas_mouse_handler';
-
 export interface Padding {
-  left: number;
   top: number;
   right: number;
   bottom: number;
-}
-
-export interface CanvasDrawer {
-  draw(): void;
-  handler: CanvasMouseHandler;
-  canvas: HTMLCanvasElement;
-  ctx: CanvasRenderingContext2D;
-  padding: Padding;
-  getXScale(): number;
-  getYScale(): number;
-  getWidth(): number;
-  getHeight(): number;
+  left: number;
 }
diff --git a/tools/winscope/src/common/string_utils.ts b/tools/winscope/src/common/string_utils.ts
index 98192ca..48621c8 100644
--- a/tools/winscope/src/common/string_utils.ts
+++ b/tools/winscope/src/common/string_utils.ts
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {assertTrue} from './assert_utils';
 
 class StringUtils {
   static parseBigIntStrippingUnit(s: string): bigint {
@@ -22,6 +23,76 @@
     }
     return BigInt(match[1]);
   }
+
+  static convertCamelToSnakeCase(s: string): string {
+    const result: string[] = [];
+
+    let prevChar: string | undefined;
+    for (const currChar of s) {
+      const prevCharCouldBeWordEnd =
+        prevChar && (StringUtils.isDigit(prevChar) || StringUtils.isLowerCase(prevChar));
+      const currCharCouldBeWordStart = StringUtils.isUpperCase(currChar);
+      if (prevCharCouldBeWordEnd && currCharCouldBeWordStart) {
+        result.push('_');
+        result.push(currChar.toLowerCase());
+      } else {
+        result.push(currChar);
+      }
+      prevChar = currChar;
+    }
+
+    return result.join('');
+  }
+
+  static convertSnakeToCamelCase(s: string): string {
+    const tokens = s.split('_').filter((token) => token.length > 0);
+    const tokensCapitalized = tokens.map((token) => {
+      return StringUtils.capitalizeFirstCharIfAlpha(token);
+    });
+
+    const inputStartsWithUnderscore = s[0] === '_';
+    let result = inputStartsWithUnderscore ? '_' : '';
+    result += tokens[0];
+    for (const token of tokensCapitalized.slice(1)) {
+      if (!StringUtils.isAlpha(token[0])) {
+        result += '_';
+      }
+      result += token;
+    }
+
+    return result;
+  }
+
+  static isAlpha(char: string): boolean {
+    assertTrue(char.length === 1, () => 'Input must be a single character');
+    return char[0].toLowerCase() !== char[0].toUpperCase();
+  }
+
+  static isDigit(char: string): boolean {
+    assertTrue(char.length === 1, () => 'Input must be a single character');
+    return char >= '0' && char <= '9';
+  }
+
+  static isLowerCase(char: string): boolean {
+    assertTrue(char.length === 1, () => 'Input must be a single character');
+    return StringUtils.isAlpha(char) && char === char.toLowerCase();
+  }
+
+  static isUpperCase(char: string): boolean {
+    assertTrue(char.length === 1, () => 'Input must be a single character');
+    return StringUtils.isAlpha(char) && char === char.toUpperCase();
+  }
+
+  private static capitalizeFirstCharIfAlpha(word: string): string {
+    if (word.length === 0) {
+      return word;
+    }
+
+    if (!StringUtils.isAlpha(word[0])) {
+      return word;
+    }
+    return word[0].toUpperCase() + word.slice(1);
+  }
 }
 
 export {StringUtils};
diff --git a/tools/winscope/src/common/string_utils_test.ts b/tools/winscope/src/common/string_utils_test.ts
index 2536c44..e0b6a7a 100644
--- a/tools/winscope/src/common/string_utils_test.ts
+++ b/tools/winscope/src/common/string_utils_test.ts
@@ -36,4 +36,98 @@
     expect(() => StringUtils.parseBigIntStrippingUnit('invalid')).toThrow();
     expect(() => StringUtils.parseBigIntStrippingUnit('invalid 10 unit')).toThrow();
   });
+
+  it('convertCamelToSnakeCase()', () => {
+    expect(StringUtils.convertCamelToSnakeCase('aaa')).toEqual('aaa');
+    expect(StringUtils.convertCamelToSnakeCase('Aaa')).toEqual('Aaa');
+    expect(StringUtils.convertCamelToSnakeCase('_aaa')).toEqual('_aaa');
+    expect(StringUtils.convertCamelToSnakeCase('_Aaa')).toEqual('_Aaa');
+
+    expect(StringUtils.convertCamelToSnakeCase('aaaBbb')).toEqual('aaa_bbb');
+    expect(StringUtils.convertCamelToSnakeCase('AaaBbb')).toEqual('Aaa_bbb');
+    expect(StringUtils.convertCamelToSnakeCase('aaa_bbb')).toEqual('aaa_bbb');
+    expect(StringUtils.convertCamelToSnakeCase('aaa_Bbb')).toEqual('aaa_Bbb');
+
+    expect(StringUtils.convertCamelToSnakeCase('aaaBbbCcc')).toEqual('aaa_bbb_ccc');
+    expect(StringUtils.convertCamelToSnakeCase('aaaBbb_ccc')).toEqual('aaa_bbb_ccc');
+    expect(StringUtils.convertCamelToSnakeCase('aaaBbb_Ccc')).toEqual('aaa_bbb_Ccc');
+
+    expect(StringUtils.convertCamelToSnakeCase('aaaBBBccc')).toEqual('aaa_bBBccc');
+    expect(StringUtils.convertCamelToSnakeCase('aaaBBBcccDDD')).toEqual('aaa_bBBccc_dDD');
+    expect(StringUtils.convertCamelToSnakeCase('aaaBBB_ccc')).toEqual('aaa_bBB_ccc');
+    expect(StringUtils.convertCamelToSnakeCase('aaaBbb_CCC')).toEqual('aaa_bbb_CCC');
+
+    expect(StringUtils.convertCamelToSnakeCase('_field_32')).toEqual('_field_32');
+    expect(StringUtils.convertCamelToSnakeCase('field_32')).toEqual('field_32');
+    expect(StringUtils.convertCamelToSnakeCase('field_32Bits')).toEqual('field_32_bits');
+    expect(StringUtils.convertCamelToSnakeCase('field_32BitsLsb')).toEqual('field_32_bits_lsb');
+    expect(StringUtils.convertCamelToSnakeCase('field_32bits')).toEqual('field_32bits');
+    expect(StringUtils.convertCamelToSnakeCase('field_32bitsLsb')).toEqual('field_32bits_lsb');
+
+    expect(StringUtils.convertCamelToSnakeCase('_aaaAaa.bbbBbb')).toEqual('_aaa_aaa.bbb_bbb');
+    expect(StringUtils.convertCamelToSnakeCase('aaaAaa.bbbBbb')).toEqual('aaa_aaa.bbb_bbb');
+    expect(StringUtils.convertCamelToSnakeCase('aaaAaa.field_32bitsLsb.bbbBbb')).toEqual(
+      'aaa_aaa.field_32bits_lsb.bbb_bbb'
+    );
+  });
+
+  it('convertSnakeToCamelCase()', () => {
+    expect(StringUtils.convertSnakeToCamelCase('_aaa')).toEqual('_aaa');
+    expect(StringUtils.convertSnakeToCamelCase('aaa')).toEqual('aaa');
+
+    expect(StringUtils.convertSnakeToCamelCase('aaa_bbb')).toEqual('aaaBbb');
+    expect(StringUtils.convertSnakeToCamelCase('_aaa_bbb')).toEqual('_aaaBbb');
+
+    expect(StringUtils.convertSnakeToCamelCase('aaa_bbb_ccc')).toEqual('aaaBbbCcc');
+    expect(StringUtils.convertSnakeToCamelCase('_aaa_bbb_ccc')).toEqual('_aaaBbbCcc');
+
+    expect(StringUtils.convertSnakeToCamelCase('_field_32')).toEqual('_field_32');
+    expect(StringUtils.convertSnakeToCamelCase('field_32')).toEqual('field_32');
+    expect(StringUtils.convertSnakeToCamelCase('field_32_bits')).toEqual('field_32Bits');
+    expect(StringUtils.convertSnakeToCamelCase('field_32_bits_lsb')).toEqual('field_32BitsLsb');
+    expect(StringUtils.convertSnakeToCamelCase('field_32bits')).toEqual('field_32bits');
+    expect(StringUtils.convertSnakeToCamelCase('field_32bits_lsb')).toEqual('field_32bitsLsb');
+
+    expect(StringUtils.convertSnakeToCamelCase('_aaa_aaa.bbb_bbb')).toEqual('_aaaAaa.bbbBbb');
+    expect(StringUtils.convertSnakeToCamelCase('aaa_aaa.bbb_bbb')).toEqual('aaaAaa.bbbBbb');
+    expect(StringUtils.convertSnakeToCamelCase('aaa_aaa.field_32bits_lsb.bbb_bbb')).toEqual(
+      'aaaAaa.field_32bitsLsb.bbbBbb'
+    );
+  });
+
+  it('isAlpha()', () => {
+    expect(StringUtils.isAlpha('a')).toBeTrue();
+    expect(StringUtils.isAlpha('A')).toBeTrue();
+    expect(StringUtils.isAlpha('_')).toBeFalse();
+    expect(StringUtils.isAlpha('0')).toBeFalse();
+    expect(StringUtils.isAlpha('9')).toBeFalse();
+  });
+
+  it('isDigit()', () => {
+    expect(StringUtils.isDigit('a')).toBeFalse();
+    expect(StringUtils.isDigit('A')).toBeFalse();
+    expect(StringUtils.isDigit('_')).toBeFalse();
+    expect(StringUtils.isDigit('0')).toBeTrue();
+    expect(StringUtils.isDigit('9')).toBeTrue();
+  });
+
+  it('isLowerCase()', () => {
+    expect(StringUtils.isLowerCase('a')).toBeTrue();
+    expect(StringUtils.isLowerCase('z')).toBeTrue();
+    expect(StringUtils.isLowerCase('A')).toBeFalse();
+    expect(StringUtils.isLowerCase('Z')).toBeFalse();
+    expect(StringUtils.isLowerCase('_')).toBeFalse();
+    expect(StringUtils.isLowerCase('0')).toBeFalse();
+    expect(StringUtils.isLowerCase('9')).toBeFalse();
+  });
+
+  it('isUpperCase()', () => {
+    expect(StringUtils.isUpperCase('A')).toBeTrue();
+    expect(StringUtils.isUpperCase('Z')).toBeTrue();
+    expect(StringUtils.isUpperCase('a')).toBeFalse();
+    expect(StringUtils.isUpperCase('z')).toBeFalse();
+    expect(StringUtils.isUpperCase('_')).toBeFalse();
+    expect(StringUtils.isUpperCase('0')).toBeFalse();
+    expect(StringUtils.isUpperCase('9')).toBeFalse();
+  });
 });
diff --git a/tools/winscope/src/trace/timestamp.ts b/tools/winscope/src/common/time.ts
similarity index 64%
rename from tools/winscope/src/trace/timestamp.ts
rename to tools/winscope/src/common/time.ts
index 23999d1..6450a0d 100644
--- a/tools/winscope/src/trace/timestamp.ts
+++ b/tools/winscope/src/common/time.ts
@@ -14,6 +14,11 @@
  * limitations under the License.
  */
 
+export interface TimeRange {
+  from: Timestamp;
+  to: Timestamp;
+}
+
 export enum TimestampType {
   ELAPSED = 'ELAPSED',
   REAL = 'REAL',
@@ -58,9 +63,43 @@
     return this.getValueNs();
   }
 
+  in(range: TimeRange): boolean {
+    if (range.from.type !== this.type || range.to.type !== this.type) {
+      throw new Error('Mismatching timestamp types');
+    }
+
+    return (
+      range.from.getValueNs() <= this.getValueNs() && this.getValueNs() <= range.to.getValueNs()
+    );
+  }
+
   add(nanoseconds: bigint): Timestamp {
     return new Timestamp(this.type, this.valueNs + nanoseconds);
   }
+
+  plus(timestamp: Timestamp): Timestamp {
+    this.validateTimestampArithmetic(timestamp);
+    return new Timestamp(this.type, timestamp.getValueNs() + this.getValueNs());
+  }
+
+  minus(timestamp: Timestamp): Timestamp {
+    this.validateTimestampArithmetic(timestamp);
+    return new Timestamp(this.type, this.getValueNs() - timestamp.getValueNs());
+  }
+
+  times(n: bigint): Timestamp {
+    return new Timestamp(this.type, this.getValueNs() * n);
+  }
+
+  div(n: bigint): Timestamp {
+    return new Timestamp(this.type, this.getValueNs() / n);
+  }
+
+  private validateTimestampArithmetic(timestamp: Timestamp) {
+    if (timestamp.type !== this.type) {
+      throw new Error('Attemping to do timestamp arithmetic on different timestamp types');
+    }
+  }
 }
 
 export class RealTimestamp extends Timestamp {
diff --git a/tools/winscope/src/common/time_utils.ts b/tools/winscope/src/common/time_utils.ts
index 42d6cf1..3d3ed23 100644
--- a/tools/winscope/src/common/time_utils.ts
+++ b/tools/winscope/src/common/time_utils.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
+import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'common/time';
 
 export class TimeUtils {
   static compareFn(a: Timestamp, b: Timestamp): number {
@@ -161,6 +161,28 @@
   static readonly HUMAN_ELAPSED_TIMESTAMP_REGEX =
     /^(?=.)([0-9]+d)?([0-9]+h)?([0-9]+m)?([0-9]+s)?([0-9]+ms)?([0-9]+ns)?$/;
   static readonly HUMAN_REAL_TIMESTAMP_REGEX =
-    /^[0-9]{4}-((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01])|(0[469]|11)-(0[1-9]|[12][0-9]|30)|(02)-(0[1-9]|[12][0-9]))T(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[1-5][0-9]):(0[0-9]|[1-5][0-9])\.[0-9]{3}([0-9]{6})?Z?$/;
+    /^[0-9]{4}-((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01])|(0[469]|11)-(0[1-9]|[12][0-9]|30)|(02)-(0[1-9]|[12][0-9]))T(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[1-5][0-9]):(0[0-9]|[1-5][0-9])(\.[0-9]{1,9})?Z?$/;
   static readonly NS_TIMESTAMP_REGEX = /^\s*[0-9]+(\s?ns)?\s*$/;
+
+  static min(ts1: Timestamp, ts2: Timestamp): Timestamp {
+    if (ts1.getType() !== ts2.getType()) {
+      throw new Error("Can't compare timestamps of different types");
+    }
+    if (ts2.getValueNs() < ts1.getValueNs()) {
+      return ts2;
+    }
+
+    return ts1;
+  }
+
+  static max(ts1: Timestamp, ts2: Timestamp): Timestamp {
+    if (ts1.getType() !== ts2.getType()) {
+      throw new Error("Can't compare timestamps of different types");
+    }
+    if (ts2.getValueNs() > ts1.getValueNs()) {
+      return ts2;
+    }
+
+    return ts1;
+  }
 }
diff --git a/tools/winscope/src/common/time_utils_test.ts b/tools/winscope/src/common/time_utils_test.ts
index b3c24b3..ea5d3b5 100644
--- a/tools/winscope/src/common/time_utils_test.ts
+++ b/tools/winscope/src/common/time_utils_test.ts
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
+import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'common/time';
 import {TimeUtils} from './time_utils';
 
 describe('TimeUtils', () => {
@@ -191,6 +191,18 @@
     expect(TimeUtils.parseHumanReal('2022-11-10T06:04:54.006000002')).toEqual(
       new RealTimestamp(1668060294006000002n)
     );
+    expect(TimeUtils.parseHumanReal('2022-11-10T06:04:54')).toEqual(
+      new RealTimestamp(1668060294000000000n)
+    );
+    expect(TimeUtils.parseHumanReal('2022-11-10T06:04:54.0')).toEqual(
+      new RealTimestamp(1668060294000000000n)
+    );
+    expect(TimeUtils.parseHumanReal('2022-11-10T06:04:54.0100')).toEqual(
+      new RealTimestamp(1668060294010000000n)
+    );
+    expect(TimeUtils.parseHumanReal('2022-11-10T06:04:54.0175328')).toEqual(
+      new RealTimestamp(1668060294017532800n)
+    );
   });
 
   it('canReverseDateFormatting', () => {
@@ -208,6 +220,10 @@
     expect(() => TimeUtils.parseHumanReal('100')).toThrow(invalidFormatError);
     expect(() => TimeUtils.parseHumanReal('06h4m54s, 10 Nov 2022')).toThrow(invalidFormatError);
     expect(() => TimeUtils.parseHumanReal('')).toThrow(invalidFormatError);
+    expect(() => TimeUtils.parseHumanReal('2022-11-10T06:04:54.')).toThrow(invalidFormatError);
+    expect(() => TimeUtils.parseHumanReal('2022-11-10T06:04:54.1234567890')).toThrow(
+      invalidFormatError
+    );
   });
 
   it('nano second regex accept all expected inputs', () => {
diff --git a/tools/winscope/src/common/times_test.ts b/tools/winscope/src/common/times_test.ts
new file mode 100644
index 0000000..4b803dd
--- /dev/null
+++ b/tools/winscope/src/common/times_test.ts
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2022 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 {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from './time';
+
+describe('Timestamp', () => {
+  describe('from', () => {
+    it('throws when missing elapsed timestamp', () => {
+      expect(() => {
+        Timestamp.from(TimestampType.REAL, 100n);
+      }).toThrow();
+    });
+
+    it('can create real timestamp', () => {
+      const timestamp = Timestamp.from(TimestampType.REAL, 100n, 500n);
+      expect(timestamp.getType()).toBe(TimestampType.REAL);
+      expect(timestamp.getValueNs()).toBe(600n);
+    });
+
+    it('can create elapsed timestamp', () => {
+      let timestamp = Timestamp.from(TimestampType.ELAPSED, 100n, 500n);
+      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
+      expect(timestamp.getValueNs()).toBe(100n);
+
+      timestamp = Timestamp.from(TimestampType.ELAPSED, 100n);
+      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
+      expect(timestamp.getValueNs()).toBe(100n);
+    });
+  });
+
+  describe('arithmetic', () => {
+    it('can add', () => {
+      let timestamp = new RealTimestamp(10n).plus(new RealTimestamp(20n));
+      expect(timestamp.getType()).toBe(TimestampType.REAL);
+      expect(timestamp.getValueNs()).toBe(30n);
+
+      timestamp = new ElapsedTimestamp(10n).plus(new ElapsedTimestamp(20n));
+      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
+      expect(timestamp.getValueNs()).toBe(30n);
+    });
+
+    it('can subtract', () => {
+      let timestamp = new RealTimestamp(20n).minus(new RealTimestamp(10n));
+      expect(timestamp.getType()).toBe(TimestampType.REAL);
+      expect(timestamp.getValueNs()).toBe(10n);
+
+      timestamp = new ElapsedTimestamp(20n).minus(new ElapsedTimestamp(10n));
+      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
+      expect(timestamp.getValueNs()).toBe(10n);
+    });
+
+    it('can divide', () => {
+      let timestamp = new RealTimestamp(10n).div(2n);
+      expect(timestamp.getType()).toBe(TimestampType.REAL);
+      expect(timestamp.getValueNs()).toBe(5n);
+
+      timestamp = new ElapsedTimestamp(10n).div(2n);
+      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
+      expect(timestamp.getValueNs()).toBe(5n);
+    });
+
+    it('fails between different timestamp types', () => {
+      const error = new Error('Attemping to do timestamp arithmetic on different timestamp types');
+      expect(() => {
+        new RealTimestamp(20n).minus(new ElapsedTimestamp(10n));
+      }).toThrow(error);
+      expect(() => {
+        new RealTimestamp(20n).plus(new ElapsedTimestamp(10n));
+      }).toThrow(error);
+      expect(() => {
+        new ElapsedTimestamp(20n).minus(new RealTimestamp(10n));
+      }).toThrow(error);
+      expect(() => {
+        new ElapsedTimestamp(20n).plus(new RealTimestamp(10n));
+      }).toThrow(error);
+    });
+  });
+});
diff --git a/tools/winscope/src/common/url_utils.ts b/tools/winscope/src/common/url_utils.ts
new file mode 100644
index 0000000..f085f85
--- /dev/null
+++ b/tools/winscope/src/common/url_utils.ts
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+export class UrlUtils {
+  static getRootUrl(): string {
+    const fullUrl = window.location.href;
+    const posLastSlash = fullUrl.lastIndexOf('/');
+    return fullUrl.slice(0, posLastSlash + 1);
+  }
+}
diff --git a/tools/winscope/src/cross_tool/cross_tool_protocol.ts b/tools/winscope/src/cross_tool/cross_tool_protocol.ts
index 316c069..b5792b6 100644
--- a/tools/winscope/src/cross_tool/cross_tool_protocol.ts
+++ b/tools/winscope/src/cross_tool/cross_tool_protocol.ts
@@ -15,10 +15,10 @@
  */
 
 import {FunctionUtils} from 'common/function_utils';
+import {RealTimestamp} from 'common/time';
 import {OnBugreportReceived, RemoteBugreportReceiver} from 'interfaces/remote_bugreport_receiver';
 import {OnTimestampReceived, RemoteTimestampReceiver} from 'interfaces/remote_timestamp_receiver';
 import {RemoteTimestampSender} from 'interfaces/remote_timestamp_sender';
-import {RealTimestamp} from 'trace/timestamp';
 import {Message, MessageBugReport, MessagePong, MessageTimestamp, MessageType} from './messages';
 import {OriginAllowList} from './origin_allow_list';
 
diff --git a/tools/winscope/src/cross_tool/cross_tool_protocol_stub.ts b/tools/winscope/src/cross_tool/cross_tool_protocol_stub.ts
index 6451684..677aacc 100644
--- a/tools/winscope/src/cross_tool/cross_tool_protocol_stub.ts
+++ b/tools/winscope/src/cross_tool/cross_tool_protocol_stub.ts
@@ -15,10 +15,10 @@
  */
 
 import {FunctionUtils} from 'common/function_utils';
+import {RealTimestamp} from 'common/time';
 import {OnBugreportReceived, RemoteBugreportReceiver} from 'interfaces/remote_bugreport_receiver';
 import {OnTimestampReceived, RemoteTimestampReceiver} from 'interfaces/remote_timestamp_receiver';
 import {RemoteTimestampSender} from 'interfaces/remote_timestamp_sender';
-import {RealTimestamp} from 'trace/timestamp';
 
 export class CrossToolProtocolStub
   implements RemoteBugreportReceiver, RemoteTimestampReceiver, RemoteTimestampSender
diff --git a/tools/winscope/src/cross_tool/origin_allow_list.ts b/tools/winscope/src/cross_tool/origin_allow_list.ts
index 55199c4..a2fe983 100644
--- a/tools/winscope/src/cross_tool/origin_allow_list.ts
+++ b/tools/winscope/src/cross_tool/origin_allow_list.ts
@@ -43,6 +43,8 @@
     switch (mode) {
       case 'DEV':
         return OriginAllowList.ALLOW_LIST_DEV;
+      case 'KARMA_TEST':
+        return OriginAllowList.ALLOW_LIST_DEV;
       case 'PROD':
         return OriginAllowList.ALLOW_LIST_PROD;
       default:
diff --git a/tools/winscope/src/trace/flickerlib/Configuration.json b/tools/winscope/src/flickerlib/Configuration.json
similarity index 100%
rename from tools/winscope/src/trace/flickerlib/Configuration.json
rename to tools/winscope/src/flickerlib/Configuration.json
diff --git a/tools/winscope/src/trace/flickerlib/ObjectFormatter.ts b/tools/winscope/src/flickerlib/ObjectFormatter.ts
similarity index 98%
rename from tools/winscope/src/trace/flickerlib/ObjectFormatter.ts
rename to tools/winscope/src/flickerlib/ObjectFormatter.ts
index ba5c704..d063e23 100644
--- a/tools/winscope/src/trace/flickerlib/ObjectFormatter.ts
+++ b/tools/winscope/src/flickerlib/ObjectFormatter.ts
@@ -16,7 +16,7 @@
 
 import {ArrayUtils} from 'common/array_utils';
 import {PropertiesDump} from 'viewers/common/ui_tree_utils';
-import intDefMapping from '../../../../../../prebuilts/misc/common/winscope/intDefMapping.json';
+import intDefMapping from '../../../../../prebuilts/misc/common/winscope/intDefMapping.json';
 import {
   toActiveBuffer,
   toColor,
diff --git a/tools/winscope/src/trace/flickerlib/README.md b/tools/winscope/src/flickerlib/README.md
similarity index 100%
rename from tools/winscope/src/trace/flickerlib/README.md
rename to tools/winscope/src/flickerlib/README.md
diff --git a/tools/winscope/src/flickerlib/common.js b/tools/winscope/src/flickerlib/common.js
new file mode 100644
index 0000000..1bb79c9
--- /dev/null
+++ b/tools/winscope/src/flickerlib/common.js
@@ -0,0 +1,367 @@
+/*
+ * Copyright 2020, 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.
+ */
+
+// Imports all the compiled common Flicker library classes and exports them
+// as clean es6 modules rather than having them be commonjs modules
+
+// WM
+const WindowManagerTrace =
+  require('flickerlib/flicker').android.tools.common.traces.wm.WindowManagerTrace;
+const WindowManagerState =
+  require('flickerlib/flicker').android.tools.common.traces.wm.WindowManagerState;
+const WindowManagerTraceEntryBuilder =
+  require('flickerlib/flicker').android.tools.common.traces.wm.WindowManagerTraceEntryBuilder;
+const Activity = require('flickerlib/flicker').android.tools.common.traces.wm.Activity;
+const Configuration = require('flickerlib/flicker').android.tools.common.traces.wm.Configuration;
+const ConfigurationContainer =
+  require('flickerlib/flicker').android.tools.common.traces.wm.ConfigurationContainer;
+const DisplayArea = require('flickerlib/flicker').android.tools.common.traces.wm.DisplayArea;
+const DisplayContent = require('flickerlib/flicker').android.tools.common.traces.wm.DisplayContent;
+const DisplayCutout = require('flickerlib/flicker').android.tools.common.traces.wm.DisplayCutout;
+const KeyguardControllerState =
+  require('flickerlib/flicker').android.tools.common.traces.wm.KeyguardControllerState;
+const RootWindowContainer =
+  require('flickerlib/flicker').android.tools.common.traces.wm.RootWindowContainer;
+const Task = require('flickerlib/flicker').android.tools.common.traces.wm.Task;
+const TaskFragment = require('flickerlib/flicker').android.tools.common.traces.wm.TaskFragment;
+const WindowConfiguration =
+  require('flickerlib/flicker').android.tools.common.traces.wm.WindowConfiguration;
+const WindowContainer =
+  require('flickerlib/flicker').android.tools.common.traces.wm.WindowContainer;
+const WindowLayoutParams =
+  require('flickerlib/flicker').android.tools.common.traces.wm.WindowLayoutParams;
+const WindowManagerPolicy =
+  require('flickerlib/flicker').android.tools.common.traces.wm.WindowManagerPolicy;
+const WindowState = require('flickerlib/flicker').android.tools.common.traces.wm.WindowState;
+const WindowToken = require('flickerlib/flicker').android.tools.common.traces.wm.WindowToken;
+
+// SF
+const HwcCompositionType =
+  require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.HwcCompositionType;
+const Layer = require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.Layer;
+const LayerProperties =
+  require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.LayerProperties;
+const LayerTraceEntry =
+  require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.LayerTraceEntry;
+const LayerTraceEntryBuilder =
+  require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.LayerTraceEntryBuilder;
+const LayersTrace =
+  require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.LayersTrace;
+const Transform =
+  require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.Transform;
+const Display = require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.Display;
+const Region = require('flickerlib/flicker').android.tools.common.datatypes.Region;
+
+// Event Log
+const EventLog = require('flickerlib/flicker').android.tools.common.traces.events.EventLog;
+const CujEvent = require('flickerlib/flicker').android.tools.common.traces.events.CujEvent;
+const CujType = require('flickerlib/flicker').android.tools.common.traces.events.CujType;
+const Event = require('flickerlib/flicker').android.tools.common.traces.events.Event;
+const FlickerEvent = require('flickerlib/flicker').android.tools.common.traces.events.FlickerEvent;
+const FocusEvent = require('flickerlib/flicker').android.tools.common.traces.events.FocusEvent;
+const EventLogParser =
+  require('flickerlib/flicker').android.tools.common.parsers.events.EventLogParser;
+const CujTrace = require('flickerlib/flicker').android.tools.common.parsers.events.CujTrace;
+const Cuj = require('flickerlib/flicker').android.tools.common.parsers.events.Cuj;
+
+// Transitions
+const Transition = require('flickerlib/flicker').android.tools.common.traces.wm.Transition;
+const TransitionType = require('flickerlib/flicker').android.tools.common.traces.wm.TransitionType;
+const TransitionChange =
+  require('flickerlib/flicker').android.tools.common.traces.wm.TransitionChange;
+const TransitionsTrace =
+  require('flickerlib/flicker').android.tools.common.traces.wm.TransitionsTrace;
+const ShellTransitionData =
+  require('flickerlib/flicker').android.tools.common.traces.wm.ShellTransitionData;
+const WmTransitionData =
+  require('flickerlib/flicker').android.tools.common.traces.wm.WmTransitionData;
+
+// Common
+const Size = require('flickerlib/flicker').android.tools.common.datatypes.Size;
+const ActiveBuffer = require('flickerlib/flicker').android.tools.common.datatypes.ActiveBuffer;
+const Color = require('flickerlib/flicker').android.tools.common.datatypes.Color;
+const Insets = require('flickerlib/flicker').android.tools.common.datatypes.Insets;
+const Matrix33 = require('flickerlib/flicker').android.tools.common.datatypes.Matrix33;
+const PlatformConsts = require('flickerlib/flicker').android.tools.common.PlatformConsts;
+const Rotation = require('flickerlib/flicker').android.tools.common.Rotation;
+const Point = require('flickerlib/flicker').android.tools.common.datatypes.Point;
+const PointF = require('flickerlib/flicker').android.tools.common.datatypes.PointF;
+const Rect = require('flickerlib/flicker').android.tools.common.datatypes.Rect;
+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 TimestampFactory = require('flickerlib/flicker').android.tools.common.TimestampFactory;
+
+const NoCache = require('flickerlib/flicker').android.tools.common.NoCache;
+
+const EMPTY_SIZE = Size.Companion.EMPTY;
+const EMPTY_BUFFER = ActiveBuffer.Companion.EMPTY;
+const EMPTY_COLOR = Color.Companion.EMPTY;
+const EMPTY_INSETS = Insets.Companion.EMPTY;
+const EMPTY_RECT = Rect.Companion.EMPTY;
+const EMPTY_RECTF = RectF.Companion.EMPTY;
+const EMPTY_POINT = Point.Companion.EMPTY;
+const EMPTY_POINTF = PointF.Companion.EMPTY;
+const EMPTY_MATRIX33 = Matrix33.Companion.identity(0, 0);
+const EMPTY_TRANSFORM = new Transform(0, EMPTY_MATRIX33);
+
+function toSize(proto) {
+  if (proto == null) {
+    return EMPTY_SIZE;
+  }
+  const width = proto.width ?? proto.w ?? 0;
+  const height = proto.height ?? proto.h ?? 0;
+  if (width || height) {
+    return new Size(width, height);
+  }
+  return EMPTY_SIZE;
+}
+
+function toActiveBuffer(proto) {
+  const width = proto?.width ?? 0;
+  const height = proto?.height ?? 0;
+  const stride = proto?.stride ?? 0;
+  const format = proto?.format ?? 0;
+
+  if (width || height || stride || format) {
+    return new ActiveBuffer(width, height, stride, format);
+  }
+  return EMPTY_BUFFER;
+}
+
+function toColor(proto, hasAlpha = true) {
+  if (proto == null) {
+    return EMPTY_COLOR;
+  }
+  const r = proto.r ?? 0;
+  const g = proto.g ?? 0;
+  const b = proto.b ?? 0;
+  let a = proto.a;
+  if (a === null && !hasAlpha) {
+    a = 1;
+  }
+  if (r || g || b || a) {
+    return new Color(r, g, b, a);
+  }
+  return EMPTY_COLOR;
+}
+
+function toPoint(proto) {
+  if (proto == null) {
+    return null;
+  }
+  const x = proto.x ?? 0;
+  const y = proto.y ?? 0;
+  if (x || y) {
+    return new Point(x, y);
+  }
+  return EMPTY_POINT;
+}
+
+function toPointF(proto) {
+  if (proto == null) {
+    return null;
+  }
+  const x = proto.x ?? 0;
+  const y = proto.y ?? 0;
+  if (x || y) {
+    return new PointF(x, y);
+  }
+  return EMPTY_POINTF;
+}
+
+function toInsets(proto) {
+  if (proto == null) {
+    return EMPTY_INSETS;
+  }
+
+  const left = proto?.left ?? 0;
+  const top = proto?.top ?? 0;
+  const right = proto?.right ?? 0;
+  const bottom = proto?.bottom ?? 0;
+  if (left || top || right || bottom) {
+    return new Insets(left, top, right, bottom);
+  }
+  return EMPTY_INSETS;
+}
+
+function toCropRect(proto) {
+  if (proto == null) return EMPTY_RECT;
+
+  const right = proto.right || 0;
+  const left = proto.left || 0;
+  const bottom = proto.bottom || 0;
+  const top = proto.top || 0;
+
+  // crop (0,0) (-1,-1) means no crop
+  if (right == -1 && left == 0 && bottom == -1 && top == 0) EMPTY_RECT;
+
+  if (right - left <= 0 || bottom - top <= 0) return EMPTY_RECT;
+
+  return Rect.Companion.from(left, top, right, bottom);
+}
+
+function toRect(proto) {
+  if (proto == null) {
+    return EMPTY_RECT;
+  }
+
+  const left = proto?.left ?? 0;
+  const top = proto?.top ?? 0;
+  const right = proto?.right ?? 0;
+  const bottom = proto?.bottom ?? 0;
+  if (left || top || right || bottom) {
+    return new Rect(left, top, right, bottom);
+  }
+  return EMPTY_RECT;
+}
+
+function toRectF(proto) {
+  if (proto == null) {
+    return EMPTY_RECTF;
+  }
+
+  const left = proto?.left ?? 0;
+  const top = proto?.top ?? 0;
+  const right = proto?.right ?? 0;
+  const bottom = proto?.bottom ?? 0;
+  if (left || top || right || bottom) {
+    return new RectF(left, top, right, bottom);
+  }
+  return EMPTY_RECTF;
+}
+
+function toRegion(proto) {
+  if (proto == null) {
+    return null;
+  }
+
+  const rects = [];
+  for (let x = 0; x < proto.rect.length; x++) {
+    const rect = proto.rect[x];
+    const parsedRect = toRect(rect);
+    rects.push(parsedRect);
+  }
+
+  return new Region(rects);
+}
+
+function toTransform(proto) {
+  if (proto == null) {
+    return EMPTY_TRANSFORM;
+  }
+  const dsdx = proto.dsdx ?? 0;
+  const dtdx = proto.dtdx ?? 0;
+  const tx = proto.tx ?? 0;
+  const dsdy = proto.dsdy ?? 0;
+  const dtdy = proto.dtdy ?? 0;
+  const ty = proto.ty ?? 0;
+
+  if (dsdx || dtdx || tx || dsdy || dtdy || ty) {
+    const matrix = new Matrix33(dsdx, dtdx, tx, dsdy, dtdy, ty);
+    return new Transform(proto.type ?? 0, matrix);
+  }
+
+  if (proto.type) {
+    return new Transform(proto.type ?? 0, EMPTY_MATRIX33);
+  }
+  return EMPTY_TRANSFORM;
+}
+
+export {
+  Activity,
+  Configuration,
+  ConfigurationContainer,
+  DisplayArea,
+  DisplayContent,
+  KeyguardControllerState,
+  DisplayCutout,
+  RootWindowContainer,
+  Task,
+  TaskFragment,
+  WindowConfiguration,
+  WindowContainer,
+  WindowState,
+  WindowToken,
+  WindowLayoutParams,
+  WindowManagerPolicy,
+  WindowManagerTrace,
+  WindowManagerState,
+  WindowManagerTraceEntryBuilder,
+  // SF
+  HwcCompositionType,
+  Layer,
+  LayerProperties,
+  LayerTraceEntry,
+  LayerTraceEntryBuilder,
+  LayersTrace,
+  Transform,
+  Matrix33,
+  Display,
+  // Eventlog
+  EventLog,
+  CujEvent,
+  CujType,
+  Event,
+  FlickerEvent,
+  FocusEvent,
+  EventLogParser,
+  CujTrace,
+  Cuj,
+  // Transitions
+  Transition,
+  TransitionType,
+  TransitionChange,
+  TransitionsTrace,
+  ShellTransitionData,
+  WmTransitionData,
+  // Common
+  Size,
+  ActiveBuffer,
+  Color,
+  Insets,
+  PlatformConsts,
+  Point,
+  Rect,
+  RectF,
+  Region,
+  Rotation,
+  WindowingMode,
+  CrossPlatform,
+  TimestampFactory,
+  NoCache,
+  // Service
+  toSize,
+  toActiveBuffer,
+  toColor,
+  toCropRect,
+  toInsets,
+  toPoint,
+  toPointF,
+  toRect,
+  toRectF,
+  toRegion,
+  toTransform,
+  // Constants
+  EMPTY_BUFFER,
+  EMPTY_COLOR,
+  EMPTY_RECT,
+  EMPTY_RECTF,
+  EMPTY_POINT,
+  EMPTY_POINTF,
+  EMPTY_MATRIX33,
+  EMPTY_TRANSFORM,
+};
diff --git a/tools/winscope/src/trace/flickerlib/layers/Layer.ts b/tools/winscope/src/flickerlib/layers/Layer.ts
similarity index 77%
rename from tools/winscope/src/trace/flickerlib/layers/Layer.ts
rename to tools/winscope/src/flickerlib/layers/Layer.ts
index 077d1b8..9de75aa 100644
--- a/tools/winscope/src/trace/flickerlib/layers/Layer.ts
+++ b/tools/winscope/src/flickerlib/layers/Layer.ts
@@ -22,7 +22,6 @@
   toActiveBuffer,
   toColor,
   toCropRect,
-  toRect,
   toRectF,
   toRegion,
 } from '../common';
@@ -35,20 +34,8 @@
   const bounds = toRectF(proto.bounds);
   const color = toColor(proto.color);
   const screenBounds = toRectF(proto.screenBounds);
-  const sourceBounds = toRectF(proto.sourceBounds);
   const transform = Transform.fromProto(proto.transform, proto.position);
   const bufferTransform = Transform.fromProto(proto.bufferTransform, /* position */ null);
-  const hwcCrop = toRectF(proto.hwcCrop);
-  const hwcFrame = toRect(proto.hwcFrame);
-  const requestedColor = toColor(proto.requestedColor);
-  const requestedTransform = Transform.fromProto(proto.requestedTransform, proto.requestedPosition);
-  const cornerRadiusCrop = toRectF(proto.cornerRadiusCrop);
-  const inputTransform = Transform.fromProto(
-    proto.inputWindowInfo ? proto.inputWindowInfo.transform : null
-  );
-  const inputRegion = toRegion(
-    proto.inputWindowInfo ? proto.inputWindowInfo.touchableRegion : null
-  );
   const crop: Rect = toCropRect(proto.crop);
 
   const properties = new LayerProperties(
@@ -60,25 +47,16 @@
     /* isOpaque */ proto.isOpaque,
     /* shadowRadius */ proto.shadowRadius,
     /* cornerRadius */ proto.cornerRadius,
-    /* type */ proto.type ?? ``,
     screenBounds,
     transform,
-    sourceBounds,
     /* effectiveScalingMode */ proto.effectiveScalingMode,
     bufferTransform,
     /* hwcCompositionType */ new HwcCompositionType(proto.hwcCompositionType),
-    hwcCrop,
-    hwcFrame,
     /* backgroundBlurRadius */ proto.backgroundBlurRadius,
     crop,
     /* isRelativeOf */ proto.isRelativeOf,
     /* zOrderRelativeOfId */ proto.zOrderRelativeOf,
     /* stackId */ proto.layerStack,
-    requestedTransform,
-    requestedColor,
-    cornerRadiusCrop,
-    inputTransform,
-    inputRegion,
     excludesCompositionState
   );
 
diff --git a/tools/winscope/src/trace/flickerlib/layers/LayerTraceEntry.ts b/tools/winscope/src/flickerlib/layers/LayerTraceEntry.ts
similarity index 97%
rename from tools/winscope/src/trace/flickerlib/layers/LayerTraceEntry.ts
rename to tools/winscope/src/flickerlib/layers/LayerTraceEntry.ts
index 66c8fb0..e4f408c 100644
--- a/tools/winscope/src/trace/flickerlib/layers/LayerTraceEntry.ts
+++ b/tools/winscope/src/flickerlib/layers/LayerTraceEntry.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
+import {ElapsedTimestamp, RealTimestamp} from 'common/time';
 import {TimeUtils} from 'common/time_utils';
-import {ElapsedTimestamp, RealTimestamp} from 'trace/timestamp';
 import {
   Display,
   LayerTraceEntry,
diff --git a/tools/winscope/src/trace/flickerlib/layers/Transform.ts b/tools/winscope/src/flickerlib/layers/Transform.ts
similarity index 100%
rename from tools/winscope/src/trace/flickerlib/layers/Transform.ts
rename to tools/winscope/src/flickerlib/layers/Transform.ts
diff --git a/tools/winscope/src/trace/flickerlib/mixin.ts b/tools/winscope/src/flickerlib/mixin.ts
similarity index 100%
rename from tools/winscope/src/trace/flickerlib/mixin.ts
rename to tools/winscope/src/flickerlib/mixin.ts
diff --git a/tools/winscope/src/flickerlib/windows/Activity.ts b/tools/winscope/src/flickerlib/windows/Activity.ts
new file mode 100644
index 0000000..553b571
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/Activity.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020, 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 {Activity, WindowContainer} from '../common';
+import {shortenName} from '../mixin';
+
+Activity.fromProto = (windowContainer: WindowContainer, proto: any): Activity => {
+  const entry = new Activity(
+    proto.state,
+    proto.frontOfTask,
+    proto.procId,
+    proto.translucent,
+    windowContainer
+  );
+
+  addAttributes(entry, proto);
+  return entry;
+};
+
+function addAttributes(entry: Activity, proto: any) {
+  entry.proto = proto;
+  entry.kind = entry.constructor.name;
+  entry.shortName = shortenName(entry.name);
+}
+
+export {Activity};
diff --git a/tools/winscope/src/trace/flickerlib/windows/DisplayArea.ts b/tools/winscope/src/flickerlib/windows/DisplayArea.ts
similarity index 60%
rename from tools/winscope/src/trace/flickerlib/windows/DisplayArea.ts
rename to tools/winscope/src/flickerlib/windows/DisplayArea.ts
index 7ed63ea..226dbde 100644
--- a/tools/winscope/src/trace/flickerlib/windows/DisplayArea.ts
+++ b/tools/winscope/src/flickerlib/windows/DisplayArea.ts
@@ -14,31 +14,14 @@
  * limitations under the License.
  */
 
-import {DisplayArea} from '../common';
+import {DisplayArea, WindowContainer} from '../common';
 import {shortenName} from '../mixin';
-import {WindowContainer} from './WindowContainer';
 
-DisplayArea.fromProto = (
-  proto: any,
-  isActivityInTree: boolean,
-  nextSeq: () => number
-): DisplayArea => {
-  if (proto == null) {
-    return null;
-  } else {
-    const windowContainer = WindowContainer.fromProto(
-      /* proto */ proto.windowContainer,
-      /* protoChildren */ proto.windowContainer?.children ?? [],
-      /* isActivityInTree */ isActivityInTree,
-      /* computedZ */ nextSeq,
-      /* nameOverride */ proto.name
-    );
+DisplayArea.fromProto = (windowContainer: WindowContainer, proto: any): DisplayArea => {
+  const entry = new DisplayArea(proto.isTaskDisplayArea, windowContainer);
 
-    const entry = new DisplayArea(proto.isTaskDisplayArea, windowContainer);
-
-    addAttributes(entry, proto);
-    return entry;
-  }
+  addAttributes(entry, proto);
+  return entry;
 };
 
 function addAttributes(entry: DisplayArea, proto: any) {
diff --git a/tools/winscope/src/flickerlib/windows/DisplayContent.ts b/tools/winscope/src/flickerlib/windows/DisplayContent.ts
new file mode 100644
index 0000000..c959479
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/DisplayContent.ts
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2020, 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 {
+  DisplayContent,
+  DisplayCutout,
+  Rect,
+  Rotation,
+  toInsets,
+  toRect,
+  WindowContainer,
+} from '../common';
+import {shortenName} from '../mixin';
+
+DisplayContent.fromProto = (windowContainer: WindowContainer, proto: any): DisplayContent => {
+  const displayRectWidth = proto.displayInfo?.logicalWidth ?? 0;
+  const displayRectHeight = proto.displayInfo?.logicalHeight ?? 0;
+  const appRectWidth = proto.displayInfo?.appWidth ?? 0;
+  const appRectHeight = proto.displayInfo?.appHeight ?? 0;
+  const defaultBounds = proto.pinnedStackController?.defaultBounds ?? null;
+  const movementBounds = proto.pinnedStackController?.movementBounds ?? null;
+
+  const entry = new DisplayContent(
+    proto.id,
+    proto.focusedRootTaskId,
+    proto.resumedActivity?.title ?? '',
+    proto.singleTaskInstance,
+    toRect(defaultBounds),
+    toRect(movementBounds),
+    new Rect(0, 0, displayRectWidth, displayRectHeight),
+    new Rect(0, 0, appRectWidth, appRectHeight),
+    proto.dpi,
+    proto.displayInfo?.flags ?? 0,
+    toRect(proto.displayFrames?.stableBounds),
+    proto.surfaceSize,
+    proto.focusedApp,
+    proto.appTransition?.lastUsedAppTransition ?? '',
+    proto.appTransition?.appTransitionState ?? '',
+    Rotation.Companion.getByValue(proto.displayRotation?.rotation ?? 0),
+    proto.displayRotation?.lastOrientation ?? 0,
+    createDisplayCutout(proto.displayInfo?.cutout),
+    windowContainer
+  );
+
+  addAttributes(entry, proto);
+  return entry;
+};
+
+function createDisplayCutout(proto: any | null): DisplayCutout | null {
+  if (proto == null) {
+    return null;
+  } else {
+    return new DisplayCutout(
+      toInsets(proto?.insets),
+      toRect(proto?.boundLeft),
+      toRect(proto?.boundTop),
+      toRect(proto?.boundRight),
+      toRect(proto?.boundBottom),
+      toInsets(proto?.waterfallInsets)
+    );
+  }
+}
+
+function addAttributes(entry: DisplayContent, proto: any) {
+  entry.proto = proto;
+  entry.kind = entry.constructor.name;
+  entry.shortName = shortenName(entry.name);
+}
+
+export {DisplayContent};
diff --git a/tools/winscope/src/flickerlib/windows/Task.ts b/tools/winscope/src/flickerlib/windows/Task.ts
new file mode 100644
index 0000000..f99a0da
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/Task.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020, 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 {Task, toRect, WindowContainer} from '../common';
+import {shortenName} from '../mixin';
+
+Task.fromProto = (windowContainer: WindowContainer, proto: any): Task => {
+  const entry = new Task(
+    proto.taskFragment?.activityType ?? proto.activityType,
+    proto.fillsParent,
+    toRect(proto.bounds),
+    proto.id,
+    proto.rootTaskId,
+    proto.taskFragment?.displayId,
+    toRect(proto.lastNonFullscreenBounds),
+    proto.realActivity,
+    proto.origActivity,
+    proto.resizeMode,
+    proto.resumedActivity?.title ?? '',
+    proto.animatingBounds,
+    proto.surfaceWidth,
+    proto.surfaceHeight,
+    proto.createdByOrganizer,
+    proto.taskFragment?.minWidth ?? proto.minWidth,
+    proto.taskFragment?.minHeight ?? proto.minHeight,
+    windowContainer
+  );
+
+  addAttributes(entry, proto);
+  return entry;
+};
+
+function addAttributes(entry: Task, proto: any) {
+  entry.proto = proto;
+  entry.proto.configurationContainer = proto.windowContainer?.configurationContainer;
+  entry.proto.surfaceControl = proto.windowContainer?.surfaceControl;
+  entry.kind = entry.constructor.name;
+  entry.shortName = shortenName(entry.name);
+}
+
+export {Task};
diff --git a/tools/winscope/src/flickerlib/windows/TaskFragment.ts b/tools/winscope/src/flickerlib/windows/TaskFragment.ts
new file mode 100644
index 0000000..42c67cf
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/TaskFragment.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright 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 {TaskFragment, WindowContainer} from '../common';
+import {shortenName} from '../mixin';
+
+TaskFragment.fromProto = (windowContainer: WindowContainer, proto: any): TaskFragment => {
+  const entry = new TaskFragment(
+    proto.activityType,
+    proto.displayId,
+    proto.minWidth,
+    proto.minHeight,
+    windowContainer
+  );
+
+  addAttributes(entry, proto);
+  return entry;
+};
+
+function addAttributes(entry: TaskFragment, proto: any) {
+  entry.proto = proto;
+  entry.proto.configurationContainer = proto.windowContainer?.configurationContainer;
+  entry.proto.surfaceControl = proto.windowContainer?.surfaceControl;
+  entry.kind = entry.constructor.name;
+  entry.shortName = shortenName(entry.name);
+}
+
+export {TaskFragment};
diff --git a/tools/winscope/src/flickerlib/windows/WindowContainer.ts b/tools/winscope/src/flickerlib/windows/WindowContainer.ts
new file mode 100644
index 0000000..6b13058
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/WindowContainer.ts
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2020, 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 {shortenName} from '../mixin';
+
+import {
+  Configuration,
+  ConfigurationContainer,
+  toRect,
+  WindowConfiguration,
+  WindowContainer,
+} from '../common';
+
+import {Activity} from './Activity';
+import {DisplayArea} from './DisplayArea';
+import {DisplayContent} from './DisplayContent';
+import {Task} from './Task';
+import {TaskFragment} from './TaskFragment';
+import {WindowState, WindowStateUtils} from './WindowState';
+import {WindowToken} from './WindowToken';
+
+WindowContainer.fromProto = (
+  proto: any,
+  protoChildren: any[],
+  isActivityInTree: boolean,
+  nextSeq: () => number,
+  nameOverride: string | null = null,
+  identifierOverride: string | null = null,
+  tokenOverride: any = null,
+  visibleOverride: boolean | null = null
+): WindowContainer => {
+  if (proto == null) {
+    return null;
+  }
+
+  const containerOrder = nextSeq();
+  const children = protoChildren
+    .filter((it) => it != null)
+    .map((it) => WindowContainer.childrenFromProto(it, isActivityInTree, nextSeq))
+    .filter((it) => it != null);
+
+  const identifier: any = identifierOverride ?? proto.identifier;
+  const name: string = nameOverride ?? identifier?.title ?? '';
+  const token: string = tokenOverride?.toString(16) ?? identifier?.hashCode?.toString(16) ?? '';
+
+  const config = createConfigurationContainer(proto.configurationContainer);
+  const entry = new WindowContainer(
+    name,
+    token,
+    proto.orientation,
+    proto.surfaceControl?.layerId ?? 0,
+    visibleOverride ?? proto.visible,
+    config,
+    children,
+    containerOrder
+  );
+
+  addAttributes(entry, proto);
+  return entry;
+};
+
+function addAttributes(entry: WindowContainer, proto: any) {
+  entry.proto = proto;
+  entry.kind = entry.constructor.name;
+  entry.shortName = shortenName(entry.name);
+}
+
+type WindowContainerChildType =
+  | DisplayContent
+  | DisplayArea
+  | Task
+  | TaskFragment
+  | Activity
+  | WindowToken
+  | WindowState
+  | WindowContainer;
+
+WindowContainer.childrenFromProto = (
+  proto: any,
+  isActivityInTree: boolean,
+  nextSeq: () => number
+): WindowContainerChildType => {
+  if (proto.displayContent !== null) {
+    const windowContainer = WindowContainer.fromProto(
+      /* proto */ proto.displayContent.rootDisplayArea.windowContainer,
+      /* protoChildren */ proto.displayContent.rootDisplayArea.windowContainer?.children ?? [],
+      /* isActivityInTree */ isActivityInTree,
+      /* computedZ */ nextSeq,
+      /* nameOverride */ proto.displayContent.displayInfo?.name ?? null
+    );
+
+    return DisplayContent.fromProto(windowContainer, proto.displayContent);
+  }
+
+  if (proto.displayArea !== null) {
+    const windowContainer = WindowContainer.fromProto(
+      /* proto */ proto.displayArea.windowContainer,
+      /* protoChildren */ proto.displayArea.windowContainer?.children ?? [],
+      /* isActivityInTree */ isActivityInTree,
+      /* computedZ */ nextSeq,
+      /* nameOverride */ proto.displayArea.name
+    );
+
+    return DisplayArea.fromProto(windowContainer, proto.displayArea);
+  }
+
+  if (proto.task !== null) {
+    const windowContainerProto =
+      proto.task.taskFragment?.windowContainer ?? proto.task.windowContainer;
+    const windowContainer = WindowContainer.fromProto(
+      /* proto */ windowContainerProto,
+      /* protoChildren */ windowContainerProto?.children ?? [],
+      /* isActivityInTree */ isActivityInTree,
+      /* computedZ */ nextSeq
+    );
+
+    return Task.fromProto(windowContainer, proto.task);
+  }
+
+  if (proto.taskFragment !== null) {
+    const windowContainer = WindowContainer.fromProto(
+      /* proto */ proto.taskFragment.windowContainer,
+      /* protoChildren */ proto.taskFragment.windowContainer?.children ?? [],
+      /* isActivityInTree */ isActivityInTree,
+      /* computedZ */ nextSeq
+    );
+
+    return TaskFragment.fromProto(windowContainer, proto.taskFragment);
+  }
+
+  if (proto.activity !== null) {
+    const windowContainer = WindowContainer.fromProto(
+      /* proto */ proto.activity.windowToken.windowContainer,
+      /* protoChildren */ proto.activity.windowToken.windowContainer?.children ?? [],
+      /* isActivityInTree */ true,
+      /* computedZ */ nextSeq,
+      /* nameOverride */ proto.activity.name,
+      /* identifierOverride */ proto.activity.identifier
+    );
+
+    return Activity.fromProto(windowContainer, proto.activity);
+  }
+
+  if (proto.windowToken !== null) {
+    const windowContainer = WindowContainer.fromProto(
+      /* proto */ proto.windowToken.windowContainer,
+      /* protoChildren */ proto.windowToken.windowContainer?.children ?? [],
+      /* isActivityInTree */ isActivityInTree,
+      /* computedZ */ nextSeq,
+      /* nameOverride */ proto.windowToken.hashCode.toString(16),
+      /* identifierOverride */ null,
+      /* tokenOverride */ proto.windowToken.hashCode
+    );
+
+    return WindowToken.fromProto(windowContainer, proto.windowToken);
+  }
+
+  if (proto.window !== null) {
+    const identifierName = WindowStateUtils.getIdentifier(proto.window);
+    const name = WindowStateUtils.getName(identifierName);
+
+    const windowContainer = WindowContainer.fromProto(
+      /* proto */ proto.window.windowContainer,
+      /* protoChildren */ proto.window.windowContainer?.children ?? [],
+      /* isActivityInTree */ isActivityInTree,
+      /* computedZ */ nextSeq,
+      /* nameOverride */ name,
+      /* identifierOverride */ proto.window.identifier
+    );
+
+    return WindowState.fromProto(windowContainer, proto.window, isActivityInTree);
+  }
+
+  if (proto.windowContainer !== null) {
+    return WindowContainer.fromProto(proto.windowContainer, nextSeq);
+  }
+};
+
+function createConfigurationContainer(proto: any): ConfigurationContainer {
+  const entry = ConfigurationContainer.Companion.from(
+    createConfiguration(proto?.overrideConfiguration ?? null),
+    createConfiguration(proto?.fullConfiguration ?? null),
+    createConfiguration(proto?.mergedOverrideConfiguration ?? null)
+  );
+
+  entry.obj = entry;
+  return entry;
+}
+
+function createConfiguration(proto: any): Configuration {
+  if (proto == null) {
+    return null;
+  }
+  let windowConfiguration = null;
+
+  if (proto != null && proto.windowConfiguration != null) {
+    windowConfiguration = createWindowConfiguration(proto.windowConfiguration);
+  }
+
+  return Configuration.Companion.from(
+    windowConfiguration,
+    proto?.densityDpi ?? 0,
+    proto?.orientation ?? 0,
+    proto?.screenHeightDp ?? 0,
+    proto?.screenHeightDp ?? 0,
+    proto?.smallestScreenWidthDp ?? 0,
+    proto?.screenLayout ?? 0,
+    proto?.uiMode ?? 0
+  );
+}
+
+function createWindowConfiguration(proto: any): WindowConfiguration {
+  return WindowConfiguration.Companion.from(
+    toRect(proto.appBounds),
+    toRect(proto.bounds),
+    toRect(proto.maxBounds),
+    proto.windowingMode,
+    proto.activityType
+  );
+}
+
+export {WindowContainer};
diff --git a/tools/winscope/src/trace/flickerlib/windows/WindowManagerState.ts b/tools/winscope/src/flickerlib/windows/WindowManagerState.ts
similarity index 98%
rename from tools/winscope/src/trace/flickerlib/windows/WindowManagerState.ts
rename to tools/winscope/src/flickerlib/windows/WindowManagerState.ts
index c479f21..cae4a2f 100644
--- a/tools/winscope/src/trace/flickerlib/windows/WindowManagerState.ts
+++ b/tools/winscope/src/flickerlib/windows/WindowManagerState.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
+import {ElapsedTimestamp, RealTimestamp} from 'common/time';
 import {TimeUtils} from 'common/time_utils';
-import {ElapsedTimestamp, RealTimestamp} from 'trace/timestamp';
 import {
   KeyguardControllerState,
   RootWindowContainer,
diff --git a/tools/winscope/src/flickerlib/windows/WindowState.ts b/tools/winscope/src/flickerlib/windows/WindowState.ts
new file mode 100644
index 0000000..22c3032
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/WindowState.ts
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2020, 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 {Size, toRect, WindowContainer, WindowLayoutParams, WindowState} from '../common';
+import {shortenName} from '../mixin';
+
+WindowState.fromProto = (
+  windowContainer: WindowContainer,
+  proto: any,
+  isActivityInTree: boolean
+): WindowState => {
+  const windowParams = createWindowLayoutParams(proto.attributes);
+  const identifierName = WindowStateUtils.getIdentifier(proto);
+  const windowType = WindowStateUtils.getWindowType(proto, identifierName);
+
+  const entry = new WindowState(
+    windowParams,
+    proto.displayId,
+    proto.stackId,
+    proto.animator?.surface?.layer ?? 0,
+    proto.animator?.surface?.shown ?? false,
+    windowType,
+    new Size(proto.requestedWidth, proto.requestedHeight),
+    toRect(proto.surfacePosition),
+    toRect(proto.windowFrames?.frame ?? null),
+    toRect(proto.windowFrames?.containingFrame ?? null),
+    toRect(proto.windowFrames?.parentFrame ?? null),
+    toRect(proto.windowFrames?.contentFrame ?? null),
+    toRect(proto.windowFrames?.contentInsets ?? null),
+    toRect(proto.surfaceInsets),
+    toRect(proto.givenContentInsets),
+    toRect(proto.animator?.lastClipRect ?? null),
+    windowContainer,
+    /* isAppWindow */ isActivityInTree
+  );
+
+  addAttributes(entry, proto);
+  return entry;
+};
+
+function createWindowLayoutParams(proto: any): WindowLayoutParams {
+  return new WindowLayoutParams(
+    /* type */ proto?.type ?? 0,
+    /* x */ proto?.x ?? 0,
+    /* y */ proto?.y ?? 0,
+    /* width */ proto?.width ?? 0,
+    /* height */ proto?.height ?? 0,
+    /* horizontalMargin */ proto?.horizontalMargin ?? 0,
+    /* verticalMargin */ proto?.verticalMargin ?? 0,
+    /* gravity */ proto?.gravity ?? 0,
+    /* softInputMode */ proto?.softInputMode ?? 0,
+    /* format */ proto?.format ?? 0,
+    /* windowAnimations */ proto?.windowAnimations ?? 0,
+    /* alpha */ proto?.alpha ?? 0,
+    /* screenBrightness */ proto?.screenBrightness ?? 0,
+    /* buttonBrightness */ proto?.buttonBrightness ?? 0,
+    /* rotationAnimation */ proto?.rotationAnimation ?? 0,
+    /* preferredRefreshRate */ proto?.preferredRefreshRate ?? 0,
+    /* preferredDisplayModeId */ proto?.preferredDisplayModeId ?? 0,
+    /* hasSystemUiListeners */ proto?.hasSystemUiListeners ?? false,
+    /* inputFeatureFlags */ proto?.inputFeatureFlags ?? 0,
+    /* userActivityTimeout */ proto?.userActivityTimeout ?? 0,
+    /* colorMode */ proto?.colorMode ?? 0,
+    /* flags */ proto?.flags ?? 0,
+    /* privateFlags */ proto?.privateFlags ?? 0,
+    /* systemUiVisibilityFlags */ proto?.systemUiVisibilityFlags ?? 0,
+    /* subtreeSystemUiVisibilityFlags */ proto?.subtreeSystemUiVisibilityFlags ?? 0,
+    /* appearance */ proto?.appearance ?? 0,
+    /* behavior */ proto?.behavior ?? 0,
+    /* fitInsetsTypes */ proto?.fitInsetsTypes ?? 0,
+    /* fitInsetsSides */ proto?.fitInsetsSides ?? 0,
+    /* fitIgnoreVisibility */ proto?.fitIgnoreVisibility ?? false
+  );
+}
+
+export class WindowStateUtils {
+  static getWindowType(proto: any, identifierName: string): number {
+    if (identifierName.startsWith(WindowState.STARTING_WINDOW_PREFIX)) {
+      return WindowState.WINDOW_TYPE_STARTING;
+    } else if (proto.animatingExit) {
+      return WindowState.WINDOW_TYPE_EXITING;
+    } else if (identifierName.startsWith(WindowState.DEBUGGER_WINDOW_PREFIX)) {
+      return WindowState.WINDOW_TYPE_STARTING;
+    }
+
+    return 0;
+  }
+
+  static getName(identifierName: string): string {
+    let name = identifierName;
+
+    if (identifierName.startsWith(WindowState.STARTING_WINDOW_PREFIX)) {
+      name = identifierName.substring(WindowState.STARTING_WINDOW_PREFIX.length);
+    } else if (identifierName.startsWith(WindowState.DEBUGGER_WINDOW_PREFIX)) {
+      name = identifierName.substring(WindowState.DEBUGGER_WINDOW_PREFIX.length);
+    }
+
+    return name;
+  }
+
+  static getIdentifier(proto: any): string {
+    return proto.windowContainer.identifier?.title ?? proto.identifier?.title ?? '';
+  }
+}
+
+function addAttributes(entry: WindowState, proto: any) {
+  entry.kind = entry.constructor.name;
+  entry.rect = entry.frame;
+  entry.rect.ref = entry;
+  entry.rect.label = entry.name;
+  entry.proto = proto;
+  entry.proto.configurationContainer = proto.windowContainer?.configurationContainer;
+  entry.proto.surfaceControl = proto.windowContainer?.surfaceControl;
+  entry.shortName = shortenName(entry.name);
+}
+
+export {WindowState};
diff --git a/tools/winscope/src/trace/flickerlib/windows/WindowToken.ts b/tools/winscope/src/flickerlib/windows/WindowToken.ts
similarity index 62%
rename from tools/winscope/src/trace/flickerlib/windows/WindowToken.ts
rename to tools/winscope/src/flickerlib/windows/WindowToken.ts
index 5c70122..4d755aa 100644
--- a/tools/winscope/src/trace/flickerlib/windows/WindowToken.ts
+++ b/tools/winscope/src/flickerlib/windows/WindowToken.ts
@@ -14,28 +14,10 @@
  * limitations under the License.
  */
 
-import {WindowToken} from '../common';
+import {WindowContainer, WindowToken} from '../common';
 import {shortenName} from '../mixin';
-import {WindowContainer} from './WindowContainer';
 
-WindowToken.fromProto = (
-  proto: any,
-  isActivityInTree: boolean,
-  nextSeq: () => number
-): WindowToken => {
-  if (!proto) {
-    return null;
-  }
-
-  const windowContainer = WindowContainer.fromProto(
-    /* proto */ proto.windowContainer,
-    /* protoChildren */ proto.windowContainer?.children ?? [],
-    /* isActivityInTree */ isActivityInTree,
-    /* computedZ */ nextSeq,
-    /* nameOverride */ proto.hashCode.toString(16),
-    /* identifierOverride */ null,
-    /* tokenOverride */ proto.hashCode
-  );
+WindowToken.fromProto = (windowContainer: WindowContainer, proto: any): WindowToken => {
   const entry = new WindowToken(windowContainer);
   entry.kind = entry.constructor.name;
   entry.proto = proto;
diff --git a/tools/winscope/src/index.html b/tools/winscope/src/index.html
index 4a9bd1d..1cd85a5 100644
--- a/tools/winscope/src/index.html
+++ b/tools/winscope/src/index.html
@@ -21,6 +21,15 @@
   <base href="/">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="icon" type="image/x-icon" href="favicon.svg">
+  <!-- Google tag (gtag.js) -->
+  <script async src="https://www.googletagmanager.com/gtag/js?id=G-95DPZ8JGEP"></script>
+  <script>
+    window.dataLayer = window.dataLayer || [];
+    function gtag(){dataLayer.push(arguments);}
+    gtag('js', new Date());
+
+    gtag('config', 'G-95DPZ8JGEP');
+  </script>
 </head>
 <body class="mat-app-background">
   <app-root></app-root>
diff --git a/tools/winscope/src/interfaces/app_event_emitter.ts b/tools/winscope/src/interfaces/app_event_emitter.ts
new file mode 100644
index 0000000..52025b1
--- /dev/null
+++ b/tools/winscope/src/interfaces/app_event_emitter.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 {AppEvent} from 'app/app_event';
+
+export type EmitAppEvent = (event: AppEvent) => Promise<void>;
+
+export interface AppEventEmitter {
+  setEmitAppEvent(callback: EmitAppEvent): void;
+}
diff --git a/tools/winscope/src/interfaces/app_event_listener.ts b/tools/winscope/src/interfaces/app_event_listener.ts
new file mode 100644
index 0000000..39cda77
--- /dev/null
+++ b/tools/winscope/src/interfaces/app_event_listener.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 {AppEvent} from 'app/app_event';
+
+export interface AppEventListener {
+  onAppEvent(event: AppEvent): Promise<void>;
+}
diff --git a/tools/winscope/src/interfaces/app_event_listener_emitter_stub.ts b/tools/winscope/src/interfaces/app_event_listener_emitter_stub.ts
new file mode 100644
index 0000000..60195e5
--- /dev/null
+++ b/tools/winscope/src/interfaces/app_event_listener_emitter_stub.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 {AppEvent} from 'app/app_event';
+import {AppEventEmitter, EmitAppEvent} from './app_event_emitter';
+import {AppEventListener} from './app_event_listener';
+
+export class AppEventListenerEmitterStub implements AppEventListener, AppEventEmitter {
+  async onAppEvent(event: AppEvent) {
+    // do nothing
+  }
+
+  setEmitAppEvent(callback: EmitAppEvent) {
+    // do nothing
+  }
+}
diff --git a/tools/winscope/src/interfaces/remote_bugreport_receiver.ts b/tools/winscope/src/interfaces/remote_bugreport_receiver.ts
index 992eef7..f7ccc1b 100644
--- a/tools/winscope/src/interfaces/remote_bugreport_receiver.ts
+++ b/tools/winscope/src/interfaces/remote_bugreport_receiver.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {RealTimestamp} from 'trace/timestamp';
+import {RealTimestamp} from 'common/time';
 
 export type OnBugreportReceived = (bugreport: File, timestamp?: RealTimestamp) => Promise<void>;
 
diff --git a/tools/winscope/src/interfaces/remote_timestamp_receiver.ts b/tools/winscope/src/interfaces/remote_timestamp_receiver.ts
index aa813e7..8af874f 100644
--- a/tools/winscope/src/interfaces/remote_timestamp_receiver.ts
+++ b/tools/winscope/src/interfaces/remote_timestamp_receiver.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {RealTimestamp} from 'trace/timestamp';
+import {RealTimestamp} from 'common/time';
 
 export type OnTimestampReceived = (timestamp: RealTimestamp) => Promise<void>;
 
diff --git a/tools/winscope/src/interfaces/remote_timestamp_sender.ts b/tools/winscope/src/interfaces/remote_timestamp_sender.ts
index 03ab553..8a8d0ae 100644
--- a/tools/winscope/src/interfaces/remote_timestamp_sender.ts
+++ b/tools/winscope/src/interfaces/remote_timestamp_sender.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {RealTimestamp} from 'trace/timestamp';
+import {RealTimestamp} from 'common/time';
 
 export interface RemoteTimestampSender {
   sendTimestamp(timestamp: RealTimestamp): void;
diff --git a/tools/winscope/src/interfaces/user_notification_listener_stub.ts b/tools/winscope/src/interfaces/user_notification_listener_stub.ts
new file mode 100644
index 0000000..e72f355
--- /dev/null
+++ b/tools/winscope/src/interfaces/user_notification_listener_stub.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 {ParserError} from 'parsers/parser_factory';
+import {UserNotificationListener} from './user_notification_listener';
+
+export class UserNotificationListenerStub implements UserNotificationListener {
+  onParserErrors(errors: ParserError[]) {
+    // do nothing
+  }
+}
diff --git a/tools/winscope/src/main_prod.ts b/tools/winscope/src/main_prod.ts
index 52ce2f5..3b9d00c 100644
--- a/tools/winscope/src/main_prod.ts
+++ b/tools/winscope/src/main_prod.ts
@@ -16,6 +16,11 @@
 import {enableProdMode} from '@angular/core';
 import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
 import {AppModule} from './app/app_module';
+import {globalConfig} from './common/global_config';
+
+globalConfig.set({
+  MODE: 'PROD',
+});
 
 enableProdMode();
 
diff --git a/tools/winscope/src/main_component_test.ts b/tools/winscope/src/main_unit_test.ts
similarity index 87%
rename from tools/winscope/src/main_component_test.ts
rename to tools/winscope/src/main_unit_test.ts
index f6c90fd..3a2b8de 100644
--- a/tools/winscope/src/main_component_test.ts
+++ b/tools/winscope/src/main_unit_test.ts
@@ -36,6 +36,6 @@
 
 TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
 
-// load all tests of Angular components
-const context = require.context('./', true, /_component_test\.ts$/);
+// filter matches all "*_test.ts" files that are not within the /test/e2e/ directory
+const context = require.context('./', true, /(?<!\/test\/e2e\/.*)_test.ts$/);
 context.keys().forEach(context);
diff --git a/tools/winscope/src/parsers/abstract_parser.ts b/tools/winscope/src/parsers/abstract_parser.ts
index ec57f46..6c8e82f 100644
--- a/tools/winscope/src/parsers/abstract_parser.ts
+++ b/tools/winscope/src/parsers/abstract_parser.ts
@@ -14,11 +14,12 @@
  * limitations under the License.
  */
 
-import {ArrayUtils} from 'common/array_utils';
+import {Timestamp, TimestampType} from 'common/time';
+import {AbsoluteEntryIndex, EntriesRange} from 'trace/index_types';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
+import {ParsingUtils} from './parsing_utils';
 
 abstract class AbstractParser<T extends object = object> implements Parser<T> {
   protected traceFile: TraceFile;
@@ -31,20 +32,15 @@
 
   async parse() {
     const traceBuffer = new Uint8Array(await this.traceFile.file.arrayBuffer());
+    ParsingUtils.throwIfMagicNumberDoesntMatch(traceBuffer, this.getMagicNumber());
+    this.decodedEntries = this.decodeTrace(traceBuffer).map((it) =>
+      ParsingUtils.addDefaultProtoFields(it)
+    );
+    this.timestamps = this.decodeTimestamps();
+  }
 
-    const magicNumber = this.getMagicNumber();
-    if (magicNumber !== undefined) {
-      const bufferContainsMagicNumber = ArrayUtils.equal(
-        magicNumber,
-        traceBuffer.slice(0, magicNumber.length)
-      );
-      if (!bufferContainsMagicNumber) {
-        throw TypeError("buffer doesn't contain expected magic number");
-      }
-    }
-
-    this.decodedEntries = this.decodeTrace(traceBuffer).map((it) => this.addDefaultProtoFields(it));
-
+  private decodeTimestamps(): Map<TimestampType, Timestamp[]> {
+    const timeStampMap = new Map<TimestampType, Timestamp[]>();
     for (const type of [TimestampType.ELAPSED, TimestampType.REAL]) {
       const timestamps: Timestamp[] = [];
       let areTimestampsValid = true;
@@ -59,9 +55,10 @@
       }
 
       if (areTimestampsValid) {
-        this.timestamps.set(type, timestamps);
+        timeStampMap.set(type, timestamps);
       }
     }
+    return timeStampMap;
   }
 
   abstract getTraceType(): TraceType;
@@ -78,43 +75,13 @@
     return this.timestamps.get(type);
   }
 
-  getEntry(index: number, timestampType: TimestampType): Promise<T> {
+  getEntry(index: AbsoluteEntryIndex, timestampType: TimestampType): Promise<T> {
     const entry = this.processDecodedEntry(index, timestampType, this.decodedEntries[index]);
     return Promise.resolve(entry);
   }
 
-  // Add default values to the proto objects.
-  private addDefaultProtoFields(protoObj: any): any {
-    if (!protoObj || protoObj !== Object(protoObj) || !protoObj.$type) {
-      return protoObj;
-    }
-
-    for (const fieldName in protoObj.$type.fields) {
-      if (Object.prototype.hasOwnProperty.call(protoObj.$type.fields, fieldName)) {
-        const fieldProperties = protoObj.$type.fields[fieldName];
-        const field = protoObj[fieldName];
-
-        if (Array.isArray(field)) {
-          field.forEach((item, _) => {
-            this.addDefaultProtoFields(item);
-          });
-          continue;
-        }
-
-        if (!field) {
-          protoObj[fieldName] = fieldProperties.defaultValue;
-        }
-
-        if (fieldProperties.resolvedType && fieldProperties.resolvedType.valuesById) {
-          protoObj[fieldName] =
-            fieldProperties.resolvedType.valuesById[protoObj[fieldProperties.name]];
-          continue;
-        }
-        this.addDefaultProtoFields(protoObj[fieldName]);
-      }
-    }
-
-    return protoObj;
+  getPartialProtos(entriesRange: EntriesRange, fieldPath: string): Promise<object[]> {
+    return ParsingUtils.getPartialProtos(this.decodedEntries, entriesRange, fieldPath);
   }
 
   protected abstract getMagicNumber(): undefined | number[];
diff --git a/tools/winscope/src/parsers/abstract_traces_parser.ts b/tools/winscope/src/parsers/abstract_traces_parser.ts
index 266c3ba..7600af9 100644
--- a/tools/winscope/src/parsers/abstract_traces_parser.ts
+++ b/tools/winscope/src/parsers/abstract_traces_parser.ts
@@ -14,8 +14,9 @@
  * limitations under the License.
  */
 
+import {Timestamp, TimestampType} from 'common/time';
+import {AbsoluteEntryIndex, EntriesRange} from 'trace/index_types';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 export abstract class AbstractTracesParser<T> implements Parser<T> {
@@ -28,7 +29,11 @@
 
   abstract getTraceType(): TraceType;
 
-  abstract getEntry(index: number, timestampType: TimestampType): Promise<T>;
+  abstract getEntry(index: AbsoluteEntryIndex, timestampType: TimestampType): Promise<T>;
+
+  getPartialProtos(entriesRange: EntriesRange, fieldPath: string): Promise<object[]> {
+    throw new Error('Not implemented');
+  }
 
   abstract getLengthEntries(): number;
 
diff --git a/tools/winscope/src/parsers/parser_accessibility.ts b/tools/winscope/src/parsers/parser_accessibility.ts
index fb30419..32aa660 100644
--- a/tools/winscope/src/parsers/parser_accessibility.ts
+++ b/tools/winscope/src/parsers/parser_accessibility.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Timestamp, TimestampType} from 'common/time';
 import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
 import {AbstractParser} from './abstract_parser';
diff --git a/tools/winscope/src/parsers/parser_accessibility_test.ts b/tools/winscope/src/parsers/parser_accessibility_test.ts
index 9362264..69a8aef 100644
--- a/tools/winscope/src/parsers/parser_accessibility_test.ts
+++ b/tools/winscope/src/parsers/parser_accessibility_test.ts
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {Timestamp, TimestampType} from 'common/time';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserAccessibility', () => {
diff --git a/tools/winscope/src/parsers/parser_common_test.ts b/tools/winscope/src/parsers/parser_common_test.ts
index 6f8ec58..0161bfe 100644
--- a/tools/winscope/src/parsers/parser_common_test.ts
+++ b/tools/winscope/src/parsers/parser_common_test.ts
@@ -13,18 +13,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {CommonTestUtils} from 'test/common/utils';
+import {Timestamp, TimestampType} from 'common/time';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
 import {UnitTestUtils} from 'test/unit/utils';
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
 import {ParserFactory} from './parser_factory';
 
 describe('Parser', () => {
   it('is robust to empty trace file', async () => {
-    const trace = new TraceFile(await CommonTestUtils.getFixtureFile('traces/empty.pb'), undefined);
+    const trace = new TraceFile(await UnitTestUtils.getFixtureFile('traces/empty.pb'), undefined);
     const [parsers, errors] = await new ParserFactory().createParsers([trace]);
     expect(parsers.length).toEqual(0);
   });
@@ -37,6 +36,31 @@
     expect(parser.getTimestamps(TimestampType.REAL)).toEqual([]);
   });
 
+  it('retrieves partial trace entries', async () => {
+    {
+      const parser = await UnitTestUtils.getParser(
+        'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb'
+      );
+      const entries = await parser.getPartialProtos({start: 0, end: 3}, 'vsyncId');
+      entries.forEach((entry) => {
+        // convert Long to bigint
+        (entry as any).vsyncId = BigInt((entry as any).vsyncId.toString());
+      });
+      expect(entries).toEqual([{vsyncId: 4891n}, {vsyncId: 5235n}, {vsyncId: 5748n}]);
+    }
+    {
+      const parser = await UnitTestUtils.getParser(
+        'traces/elapsed_and_real_timestamp/Transactions.pb'
+      );
+      const entries = await parser.getPartialProtos({start: 0, end: 3}, 'vsyncId');
+      entries.forEach((entry) => {
+        // convert Long to bigint
+        (entry as any).vsyncId = BigInt((entry as any).vsyncId.toString());
+      });
+      expect(entries).toEqual([{vsyncId: 1n}, {vsyncId: 2n}, {vsyncId: 3n}]);
+    }
+  });
+
   describe('real timestamp', () => {
     let parser: Parser<WindowManagerState>;
 
diff --git a/tools/winscope/src/parsers/parser_eventlog.ts b/tools/winscope/src/parsers/parser_eventlog.ts
index 902e80c..ff03aca 100644
--- a/tools/winscope/src/parsers/parser_eventlog.ts
+++ b/tools/winscope/src/parsers/parser_eventlog.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-import {Event, EventLogParser} from 'trace/flickerlib/common';
-import {RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
+import {RealTimestamp, Timestamp, TimestampType} from 'common/time';
+import {Event, EventLogParser} from 'flickerlib/common';
 import {TraceType} from 'trace/trace_type';
 import {AbstractParser} from './abstract_parser';
 
diff --git a/tools/winscope/src/parsers/parser_eventlog_test.ts b/tools/winscope/src/parsers/parser_eventlog_test.ts
index d1834d8..5c0b1f6 100644
--- a/tools/winscope/src/parsers/parser_eventlog_test.ts
+++ b/tools/winscope/src/parsers/parser_eventlog_test.ts
@@ -15,10 +15,10 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp, TimestampType} from 'common/time';
+import {CujEvent} from 'flickerlib/common';
 import {UnitTestUtils} from 'test/unit/utils';
-import {CujEvent} from 'trace/flickerlib/common';
 import {Parser} from 'trace/parser';
-import {RealTimestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserEventLog', () => {
diff --git a/tools/winscope/src/parsers/parser_factory.ts b/tools/winscope/src/parsers/parser_factory.ts
index 577f49d..6624964 100644
--- a/tools/winscope/src/parsers/parser_factory.ts
+++ b/tools/winscope/src/parsers/parser_factory.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {FunctionUtils, OnProgressUpdateType} from 'common/function_utils';
+import {ProgressListener} from 'interfaces/progress_listener';
 import {Parser} from 'trace/parser';
 import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
@@ -57,17 +57,22 @@
 
   async createParsers(
     traceFiles: TraceFile[],
-    onProgressUpdate: OnProgressUpdateType = FunctionUtils.DO_NOTHING
+    progressListener?: ProgressListener
   ): Promise<[Array<{file: TraceFile; parser: Parser<object>}>, ParserError[]]> {
     const errors: ParserError[] = [];
 
     const parsers = new Array<{file: TraceFile; parser: Parser<object>}>();
 
-    if (traceFiles.length === 0) {
-      errors.push(new ParserError(ParserErrorType.NO_INPUT_FILES));
-    }
+    const maybeAddParser = (p: Parser<object>, f: TraceFile) => {
+      if (this.shouldUseParser(p, errors)) {
+        this.parsers.set(p.getTraceType(), p);
+        parsers.push({file: f, parser: p});
+      }
+    };
 
     for (const [index, traceFile] of traceFiles.entries()) {
+      progressListener?.onProgressUpdate('Parsing proto files', (index / traceFiles.length) * 100);
+
       let hasFoundParser = false;
 
       for (const ParserType of ParserFactory.PARSERS) {
@@ -75,9 +80,10 @@
           const parser = new ParserType(traceFile);
           await parser.parse();
           hasFoundParser = true;
-          if (this.shouldUseParser(parser, errors)) {
-            this.parsers.set(parser.getTraceType(), parser);
-            parsers.push({file: traceFile, parser});
+          if (parser instanceof ParserViewCapture) {
+            parser.windowParsers.forEach((it) => maybeAddParser(it, traceFile));
+          } else {
+            maybeAddParser(parser, traceFile);
           }
           break;
         } catch (error) {
@@ -86,11 +92,9 @@
       }
 
       if (!hasFoundParser) {
-        console.log(`Failed to load trace ${traceFile.file.name}`);
+        console.error(`Failed to find parser for trace ${traceFile.file.name}`);
         errors.push(new ParserError(ParserErrorType.UNSUPPORTED_FORMAT, traceFile.getDescriptor()));
       }
-
-      onProgressUpdate((100 * (index + 1)) / traceFiles.length);
     }
 
     return [parsers, errors];
@@ -142,6 +146,7 @@
 }
 
 export enum ParserErrorType {
+  CORRUPTED_ARCHIVE,
   NO_INPUT_FILES,
   UNSUPPORTED_FORMAT,
   OVERRIDE,
diff --git a/tools/winscope/src/parsers/parser_input_method_clients.ts b/tools/winscope/src/parsers/parser_input_method_clients.ts
index 796df79..d290b23 100644
--- a/tools/winscope/src/parsers/parser_input_method_clients.ts
+++ b/tools/winscope/src/parsers/parser_input_method_clients.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
+import {Timestamp, TimestampType} from 'common/time';
 import {TimeUtils} from 'common/time_utils';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceFile} from 'trace/trace_file';
 import {TraceTreeNode} from 'trace/trace_tree_node';
 import {TraceType} from 'trace/trace_type';
diff --git a/tools/winscope/src/parsers/parser_input_method_clients_test.ts b/tools/winscope/src/parsers/parser_input_method_clients_test.ts
index 265cb19..bac465d 100644
--- a/tools/winscope/src/parsers/parser_input_method_clients_test.ts
+++ b/tools/winscope/src/parsers/parser_input_method_clients_test.ts
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {Timestamp, TimestampType} from 'common/time';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserInputMethodlClients', () => {
diff --git a/tools/winscope/src/parsers/parser_input_method_manager_service.ts b/tools/winscope/src/parsers/parser_input_method_manager_service.ts
index a256f54..a28cf89 100644
--- a/tools/winscope/src/parsers/parser_input_method_manager_service.ts
+++ b/tools/winscope/src/parsers/parser_input_method_manager_service.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
+import {Timestamp, TimestampType} from 'common/time';
 import {TimeUtils} from 'common/time_utils';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceFile} from 'trace/trace_file';
 import {TraceTreeNode} from 'trace/trace_tree_node';
 import {TraceType} from 'trace/trace_type';
diff --git a/tools/winscope/src/parsers/parser_input_method_manager_service_test.ts b/tools/winscope/src/parsers/parser_input_method_manager_service_test.ts
index 139f114..60108c5 100644
--- a/tools/winscope/src/parsers/parser_input_method_manager_service_test.ts
+++ b/tools/winscope/src/parsers/parser_input_method_manager_service_test.ts
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {Timestamp, TimestampType} from 'common/time';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserInputMethodManagerService', () => {
diff --git a/tools/winscope/src/parsers/parser_input_method_service.ts b/tools/winscope/src/parsers/parser_input_method_service.ts
index 5e31ea7..6f704c9 100644
--- a/tools/winscope/src/parsers/parser_input_method_service.ts
+++ b/tools/winscope/src/parsers/parser_input_method_service.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
+import {Timestamp, TimestampType} from 'common/time';
 import {TimeUtils} from 'common/time_utils';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceFile} from 'trace/trace_file';
 import {TraceTreeNode} from 'trace/trace_tree_node';
 import {TraceType} from 'trace/trace_type';
diff --git a/tools/winscope/src/parsers/parser_input_method_service_test.ts b/tools/winscope/src/parsers/parser_input_method_service_test.ts
index 2c0f47a..8bc451a 100644
--- a/tools/winscope/src/parsers/parser_input_method_service_test.ts
+++ b/tools/winscope/src/parsers/parser_input_method_service_test.ts
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {Timestamp, TimestampType} from 'common/time';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserInputMethodService', () => {
diff --git a/tools/winscope/src/parsers/parser_protolog.ts b/tools/winscope/src/parsers/parser_protolog.ts
index 4c0c087..d2bb4a3 100644
--- a/tools/winscope/src/parsers/parser_protolog.ts
+++ b/tools/winscope/src/parsers/parser_protolog.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
+import {Timestamp, TimestampType} from 'common/time';
 import {FormattedLogMessage, LogMessage, UnformattedLogMessage} from 'trace/protolog';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
 import configJson from '../../../../../frameworks/base/data/etc/services.core.protolog.json';
diff --git a/tools/winscope/src/parsers/parser_protolog_test.ts b/tools/winscope/src/parsers/parser_protolog_test.ts
index e6d0a70..36be6f0 100644
--- a/tools/winscope/src/parsers/parser_protolog_test.ts
+++ b/tools/winscope/src/parsers/parser_protolog_test.ts
@@ -13,10 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {Timestamp, TimestampType} from 'common/time';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
 import {LogMessage} from 'trace/protolog';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserProtoLog', () => {
diff --git a/tools/winscope/src/parsers/parser_screen_recording.ts b/tools/winscope/src/parsers/parser_screen_recording.ts
index 0550da5..9ae35ea 100644
--- a/tools/winscope/src/parsers/parser_screen_recording.ts
+++ b/tools/winscope/src/parsers/parser_screen_recording.ts
@@ -15,9 +15,9 @@
  */
 
 import {ArrayUtils} from 'common/array_utils';
+import {Timestamp, TimestampType} from 'common/time';
 import {ScreenRecordingTraceEntry} from 'trace/screen_recording';
 import {ScreenRecordingUtils} from 'trace/screen_recording_utils';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
 import {AbstractParser} from './abstract_parser';
diff --git a/tools/winscope/src/parsers/parser_screen_recording_legacy.ts b/tools/winscope/src/parsers/parser_screen_recording_legacy.ts
index 1aac6ca..603a389 100644
--- a/tools/winscope/src/parsers/parser_screen_recording_legacy.ts
+++ b/tools/winscope/src/parsers/parser_screen_recording_legacy.ts
@@ -15,8 +15,8 @@
  */
 
 import {ArrayUtils} from 'common/array_utils';
+import {Timestamp, TimestampType} from 'common/time';
 import {ScreenRecordingTraceEntry} from 'trace/screen_recording';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
 import {AbstractParser} from './abstract_parser';
diff --git a/tools/winscope/src/parsers/parser_screen_recording_legacy_test.ts b/tools/winscope/src/parsers/parser_screen_recording_legacy_test.ts
index a595e12..e192338 100644
--- a/tools/winscope/src/parsers/parser_screen_recording_legacy_test.ts
+++ b/tools/winscope/src/parsers/parser_screen_recording_legacy_test.ts
@@ -13,10 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {Timestamp, TimestampType} from 'common/time';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
 import {ScreenRecordingTraceEntry} from 'trace/screen_recording';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserScreenRecordingLegacy', () => {
diff --git a/tools/winscope/src/parsers/parser_screen_recording_test.ts b/tools/winscope/src/parsers/parser_screen_recording_test.ts
index 5bb830b..7e764c8 100644
--- a/tools/winscope/src/parsers/parser_screen_recording_test.ts
+++ b/tools/winscope/src/parsers/parser_screen_recording_test.ts
@@ -13,10 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {Timestamp, TimestampType} from 'common/time';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
 import {ScreenRecordingTraceEntry} from 'trace/screen_recording';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserScreenRecording', () => {
diff --git a/tools/winscope/src/parsers/parser_surface_flinger.ts b/tools/winscope/src/parsers/parser_surface_flinger.ts
index 5cb0cce..883c848 100644
--- a/tools/winscope/src/parsers/parser_surface_flinger.ts
+++ b/tools/winscope/src/parsers/parser_surface_flinger.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry';
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Timestamp, TimestampType} from 'common/time';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
 import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
 import {AbstractParser} from './abstract_parser';
diff --git a/tools/winscope/src/parsers/parser_surface_flinger_dump_test.ts b/tools/winscope/src/parsers/parser_surface_flinger_dump_test.ts
index 8af0d5b..2a9f787 100644
--- a/tools/winscope/src/parsers/parser_surface_flinger_dump_test.ts
+++ b/tools/winscope/src/parsers/parser_surface_flinger_dump_test.ts
@@ -13,10 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {Timestamp, TimestampType} from 'common/time';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
 import {UnitTestUtils} from 'test/unit/utils';
-import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserSurfaceFlingerDump', () => {
diff --git a/tools/winscope/src/parsers/parser_surface_flinger_test.ts b/tools/winscope/src/parsers/parser_surface_flinger_test.ts
index c839cb9..d33429b 100644
--- a/tools/winscope/src/parsers/parser_surface_flinger_test.ts
+++ b/tools/winscope/src/parsers/parser_surface_flinger_test.ts
@@ -13,11 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {Timestamp, TimestampType} from 'common/time';
+import {Layer} from 'flickerlib/layers/Layer';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
 import {UnitTestUtils} from 'test/unit/utils';
-import {Layer} from 'trace/flickerlib/layers/Layer';
-import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserSurfaceFlinger', () => {
diff --git a/tools/winscope/src/parsers/parser_transactions.ts b/tools/winscope/src/parsers/parser_transactions.ts
index 14fc2a9..5e0ce96 100644
--- a/tools/winscope/src/parsers/parser_transactions.ts
+++ b/tools/winscope/src/parsers/parser_transactions.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Timestamp, TimestampType} from 'common/time';
 import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
 import {AbstractParser} from './abstract_parser';
diff --git a/tools/winscope/src/parsers/parser_transactions_test.ts b/tools/winscope/src/parsers/parser_transactions_test.ts
index 5d4089d..979d9be 100644
--- a/tools/winscope/src/parsers/parser_transactions_test.ts
+++ b/tools/winscope/src/parsers/parser_transactions_test.ts
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {Timestamp, TimestampType} from 'common/time';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserTransactions', () => {
diff --git a/tools/winscope/src/parsers/parser_transitions_shell.ts b/tools/winscope/src/parsers/parser_transitions_shell.ts
index db7941b..4e0601f 100644
--- a/tools/winscope/src/parsers/parser_transitions_shell.ts
+++ b/tools/winscope/src/parsers/parser_transitions_shell.ts
@@ -14,13 +14,9 @@
  * limitations under the License.
  */
 
-import {
-  CrossPlatform,
-  ShellTransitionData,
-  Transition,
-  WmTransitionData,
-} from 'trace/flickerlib/common';
-import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
+import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'common/time';
+import {CrossPlatform, ShellTransitionData, Transition, WmTransitionData} from 'flickerlib/common';
+import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
 import {AbstractParser} from './abstract_parser';
 import {ShellTransitionsTraceFileProto} from './proto_types';
@@ -29,6 +25,10 @@
   private realToElapsedTimeOffsetNs: undefined | bigint;
   private handlerMapping: undefined | Map<number, string>;
 
+  constructor(trace: TraceFile) {
+    super(trace);
+  }
+
   override getTraceType(): TraceType {
     return TraceType.SHELL_TRANSITION;
   }
diff --git a/tools/winscope/src/parsers/parser_transitions_shell_test.ts b/tools/winscope/src/parsers/parser_transitions_shell_test.ts
index f986db7..0e85f2e 100644
--- a/tools/winscope/src/parsers/parser_transitions_shell_test.ts
+++ b/tools/winscope/src/parsers/parser_transitions_shell_test.ts
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
+import {ElapsedTimestamp, RealTimestamp, TimestampType} from 'common/time';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
-import {ElapsedTimestamp, RealTimestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ShellFileParserTransitions', () => {
diff --git a/tools/winscope/src/parsers/parser_transitions_wm.ts b/tools/winscope/src/parsers/parser_transitions_wm.ts
index 0fce3f6..5d3fb14 100644
--- a/tools/winscope/src/parsers/parser_transitions_wm.ts
+++ b/tools/winscope/src/parsers/parser_transitions_wm.ts
@@ -1,3 +1,4 @@
+import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'common/time';
 import {
   CrossPlatform,
   ShellTransitionData,
@@ -5,8 +6,8 @@
   TransitionChange,
   TransitionType,
   WmTransitionData,
-} from 'trace/flickerlib/common';
-import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
+} from 'flickerlib/common';
+import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
 import {AbstractParser} from './abstract_parser';
 import {WmTransitionsTraceFileProto} from './proto_types';
@@ -14,6 +15,10 @@
 export class ParserTransitionsWm extends AbstractParser {
   private realToElapsedTimeOffsetNs: undefined | bigint;
 
+  constructor(trace: TraceFile) {
+    super(trace);
+  }
+
   override getTraceType(): TraceType {
     return TraceType.WM_TRANSITION;
   }
@@ -107,6 +112,20 @@
       );
     }
 
+    const startingWindowRemoveTime = null;
+    if (
+      entry.startingWindowRemoveTimeNs &&
+      BigInt(entry.startingWindowRemoveTimeNs.toString()) !== 0n
+    ) {
+      const unixNs =
+        BigInt(entry.startingWindowRemoveTimeNs.toString()) + this.realToElapsedTimeOffsetNs;
+      finishTime = CrossPlatform.timestamp.fromString(
+        entry.startingWindowRemoveTimeNs.toString(),
+        null,
+        unixNs.toString()
+      );
+    }
+
     let startTransactionId = null;
     if (entry.startTransactionId && BigInt(entry.startTransactionId.toString()) !== 0n) {
       startTransactionId = BigInt(entry.startTransactionId.toString());
@@ -129,6 +148,7 @@
         sendTime,
         abortTime,
         finishTime,
+        startingWindowRemoveTime,
         startTransactionId?.toString(),
         finishTransactionId?.toString(),
         type,
diff --git a/tools/winscope/src/parsers/parser_transitions_wm_test.ts b/tools/winscope/src/parsers/parser_transitions_wm_test.ts
index 0f0cdd1..06ba000 100644
--- a/tools/winscope/src/parsers/parser_transitions_wm_test.ts
+++ b/tools/winscope/src/parsers/parser_transitions_wm_test.ts
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
+import {ElapsedTimestamp, RealTimestamp, TimestampType} from 'common/time';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
-import {ElapsedTimestamp, RealTimestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('WmFileParserTransitions', () => {
diff --git a/tools/winscope/src/parsers/parser_view_capture.ts b/tools/winscope/src/parsers/parser_view_capture.ts
index d275bd9..7c5af50 100644
--- a/tools/winscope/src/parsers/parser_view_capture.ts
+++ b/tools/winscope/src/parsers/parser_view_capture.ts
@@ -14,164 +14,54 @@
  * limitations under the License.
  */
 
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Parser} from 'trace/parser';
 import {TraceFile} from 'trace/trace_file';
-import {TraceType} from 'trace/trace_type';
-import {AbstractParser} from './abstract_parser';
+import {FrameData, TraceType, WindowData} from 'trace/trace_type';
+import {ParserViewCaptureWindow} from './parser_view_capture_window';
+import {ParsingUtils} from './parsing_utils';
 import {ExportedData} from './proto_types';
 
-/* TODO: Support multiple Windows in one file upload.  */
-export class ParserViewCapture extends AbstractParser {
-  private classNames: string[] = [];
-  private realToElapsedTimeOffsetNanos: bigint | undefined = undefined;
-  packageName: string = '';
-  windowTitle: string = '';
+export class ParserViewCapture {
+  // TODO(b/291213403): viewer should read this data from the Trace object
+  static packageNames: string[] = [];
+  readonly windowParsers: Array<Parser<FrameData>> = [];
 
-  constructor(trace: TraceFile) {
-    super(trace);
+  constructor(private readonly traceFile: TraceFile) {}
+
+  async parse() {
+    const traceBuffer = new Uint8Array(await this.traceFile.file.arrayBuffer());
+    ParsingUtils.throwIfMagicNumberDoesntMatch(traceBuffer, ParserViewCapture.MAGIC_NUMBER);
+
+    const exportedData = ExportedData.decode(traceBuffer) as any;
+    ParserViewCapture.packageNames.push(exportedData.package);
+
+    exportedData.windowData.forEach((windowData: WindowData) =>
+      this.windowParsers.push(
+        new ParserViewCaptureWindow(
+          [this.traceFile.getDescriptor()],
+          windowData.frameData,
+          ParserViewCapture.toTraceType(windowData),
+          BigInt(exportedData.realToElapsedTimeOffsetNanos),
+          exportedData.classname
+        )
+      )
+    );
   }
 
-  override getTraceType(): TraceType {
+  getTraceType(): TraceType {
     return TraceType.VIEW_CAPTURE;
   }
 
-  override getMagicNumber(): number[] {
-    return ParserViewCapture.MAGIC_NUMBER;
-  }
-
-  override decodeTrace(buffer: Uint8Array): any[] {
-    const exportedData = ExportedData.decode(buffer) as any;
-    this.classNames = exportedData.classname;
-    this.realToElapsedTimeOffsetNanos = BigInt(exportedData.realToElapsedTimeOffsetNanos);
-    this.packageName = this.shortenAndCapitalize(exportedData.package);
-
-    const firstWindowData = exportedData.windowData[0];
-    this.windowTitle = this.shortenAndCapitalize(firstWindowData.title);
-
-    return firstWindowData.frameData;
-  }
-
-  override processDecodedEntry(index: number, timestampType: TimestampType, decodedEntry: any) {
-    this.formatProperties(decodedEntry.node, this.classNames);
-    return decodedEntry;
-  }
-
-  private shortenAndCapitalize(name: string): string {
-    const shortName = name.substring(name.lastIndexOf('.') + 1);
-    return shortName.charAt(0).toUpperCase() + shortName.slice(1);
-  }
-
-  private formatProperties(root: any /* ViewNode */, classNames: string[]): any /* ViewNode */ {
-    const DEPTH_MAGNIFICATION = 4;
-    const VISIBLE = 0;
-
-    function inner(
-      node: any /* ViewNode */,
-      leftShift: number,
-      topShift: number,
-      scaleX: number,
-      scaleY: number,
-      depth: number,
-      isParentVisible: boolean
-    ) {
-      const newScaleX = scaleX * node.scaleX;
-      const newScaleY = scaleY * node.scaleY;
-
-      const l =
-        leftShift +
-        (node.left + node.translationX) * scaleX +
-        (node.width * (scaleX - newScaleX)) / 2;
-      const t =
-        topShift +
-        (node.top + node.translationY) * scaleY +
-        (node.height * (scaleY - newScaleY)) / 2;
-      node.boxPos = {
-        left: l,
-        top: t,
-        width: node.width * newScaleX,
-        height: node.height * newScaleY,
-      };
-
-      node.name = `${classNames[node.classnameIndex]}@${node.hashcode}`;
-
-      node.shortName = node.name.split('.');
-      node.shortName = node.shortName[node.shortName.length - 1];
-
-      node.isVisible = isParentVisible && VISIBLE === node.visibility;
-
-      for (let i = 0; i < node.children.length; i++) {
-        inner(
-          node.children[i],
-          l - node.scrollX,
-          t - node.scrollY,
-          newScaleX,
-          newScaleY,
-          depth + 1,
-          node.isVisible
-        );
-        node.children[i].parent = node;
-      }
-
-      // TODO: Audit these properties
-      node.depth = depth * DEPTH_MAGNIFICATION;
-      node.type = 'ViewNode';
-      node.layerId = 0;
-      node.isMissing = false;
-      node.hwcCompositionType = 0;
-      node.zOrderRelativeOfId = -1;
-      node.isRootLayer = false;
-      node.skip = null;
-      node.id = node.name;
-      node.stableId = node.id;
-      node.equals = (other: any /* ViewNode */) => ParserViewCapture.equals(node, other);
+  private static toTraceType(windowData: WindowData): TraceType {
+    switch (windowData.title) {
+      case '.Taskbar':
+        return TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER;
+      case '.TaskbarOverlay':
+        return TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER;
+      default:
+        return TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY;
     }
-
-    root.scaleX = root.scaleY = 1;
-    root.translationX = root.translationY = 0;
-    inner(root, 0, 0, 1, 1, 0, true);
-
-    root.isRootLayer = true;
-    return root;
-  }
-
-  override getTimestamp(timestampType: TimestampType, frameData: any): undefined | Timestamp {
-    return Timestamp.from(
-      timestampType,
-      BigInt(frameData.timestamp),
-      this.realToElapsedTimeOffsetNanos
-    );
   }
 
   private static readonly MAGIC_NUMBER = [0x9, 0x78, 0x65, 0x90, 0x65, 0x73, 0x82, 0x65, 0x68];
-
-  /** This method is used by the tree_generator to determine if 2 nodes have equivalent properties. */
-  private static equals(node: any /* ViewNode */, other: any /* ViewNode */): boolean {
-    if (!node && !other) {
-      return true;
-    }
-    if (!node || !other) {
-      return false;
-    }
-    return (
-      node.id === other.id &&
-      node.name === other.name &&
-      node.hashcode === other.hashcode &&
-      node.left === other.left &&
-      node.top === other.top &&
-      node.height === other.height &&
-      node.width === other.width &&
-      node.elevation === other.elevation &&
-      node.scaleX === other.scaleX &&
-      node.scaleY === other.scaleY &&
-      node.scrollX === other.scrollX &&
-      node.scrollY === other.scrollY &&
-      node.translationX === other.translationX &&
-      node.translationY === other.translationY &&
-      node.alpha === other.alpha &&
-      node.visibility === other.visibility &&
-      node.willNotDraw === other.willNotDraw &&
-      node.clipChildren === other.clipChildren &&
-      node.depth === other.depth
-    );
-  }
 }
diff --git a/tools/winscope/src/parsers/parser_view_capture_test.ts b/tools/winscope/src/parsers/parser_view_capture_test.ts
index 9b1443a..195a01d 100644
--- a/tools/winscope/src/parsers/parser_view_capture_test.ts
+++ b/tools/winscope/src/parsers/parser_view_capture_test.ts
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {Timestamp, TimestampType} from 'common/time';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserViewCapture', () => {
@@ -28,23 +28,23 @@
   });
 
   it('has expected trace type', () => {
-    expect(parser.getTraceType()).toEqual(TraceType.VIEW_CAPTURE);
+    expect(parser.getTraceType()).toEqual(TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER);
   });
 
   it('provides elapsed timestamps', () => {
     const expected = [
-      new Timestamp(TimestampType.ELAPSED, 26231798759n),
-      new Timestamp(TimestampType.ELAPSED, 26242905367n),
-      new Timestamp(TimestampType.ELAPSED, 26255550549n),
+      new Timestamp(TimestampType.ELAPSED, 181114412436130n),
+      new Timestamp(TimestampType.ELAPSED, 181114421012750n),
+      new Timestamp(TimestampType.ELAPSED, 181114429047540n),
     ];
     expect(parser.getTimestamps(TimestampType.ELAPSED)!.slice(0, 3)).toEqual(expected);
   });
 
   it('provides real timestamps', () => {
     const expected = [
-      new Timestamp(TimestampType.REAL, 1686674380113072216n),
-      new Timestamp(TimestampType.REAL, 1686674380124178824n),
-      new Timestamp(TimestampType.REAL, 1686674380136824006n),
+      new Timestamp(TimestampType.REAL, 1691692936292808460n),
+      new Timestamp(TimestampType.REAL, 1691692936301385080n),
+      new Timestamp(TimestampType.REAL, 1691692936309419870n),
     ];
     expect(parser.getTimestamps(TimestampType.REAL)!.slice(0, 3)).toEqual(expected);
   });
diff --git a/tools/winscope/src/parsers/parser_view_capture_window.ts b/tools/winscope/src/parsers/parser_view_capture_window.ts
new file mode 100644
index 0000000..7887473
--- /dev/null
+++ b/tools/winscope/src/parsers/parser_view_capture_window.ts
@@ -0,0 +1,204 @@
+/*
+ * 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 {Timestamp, TimestampType} from 'common/time';
+import {EntriesRange} from 'trace/index_types';
+import {Parser} from 'trace/parser';
+import {FrameData, TraceType, ViewNode} from 'trace/trace_type';
+import {ParsingUtils} from './parsing_utils';
+
+export class ParserViewCaptureWindow implements Parser<FrameData> {
+  private timestamps: Map<TimestampType, Timestamp[]> = new Map<TimestampType, Timestamp[]>();
+
+  constructor(
+    private readonly descriptors: string[],
+    private readonly frameData: FrameData[],
+    private readonly traceType: TraceType,
+    private readonly realToElapsedTimeOffsetNanos: bigint,
+    private readonly classNames: string[]
+  ) {
+    /*
+      TODO: Enable this once multiple ViewCapture Tabs becomes generic. Right now it doesn't matter since
+        the title is dependent upon the view capture type.
+
+      this.title = `${ParserViewCapture.shortenAndCapitalize(packageName)} - ${ParserViewCapture.shortenAndCapitalize(
+        windowData.title
+      )}`;
+      */
+    this.parse();
+  }
+
+  parse() {
+    this.frameData.map((it) => ParsingUtils.addDefaultProtoFields(it));
+    this.timestamps = this.decodeTimestamps();
+  }
+
+  private decodeTimestamps(): Map<TimestampType, Timestamp[]> {
+    const timeStampMap = new Map<TimestampType, Timestamp[]>();
+    for (const type of [TimestampType.ELAPSED, TimestampType.REAL]) {
+      const timestamps: Timestamp[] = [];
+      let areTimestampsValid = true;
+
+      for (const entry of this.frameData) {
+        const timestamp = Timestamp.from(
+          type,
+          BigInt(entry.timestamp),
+          this.realToElapsedTimeOffsetNanos
+        );
+        if (timestamp === undefined) {
+          areTimestampsValid = false;
+          break;
+        }
+        timestamps.push(timestamp);
+      }
+
+      if (areTimestampsValid) {
+        timeStampMap.set(type, timestamps);
+      }
+    }
+    return timeStampMap;
+  }
+
+  getTraceType(): TraceType {
+    return this.traceType;
+  }
+
+  getLengthEntries(): number {
+    return this.frameData.length;
+  }
+
+  getTimestamps(type: TimestampType): Timestamp[] | undefined {
+    return this.timestamps.get(type);
+  }
+
+  getEntry(index: number, _: TimestampType): Promise<FrameData> {
+    ParserViewCaptureWindow.formatProperties(this.frameData[index].node, this.classNames);
+    return Promise.resolve(this.frameData[index]);
+  }
+
+  getPartialProtos(entriesRange: EntriesRange, fieldPath: string): Promise<object[]> {
+    return ParsingUtils.getPartialProtos(this.frameData, entriesRange, fieldPath);
+  }
+
+  getDescriptors(): string[] {
+    return this.descriptors;
+  }
+
+  private static formatProperties(root: ViewNode, classNames: string[]): ViewNode {
+    const DEPTH_MAGNIFICATION = 4;
+    const VISIBLE = 0;
+
+    function inner(
+      node: ViewNode,
+      leftShift: number,
+      topShift: number,
+      scaleX: number,
+      scaleY: number,
+      depth: number,
+      isParentVisible: boolean
+    ) {
+      const newScaleX = scaleX * node.scaleX;
+      const newScaleY = scaleY * node.scaleY;
+
+      const l =
+        leftShift +
+        (node.left + node.translationX) * scaleX +
+        (node.width * (scaleX - newScaleX)) / 2;
+      const t =
+        topShift +
+        (node.top + node.translationY) * scaleY +
+        (node.height * (scaleY - newScaleY)) / 2;
+      node.boxPos = {
+        left: l,
+        top: t,
+        width: node.width * newScaleX,
+        height: node.height * newScaleY,
+      };
+
+      node.name = `${classNames[node.classnameIndex]}@${node.hashcode}`;
+
+      node.shortName = node.name.split('.');
+      node.shortName = node.shortName[node.shortName.length - 1];
+
+      node.isVisible = isParentVisible && VISIBLE === node.visibility;
+
+      for (let i = 0; i < node.children.length; i++) {
+        inner(
+          node.children[i],
+          l - node.scrollX,
+          t - node.scrollY,
+          newScaleX,
+          newScaleY,
+          depth + 1,
+          node.isVisible
+        );
+        node.children[i].parent = node;
+      }
+
+      // TODO: Audit these properties
+      node.depth = depth * DEPTH_MAGNIFICATION;
+      node.className = node.name.substring(0, node.name.indexOf('@'));
+      node.type = 'ViewNode';
+      node.layerId = 0;
+      node.isMissing = false;
+      node.hwcCompositionType = 0;
+      node.zOrderRelativeOfId = -1;
+      node.isRootLayer = false;
+      node.skip = null;
+      node.id = node.name;
+      node.stableId = node.id;
+      node.equals = (other: ViewNode) => ParserViewCaptureWindow.equals(node, other);
+    }
+
+    root.scaleX = root.scaleY = 1;
+    root.translationX = root.translationY = 0;
+    inner(root, 0, 0, 1, 1, 0, true);
+
+    root.isRootLayer = true;
+    return root;
+  }
+
+  /** This method is used by the tree_generator to determine if 2 nodes have equivalent properties. */
+  private static equals(node: ViewNode, other: ViewNode): boolean {
+    if (!node && !other) {
+      return true;
+    }
+    if (!node || !other) {
+      return false;
+    }
+    return (
+      node.id === other.id &&
+      node.name === other.name &&
+      node.hashcode === other.hashcode &&
+      node.left === other.left &&
+      node.top === other.top &&
+      node.height === other.height &&
+      node.width === other.width &&
+      node.elevation === other.elevation &&
+      node.scaleX === other.scaleX &&
+      node.scaleY === other.scaleY &&
+      node.scrollX === other.scrollX &&
+      node.scrollY === other.scrollY &&
+      node.translationX === other.translationX &&
+      node.translationY === other.translationY &&
+      node.alpha === other.alpha &&
+      node.visibility === other.visibility &&
+      node.willNotDraw === other.willNotDraw &&
+      node.clipChildren === other.clipChildren &&
+      node.depth === other.depth
+    );
+  }
+}
diff --git a/tools/winscope/src/parsers/parser_window_manager.ts b/tools/winscope/src/parsers/parser_window_manager.ts
index 8e64f88..75bbcff 100644
--- a/tools/winscope/src/parsers/parser_window_manager.ts
+++ b/tools/winscope/src/parsers/parser_window_manager.ts
@@ -13,8 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Timestamp, TimestampType} from 'common/time';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
 import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
 import {AbstractParser} from './abstract_parser';
diff --git a/tools/winscope/src/parsers/parser_window_manager_dump.ts b/tools/winscope/src/parsers/parser_window_manager_dump.ts
index 29b0c18..138c9f7 100644
--- a/tools/winscope/src/parsers/parser_window_manager_dump.ts
+++ b/tools/winscope/src/parsers/parser_window_manager_dump.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Timestamp, TimestampType} from 'common/time';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
 import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
 import {AbstractParser} from './abstract_parser';
diff --git a/tools/winscope/src/parsers/parser_window_manager_dump_test.ts b/tools/winscope/src/parsers/parser_window_manager_dump_test.ts
index dd6dfc2..6a50667 100644
--- a/tools/winscope/src/parsers/parser_window_manager_dump_test.ts
+++ b/tools/winscope/src/parsers/parser_window_manager_dump_test.ts
@@ -13,10 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {Timestamp, TimestampType} from 'common/time';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
 import {UnitTestUtils} from 'test/unit/utils';
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserWindowManagerDump', () => {
diff --git a/tools/winscope/src/parsers/parser_window_manager_test.ts b/tools/winscope/src/parsers/parser_window_manager_test.ts
index dd35d65..7ccbd87 100644
--- a/tools/winscope/src/parsers/parser_window_manager_test.ts
+++ b/tools/winscope/src/parsers/parser_window_manager_test.ts
@@ -13,10 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {Timestamp, TimestampType} from 'common/time';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
 import {UnitTestUtils} from 'test/unit/utils';
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserWindowManager', () => {
diff --git a/tools/winscope/src/parsers/parsing_utils.ts b/tools/winscope/src/parsers/parsing_utils.ts
new file mode 100644
index 0000000..0913065
--- /dev/null
+++ b/tools/winscope/src/parsers/parsing_utils.ts
@@ -0,0 +1,83 @@
+/*
+ * 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 {ArrayUtils} from 'common/array_utils';
+import {ObjectUtils} from 'common/object_utils';
+import {EntriesRange} from 'trace/index_types';
+
+export class ParsingUtils {
+  static throwIfMagicNumberDoesntMatch(traceBuffer: Uint8Array, magicNumber: number[] | undefined) {
+    if (magicNumber !== undefined) {
+      const bufferContainsMagicNumber = ArrayUtils.equal(
+        magicNumber,
+        traceBuffer.slice(0, magicNumber.length)
+      );
+      if (!bufferContainsMagicNumber) {
+        throw TypeError("buffer doesn't contain expected magic number");
+      }
+    }
+  }
+
+  // Add default values to the proto objects.
+  static addDefaultProtoFields(protoObj: any): any {
+    if (!protoObj || protoObj !== Object(protoObj) || !protoObj.$type) {
+      return protoObj;
+    }
+
+    for (const fieldName in protoObj.$type.fields) {
+      if (Object.prototype.hasOwnProperty.call(protoObj.$type.fields, fieldName)) {
+        const fieldProperties = protoObj.$type.fields[fieldName];
+        const field = protoObj[fieldName];
+
+        if (Array.isArray(field)) {
+          field.forEach((item, _) => {
+            ParsingUtils.addDefaultProtoFields(item);
+          });
+          continue;
+        }
+
+        if (!field) {
+          protoObj[fieldName] = fieldProperties.defaultValue;
+        }
+
+        if (fieldProperties.resolvedType && fieldProperties.resolvedType.valuesById) {
+          protoObj[fieldName] =
+            fieldProperties.resolvedType.valuesById[protoObj[fieldProperties.name]];
+          continue;
+        }
+        ParsingUtils.addDefaultProtoFields(protoObj[fieldName]);
+      }
+    }
+
+    return protoObj;
+  }
+
+  static getPartialProtos(
+    decodedEntries: any[],
+    entriesRange: EntriesRange,
+    fieldPath: string
+  ): Promise<object[]> {
+    const partialProtos = decodedEntries
+      .slice(entriesRange.start, entriesRange.end)
+      .map((entry) => {
+        const fieldValue = ObjectUtils.getProperty(entry, fieldPath);
+        const proto = {};
+        ObjectUtils.setProperty(proto, fieldPath, fieldValue);
+        return proto;
+      });
+    return Promise.resolve(partialProtos);
+  }
+}
diff --git a/tools/winscope/src/parsers/perfetto/abstract_parser.ts b/tools/winscope/src/parsers/perfetto/abstract_parser.ts
new file mode 100644
index 0000000..3697d25
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/abstract_parser.ts
@@ -0,0 +1,159 @@
+/*
+ * 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, assertTrue} from 'common/assert_utils';
+import {StringUtils} from 'common/string_utils';
+import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'common/time';
+import {AbsoluteEntryIndex, EntriesRange} from 'trace/index_types';
+import {Parser} from 'trace/parser';
+import {TraceFile} from 'trace/trace_file';
+import {TraceType} from 'trace/trace_type';
+import {WasmEngineProxy} from 'trace_processor/wasm_engine_proxy';
+import {FakeProtoBuilder} from './fake_proto_builder';
+
+export abstract class AbstractParser<T> implements Parser<T> {
+  protected traceProcessor: WasmEngineProxy;
+  protected realToElapsedTimeOffsetNs?: bigint;
+  private timestamps = new Map<TimestampType, Timestamp[]>();
+  private lengthEntries = 0;
+  private traceFile: TraceFile;
+
+  constructor(traceFile: TraceFile, traceProcessor: WasmEngineProxy) {
+    this.traceFile = traceFile;
+    this.traceProcessor = traceProcessor;
+  }
+
+  async parse() {
+    const elapsedTimestamps = await this.queryElapsedTimestamps();
+    this.lengthEntries = elapsedTimestamps.length;
+    assertTrue(
+      this.lengthEntries > 0,
+      () => `Trace processor tables don't contain entries of type ${this.getTraceType()}`
+    );
+
+    this.realToElapsedTimeOffsetNs = await this.queryRealToElapsedTimeOffset(
+      assertDefined(elapsedTimestamps.at(-1))
+    );
+
+    this.timestamps.set(
+      TimestampType.ELAPSED,
+      elapsedTimestamps.map((value) => new ElapsedTimestamp(value))
+    );
+
+    this.timestamps.set(
+      TimestampType.REAL,
+      elapsedTimestamps.map(
+        (value) => new RealTimestamp(value + assertDefined(this.realToElapsedTimeOffsetNs))
+      )
+    );
+
+    if (this.lengthEntries > 0) {
+      // Make sure there are trace entries that can be parsed
+      await this.getEntry(0, TimestampType.ELAPSED);
+    }
+  }
+
+  abstract getTraceType(): TraceType;
+
+  getLengthEntries(): number {
+    return this.lengthEntries;
+  }
+
+  getTimestamps(type: TimestampType): Timestamp[] | undefined {
+    return this.timestamps.get(type);
+  }
+
+  abstract getEntry(index: AbsoluteEntryIndex, timestampType: TimestampType): Promise<T>;
+
+  async getPartialProtos(entriesRange: EntriesRange, fieldPath: string): Promise<object[]> {
+    const fieldPathSnakeCase = StringUtils.convertCamelToSnakeCase(fieldPath);
+    const sql = `
+      SELECT
+        tbl.id as entry_index,
+        args.key,
+        args.value_type,
+        args.int_value,
+        args.string_value,
+        args.real_value
+      FROM ${this.getTableName()} AS tbl
+      INNER JOIN args ON tbl.arg_set_id = args.arg_set_id
+      WHERE
+        entry_index BETWEEN ${entriesRange.start} AND ${entriesRange.end - 1}
+        AND (args.key = '${fieldPathSnakeCase}' OR args.key LIKE '${fieldPathSnakeCase}.%')
+        ORDER BY entry_index;
+    `;
+    const result = await this.traceProcessor.query(sql).waitAllRows();
+
+    const entries: object[] = [];
+    for (const it = result.iter({}); it.valid(); it.next()) {
+      const builder = new FakeProtoBuilder();
+      builder.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
+      );
+      entries.push(builder.build());
+    }
+    return entries;
+  }
+
+  getDescriptors(): string[] {
+    return [this.traceFile.getDescriptor()];
+  }
+
+  protected abstract getTableName(): string;
+
+  private async queryElapsedTimestamps(): Promise<Array<bigint>> {
+    const sql = `SELECT ts FROM ${this.getTableName()} ORDER BY id;`;
+    const result = await this.traceProcessor.query(sql).waitAllRows();
+    const timestamps: Array<bigint> = [];
+    for (const it = result.iter({}); it.valid(); it.next()) {
+      timestamps.push(it.get('ts') as bigint);
+    }
+    return timestamps;
+  }
+
+  // Query the real-to-elapsed time offset at the specified time
+  // (timestamp parameter).
+  // The timestamp parameter must be a timestamp queried/provided by TP,
+  // otherwise the TO_REALTIME() SQL function might return invalid values.
+  private async queryRealToElapsedTimeOffset(elapsedTimestamp: bigint): Promise<bigint> {
+    const sql = `
+      SELECT TO_REALTIME(${elapsedTimestamp}) as realtime;
+    `;
+
+    const result = await this.traceProcessor.query(sql).waitAllRows();
+    assertTrue(result.numRows() === 1, () => 'Failed to query realtime timestamp');
+
+    const real = result.iter({}).get('realtime') as bigint;
+    return real - elapsedTimestamp;
+  }
+
+  private async queryLastClockSnapshot(clockName: string): Promise<bigint> {
+    const sql = `
+      SELECT
+          snapshot_id, clock_name, clock_value
+      FROM clock_snapshot
+      WHERE
+          snapshot_id = ( SELECT MAX(snapshot_id) FROM clock_snapshot )
+      AND clock_name = '${clockName}'`;
+
+    const result = await this.traceProcessor.query(sql).waitAllRows();
+    assertTrue(result.numRows() === 1, () => "Failed to query clock '${clockName}'");
+    return result.iter({}).get('clock_value') as bigint;
+  }
+}
diff --git a/tools/winscope/src/parsers/perfetto/abstract_parser_test.ts b/tools/winscope/src/parsers/perfetto/abstract_parser_test.ts
new file mode 100644
index 0000000..dba9fed
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/abstract_parser_test.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 {UnitTestUtils} from 'test/unit/utils';
+import {TraceType} from 'trace/trace_type';
+
+describe('Perfetto AbstractParser', () => {
+  it('fails parsing if there are no trace entries', async () => {
+    const parsers = await UnitTestUtils.getPerfettoParsers(
+      'traces/perfetto/no_winscope_traces.perfetto-trace'
+    );
+    expect(parsers.length).toEqual(0);
+  });
+
+  it('retrieves partial trace entries', async () => {
+    {
+      const parser = await UnitTestUtils.getPerfettoParser(
+        TraceType.SURFACE_FLINGER,
+        'traces/perfetto/layers_trace.perfetto-trace'
+      );
+      const entries = await parser.getPartialProtos({start: 0, end: 3}, 'vsyncId');
+      expect(entries).toEqual([{vsyncId: 4891n}, {vsyncId: 5235n}, {vsyncId: 5748n}]);
+    }
+    {
+      const parser = await UnitTestUtils.getPerfettoParser(
+        TraceType.TRANSACTIONS,
+        'traces/perfetto/transactions_trace.perfetto-trace'
+      );
+      const entries = await parser.getPartialProtos({start: 0, end: 3}, 'vsyncId');
+      expect(entries).toEqual([{vsyncId: 1n}, {vsyncId: 2n}, {vsyncId: 3n}]);
+    }
+  });
+});
diff --git a/tools/winscope/src/parsers/perfetto/fake_proto_builder.ts b/tools/winscope/src/parsers/perfetto/fake_proto_builder.ts
new file mode 100644
index 0000000..a8e43ef
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/fake_proto_builder.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 {ObjectUtils} from 'common/object_utils';
+import {StringUtils} from 'common/string_utils';
+
+export type FakeProto = any;
+
+export class FakeProtoBuilder {
+  private proto = {};
+
+  addArg(
+    key: string,
+    valueType: string,
+    intValue: bigint | undefined,
+    realValue: number | undefined,
+    stringValue: string | undefined
+  ): FakeProtoBuilder {
+    const keyCamelCase = key
+      .split('.')
+      .map((token) => {
+        return StringUtils.convertSnakeToCamelCase(token);
+      })
+      .join('.');
+    const value = this.makeValue(valueType, intValue, realValue, stringValue);
+    ObjectUtils.setProperty(this.proto, keyCamelCase, value);
+    return this;
+  }
+
+  build(): FakeProto {
+    return this.proto;
+  }
+
+  private makeValue(
+    valueType: string,
+    intValue: bigint | undefined,
+    realValue: number | undefined,
+    stringValue: string | undefined
+  ): any {
+    switch (valueType) {
+      case 'bool':
+        return Boolean(intValue);
+      case 'int':
+        return intValue;
+      case 'null':
+        return null;
+      case 'real':
+        return realValue;
+      case 'string':
+        return stringValue;
+      case 'uint':
+        return intValue;
+      default:
+      // do nothing
+    }
+  }
+}
diff --git a/tools/winscope/src/parsers/perfetto/fake_proto_builder_test.ts b/tools/winscope/src/parsers/perfetto/fake_proto_builder_test.ts
new file mode 100644
index 0000000..f572379
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/fake_proto_builder_test.ts
@@ -0,0 +1,137 @@
+/*
+ * 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 {fakeProtoTestJson} from 'test/protos/proto_types';
+import {FakeProto, FakeProtoBuilder} from './fake_proto_builder';
+import {FakeProtoTransformer} from './fake_proto_transformer';
+
+interface Arg {
+  key: string;
+  value_type: string;
+  int_value?: bigint;
+  real_value?: number;
+  string_value?: string;
+}
+
+describe('FakeProtoBuilder', () => {
+  it('boolean', () => {
+    const args = [makeArg('a', false), makeArg('b', true)];
+    const proto = buildFakeProto(args);
+    expect(proto.a).toEqual(false);
+    expect(proto.b).toEqual(true);
+  });
+
+  it('number', () => {
+    const args = [makeArg('a', 1), makeArg('b', 10)];
+    const proto = buildFakeProto(args);
+    expect(proto.a).toEqual(1);
+    expect(proto.b).toEqual(10);
+  });
+
+  it('bigint', () => {
+    const args = [makeArg('a', 1n), makeArg('b', 10n)];
+    const proto = buildFakeProto(args);
+    expect(proto.a).toEqual(1n);
+    expect(proto.b).toEqual(10n);
+  });
+
+  it('string', () => {
+    const args = [makeArg('a', 'valuea'), makeArg('b', 'valueb')];
+    const proto = buildFakeProto(args);
+    expect(proto.a).toEqual('valuea');
+    expect(proto.b).toEqual('valueb');
+  });
+
+  it('array', () => {
+    const args = [
+      // Note: intentional random order + gap
+      makeArg('array[3]', 13),
+      makeArg('array[1]', 11),
+      makeArg('array[0]', 10),
+    ];
+    const proto = buildFakeProto(args);
+    expect(proto.array).toEqual([10, 11, undefined, 13]);
+  });
+
+  it('handles complex structure', () => {
+    const args = [
+      makeArg('a.b', false),
+      makeArg('a.numbers[0]', 10),
+      makeArg('a.numbers[1]', 11),
+      makeArg('a.objects[0].c.d', '20'),
+      makeArg('a.objects[0].c.e', '21'),
+      makeArg('a.objects[1].c', 21n),
+    ];
+    const proto = buildFakeProto(args);
+    expect(proto.a.b).toEqual(false);
+    expect(proto.a.numbers[0]).toEqual(10);
+    expect(proto.a.numbers[1]).toEqual(11);
+    expect(proto.a.objects[0].c.d).toEqual('20');
+    expect(proto.a.objects[0].c.e).toEqual('21');
+    expect(proto.a.objects[1].c).toEqual(21n);
+  });
+
+  it('converts snake_case to camelCase', () => {
+    const args = [
+      makeArg('_case_64bit', 10),
+      makeArg('case_64bit', 11),
+      makeArg('case_64bit_lsb', 12),
+      makeArg('case_64_bit', 13),
+      makeArg('case_64_bit_lsb', 14),
+    ];
+    const proto = buildFakeProto(args);
+
+    // Check it matches the snake_case to camelCase conversion performed by protobuf library (within the transformer)
+    const transformed = new FakeProtoTransformer(
+      fakeProtoTestJson,
+      'RootMessage',
+      'entry'
+    ).transform(proto);
+
+    expect(transformed._case_64bit).toEqual(10n);
+    expect(transformed.case_64bit).toEqual(11n);
+    expect(transformed.case_64bitLsb).toEqual(12n);
+    expect(transformed.case_64Bit).toEqual(13n);
+    expect(transformed.case_64BitLsb).toEqual(14n);
+  });
+
+  const makeArg = (key: string, value: any): Arg => {
+    if (value === null) {
+      return {key, value_type: 'null'};
+    }
+
+    switch (typeof value) {
+      case 'boolean':
+        return {key, value_type: 'bool', int_value: BigInt(value)};
+      case 'bigint':
+        return {key, value_type: 'int', int_value: value};
+      case 'number':
+        return {key, value_type: 'real', real_value: value};
+      case 'string':
+        return {key, value_type: 'string', string_value: value};
+      default:
+        throw new Error(`Unexpected value type: ${typeof value}`);
+    }
+  };
+
+  const buildFakeProto = (args: Arg[]): FakeProto => {
+    const builder = new FakeProtoBuilder();
+    args.forEach((arg) => {
+      builder.addArg(arg.key, arg.value_type, arg.int_value, arg.real_value, arg.string_value);
+    });
+    return builder.build();
+  };
+});
diff --git a/tools/winscope/src/parsers/perfetto/fake_proto_transformer.ts b/tools/winscope/src/parsers/perfetto/fake_proto_transformer.ts
new file mode 100644
index 0000000..61899f2
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/fake_proto_transformer.ts
@@ -0,0 +1,137 @@
+/*
+ * 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 * as protobuf from 'protobufjs';
+import {FakeProto} from './fake_proto_builder';
+
+export class FakeProtoTransformer {
+  private root: protobuf.Root;
+  private rootField: protobuf.Field;
+
+  constructor(protoDefinitionJson: protobuf.INamespace, parentType: string, fieldName: string) {
+    this.root = protobuf.Root.fromJSON(protoDefinitionJson);
+    this.rootField = this.root.lookupType(parentType).fields[fieldName];
+  }
+
+  transform(proto: FakeProto): FakeProto {
+    return this.transformRec(proto, this.rootField);
+  }
+
+  private transformRec(proto: FakeProto, field: protobuf.Field): FakeProto {
+    // Leaf (primitive type)
+    if (!field.repeated) {
+      switch (field.type) {
+        case 'double':
+          return Number(proto ?? 0);
+        case 'float':
+          return Number(proto ?? 0);
+        case 'int32':
+          return Number(proto ?? 0);
+        case 'uint32':
+          return Number(proto ?? 0);
+        case 'sint32':
+          return Number(proto ?? 0);
+        case 'fixed32':
+          return Number(proto ?? 0);
+        case 'sfixed32':
+          return Number(proto ?? 0);
+        case 'int64':
+          return BigInt(proto ?? 0);
+        case 'uint64':
+          return BigInt(proto ?? 0);
+        case 'sint64':
+          return BigInt(proto ?? 0);
+        case 'fixed64':
+          return BigInt(proto ?? 0);
+        case 'sfixed64':
+          return BigInt(proto ?? 0);
+        case 'string':
+          return proto;
+        case 'bool':
+          return Boolean(proto);
+        case 'bytes':
+          return proto;
+        default:
+        // do nothing
+      }
+    }
+
+    // Leaf (enum)
+    if (
+      field.resolvedType &&
+      field.resolvedType instanceof protobuf.Enum &&
+      field.resolvedType.valuesById
+    ) {
+      return field.resolvedType.valuesById[Number(proto)];
+    }
+
+    // Leaf (enum)
+    let enumType: protobuf.Enum | undefined;
+    try {
+      enumType = field.parent?.lookupEnum(field.type);
+    } catch (e) {
+      // do nothing
+    }
+    const enumId = this.tryGetEnumId(proto);
+    if (enumType && enumId !== undefined) {
+      return enumType.valuesById[enumId];
+    }
+
+    // Leaf (default value)
+    if (proto === null || proto === undefined) {
+      return field.repeated ? [] : field.defaultValue;
+    }
+
+    let protoType: protobuf.Type | undefined;
+    try {
+      protoType = this.root.lookupType(field.type);
+    } catch (e) {
+      return proto;
+    }
+
+    for (const childName in protoType.fields) {
+      if (!Object.prototype.hasOwnProperty.call(protoType.fields, childName)) {
+        continue;
+      }
+      const childField = protoType.fields[childName];
+
+      if (Array.isArray(proto[childName])) {
+        for (let i = 0; i < proto[childName].length; ++i) {
+          proto[childName][i] = this.transformRec(proto[childName][i], childField);
+        }
+      } else {
+        proto[childName] = this.transformRec(proto[childName], childField);
+      }
+    }
+
+    return proto;
+  }
+
+  private tryGetEnumId(proto: FakeProto): number | undefined {
+    if (proto === null || proto === undefined) {
+      return 0;
+    }
+
+    switch (typeof proto) {
+      case 'number':
+        return proto;
+      case 'bigint':
+        return Number(proto);
+      default:
+        return undefined;
+    }
+  }
+}
diff --git a/tools/winscope/src/parsers/perfetto/fake_proto_transformer_test.ts b/tools/winscope/src/parsers/perfetto/fake_proto_transformer_test.ts
new file mode 100644
index 0000000..ae059ad
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/fake_proto_transformer_test.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 {fakeProtoTestJson} from 'test/protos/proto_types';
+import {FakeProtoTransformer} from './fake_proto_transformer';
+
+describe('FakeProtoTransformer', () => {
+  let transformer: FakeProtoTransformer;
+
+  beforeAll(() => {
+    transformer = new FakeProtoTransformer(fakeProtoTestJson, 'RootMessage', 'entry');
+  });
+
+  it('sets default value (0) of number fields', () => {
+    const proto = {};
+    const transformed = transformer.transform(proto);
+    expect(transformed.number_32bit).toEqual(0);
+    expect(transformed.number_64bit).toEqual(0n);
+  });
+
+  it('sets default value (empty array) of array fields', () => {
+    const proto = {};
+    const transformed = transformer.transform(proto);
+    expect(transformed.array).toEqual([]);
+  });
+
+  it('sets default value (id 0) of enum fields', () => {
+    const proto = {};
+    const transformed = transformer.transform(proto);
+    expect(transformed.enum0).toEqual('ENUM0_VALUE_ZERO');
+    expect(transformed.enum1).toEqual('ENUM1_VALUE_ZERO');
+  });
+
+  it('decodes enum fields', () => {
+    const proto = {
+      enum0: 1n,
+      enum1: 1n,
+    };
+    const transformed = transformer.transform(proto);
+    expect(transformed.enum0).toEqual('ENUM0_VALUE_ONE');
+    expect(transformed.enum1).toEqual('ENUM1_VALUE_ONE');
+  });
+
+  it('converts fields to number if 32-bits type', () => {
+    const proto = {
+      number_32bit: 32n,
+    };
+    const transformed = transformer.transform(proto);
+    expect(transformed.number_32bit).toEqual(32);
+  });
+
+  it('converts fields to bigint if 64-bits type', () => {
+    const proto = {
+      number_64bit: 64,
+    };
+    const transformed = transformer.transform(proto);
+    expect(transformed.number_64bit).toEqual(64n);
+  });
+});
diff --git a/tools/winscope/src/parsers/perfetto/parser_factory.ts b/tools/winscope/src/parsers/perfetto/parser_factory.ts
new file mode 100644
index 0000000..8f64522
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_factory.ts
@@ -0,0 +1,91 @@
+/*
+ * 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 {globalConfig} from 'common/global_config';
+import {UrlUtils} from 'common/url_utils';
+import {ProgressListener} from 'interfaces/progress_listener';
+import {Parser} from 'trace/parser';
+import {TraceFile} from 'trace/trace_file';
+import {initWasm, resetEngineWorker, WasmEngineProxy} from 'trace_processor/wasm_engine_proxy';
+import {ParserSurfaceFlinger} from './parser_surface_flinger';
+import {ParserTransactions} from './parser_transactions';
+
+export class ParserFactory {
+  private static readonly PARSERS = [ParserSurfaceFlinger, ParserTransactions];
+  private static readonly CHUNK_SIZE_BYTES = 50 * 1024 * 1024;
+  private static traceProcessor?: WasmEngineProxy;
+
+  async createParsers(
+    traceFile: TraceFile,
+    progressListener?: ProgressListener
+  ): Promise<Array<Parser<object>>> {
+    const traceProcessor = await this.initializeTraceProcessor();
+    for (
+      let chunkStart = 0;
+      chunkStart < traceFile.file.size;
+      chunkStart += ParserFactory.CHUNK_SIZE_BYTES
+    ) {
+      progressListener?.onProgressUpdate(
+        'Loading perfetto trace...',
+        (chunkStart / traceFile.file.size) * 100
+      );
+      const chunkEnd = chunkStart + ParserFactory.CHUNK_SIZE_BYTES;
+      const data = await traceFile.file.slice(chunkStart, chunkEnd).arrayBuffer();
+      try {
+        await traceProcessor.parse(new Uint8Array(data));
+      } catch (e) {
+        console.error('Trace processor failed to parse data:', e);
+        return [];
+      }
+    }
+    await traceProcessor.notifyEof();
+
+    progressListener?.onProgressUpdate('Reading from trace processor...', undefined);
+    const parsers: Array<Parser<object>> = [];
+    for (const ParserType of ParserFactory.PARSERS) {
+      try {
+        const parser = new ParserType(traceFile, traceProcessor);
+        await parser.parse();
+        parsers.push(parser);
+      } catch (error) {
+        // skip current parser
+      }
+    }
+
+    return parsers;
+  }
+
+  private async initializeTraceProcessor(): Promise<WasmEngineProxy> {
+    if (!ParserFactory.traceProcessor) {
+      const traceProcessorRootUrl =
+        globalConfig.MODE === 'KARMA_TEST'
+          ? UrlUtils.getRootUrl() + 'base/deps_build/trace_processor/to_be_served/'
+          : UrlUtils.getRootUrl();
+      initWasm(traceProcessorRootUrl);
+      const engineId = 'random-id';
+      const enginePort = resetEngineWorker();
+      ParserFactory.traceProcessor = new WasmEngineProxy(engineId, enginePort);
+    }
+
+    await ParserFactory.traceProcessor.resetTraceProcessor({
+      cropTrackEvents: false,
+      ingestFtraceInRawTable: false,
+      analyzeTraceProtoContent: false,
+    });
+
+    return ParserFactory.traceProcessor;
+  }
+}
diff --git a/tools/winscope/src/parsers/perfetto/parser_surface_flinger.ts b/tools/winscope/src/parsers/perfetto/parser_surface_flinger.ts
new file mode 100644
index 0000000..a1ee5bf
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_surface_flinger.ts
@@ -0,0 +1,138 @@
+/*
+ * 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, assertTrue} from 'common/assert_utils';
+import {TimestampType} from 'common/time';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {winscopeJson} from 'parsers/proto_types';
+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';
+import {FakeProtoTransformer} from './fake_proto_transformer';
+
+export class ParserSurfaceFlinger extends AbstractParser<LayerTraceEntry> {
+  private layersSnapshotProtoTransformer = new FakeProtoTransformer(
+    winscopeJson,
+    'WinscopeTraceData',
+    'layersSnapshot'
+  );
+  private layerProtoTransformer = new FakeProtoTransformer(winscopeJson, 'LayersProto', 'layers');
+
+  constructor(traceFile: TraceFile, traceProcessor: WasmEngineProxy) {
+    super(traceFile, traceProcessor);
+  }
+
+  override getTraceType(): TraceType {
+    return TraceType.SURFACE_FLINGER;
+  }
+
+  override async getEntry(index: number, timestampType: TimestampType): Promise<LayerTraceEntry> {
+    let snapshotProto = await this.querySnapshot(index);
+    snapshotProto = this.layersSnapshotProtoTransformer.transform(snapshotProto);
+    const layerProtos = (await this.querySnapshotLayers(index)).map((layerProto) =>
+      this.layerProtoTransformer.transform(layerProto)
+    );
+    return LayerTraceEntry.fromProto(
+      layerProtos,
+      snapshotProto.displays,
+      BigInt(snapshotProto.elapsedRealtimeNanos.toString()),
+      snapshotProto.vsyncId,
+      snapshotProto.hwcBlob,
+      snapshotProto.where,
+      this.realToElapsedTimeOffsetNs,
+      timestampType === TimestampType.ELAPSED /*useElapsedTime*/,
+      snapshotProto.excludesCompositionState ?? false
+    );
+  }
+
+  protected override getTableName(): string {
+    return 'surfaceflinger_layers_snapshot';
+  }
+
+  private async querySnapshot(index: number): Promise<FakeProto> {
+    const sql = `
+      SELECT
+      sfs.id AS snapshot_id,
+      sfs.ts as ts,
+      args.key,
+      args.value_type,
+      args.int_value,
+      args.string_value,
+      args.real_value
+      FROM surfaceflinger_layers_snapshot AS sfs
+      INNER JOIN args ON sfs.arg_set_id = args.arg_set_id
+      WHERE snapshot_id = ${index};
+    `;
+    const result = await this.traceProcessor.query(sql).waitAllRows();
+    assertTrue(result.numRows() > 0, () => `Layers snapshot not available (snapshot_id: ${index})`);
+    const builder = new FakeProtoBuilder();
+    for (const it = result.iter({}); it.valid(); it.next()) {
+      builder.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 builder.build();
+  }
+
+  private async querySnapshotLayers(index: number): Promise<FakeProto[]> {
+    const layerIdToBuilder = new Map<number, FakeProtoBuilder>();
+    const getBuilder = (layerId: number) => {
+      if (!layerIdToBuilder.has(layerId)) {
+        layerIdToBuilder.set(layerId, new FakeProtoBuilder());
+      }
+      return assertDefined(layerIdToBuilder.get(layerId));
+    };
+
+    const sql = `
+      SELECT
+          sfl.snapshot_id,
+          sfl.id as layer_id,
+          args.key,
+          args.value_type,
+          args.int_value,
+          args.string_value,
+          args.real_value
+      FROM
+          surfaceflinger_layer as sfl
+          INNER JOIN args ON sfl.arg_set_id = args.arg_set_id
+      WHERE snapshot_id = ${index};
+    `;
+    const result = await this.traceProcessor.query(sql).waitAllRows();
+
+    for (const it = result.iter({}); it.valid(); it.next()) {
+      const builder = getBuilder(it.get('layer_id') as number);
+      builder.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
+      );
+    }
+
+    const layerProtos: FakeProto[] = [];
+    layerIdToBuilder.forEach((builder) => {
+      layerProtos.push(builder.build());
+    });
+
+    return layerProtos;
+  }
+}
diff --git a/tools/winscope/src/parsers/perfetto/parser_surface_flinger_test.ts b/tools/winscope/src/parsers/perfetto/parser_surface_flinger_test.ts
new file mode 100644
index 0000000..6568fc8
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_surface_flinger_test.ts
@@ -0,0 +1,93 @@
+/*
+ * 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 {Layer} from 'flickerlib/common';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {UnitTestUtils} from 'test/unit/utils';
+import {Parser} from 'trace/parser';
+import {TraceType} from 'trace/trace_type';
+
+describe('Perfetto ParserSurfaceFlinger', () => {
+  let parser: Parser<LayerTraceEntry>;
+
+  beforeAll(async () => {
+    parser = await UnitTestUtils.getPerfettoParser(
+      TraceType.SURFACE_FLINGER,
+      'traces/perfetto/layers_trace.perfetto-trace'
+    );
+  });
+
+  it('has expected trace type', () => {
+    expect(parser.getTraceType()).toEqual(TraceType.SURFACE_FLINGER);
+  });
+
+  it('provides elapsed timestamps', () => {
+    const expected = [
+      new ElapsedTimestamp(14500282843n),
+      new ElapsedTimestamp(14631249355n),
+      new ElapsedTimestamp(15403446377n),
+    ];
+    const actual = assertDefined(parser.getTimestamps(TimestampType.ELAPSED)).slice(0, 3);
+    expect(actual).toEqual(expected);
+  });
+
+  it('provides real timestamps', () => {
+    const expected = [
+      new RealTimestamp(1659107089102062832n),
+      new RealTimestamp(1659107089233029344n),
+      new RealTimestamp(1659107090005226366n),
+    ];
+    const actual = assertDefined(parser.getTimestamps(TimestampType.REAL)).slice(0, 3);
+    expect(actual).toEqual(expected);
+  });
+
+  it('formats entry timestamps (elapsed)', async () => {
+    const entry = await parser.getEntry(1, TimestampType.ELAPSED);
+    expect(entry.name).toEqual('14s631ms249355ns');
+    expect(BigInt(entry.timestamp.systemUptimeNanos.toString())).toEqual(14631249355n);
+    expect(BigInt(entry.timestamp.unixNanos.toString())).toEqual(1659107089233029344n);
+  });
+
+  it('formats entry timestamps (real)', async () => {
+    const entry = await parser.getEntry(1, TimestampType.REAL);
+    expect(entry.name).toEqual('2022-07-29T15:04:49.233029376');
+    expect(BigInt(entry.timestamp.systemUptimeNanos.toString())).toEqual(14631249355n);
+    expect(BigInt(entry.timestamp.unixNanos.toString())).toEqual(1659107089233029344n);
+  });
+
+  it('decodes layer state flags', async () => {
+    const entry = await parser.getEntry(0, TimestampType.REAL);
+    {
+      const layer = entry.flattenedLayers.find((layer: Layer) => layer.id === 27);
+      expect(layer.name).toEqual('Leaf:24:25#27');
+      expect(layer.flags).toEqual(0x0);
+      expect(layer.verboseFlags).toEqual('');
+    }
+    {
+      const layer = entry.flattenedLayers.find((layer: Layer) => layer.id === 48);
+      expect(layer.name).toEqual('Task=4#48');
+      expect(layer.flags).toEqual(0x1);
+      expect(layer.verboseFlags).toEqual('HIDDEN (0x1)');
+    }
+    {
+      const layer = entry.flattenedLayers.find((layer: Layer) => layer.id === 77);
+      expect(layer.name).toEqual('Wallpaper BBQ wrapper#77');
+      expect(layer.flags).toEqual(0x100);
+      expect(layer.verboseFlags).toEqual('ENABLE_BACKPRESSURE (0x100)');
+    }
+  });
+});
diff --git a/tools/winscope/src/parsers/perfetto/parser_transactions.ts b/tools/winscope/src/parsers/perfetto/parser_transactions.ts
new file mode 100644
index 0000000..0872e5e
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_transactions.ts
@@ -0,0 +1,125 @@
+/*
+ * 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 {TimestampType} from 'common/time';
+import {TransactionsTraceFileProto, winscopeJson} from 'parsers/proto_types';
+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';
+import {FakeProtoTransformer} from './fake_proto_transformer';
+
+export class ParserTransactions extends AbstractParser<object> {
+  private protoTransformer = new FakeProtoTransformer(
+    winscopeJson,
+    'WinscopeTraceData',
+    'transactions'
+  );
+
+  constructor(traceFile: TraceFile, traceProcessor: WasmEngineProxy) {
+    super(traceFile, traceProcessor);
+  }
+
+  override getTraceType(): TraceType {
+    return TraceType.TRANSACTIONS;
+  }
+
+  override async getEntry(index: number, timestampType: TimestampType): Promise<object> {
+    let entryProto = await this.queryEntry(index);
+    entryProto = this.protoTransformer.transform(entryProto);
+    entryProto = this.decodeWhatBitsetFields(entryProto);
+    return entryProto;
+  }
+
+  protected override getTableName(): string {
+    return 'surfaceflinger_transactions';
+  }
+
+  private async queryEntry(index: number): Promise<FakeProto> {
+    const sql = `
+      SELECT
+          sft.id AS trace_entry_id,
+          args.key,
+          args.value_type,
+          args.int_value,
+          args.string_value,
+          args.real_value
+      FROM surfaceflinger_transactions AS sft
+      INNER JOIN args ON sft.arg_set_id = args.arg_set_id
+      WHERE trace_entry_id = ${index};
+    `;
+    const result = await this.traceProcessor.query(sql).waitAllRows();
+
+    const builder = new FakeProtoBuilder();
+    for (const it = result.iter({}); it.valid(); it.next()) {
+      builder.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 builder.build();
+  }
+
+  private decodeWhatBitsetFields(transactionTraceEntry: FakeProto): FakeProto {
+    const decodeBitset32 = (bitset: number, EnumProto: any) => {
+      return Object.keys(EnumProto).filter((key) => {
+        const value = EnumProto[key];
+        return (bitset & value) !== 0;
+      });
+    };
+
+    const concatBitsetTokens = (tokens: string[]) => {
+      if (tokens.length === 0) {
+        return '0';
+      }
+      return tokens.join(' | ');
+    };
+
+    const LayerStateChangesLsbEnum = (TransactionsTraceFileProto?.parent as any).LayerState
+      .ChangesLsb;
+    const LayerStateChangesMsbEnum = (TransactionsTraceFileProto?.parent as any).LayerState
+      .ChangesMsb;
+    const DisplayStateChangesEnum = (TransactionsTraceFileProto?.parent as any).DisplayState
+      .Changes;
+
+    transactionTraceEntry.transactions.forEach((transactionState: any) => {
+      transactionState.layerChanges.forEach((layerState: any) => {
+        layerState.what = concatBitsetTokens(
+          decodeBitset32(Number(layerState.what), LayerStateChangesLsbEnum).concat(
+            decodeBitset32(Number(layerState.what >> 32n), LayerStateChangesMsbEnum)
+          )
+        );
+      });
+
+      transactionState.displayChanges.forEach((displayState: any) => {
+        displayState.what = concatBitsetTokens(
+          decodeBitset32(Number(displayState.what), DisplayStateChangesEnum)
+        );
+      });
+    });
+
+    transactionTraceEntry?.addedDisplays?.forEach((displayState: any) => {
+      displayState.what = concatBitsetTokens(
+        decodeBitset32(Number(displayState.what), DisplayStateChangesEnum)
+      );
+    });
+
+    return transactionTraceEntry;
+  }
+}
diff --git a/tools/winscope/src/parsers/perfetto/parser_transactions_test.ts b/tools/winscope/src/parsers/perfetto/parser_transactions_test.ts
new file mode 100644
index 0000000..11fbe93
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_transactions_test.ts
@@ -0,0 +1,106 @@
+/*
+ * 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 {UnitTestUtils} from 'test/unit/utils';
+import {Parser} from 'trace/parser';
+import {TraceType} from 'trace/trace_type';
+
+describe('Perfetto ParserTransactions', () => {
+  let parser: Parser<object>;
+
+  beforeAll(async () => {
+    parser = await UnitTestUtils.getPerfettoParser(
+      TraceType.TRANSACTIONS,
+      'traces/perfetto/transactions_trace.perfetto-trace'
+    );
+  });
+
+  it('has expected trace type', () => {
+    expect(parser.getTraceType()).toEqual(TraceType.TRANSACTIONS);
+  });
+
+  it('provides elapsed timestamps', () => {
+    const timestamps = assertDefined(parser.getTimestamps(TimestampType.ELAPSED));
+
+    expect(timestamps.length).toEqual(712);
+
+    const expected = [
+      new ElapsedTimestamp(2450981445n),
+      new ElapsedTimestamp(2517952515n),
+      new ElapsedTimestamp(4021151449n),
+    ];
+    expect(timestamps.slice(0, 3)).toEqual(expected);
+  });
+
+  it('provides real timestamps', () => {
+    const timestamps = assertDefined(parser.getTimestamps(TimestampType.REAL));
+
+    expect(timestamps.length).toEqual(712);
+
+    const expected = [
+      new RealTimestamp(1659507541051480997n),
+      new RealTimestamp(1659507541118452067n),
+      new RealTimestamp(1659507542621651001n),
+    ];
+    expect(timestamps.slice(0, 3)).toEqual(expected);
+  });
+
+  it('retrieves trace entry from real timestamp', async () => {
+    const entry = await parser.getEntry(1, TimestampType.REAL);
+    expect(BigInt((entry as any).elapsedRealtimeNanos)).toEqual(2517952515n);
+  });
+
+  it('transforms fake proto built from trace processor args', async () => {
+    const entry0 = (await parser.getEntry(0, TimestampType.REAL)) as any;
+    const entry2 = (await parser.getEntry(2, TimestampType.REAL)) as any;
+
+    // Add empty arrays
+    expect(entry0.addedDisplays).toEqual([]);
+    expect(entry0.destroyedLayers).toEqual([]);
+    expect(entry0.removedDisplays).toEqual([]);
+    expect(entry0.destroyedLayerHandles).toEqual([]);
+    expect(entry0.displays).toEqual([]);
+
+    // Add default values
+    expect(entry0.transactions[1].pid).toEqual(0);
+
+    // Convert value types (bigint -> number)
+    expect(entry0.transactions[1].uid).toEqual(1003);
+
+    // Decode enum IDs
+    expect(entry0.transactions[0].layerChanges[0].dropInputMode).toEqual('NONE');
+    expect(entry2.transactions[0].layerChanges[0].bufferData.pixelFormat).toEqual(
+      'PIXEL_FORMAT_RGBA_1010102'
+    );
+  });
+
+  it("decodes 'what' field in proto", async () => {
+    {
+      const entry = (await parser.getEntry(0, TimestampType.REAL)) as any;
+      expect(entry.transactions[0].layerChanges[0].what).toEqual('eLayerChanged');
+      expect(entry.transactions[1].layerChanges[0].what).toEqual(
+        'eFlagsChanged | eDestinationFrameChanged'
+      );
+    }
+    {
+      const entry = (await parser.getEntry(222, TimestampType.REAL)) as any;
+      expect(entry.transactions[1].displayChanges[0].what).toEqual(
+        'eLayerStackChanged | eDisplayProjectionChanged | eFlagsChanged'
+      );
+    }
+  });
+});
diff --git a/tools/winscope/src/parsers/proto_types.js b/tools/winscope/src/parsers/proto_types.js
index f37f91b..fb913f9 100644
--- a/tools/winscope/src/parsers/proto_types.js
+++ b/tools/winscope/src/parsers/proto_types.js
@@ -19,6 +19,7 @@
 protobuf.util.Long = Long; // otherwise 64-bit types would be decoded as javascript number (only 53-bits precision)
 protobuf.configure();
 
+import winscopeJson from 'external/perfetto/protos/perfetto/trace/android/winscope.proto';
 import protoLogJson from 'frameworks/base/core/proto/android/internal/protolog.proto';
 import accessibilityJson from 'frameworks/base/core/proto/android/server/accessibilitytrace.proto';
 import windowManagerJson from 'frameworks/base/core/proto/android/server/windowmanagertrace.proto';
@@ -26,8 +27,8 @@
 import inputMethodClientsJson from 'frameworks/base/core/proto/android/view/inputmethod/inputmethodeditortrace.proto';
 import shellTransitionsJson from 'frameworks/base/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto';
 import viewCaptureJson from 'frameworks/libs/systemui/viewcapturelib/src/com/android/app/viewcapture/proto/view_capture.proto';
-import layersJson from 'frameworks/native/services/surfaceflinger/layerproto/layerstrace.proto';
-import transactionsJson from 'frameworks/native/services/surfaceflinger/layerproto/transactions.proto';
+import layersJson from 'protos/udc/surfaceflinger/layerstrace.proto';
+import transactionsJson from 'protos/udc/surfaceflinger/transactions.proto';
 
 const AccessibilityTraceFileProto = protobuf.Root.fromJSON(accessibilityJson).lookupType(
   'com.android.server.accessibility.AccessibilityTraceFileProto'
@@ -69,15 +70,17 @@
 
 export {
   AccessibilityTraceFileProto,
+  ExportedData,
   InputMethodClientsTraceFileProto,
   InputMethodManagerServiceTraceFileProto,
   InputMethodServiceTraceFileProto,
   LayersTraceFileProto,
   ProtoLogFileProto,
+  ShellTransitionsTraceFileProto,
   TransactionsTraceFileProto,
   WindowManagerServiceDumpProto,
   WindowManagerTraceFileProto,
   WmTransitionsTraceFileProto,
-  ShellTransitionsTraceFileProto,
-  ExportedData,
+  transactionsJson,
+  winscopeJson,
 };
diff --git a/tools/winscope/src/parsers/traces_parser_cujs.ts b/tools/winscope/src/parsers/traces_parser_cujs.ts
index 4b2b7f7..ec754b4 100644
--- a/tools/winscope/src/parsers/traces_parser_cujs.ts
+++ b/tools/winscope/src/parsers/traces_parser_cujs.ts
@@ -15,9 +15,9 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Cuj, EventLog, Transition} from 'trace/flickerlib/common';
+import {Timestamp, TimestampType} from 'common/time';
+import {Cuj, EventLog, Transition} from 'flickerlib/common';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 import {AbstractTracesParser} from './abstract_traces_parser';
 import {ParserEventLog} from './parser_eventlog';
diff --git a/tools/winscope/src/parsers/traces_parser_cujs_test.ts b/tools/winscope/src/parsers/traces_parser_cujs_test.ts
index 9541e03..3aebded 100644
--- a/tools/winscope/src/parsers/traces_parser_cujs_test.ts
+++ b/tools/winscope/src/parsers/traces_parser_cujs_test.ts
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
+import {Timestamp, TimestampType} from 'common/time';
+import {Cuj} from 'flickerlib/common';
 import {UnitTestUtils} from 'test/unit/utils';
-import {Cuj} from 'trace/flickerlib/common';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserCujs', () => {
diff --git a/tools/winscope/src/parsers/traces_parser_factory.ts b/tools/winscope/src/parsers/traces_parser_factory.ts
index 7eb276f..3c8a64c 100644
--- a/tools/winscope/src/parsers/traces_parser_factory.ts
+++ b/tools/winscope/src/parsers/traces_parser_factory.ts
@@ -29,7 +29,6 @@
         const parser = new ParserType(parsers);
         await parser.parse();
         tracesParsers.push(parser);
-        break;
       } catch (error) {
         // skip current parser
       }
diff --git a/tools/winscope/src/parsers/traces_parser_transitions.ts b/tools/winscope/src/parsers/traces_parser_transitions.ts
index b364e5c..40c564b 100644
--- a/tools/winscope/src/parsers/traces_parser_transitions.ts
+++ b/tools/winscope/src/parsers/traces_parser_transitions.ts
@@ -15,9 +15,9 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Transition, TransitionsTrace} from 'trace/flickerlib/common';
+import {Timestamp, TimestampType} from 'common/time';
+import {Transition, TransitionsTrace} from 'flickerlib/common';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 import {AbstractTracesParser} from './abstract_traces_parser';
 
diff --git a/tools/winscope/src/parsers/traces_parser_transitions_test.ts b/tools/winscope/src/parsers/traces_parser_transitions_test.ts
index 3c10a1a..7f9dbe0 100644
--- a/tools/winscope/src/parsers/traces_parser_transitions_test.ts
+++ b/tools/winscope/src/parsers/traces_parser_transitions_test.ts
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
+import {Timestamp, TimestampType} from 'common/time';
+import {Transition} from 'flickerlib/common';
 import {UnitTestUtils} from 'test/unit/utils';
-import {Transition} from 'trace/flickerlib/common';
 import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {TraceType} from 'trace/trace_type';
 
 describe('ParserTransitions', () => {
diff --git a/tools/winscope/src/test/common/file_impl.ts b/tools/winscope/src/test/common/file_impl.ts
deleted file mode 100644
index 315bbe1..0000000
--- a/tools/winscope/src/test/common/file_impl.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2022 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.
- */
-
-// This class is needed for unit tests because Node.js doesn't provide
-// an implementation of the Web API's File type
-class FileImpl {
-  readonly size: number;
-  readonly type: string;
-  readonly name: string;
-  readonly lastModified: number = 0;
-  readonly webkitRelativePath: string = '';
-  private readonly buffer: ArrayBuffer;
-
-  constructor(buffer: ArrayBuffer, fileName: string) {
-    this.buffer = buffer;
-    this.size = this.buffer.byteLength;
-    this.type = 'application/octet-stream';
-    this.name = fileName;
-  }
-
-  arrayBuffer(): Promise<ArrayBuffer> {
-    return new Promise<ArrayBuffer>((resolve) => {
-      resolve(this.buffer);
-    });
-  }
-
-  slice(start?: number, end?: number, contentType?: string): Blob {
-    throw new Error('Not implemented!');
-  }
-
-  stream(): any {
-    throw new Error('Not implemented!');
-  }
-
-  text(): Promise<string> {
-    const utf8Decoder = new TextDecoder();
-    const text = utf8Decoder.decode(this.buffer);
-    return new Promise<string>((resolve) => {
-      resolve(text);
-    });
-  }
-}
-
-export {FileImpl};
diff --git a/tools/winscope/src/test/common/utils.ts b/tools/winscope/src/test/common/utils.ts
deleted file mode 100644
index 8c81a63..0000000
--- a/tools/winscope/src/test/common/utils.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2022 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 * as fs from 'fs';
-import * as path from 'path';
-import {FileImpl} from './file_impl';
-
-class CommonTestUtils {
-  static async getFixtureFile(
-    srcFilename: string,
-    dstFilename: string = srcFilename
-  ): Promise<File> {
-    const buffer = CommonTestUtils.loadFixture(srcFilename);
-    return new FileImpl(buffer, dstFilename) as unknown as File;
-  }
-
-  static loadFixture(filename: string): ArrayBuffer {
-    return fs.readFileSync(CommonTestUtils.getFixturePath(filename));
-  }
-
-  static getFixturePath(filename: string): string {
-    if (path.isAbsolute(filename)) {
-      return filename;
-    }
-    return path.join(CommonTestUtils.getProjectRootPath(), 'src/test/fixtures', filename);
-  }
-
-  static getProjectRootPath(): string {
-    let root = __dirname;
-    while (path.basename(root) !== 'winscope') {
-      root = path.dirname(root);
-    }
-    return root;
-  }
-}
-
-export {CommonTestUtils};
diff --git a/tools/winscope/src/test/e2e/cross_tool_protocol_test.ts b/tools/winscope/src/test/e2e/cross_tool_protocol_test.ts
index 32b9383..ac5cdb1 100644
--- a/tools/winscope/src/test/e2e/cross_tool_protocol_test.ts
+++ b/tools/winscope/src/test/e2e/cross_tool_protocol_test.ts
@@ -24,7 +24,7 @@
 
   beforeAll(async () => {
     jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000;
-    await browser.manage().timeouts().implicitlyWait(15000);
+    await browser.manage().timeouts().implicitlyWait(20000);
     await E2eTestUtils.checkServerIsUp('Remote tool mock', E2eTestUtils.REMOTE_TOOL_MOCK_URL);
     await E2eTestUtils.checkServerIsUp('Winscope', E2eTestUtils.WINSCOPE_URL);
   });
diff --git a/tools/winscope/src/test/e2e/upload_traces_test.ts b/tools/winscope/src/test/e2e/upload_traces_test.ts
index cbb7bdb..16d0bc2 100644
--- a/tools/winscope/src/test/e2e/upload_traces_test.ts
+++ b/tools/winscope/src/test/e2e/upload_traces_test.ts
@@ -57,6 +57,10 @@
     expect(text).toContain('Transactions');
     expect(text).toContain('Transitions');
 
+    // Should be merged into a single Transitions trace
+    expect(text.includes('WM Transitions')).toBeFalsy();
+    expect(text.includes('Shell Transitions')).toBeFalsy();
+
     expect(text).toContain('wm_log.winscope');
     expect(text).toContain('ime_trace_service.winscope');
     expect(text).toContain('ime_trace_managerservice.winscope');
diff --git a/tools/winscope/src/test/e2e/utils.ts b/tools/winscope/src/test/e2e/utils.ts
index 685d94b..2276e26 100644
--- a/tools/winscope/src/test/e2e/utils.ts
+++ b/tools/winscope/src/test/e2e/utils.ts
@@ -13,10 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import * as path from 'path';
 import {browser, by, element} from 'protractor';
-import {CommonTestUtils} from '../common/utils';
 
-class E2eTestUtils extends CommonTestUtils {
+class E2eTestUtils {
   static readonly WINSCOPE_URL = 'http://localhost:8080';
   static readonly REMOTE_TOOL_MOCK_URL = 'http://localhost:8081';
 
@@ -28,14 +28,6 @@
     }
   }
 
-  static async uploadFixture(...paths: string[]) {
-    const inputFile = element(by.css('input[type="file"]'));
-
-    // Uploading multiple files is not properly supported but
-    // chrome handles file paths joined with new lines
-    await inputFile.sendKeys(paths.map((it) => E2eTestUtils.getFixturePath(it)).join('\n'));
-  }
-
   static async clickViewTracesButton() {
     const button = element(by.css('.load-btn'));
     await button.click();
@@ -48,6 +40,29 @@
       await closeButton.click();
     }
   }
+
+  static async uploadFixture(...paths: string[]) {
+    const inputFile = element(by.css('input[type="file"]'));
+
+    // Uploading multiple files is not properly supported but
+    // chrome handles file paths joined with new lines
+    await inputFile.sendKeys(paths.map((it) => E2eTestUtils.getFixturePath(it)).join('\n'));
+  }
+
+  static getFixturePath(filename: string): string {
+    if (path.isAbsolute(filename)) {
+      return filename;
+    }
+    return path.join(E2eTestUtils.getProjectRootPath(), 'src/test/fixtures', filename);
+  }
+
+  private static getProjectRootPath(): string {
+    let root = __dirname;
+    while (path.basename(root) !== 'winscope') {
+      root = path.dirname(root);
+    }
+    return root;
+  }
 }
 
 export {E2eTestUtils};
diff --git a/tools/winscope/src/test/e2e/viewer_surface_flinger_test.ts b/tools/winscope/src/test/e2e/viewer_surface_flinger_test.ts
index ba88871..7611e0b 100644
--- a/tools/winscope/src/test/e2e/viewer_surface_flinger_test.ts
+++ b/tools/winscope/src/test/e2e/viewer_surface_flinger_test.ts
@@ -17,13 +17,22 @@
 import {E2eTestUtils} from './utils';
 
 describe('Viewer SurfaceFlinger', () => {
-  beforeAll(async () => {
-    browser.manage().timeouts().implicitlyWait(1000);
+  beforeEach(async () => {
+    browser.manage().timeouts().implicitlyWait(5000);
     await E2eTestUtils.checkServerIsUp('Winscope', E2eTestUtils.WINSCOPE_URL);
     await browser.get(E2eTestUtils.WINSCOPE_URL);
   });
 
-  it('processes trace and renders view', async () => {
+  it('processes perfetto trace and renders view', async () => {
+    await E2eTestUtils.uploadFixture('traces/perfetto/layers_trace.perfetto-trace');
+    await E2eTestUtils.closeSnackBarIfNeeded();
+    await E2eTestUtils.clickViewTracesButton();
+
+    const viewerPresent = await element(by.css('viewer-surface-flinger')).isPresent();
+    expect(viewerPresent).toBeTruthy();
+  });
+
+  it('processes legacy trace and renders view', async () => {
     await E2eTestUtils.uploadFixture('traces/elapsed_and_real_timestamp/SurfaceFlinger.pb');
     await E2eTestUtils.closeSnackBarIfNeeded();
     await E2eTestUtils.clickViewTracesButton();
diff --git a/tools/winscope/src/test/e2e/viewer_transactions_test.ts b/tools/winscope/src/test/e2e/viewer_transactions_test.ts
index f2eaf9f..7f1df74 100644
--- a/tools/winscope/src/test/e2e/viewer_transactions_test.ts
+++ b/tools/winscope/src/test/e2e/viewer_transactions_test.ts
@@ -17,12 +17,27 @@
 import {E2eTestUtils} from './utils';
 
 describe('Viewer Transactions', () => {
-  beforeAll(async () => {
-    browser.manage().timeouts().implicitlyWait(1000);
+  beforeEach(async () => {
+    browser.manage().timeouts().implicitlyWait(5000);
     await E2eTestUtils.checkServerIsUp('Winscope', E2eTestUtils.WINSCOPE_URL);
     await browser.get(E2eTestUtils.WINSCOPE_URL);
   });
-  it('processes trace and renders view', async () => {
+
+  it('processes perfetto trace and renders view', async () => {
+    await E2eTestUtils.uploadFixture('traces/perfetto/transactions_trace.perfetto-trace');
+    await E2eTestUtils.closeSnackBarIfNeeded();
+    await E2eTestUtils.clickViewTracesButton();
+
+    const isViewerRendered = await element(by.css('viewer-transactions')).isPresent();
+    expect(isViewerRendered).toBeTruthy();
+
+    const isFirstEntryRendered = await element(
+      by.css('viewer-transactions .scroll .entry')
+    ).isPresent();
+    expect(isFirstEntryRendered).toBeTruthy();
+  });
+
+  it('processes legacy trace and renders view', async () => {
     await E2eTestUtils.uploadFixture('traces/elapsed_and_real_timestamp/Transactions.pb');
     await E2eTestUtils.closeSnackBarIfNeeded();
     await E2eTestUtils.clickViewTracesButton();
diff --git a/tools/winscope/src/test/fixtures/corrupted_archive.zip b/tools/winscope/src/test/fixtures/corrupted_archive.zip
new file mode 100644
index 0000000..c710b0a
--- /dev/null
+++ b/tools/winscope/src/test/fixtures/corrupted_archive.zip
Binary files differ
diff --git a/tools/winscope/src/test/fixtures/traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc b/tools/winscope/src/test/fixtures/traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc
index f8ed4d6..649f540 100644
--- a/tools/winscope/src/test/fixtures/traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc
+++ b/tools/winscope/src/test/fixtures/traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc
Binary files differ
diff --git a/tools/winscope/src/test/fixtures/traces/perfetto/layers_trace.perfetto-trace b/tools/winscope/src/test/fixtures/traces/perfetto/layers_trace.perfetto-trace
new file mode 100644
index 0000000..6117142
--- /dev/null
+++ b/tools/winscope/src/test/fixtures/traces/perfetto/layers_trace.perfetto-trace
Binary files differ
diff --git a/tools/winscope/src/test/fixtures/traces/perfetto/no_winscope_traces.perfetto-trace b/tools/winscope/src/test/fixtures/traces/perfetto/no_winscope_traces.perfetto-trace
new file mode 100644
index 0000000..abd266b
--- /dev/null
+++ b/tools/winscope/src/test/fixtures/traces/perfetto/no_winscope_traces.perfetto-trace
Binary files differ
diff --git a/tools/winscope/src/test/fixtures/traces/perfetto/transactions_trace.perfetto-trace b/tools/winscope/src/test/fixtures/traces/perfetto/transactions_trace.perfetto-trace
new file mode 100644
index 0000000..fb45404
--- /dev/null
+++ b/tools/winscope/src/test/fixtures/traces/perfetto/transactions_trace.perfetto-trace
Binary files differ
diff --git a/tools/winscope/src/test/protos/fake_proto_test.proto b/tools/winscope/src/test/protos/fake_proto_test.proto
new file mode 100644
index 0000000..b551705
--- /dev/null
+++ b/tools/winscope/src/test/protos/fake_proto_test.proto
@@ -0,0 +1,31 @@
+syntax = "proto2";
+
+package winscope.test;
+
+// Message used for testing the fake proto machinery
+message RootMessage {
+  optional Entry entry = 1;
+}
+
+enum Enum0 {
+    ENUM0_VALUE_ZERO = 0;
+    ENUM0_VALUE_ONE = 1;
+}
+
+message Entry {
+  enum Enum1 {
+    ENUM1_VALUE_ZERO = 0;
+    ENUM1_VALUE_ONE = 1;
+  }
+  optional Enum0 enum0 = 1;
+  optional Enum1 enum1 = 2;
+  repeated int32 array = 3;
+  optional int32 number_32bit = 4;
+  optional int64 number_64bit = 5;
+
+  optional int64 _case_64bit = 6;
+  optional int64 case_64bit = 7;
+  optional int64 case_64bit_lsb = 8;
+  optional int64 case_64_bit = 9;
+  optional int64 case_64_bit_lsb = 10;
+}
diff --git a/tools/winscope/src/test/protos/proto_types.js b/tools/winscope/src/test/protos/proto_types.js
new file mode 100644
index 0000000..2f6dcc6
--- /dev/null
+++ b/tools/winscope/src/test/protos/proto_types.js
@@ -0,0 +1,24 @@
+/*
+ * 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 Long from 'long';
+import * as protobuf from 'protobufjs';
+
+protobuf.util.Long = Long; // otherwise 64-bit types would be decoded as javascript number (only 53-bits precision)
+protobuf.configure();
+
+import fakeProtoTestJson from 'src/test/protos/fake_proto_test.proto';
+
+export {fakeProtoTestJson};
diff --git a/tools/winscope/src/test/unit/layer_builder.ts b/tools/winscope/src/test/unit/layer_builder.ts
index 540a66e..945777a 100644
--- a/tools/winscope/src/test/unit/layer_builder.ts
+++ b/tools/winscope/src/test/unit/layer_builder.ts
@@ -22,7 +22,7 @@
   EMPTY_TRANSFORM,
   Layer,
   LayerProperties,
-} from 'trace/flickerlib/common';
+} from 'flickerlib/common';
 
 class LayerBuilder {
   setFlags(value: number): LayerBuilder {
@@ -40,24 +40,16 @@
       false /* isOpaque */,
       0 /* shadowRadius */,
       0 /* cornerRadius */,
-      'type' /* type */,
       EMPTY_RECTF /* screenBounds */,
       EMPTY_TRANSFORM /* transform */,
-      EMPTY_RECTF /* sourceBounds */,
       0 /* effectiveScalingMode */,
       EMPTY_TRANSFORM /* bufferTransform */,
       0 /* hwcCompositionType */,
-      EMPTY_RECTF /* hwcCrop */,
-      EMPTY_RECT /* hwcFrame */,
       0 /* backgroundBlurRadius */,
       EMPTY_RECT /* crop */,
       false /* isRelativeOf */,
       -1 /* zOrderRelativeOfId */,
       0 /* stackId */,
-      EMPTY_TRANSFORM /* requestedTransform */,
-      EMPTY_COLOR /* requestedColor */,
-      EMPTY_RECTF /* cornerRadiusCrop */,
-      EMPTY_TRANSFORM /* inputTransform */,
       null /* inputRegion */
     );
 
diff --git a/tools/winscope/src/test/unit/trace_builder.ts b/tools/winscope/src/test/unit/trace_builder.ts
index e4edb55..4edbc83 100644
--- a/tools/winscope/src/test/unit/trace_builder.ts
+++ b/tools/winscope/src/test/unit/trace_builder.ts
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
+import {Timestamp, TimestampType} from 'common/time';
 import {FrameMap} from 'trace/frame_map';
 import {FrameMapBuilder} from 'trace/frame_map_builder';
 import {AbsoluteEntryIndex, AbsoluteFrameIndex, EntriesRange} from 'trace/index_types';
 import {Parser} from 'trace/parser';
 import {ParserMock} from 'trace/parser_mock';
-import {Timestamp, TimestampType} from 'trace/timestamp';
 import {Trace} from 'trace/trace';
 import {TraceType} from 'trace/trace_type';
 
diff --git a/tools/winscope/src/test/unit/trace_utils.ts b/tools/winscope/src/test/unit/trace_utils.ts
index 2f5b882..d8a9914 100644
--- a/tools/winscope/src/test/unit/trace_utils.ts
+++ b/tools/winscope/src/test/unit/trace_utils.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {Timestamp} from 'trace/timestamp';
+import {Timestamp} from 'common/time';
 import {AbsoluteFrameIndex, Trace} from 'trace/trace';
 
 export class TraceUtils {
diff --git a/tools/winscope/src/test/unit/traces_builder.ts b/tools/winscope/src/test/unit/traces_builder.ts
index a7c8a97..49a9936 100644
--- a/tools/winscope/src/test/unit/traces_builder.ts
+++ b/tools/winscope/src/test/unit/traces_builder.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
+import {Timestamp} from 'common/time';
 import {FrameMap} from 'trace/frame_map';
-import {Timestamp} from 'trace/timestamp';
 import {Traces} from 'trace/traces';
 import {TraceType} from 'trace/trace_type';
 import {TraceBuilder} from './trace_builder';
diff --git a/tools/winscope/src/test/unit/utils.ts b/tools/winscope/src/test/unit/utils.ts
index ba450bd..ed3cdf7 100644
--- a/tools/winscope/src/test/unit/utils.ts
+++ b/tools/winscope/src/test/unit/utils.ts
@@ -14,42 +14,62 @@
  * limitations under the License.
  */
 
+import {assertDefined} from 'common/assert_utils';
+import {TimestampType} from 'common/time';
+import {UrlUtils} from 'common/url_utils';
+import {LayerTraceEntry, WindowManagerState} from 'flickerlib/common';
 import {ParserFactory} from 'parsers/parser_factory';
+import {ParserFactory as PerfettoParserFactory} from 'parsers/perfetto/parser_factory';
 import {TracesParserFactory} from 'parsers/traces_parser_factory';
-import {CommonTestUtils} from 'test/common/utils';
-import {LayerTraceEntry, WindowManagerState} from 'trace/flickerlib/common';
 import {Parser} from 'trace/parser';
-import {TimestampType} from 'trace/timestamp';
-import {Trace} from 'trace/trace';
 import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
 
-class UnitTestUtils extends CommonTestUtils {
-  static async getTraceFromFile(filename: string): Promise<Trace<object>> {
-    const parser = await UnitTestUtils.getParser(filename);
-
-    const trace = Trace.newUninitializedTrace(parser);
-    trace.init(
-      parser.getTimestamps(TimestampType.REAL) !== undefined
-        ? TimestampType.REAL
-        : TimestampType.ELAPSED
-    );
-    return trace;
+class UnitTestUtils {
+  static async getFixtureFile(
+    srcFilename: string,
+    dstFilename: string = srcFilename
+  ): Promise<File> {
+    const url = UrlUtils.getRootUrl() + 'base/src/test/fixtures/' + srcFilename;
+    const response = await fetch(url);
+    expect(response.ok).toBeTrue();
+    const blob = await response.blob();
+    const file = new File([blob], dstFilename);
+    return file;
   }
 
   static async getParser(filename: string): Promise<Parser<object>> {
-    const file = new TraceFile(await CommonTestUtils.getFixtureFile(filename), undefined);
+    const file = new TraceFile(await UnitTestUtils.getFixtureFile(filename), undefined);
     const [parsers, errors] = await new ParserFactory().createParsers([file]);
-    expect(parsers.length).toEqual(1);
+    expect(parsers.length)
+      .withContext(`Should have been able to create a parser for ${filename}`)
+      .toBeGreaterThanOrEqual(1);
     return parsers[0].parser;
   }
 
+  static async getPerfettoParser(
+    traceType: TraceType,
+    fixturePath: string
+  ): Promise<Parser<object>> {
+    const parsers = await UnitTestUtils.getPerfettoParsers(fixturePath);
+    const parser = assertDefined(parsers.find((parser) => parser.getTraceType() === traceType));
+    return parser;
+  }
+
+  static async getPerfettoParsers(fixturePath: string): Promise<Array<Parser<object>>> {
+    const file = await UnitTestUtils.getFixtureFile(fixturePath);
+    const traceFile = new TraceFile(file);
+    return await new PerfettoParserFactory().createParsers(traceFile);
+  }
+
   static async getTracesParser(filenames: string[]): Promise<Parser<object>> {
     const parsers = await Promise.all(
       filenames.map((filename) => UnitTestUtils.getParser(filename))
     );
     const tracesParsers = await new TracesParserFactory().createParsers(parsers);
-    expect(tracesParsers.length).toEqual(1);
+    expect(tracesParsers.length)
+      .withContext(`Should have been able to create a traces parser for [${filenames.join()}]`)
+      .toEqual(1);
     return tracesParsers[0];
   }
 
diff --git a/tools/winscope/src/test/utils.ts b/tools/winscope/src/test/utils.ts
new file mode 100644
index 0000000..5632e50
--- /dev/null
+++ b/tools/winscope/src/test/utils.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 {ComponentFixture, flush} from '@angular/core/testing';
+
+export function dispatchMouseEvent(
+  source: Node,
+  type: string,
+  screenX: number,
+  screenY: number,
+  clientX: number,
+  clientY: number
+) {
+  const event = document.createEvent('MouseEvent');
+
+  event.initMouseEvent(
+    type,
+    true /* canBubble */,
+    false /* cancelable */,
+    window /* view */,
+    0 /* detail */,
+    screenX /* screenX */,
+    screenY /* screenY */,
+    clientX /* clientX */,
+    clientY /* clientY */,
+    false /* ctrlKey */,
+    false /* altKey */,
+    false /* shiftKey */,
+    false /* metaKey */,
+    0 /* button */,
+    null /* relatedTarget */
+  );
+  Object.defineProperty(event, 'buttons', {get: () => 1});
+
+  source.dispatchEvent(event);
+}
+
+export function dragElement<T>(
+  fixture: ComponentFixture<T>,
+  target: Element,
+  x: number,
+  y: number
+) {
+  const {left, top} = target.getBoundingClientRect();
+
+  dispatchMouseEvent(target, 'mousedown', left, top, 0, 0);
+  fixture.detectChanges();
+  flush();
+  dispatchMouseEvent(document, 'mousemove', left + 1, top + 0, 1, y);
+  fixture.detectChanges();
+  flush();
+  dispatchMouseEvent(document, 'mousemove', left + x, top + y, x, y);
+  fixture.detectChanges();
+  flush();
+  dispatchMouseEvent(document, 'mouseup', left + x, top + y, x, y);
+  fixture.detectChanges();
+
+  flush();
+}
+
+export async function waitToBeCalled(spy: jasmine.Spy, times: number = 1, timeout = 10000) {
+  return new Promise<void>((resolve, reject) => {
+    let called = 0;
+    spy.and.callThrough().and.callFake(() => {
+      called++;
+      if (called === times) {
+        resolve();
+      }
+    });
+
+    setTimeout(() => reject(`not called ${times} times within ${timeout}ms`), timeout);
+  });
+}
diff --git a/tools/winscope/src/trace/flickerlib/common.js b/tools/winscope/src/trace/flickerlib/common.js
deleted file mode 100644
index 5c39b99..0000000
--- a/tools/winscope/src/trace/flickerlib/common.js
+++ /dev/null
@@ -1,350 +0,0 @@
-/*
- * Copyright 2020, 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.
- */
-
-// Imports all the compiled common Flicker library classes and exports them
-// as clean es6 modules rather than having them be commonjs modules
-
-// WM
-const WindowManagerTrace = require('flicker').android.tools.common.traces.wm.WindowManagerTrace;
-const WindowManagerState = require('flicker').android.tools.common.traces.wm.WindowManagerState;
-const WindowManagerTraceEntryBuilder =
-  require('flicker').android.tools.common.traces.wm.WindowManagerTraceEntryBuilder;
-const Activity = require('flicker').android.tools.common.traces.wm.Activity;
-const Configuration = require('flicker').android.tools.common.traces.wm.Configuration;
-const ConfigurationContainer =
-  require('flicker').android.tools.common.traces.wm.ConfigurationContainer;
-const DisplayArea = require('flicker').android.tools.common.traces.wm.DisplayArea;
-const DisplayContent = require('flicker').android.tools.common.traces.wm.DisplayContent;
-const DisplayCutout = require('flicker').android.tools.common.traces.wm.DisplayCutout;
-const KeyguardControllerState =
-  require('flicker').android.tools.common.traces.wm.KeyguardControllerState;
-const RootWindowContainer = require('flicker').android.tools.common.traces.wm.RootWindowContainer;
-const Task = require('flicker').android.tools.common.traces.wm.Task;
-const TaskFragment = require('flicker').android.tools.common.traces.wm.TaskFragment;
-const WindowConfiguration = require('flicker').android.tools.common.traces.wm.WindowConfiguration;
-const WindowContainer = require('flicker').android.tools.common.traces.wm.WindowContainer;
-const WindowLayoutParams = require('flicker').android.tools.common.traces.wm.WindowLayoutParams;
-const WindowManagerPolicy = require('flicker').android.tools.common.traces.wm.WindowManagerPolicy;
-const WindowState = require('flicker').android.tools.common.traces.wm.WindowState;
-const WindowToken = require('flicker').android.tools.common.traces.wm.WindowToken;
-
-// SF
-const HwcCompositionType =
-  require('flicker').android.tools.common.traces.surfaceflinger.HwcCompositionType;
-const Layer = require('flicker').android.tools.common.traces.surfaceflinger.Layer;
-const LayerProperties =
-  require('flicker').android.tools.common.traces.surfaceflinger.LayerProperties;
-const LayerTraceEntry =
-  require('flicker').android.tools.common.traces.surfaceflinger.LayerTraceEntry;
-const LayerTraceEntryBuilder =
-  require('flicker').android.tools.common.traces.surfaceflinger.LayerTraceEntryBuilder;
-const LayersTrace = require('flicker').android.tools.common.traces.surfaceflinger.LayersTrace;
-const Transform = require('flicker').android.tools.common.traces.surfaceflinger.Transform;
-const Display = require('flicker').android.tools.common.traces.surfaceflinger.Display;
-const Region = require('flicker').android.tools.common.datatypes.Region;
-
-// Event Log
-const EventLog = require('flicker').android.tools.common.traces.events.EventLog;
-const CujEvent = require('flicker').android.tools.common.traces.events.CujEvent;
-const CujType = require('flicker').android.tools.common.traces.events.CujType;
-const Event = require('flicker').android.tools.common.traces.events.Event;
-const FlickerEvent = require('flicker').android.tools.common.traces.events.FlickerEvent;
-const FocusEvent = require('flicker').android.tools.common.traces.events.FocusEvent;
-const EventLogParser = require('flicker').android.tools.common.parsers.events.EventLogParser;
-const CujTrace = require('flicker').android.tools.common.parsers.events.CujTrace;
-const Cuj = require('flicker').android.tools.common.parsers.events.Cuj;
-
-// Transitions
-const Transition = require('flicker').android.tools.common.traces.wm.Transition;
-const TransitionType = require('flicker').android.tools.common.traces.wm.TransitionType;
-const TransitionChange = require('flicker').android.tools.common.traces.wm.TransitionChange;
-const TransitionsTrace = require('flicker').android.tools.common.traces.wm.TransitionsTrace;
-const ShellTransitionData = require('flicker').android.tools.common.traces.wm.ShellTransitionData;
-const WmTransitionData = require('flicker').android.tools.common.traces.wm.WmTransitionData;
-
-// Common
-const Size = require('flicker').android.tools.common.datatypes.Size;
-const ActiveBuffer = require('flicker').android.tools.common.datatypes.ActiveBuffer;
-const Color = require('flicker').android.tools.common.datatypes.Color;
-const Insets = require('flicker').android.tools.common.datatypes.Insets;
-const Matrix33 = require('flicker').android.tools.common.datatypes.Matrix33;
-const PlatformConsts = require('flicker').android.tools.common.PlatformConsts;
-const Rotation = require('flicker').android.tools.common.Rotation;
-const Point = require('flicker').android.tools.common.datatypes.Point;
-const PointF = require('flicker').android.tools.common.datatypes.PointF;
-const Rect = require('flicker').android.tools.common.datatypes.Rect;
-const RectF = require('flicker').android.tools.common.datatypes.RectF;
-const WindowingMode = require('flicker').android.tools.common.traces.wm.WindowingMode;
-const CrossPlatform = require('flicker').android.tools.common.CrossPlatform;
-const TimestampFactory = require('flicker').android.tools.common.TimestampFactory;
-
-const EMPTY_SIZE = Size.Companion.EMPTY;
-const EMPTY_BUFFER = ActiveBuffer.Companion.EMPTY;
-const EMPTY_COLOR = Color.Companion.EMPTY;
-const EMPTY_INSETS = Insets.Companion.EMPTY;
-const EMPTY_RECT = Rect.Companion.EMPTY;
-const EMPTY_RECTF = RectF.Companion.EMPTY;
-const EMPTY_POINT = Point.Companion.EMPTY;
-const EMPTY_POINTF = PointF.Companion.EMPTY;
-const EMPTY_MATRIX33 = Matrix33.Companion.identity(0, 0);
-const EMPTY_TRANSFORM = new Transform(0, EMPTY_MATRIX33);
-
-function toSize(proto) {
-  if (proto == null) {
-    return EMPTY_SIZE;
-  }
-  const width = proto.width ?? proto.w ?? 0;
-  const height = proto.height ?? proto.h ?? 0;
-  if (width || height) {
-    return new Size(width, height);
-  }
-  return EMPTY_SIZE;
-}
-
-function toActiveBuffer(proto) {
-  const width = proto?.width ?? 0;
-  const height = proto?.height ?? 0;
-  const stride = proto?.stride ?? 0;
-  const format = proto?.format ?? 0;
-
-  if (width || height || stride || format) {
-    return new ActiveBuffer(width, height, stride, format);
-  }
-  return EMPTY_BUFFER;
-}
-
-function toColor(proto, hasAlpha = true) {
-  if (proto == null) {
-    return EMPTY_COLOR;
-  }
-  const r = proto.r ?? 0;
-  const g = proto.g ?? 0;
-  const b = proto.b ?? 0;
-  let a = proto.a;
-  if (a === null && !hasAlpha) {
-    a = 1;
-  }
-  if (r || g || b || a) {
-    return new Color(r, g, b, a);
-  }
-  return EMPTY_COLOR;
-}
-
-function toPoint(proto) {
-  if (proto == null) {
-    return null;
-  }
-  const x = proto.x ?? 0;
-  const y = proto.y ?? 0;
-  if (x || y) {
-    return new Point(x, y);
-  }
-  return EMPTY_POINT;
-}
-
-function toPointF(proto) {
-  if (proto == null) {
-    return null;
-  }
-  const x = proto.x ?? 0;
-  const y = proto.y ?? 0;
-  if (x || y) {
-    return new PointF(x, y);
-  }
-  return EMPTY_POINTF;
-}
-
-function toInsets(proto) {
-  if (proto == null) {
-    return EMPTY_INSETS;
-  }
-
-  const left = proto?.left ?? 0;
-  const top = proto?.top ?? 0;
-  const right = proto?.right ?? 0;
-  const bottom = proto?.bottom ?? 0;
-  if (left || top || right || bottom) {
-    return new Insets(left, top, right, bottom);
-  }
-  return EMPTY_INSETS;
-}
-
-function toCropRect(proto) {
-  if (proto == null) return EMPTY_RECT;
-
-  const right = proto.right || 0;
-  const left = proto.left || 0;
-  const bottom = proto.bottom || 0;
-  const top = proto.top || 0;
-
-  // crop (0,0) (-1,-1) means no crop
-  if (right == -1 && left == 0 && bottom == -1 && top == 0) EMPTY_RECT;
-
-  if (right - left <= 0 || bottom - top <= 0) return EMPTY_RECT;
-
-  return Rect.Companion.from(left, top, right, bottom);
-}
-
-function toRect(proto) {
-  if (proto == null) {
-    return EMPTY_RECT;
-  }
-
-  const left = proto?.left ?? 0;
-  const top = proto?.top ?? 0;
-  const right = proto?.right ?? 0;
-  const bottom = proto?.bottom ?? 0;
-  if (left || top || right || bottom) {
-    return new Rect(left, top, right, bottom);
-  }
-  return EMPTY_RECT;
-}
-
-function toRectF(proto) {
-  if (proto == null) {
-    return EMPTY_RECTF;
-  }
-
-  const left = proto?.left ?? 0;
-  const top = proto?.top ?? 0;
-  const right = proto?.right ?? 0;
-  const bottom = proto?.bottom ?? 0;
-  if (left || top || right || bottom) {
-    return new RectF(left, top, right, bottom);
-  }
-  return EMPTY_RECTF;
-}
-
-function toRegion(proto) {
-  if (proto == null) {
-    return null;
-  }
-
-  const rects = [];
-  for (let x = 0; x < proto.rect.length; x++) {
-    const rect = proto.rect[x];
-    const parsedRect = toRect(rect);
-    rects.push(parsedRect);
-  }
-
-  return new Region(rects);
-}
-
-function toTransform(proto) {
-  if (proto == null) {
-    return EMPTY_TRANSFORM;
-  }
-  const dsdx = proto.dsdx ?? 0;
-  const dtdx = proto.dtdx ?? 0;
-  const tx = proto.tx ?? 0;
-  const dsdy = proto.dsdy ?? 0;
-  const dtdy = proto.dtdy ?? 0;
-  const ty = proto.ty ?? 0;
-
-  if (dsdx || dtdx || tx || dsdy || dtdy || ty) {
-    const matrix = new Matrix33(dsdx, dtdx, tx, dsdy, dtdy, ty);
-    return new Transform(proto.type ?? 0, matrix);
-  }
-
-  if (proto.type) {
-    return new Transform(proto.type ?? 0, EMPTY_MATRIX33);
-  }
-  return EMPTY_TRANSFORM;
-}
-
-export {
-  Activity,
-  Configuration,
-  ConfigurationContainer,
-  DisplayArea,
-  DisplayContent,
-  KeyguardControllerState,
-  DisplayCutout,
-  RootWindowContainer,
-  Task,
-  TaskFragment,
-  WindowConfiguration,
-  WindowContainer,
-  WindowState,
-  WindowToken,
-  WindowLayoutParams,
-  WindowManagerPolicy,
-  WindowManagerTrace,
-  WindowManagerState,
-  WindowManagerTraceEntryBuilder,
-  // SF
-  HwcCompositionType,
-  Layer,
-  LayerProperties,
-  LayerTraceEntry,
-  LayerTraceEntryBuilder,
-  LayersTrace,
-  Transform,
-  Matrix33,
-  Display,
-  // Eventlog
-  EventLog,
-  CujEvent,
-  CujType,
-  Event,
-  FlickerEvent,
-  FocusEvent,
-  EventLogParser,
-  CujTrace,
-  Cuj,
-  // Transitions
-  Transition,
-  TransitionType,
-  TransitionChange,
-  TransitionsTrace,
-  ShellTransitionData,
-  WmTransitionData,
-  // Common
-  Size,
-  ActiveBuffer,
-  Color,
-  Insets,
-  PlatformConsts,
-  Point,
-  Rect,
-  RectF,
-  Region,
-  Rotation,
-  WindowingMode,
-  CrossPlatform,
-  TimestampFactory,
-  // Service
-  toSize,
-  toActiveBuffer,
-  toColor,
-  toCropRect,
-  toInsets,
-  toPoint,
-  toPointF,
-  toRect,
-  toRectF,
-  toRegion,
-  toTransform,
-  // Constants
-  EMPTY_BUFFER,
-  EMPTY_COLOR,
-  EMPTY_RECT,
-  EMPTY_RECTF,
-  EMPTY_POINT,
-  EMPTY_POINTF,
-  EMPTY_MATRIX33,
-  EMPTY_TRANSFORM,
-};
diff --git a/tools/winscope/src/trace/flickerlib/windows/Activity.ts b/tools/winscope/src/trace/flickerlib/windows/Activity.ts
deleted file mode 100644
index 1e3342f..0000000
--- a/tools/winscope/src/trace/flickerlib/windows/Activity.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 2020, 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 {Activity} from '../common';
-import {shortenName} from '../mixin';
-import {WindowContainer} from './WindowContainer';
-
-Activity.fromProto = (proto: any, nextSeq: () => number): Activity => {
-  if (proto == null) {
-    return null;
-  } else {
-    const windowContainer = WindowContainer.fromProto(
-      /* proto */ proto.windowToken.windowContainer,
-      /* protoChildren */ proto.windowToken.windowContainer?.children ?? [],
-      /* isActivityInTree */ true,
-      /* computedZ */ nextSeq,
-      /* nameOverride */ proto.name,
-      /* identifierOverride */ proto.identifier
-    );
-
-    const entry = new Activity(
-      proto.state,
-      proto.frontOfTask,
-      proto.procId,
-      proto.translucent,
-      windowContainer
-    );
-
-    addAttributes(entry, proto);
-    return entry;
-  }
-};
-
-function addAttributes(entry: Activity, proto: any) {
-  entry.proto = proto;
-  entry.kind = entry.constructor.name;
-  entry.shortName = shortenName(entry.name);
-}
-
-export {Activity};
diff --git a/tools/winscope/src/trace/flickerlib/windows/DisplayContent.ts b/tools/winscope/src/trace/flickerlib/windows/DisplayContent.ts
deleted file mode 100644
index 812b867..0000000
--- a/tools/winscope/src/trace/flickerlib/windows/DisplayContent.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright 2020, 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 {DisplayContent, DisplayCutout, Rect, Rotation, toInsets, toRect} from '../common';
-import {shortenName} from '../mixin';
-import {WindowContainer} from './WindowContainer';
-
-DisplayContent.fromProto = (
-  proto: any,
-  isActivityInTree: boolean,
-  nextSeq: () => number
-): DisplayContent => {
-  if (proto == null) {
-    return null;
-  } else {
-    const windowContainer = WindowContainer.fromProto(
-      /* proto */ proto.rootDisplayArea.windowContainer,
-      /* protoChildren */ proto.rootDisplayArea.windowContainer?.children ?? [],
-      /* isActivityInTree */ isActivityInTree,
-      /* computedZ */ nextSeq,
-      /* nameOverride */ proto.displayInfo?.name ?? null
-    );
-    const displayRectWidth = proto.displayInfo?.logicalWidth ?? 0;
-    const displayRectHeight = proto.displayInfo?.logicalHeight ?? 0;
-    const appRectWidth = proto.displayInfo?.appWidth ?? 0;
-    const appRectHeight = proto.displayInfo?.appHeight ?? 0;
-    const defaultBounds = proto.pinnedStackController?.defaultBounds ?? null;
-    const movementBounds = proto.pinnedStackController?.movementBounds ?? null;
-
-    const entry = new DisplayContent(
-      proto.id,
-      proto.focusedRootTaskId,
-      proto.resumedActivity?.title ?? '',
-      proto.singleTaskInstance,
-      toRect(defaultBounds),
-      toRect(movementBounds),
-      new Rect(0, 0, displayRectWidth, displayRectHeight),
-      new Rect(0, 0, appRectWidth, appRectHeight),
-      proto.dpi,
-      proto.displayInfo?.flags ?? 0,
-      toRect(proto.displayFrames?.stableBounds),
-      proto.surfaceSize,
-      proto.focusedApp,
-      proto.appTransition?.lastUsedAppTransition ?? '',
-      proto.appTransition?.appTransitionState ?? '',
-      Rotation.Companion.getByValue(proto.displayRotation?.rotation ?? 0),
-      proto.displayRotation?.lastOrientation ?? 0,
-      createDisplayCutout(proto.displayInfo?.cutout),
-      windowContainer
-    );
-
-    addAttributes(entry, proto);
-    return entry;
-  }
-};
-
-function createDisplayCutout(proto: any | null): DisplayCutout | null {
-  if (proto == null) {
-    return null;
-  } else {
-    return new DisplayCutout(
-      toInsets(proto?.insets),
-      toRect(proto?.boundLeft),
-      toRect(proto?.boundTop),
-      toRect(proto?.boundRight),
-      toRect(proto?.boundBottom),
-      toInsets(proto?.waterfallInsets)
-    );
-  }
-}
-
-function addAttributes(entry: DisplayContent, proto: any) {
-  entry.proto = proto;
-  entry.kind = entry.constructor.name;
-  entry.shortName = shortenName(entry.name);
-}
-
-export {DisplayContent};
diff --git a/tools/winscope/src/trace/flickerlib/windows/Task.ts b/tools/winscope/src/trace/flickerlib/windows/Task.ts
deleted file mode 100644
index f5e4878..0000000
--- a/tools/winscope/src/trace/flickerlib/windows/Task.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2020, 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 {Task, toRect} from '../common';
-import {shortenName} from '../mixin';
-import {WindowContainer} from './WindowContainer';
-
-Task.fromProto = (proto: any, isActivityInTree: boolean, nextSeq: () => number): Task => {
-  if (proto == null) {
-    return null;
-  } else {
-    const windowContainerProto = proto.taskFragment?.windowContainer ?? proto.windowContainer;
-    const windowContainer = WindowContainer.fromProto(
-      /* proto */ windowContainerProto,
-      /* protoChildren */ windowContainerProto?.children ?? [],
-      /* isActivityInTree */ isActivityInTree,
-      /* computedZ */ nextSeq
-    );
-
-    const entry = new Task(
-      proto.taskFragment?.activityType ?? proto.activityType,
-      proto.fillsParent,
-      toRect(proto.bounds),
-      proto.id,
-      proto.rootTaskId,
-      proto.taskFragment?.displayId,
-      toRect(proto.lastNonFullscreenBounds),
-      proto.realActivity,
-      proto.origActivity,
-      proto.resizeMode,
-      proto.resumedActivity?.title ?? '',
-      proto.animatingBounds,
-      proto.surfaceWidth,
-      proto.surfaceHeight,
-      proto.createdByOrganizer,
-      proto.taskFragment?.minWidth ?? proto.minWidth,
-      proto.taskFragment?.minHeight ?? proto.minHeight,
-      windowContainer
-    );
-
-    addAttributes(entry, proto);
-    return entry;
-  }
-};
-
-function addAttributes(entry: Task, proto: any) {
-  entry.proto = proto;
-  entry.proto.configurationContainer = proto.windowContainer?.configurationContainer;
-  entry.proto.surfaceControl = proto.windowContainer?.surfaceControl;
-  entry.kind = entry.constructor.name;
-  entry.shortName = shortenName(entry.name);
-}
-
-export {Task};
diff --git a/tools/winscope/src/trace/flickerlib/windows/TaskFragment.ts b/tools/winscope/src/trace/flickerlib/windows/TaskFragment.ts
deleted file mode 100644
index b6613af..0000000
--- a/tools/winscope/src/trace/flickerlib/windows/TaskFragment.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright 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 {TaskFragment} from '../common';
-import {shortenName} from '../mixin';
-import {WindowContainer} from './WindowContainer';
-
-TaskFragment.fromProto = (
-  proto: any,
-  isActivityInTree: boolean,
-  nextSeq: () => number
-): TaskFragment => {
-  if (proto == null) {
-    return null;
-  } else {
-    const windowContainer = WindowContainer.fromProto(
-      /* proto */ proto.windowContainer,
-      /* protoChildren */ proto.windowContainer?.children ?? [],
-      /* isActivityInTree */ isActivityInTree,
-      /* computedZ */ nextSeq
-    );
-    const entry = new TaskFragment(
-      proto.activityType,
-      proto.displayId,
-      proto.minWidth,
-      proto.minHeight,
-      windowContainer
-    );
-
-    addAttributes(entry, proto);
-    return entry;
-  }
-};
-
-function addAttributes(entry: TaskFragment, proto: any) {
-  entry.proto = proto;
-  entry.proto.configurationContainer = proto.windowContainer?.configurationContainer;
-  entry.proto.surfaceControl = proto.windowContainer?.surfaceControl;
-  entry.kind = entry.constructor.name;
-  entry.shortName = shortenName(entry.name);
-}
-
-export {TaskFragment};
diff --git a/tools/winscope/src/trace/flickerlib/windows/WindowContainer.ts b/tools/winscope/src/trace/flickerlib/windows/WindowContainer.ts
deleted file mode 100644
index f18d09b..0000000
--- a/tools/winscope/src/trace/flickerlib/windows/WindowContainer.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright 2020, 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 {shortenName} from '../mixin';
-
-import {
-  Configuration,
-  ConfigurationContainer,
-  toRect,
-  WindowConfiguration,
-  WindowContainer,
-} from '../common';
-
-import {Activity} from './Activity';
-import {DisplayArea} from './DisplayArea';
-import {DisplayContent} from './DisplayContent';
-import {Task} from './Task';
-import {TaskFragment} from './TaskFragment';
-import {WindowState} from './WindowState';
-import {WindowToken} from './WindowToken';
-
-WindowContainer.fromProto = (
-  proto: any,
-  protoChildren: any[],
-  isActivityInTree: boolean,
-  nextSeq: () => number,
-  nameOverride: string | null = null,
-  identifierOverride: string | null = null,
-  tokenOverride: any = null,
-  visibleOverride: boolean | null = null
-): WindowContainer => {
-  if (proto == null) {
-    return null;
-  }
-
-  const containerOrder = nextSeq();
-  const children = protoChildren
-    .filter((it) => it != null)
-    .map((it) => WindowContainer.childrenFromProto(it, isActivityInTree, nextSeq))
-    .filter((it) => it != null);
-
-  const identifier: any = identifierOverride ?? proto.identifier;
-  const name: string = nameOverride ?? identifier?.title ?? '';
-  const token: string = tokenOverride?.toString(16) ?? identifier?.hashCode?.toString(16) ?? '';
-
-  const config = createConfigurationContainer(proto.configurationContainer);
-  const entry = new WindowContainer(
-    name,
-    token,
-    proto.orientation,
-    proto.surfaceControl?.layerId ?? 0,
-    visibleOverride ?? proto.visible,
-    config,
-    children,
-    containerOrder
-  );
-
-  addAttributes(entry, proto);
-  return entry;
-};
-
-function addAttributes(entry: WindowContainer, proto: any) {
-  entry.proto = proto;
-  entry.kind = entry.constructor.name;
-  entry.shortName = shortenName(entry.name);
-}
-
-type WindowContainerChildType =
-  | DisplayContent
-  | DisplayArea
-  | Task
-  | TaskFragment
-  | Activity
-  | WindowToken
-  | WindowState
-  | WindowContainer;
-
-WindowContainer.childrenFromProto = (
-  proto: any,
-  isActivityInTree: boolean,
-  nextSeq: () => number
-): WindowContainerChildType => {
-  return (
-    DisplayContent.fromProto(proto.displayContent, isActivityInTree, nextSeq) ??
-    DisplayArea.fromProto(proto.displayArea, isActivityInTree, nextSeq) ??
-    Task.fromProto(proto.task, isActivityInTree, nextSeq) ??
-    TaskFragment.fromProto(proto.taskFragment, isActivityInTree, nextSeq) ??
-    Activity.fromProto(proto.activity, nextSeq) ??
-    WindowToken.fromProto(proto.windowToken, isActivityInTree, nextSeq) ??
-    WindowState.fromProto(proto.window, isActivityInTree, nextSeq) ??
-    WindowContainer.fromProto(proto.windowContainer, nextSeq)
-  );
-};
-
-function createConfigurationContainer(proto: any): ConfigurationContainer {
-  const entry = ConfigurationContainer.Companion.from(
-    createConfiguration(proto?.overrideConfiguration ?? null),
-    createConfiguration(proto?.fullConfiguration ?? null),
-    createConfiguration(proto?.mergedOverrideConfiguration ?? null)
-  );
-
-  entry.obj = entry;
-  return entry;
-}
-
-function createConfiguration(proto: any): Configuration {
-  if (proto == null) {
-    return null;
-  }
-  let windowConfiguration = null;
-
-  if (proto != null && proto.windowConfiguration != null) {
-    windowConfiguration = createWindowConfiguration(proto.windowConfiguration);
-  }
-
-  return Configuration.Companion.from(
-    windowConfiguration,
-    proto?.densityDpi ?? 0,
-    proto?.orientation ?? 0,
-    proto?.screenHeightDp ?? 0,
-    proto?.screenHeightDp ?? 0,
-    proto?.smallestScreenWidthDp ?? 0,
-    proto?.screenLayout ?? 0,
-    proto?.uiMode ?? 0
-  );
-}
-
-function createWindowConfiguration(proto: any): WindowConfiguration {
-  return WindowConfiguration.Companion.from(
-    toRect(proto.appBounds),
-    toRect(proto.bounds),
-    toRect(proto.maxBounds),
-    proto.windowingMode,
-    proto.activityType
-  );
-}
-
-export {WindowContainer};
diff --git a/tools/winscope/src/trace/flickerlib/windows/WindowState.ts b/tools/winscope/src/trace/flickerlib/windows/WindowState.ts
deleted file mode 100644
index 3d47c4b..0000000
--- a/tools/winscope/src/trace/flickerlib/windows/WindowState.ts
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright 2020, 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 {Size, toRect, WindowLayoutParams, WindowState} from '../common';
-import {shortenName} from '../mixin';
-import {WindowContainer} from './WindowContainer';
-
-WindowState.fromProto = (
-  proto: any,
-  isActivityInTree: boolean,
-  nextSeq: () => number
-): WindowState => {
-  if (proto == null) {
-    return null;
-  } else {
-    const windowParams = createWindowLayoutParams(proto.attributes);
-    const identifierName = getIdentifier(proto);
-    const windowType = getWindowType(proto, identifierName);
-    const name = getName(identifierName);
-    const windowContainer = WindowContainer.fromProto(
-      /* proto */ proto.windowContainer,
-      /* protoChildren */ proto.windowContainer?.children ?? [],
-      /* isActivityInTree */ isActivityInTree,
-      /* computedZ */ nextSeq,
-      /* nameOverride */ name,
-      /* identifierOverride */ proto.identifier
-    );
-
-    const entry = new WindowState(
-      windowParams,
-      proto.displayId,
-      proto.stackId,
-      proto.animator?.surface?.layer ?? 0,
-      proto.animator?.surface?.shown ?? false,
-      windowType,
-      new Size(proto.requestedWidth, proto.requestedHeight),
-      toRect(proto.surfacePosition),
-      toRect(proto.windowFrames?.frame ?? null),
-      toRect(proto.windowFrames?.containingFrame ?? null),
-      toRect(proto.windowFrames?.parentFrame ?? null),
-      toRect(proto.windowFrames?.contentFrame ?? null),
-      toRect(proto.windowFrames?.contentInsets ?? null),
-      toRect(proto.surfaceInsets),
-      toRect(proto.givenContentInsets),
-      toRect(proto.animator?.lastClipRect ?? null),
-      windowContainer,
-      /* isAppWindow */ isActivityInTree
-    );
-
-    addAttributes(entry, proto);
-    return entry;
-  }
-};
-
-function createWindowLayoutParams(proto: any): WindowLayoutParams {
-  return new WindowLayoutParams(
-    /* type */ proto?.type ?? 0,
-    /* x */ proto?.x ?? 0,
-    /* y */ proto?.y ?? 0,
-    /* width */ proto?.width ?? 0,
-    /* height */ proto?.height ?? 0,
-    /* horizontalMargin */ proto?.horizontalMargin ?? 0,
-    /* verticalMargin */ proto?.verticalMargin ?? 0,
-    /* gravity */ proto?.gravity ?? 0,
-    /* softInputMode */ proto?.softInputMode ?? 0,
-    /* format */ proto?.format ?? 0,
-    /* windowAnimations */ proto?.windowAnimations ?? 0,
-    /* alpha */ proto?.alpha ?? 0,
-    /* screenBrightness */ proto?.screenBrightness ?? 0,
-    /* buttonBrightness */ proto?.buttonBrightness ?? 0,
-    /* rotationAnimation */ proto?.rotationAnimation ?? 0,
-    /* preferredRefreshRate */ proto?.preferredRefreshRate ?? 0,
-    /* preferredDisplayModeId */ proto?.preferredDisplayModeId ?? 0,
-    /* hasSystemUiListeners */ proto?.hasSystemUiListeners ?? false,
-    /* inputFeatureFlags */ proto?.inputFeatureFlags ?? 0,
-    /* userActivityTimeout */ proto?.userActivityTimeout ?? 0,
-    /* colorMode */ proto?.colorMode ?? 0,
-    /* flags */ proto?.flags ?? 0,
-    /* privateFlags */ proto?.privateFlags ?? 0,
-    /* systemUiVisibilityFlags */ proto?.systemUiVisibilityFlags ?? 0,
-    /* subtreeSystemUiVisibilityFlags */ proto?.subtreeSystemUiVisibilityFlags ?? 0,
-    /* appearance */ proto?.appearance ?? 0,
-    /* behavior */ proto?.behavior ?? 0,
-    /* fitInsetsTypes */ proto?.fitInsetsTypes ?? 0,
-    /* fitInsetsSides */ proto?.fitInsetsSides ?? 0,
-    /* fitIgnoreVisibility */ proto?.fitIgnoreVisibility ?? false
-  );
-}
-
-function getWindowType(proto: any, identifierName: string): number {
-  if (identifierName.startsWith(WindowState.STARTING_WINDOW_PREFIX)) {
-    return WindowState.WINDOW_TYPE_STARTING;
-  } else if (proto.animatingExit) {
-    return WindowState.WINDOW_TYPE_EXITING;
-  } else if (identifierName.startsWith(WindowState.DEBUGGER_WINDOW_PREFIX)) {
-    return WindowState.WINDOW_TYPE_STARTING;
-  }
-
-  return 0;
-}
-
-function getName(identifierName: string): string {
-  let name = identifierName;
-
-  if (identifierName.startsWith(WindowState.STARTING_WINDOW_PREFIX)) {
-    name = identifierName.substring(WindowState.STARTING_WINDOW_PREFIX.length);
-  } else if (identifierName.startsWith(WindowState.DEBUGGER_WINDOW_PREFIX)) {
-    name = identifierName.substring(WindowState.DEBUGGER_WINDOW_PREFIX.length);
-  }
-
-  return name;
-}
-
-function getIdentifier(proto: any): string {
-  return proto.windowContainer.identifier?.title ?? proto.identifier?.title ?? '';
-}
-
-function addAttributes(entry: WindowState, proto: any) {
-  entry.kind = entry.constructor.name;
-  entry.rect = entry.frame;
-  entry.rect.ref = entry;
-  entry.rect.label = entry.name;
-  entry.proto = proto;
-  entry.proto.configurationContainer = proto.windowContainer?.configurationContainer;
-  entry.proto.surfaceControl = proto.windowContainer?.surfaceControl;
-  entry.shortName = shortenName(entry.name);
-}
-
-export {WindowState};
diff --git a/tools/winscope/src/trace/frame_mapper.ts b/tools/winscope/src/trace/frame_mapper.ts
index b4f4dd7..00ace77 100644
--- a/tools/winscope/src/trace/frame_mapper.ts
+++ b/tools/winscope/src/trace/frame_mapper.ts
@@ -100,19 +100,21 @@
     }
 
     const transactions = assertDefined(this.traces.getTrace(TraceType.TRANSACTIONS));
+    const transactionEntries = await transactions.prefetchPartialProtos('vsyncId');
+
     const surfaceFlinger = assertDefined(this.traces.getTrace(TraceType.SURFACE_FLINGER));
+    const surfaceFlingerEntries = await surfaceFlinger.prefetchPartialProtos('vsyncId');
 
     const vsyncIdToFrames = new Map<bigint, FramesRange>();
 
-    for (let srcEntryIndex = 0; srcEntryIndex < surfaceFlinger.lengthEntries; ++srcEntryIndex) {
-      const srcEntry = surfaceFlinger.getEntry(srcEntryIndex);
-      const vsyncId = await this.getVsyncIdProperty(srcEntry, 'vSyncId');
+    surfaceFlingerEntries.forEach((srcEntry) => {
+      const vsyncId = this.getVsyncId(srcEntry);
       if (vsyncId === undefined) {
-        continue;
+        return;
       }
       const srcFrames = srcEntry.getFramesRange();
       if (!srcFrames) {
-        continue;
+        return;
       }
       let frames = vsyncIdToFrames.get(vsyncId);
       if (!frames) {
@@ -121,20 +123,19 @@
       frames.start = Math.min(frames.start, srcFrames.start);
       frames.end = Math.max(frames.end, srcFrames.end);
       vsyncIdToFrames.set(vsyncId, frames);
-    }
+    });
 
-    for (let dstEntryIndex = 0; dstEntryIndex < transactions.lengthEntries; ++dstEntryIndex) {
-      const dstEntry = transactions.getEntry(dstEntryIndex);
-      const vsyncId = await this.getVsyncIdProperty(dstEntry, 'vsyncId');
+    transactionEntries.forEach((dstEntry) => {
+      const vsyncId = this.getVsyncId(dstEntry);
       if (vsyncId === undefined) {
-        continue;
+        return;
       }
       const frames = vsyncIdToFrames.get(vsyncId);
       if (frames === undefined) {
-        continue;
+        return;
       }
       frameMapBuilder.setFrames(dstEntry.getIndex(), frames);
-    }
+    });
 
     const frameMap = frameMapBuilder.build();
     transactions.setFrameInfo(frameMap, frameMap.getFullTraceFramesRange());
@@ -279,20 +280,17 @@
     return new FrameMapBuilder(dstTrace.lengthEntries, lengthFrames);
   }
 
-  private async getVsyncIdProperty(
-    entry: TraceEntry<object>,
-    propertyKey: string
-  ): Promise<bigint | undefined> {
-    const entryValue = await entry.getValue();
-    const vsyncId = (entryValue as any)[propertyKey];
+  private getVsyncId(entry: TraceEntry<object>): bigint | undefined {
+    const proto = assertDefined(entry.getPrefetchedPartialProto());
+    const vsyncId = (proto as any).vsyncId;
     if (vsyncId === undefined) {
-      console.error(`Failed to get trace entry's '${propertyKey}' property:`, entryValue);
+      console.error(`Failed to get partial trace entry's 'vsyncId' property:`, proto);
       return undefined;
     }
     try {
       return BigInt(vsyncId.toString());
     } catch (e) {
-      console.error(`Failed to convert trace entry's vsyncId to bigint:`, entryValue);
+      console.error(`Failed to convert trace entry's vsyncId to bigint:`, proto);
       return undefined;
     }
   }
diff --git a/tools/winscope/src/trace/frame_mapper_test.ts b/tools/winscope/src/trace/frame_mapper_test.ts
index 5a3619d..ae791aa 100644
--- a/tools/winscope/src/trace/frame_mapper_test.ts
+++ b/tools/winscope/src/trace/frame_mapper_test.ts
@@ -14,15 +14,15 @@
  * limitations under the License.
  */
 
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
 import {TracesUtils} from 'test/unit/traces_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
-import {LayerTraceEntry} from './flickerlib/layers/LayerTraceEntry';
-import {WindowManagerState} from './flickerlib/windows/WindowManagerState';
+import {RealTimestamp} from '../common/time';
 import {FrameMapper} from './frame_mapper';
 import {AbsoluteFrameIndex} from './index_types';
 import {LogMessage} from './protolog';
 import {ScreenRecordingTraceEntry} from './screen_recording';
-import {RealTimestamp} from './timestamp';
 import {Trace} from './trace';
 import {Traces} from './traces';
 import {TraceType} from './trace_type';
@@ -282,9 +282,9 @@
 
       surfaceFlinger = new TraceBuilder<LayerTraceEntry>()
         .setEntries([
-          {id: 0, vSyncId: createVsyncId(0)} as unknown as LayerTraceEntry,
-          {id: 1, vSyncId: createVsyncId(10)} as unknown as LayerTraceEntry,
-          {id: 2, vSyncId: createVsyncId(20)} as unknown as LayerTraceEntry,
+          {id: 0, vsyncId: createVsyncId(0)} as unknown as LayerTraceEntry,
+          {id: 1, vsyncId: createVsyncId(10)} as unknown as LayerTraceEntry,
+          {id: 2, vsyncId: createVsyncId(20)} as unknown as LayerTraceEntry,
         ])
         .setTimestamps([time0, time1, time2])
         .build();
diff --git a/tools/winscope/src/trace/parser.ts b/tools/winscope/src/trace/parser.ts
index 302baf0..bf737e9 100644
--- a/tools/winscope/src/trace/parser.ts
+++ b/tools/winscope/src/trace/parser.ts
@@ -14,14 +14,15 @@
  * limitations under the License.
  */
 
-import {Timestamp, TimestampType} from './timestamp';
+import {Timestamp, TimestampType} from '../common/time';
+import {AbsoluteEntryIndex, EntriesRange} from './index_types';
 import {TraceType} from './trace_type';
 
 export interface Parser<T> {
   getTraceType(): TraceType;
   getLengthEntries(): number;
   getTimestamps(type: TimestampType): Timestamp[] | undefined;
-  getEntry(index: number, timestampType: TimestampType): Promise<T>;
-
+  getEntry(index: AbsoluteEntryIndex, timestampType: TimestampType): Promise<T>;
+  getPartialProtos(entriesRange: EntriesRange, fieldPath: string): Promise<object[]>;
   getDescriptors(): string[];
 }
diff --git a/tools/winscope/src/trace/parser_mock.ts b/tools/winscope/src/trace/parser_mock.ts
index 77adbf3..700db01 100644
--- a/tools/winscope/src/trace/parser_mock.ts
+++ b/tools/winscope/src/trace/parser_mock.ts
@@ -14,8 +14,10 @@
  * limitations under the License.
  */
 
+import {ObjectUtils} from 'common/object_utils';
+import {RealTimestamp, Timestamp, TimestampType} from '../common/time';
+import {AbsoluteEntryIndex, EntriesRange} from './index_types';
 import {Parser} from './parser';
-import {RealTimestamp, Timestamp, TimestampType} from './timestamp';
 import {TraceType} from './trace_type';
 
 export class ParserMock<T> implements Parser<T> {
@@ -40,10 +42,20 @@
     return this.timestamps;
   }
 
-  getEntry(index: number): Promise<T> {
+  getEntry(index: AbsoluteEntryIndex): Promise<T> {
     return Promise.resolve(this.entries[index]);
   }
 
+  getPartialProtos(entriesRange: EntriesRange, fieldPath: string): Promise<object[]> {
+    const partialEntries = this.entries.slice(entriesRange.start, entriesRange.end).map((entry) => {
+      const fieldValue = ObjectUtils.getProperty(entry as object, fieldPath);
+      const proto = {};
+      ObjectUtils.setProperty(proto, fieldPath, fieldValue);
+      return proto;
+    });
+    return Promise.resolve(partialEntries);
+  }
+
   getDescriptors(): string[] {
     return ['MockTrace'];
   }
diff --git a/tools/winscope/src/trace/protolog.ts b/tools/winscope/src/trace/protolog.ts
index 7139a08..a20b5aa 100644
--- a/tools/winscope/src/trace/protolog.ts
+++ b/tools/winscope/src/trace/protolog.ts
@@ -16,7 +16,7 @@
 
 import {TimeUtils} from 'common/time_utils';
 import configJson from '../../../../../frameworks/base/data/etc/services.core.protolog.json';
-import {ElapsedTimestamp, RealTimestamp, TimestampType} from './timestamp';
+import {ElapsedTimestamp, RealTimestamp, TimestampType} from '../common/time';
 
 class LogMessage {
   text: string;
diff --git a/tools/winscope/src/trace/screen_recording_utils.ts b/tools/winscope/src/trace/screen_recording_utils.ts
index e882f8c..38cb5d6 100644
--- a/tools/winscope/src/trace/screen_recording_utils.ts
+++ b/tools/winscope/src/trace/screen_recording_utils.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {Timestamp} from './timestamp';
+import {Timestamp} from '../common/time';
 
 class ScreenRecordingUtils {
   // Video time correction epsilon. Without correction, we could display the previous frame.
diff --git a/tools/winscope/src/trace/timestamp_test.ts b/tools/winscope/src/trace/timestamp_test.ts
deleted file mode 100644
index 91a8b1a..0000000
--- a/tools/winscope/src/trace/timestamp_test.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2022 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 {Timestamp, TimestampType} from './timestamp';
-
-describe('Timestamp', () => {
-  describe('from', () => {
-    it('throws when missing elapsed timestamp', () => {
-      expect(() => {
-        Timestamp.from(TimestampType.REAL, 100n);
-      }).toThrow();
-    });
-
-    it('can create real timestamp', () => {
-      const timestamp = Timestamp.from(TimestampType.REAL, 100n, 500n);
-      expect(timestamp.getType()).toBe(TimestampType.REAL);
-      expect(timestamp.getValueNs()).toBe(600n);
-    });
-
-    it('can create elapsed timestamp', () => {
-      let timestamp = Timestamp.from(TimestampType.ELAPSED, 100n, 500n);
-      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
-      expect(timestamp.getValueNs()).toBe(100n);
-
-      timestamp = Timestamp.from(TimestampType.ELAPSED, 100n);
-      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
-      expect(timestamp.getValueNs()).toBe(100n);
-    });
-  });
-});
diff --git a/tools/winscope/src/trace/trace.ts b/tools/winscope/src/trace/trace.ts
index c2be329..a738adf 100644
--- a/tools/winscope/src/trace/trace.ts
+++ b/tools/winscope/src/trace/trace.ts
@@ -15,6 +15,7 @@
  */
 
 import {ArrayUtils} from 'common/array_utils';
+import {Timestamp, TimestampType} from '../common/time';
 import {FrameMap} from './frame_map';
 import {
   AbsoluteEntryIndex,
@@ -24,7 +25,6 @@
   RelativeEntryIndex,
 } from './index_types';
 import {Parser} from './parser';
-import {Timestamp, TimestampType} from './timestamp';
 import {TraceType} from './trace_type';
 
 export {
@@ -41,7 +41,8 @@
     private readonly parser: Parser<T>,
     private readonly index: AbsoluteEntryIndex,
     private readonly timestamp: Timestamp,
-    private readonly framesRange: FramesRange | undefined
+    private readonly framesRange: FramesRange | undefined,
+    private readonly prefetchedPartialProto: object | undefined
   ) {}
 
   getFullTrace(): Trace<T> {
@@ -65,6 +66,10 @@
     return this.framesRange;
   }
 
+  getPrefetchedPartialProto(): object | undefined {
+    return this.prefetchedPartialProto;
+  }
+
   async getValue(): Promise<T> {
     return await this.parser.getEntry(this.index, this.timestamp.getType());
   }
@@ -148,22 +153,15 @@
   }
 
   getEntry(index: RelativeEntryIndex): TraceEntry<T> {
-    const entry = this.convertToAbsoluteEntryIndex(index) as AbsoluteEntryIndex;
-    if (entry < this.entriesRange.start || entry >= this.entriesRange.end) {
-      throw new Error(
-        `Trace entry's index out of bounds. Input relative index: ${index}. Slice length: ${this.lengthEntries}.`
-      );
-    }
-    const frames = this.clampFramesRangeToSliceBounds(
-      this.frameMap?.getFramesRange({start: entry, end: entry + 1})
+    return this.getEntryInternal(index, undefined);
+  }
+
+  async prefetchPartialProtos(fieldPath: string): Promise<Array<TraceEntry<T>>> {
+    const partialProtos = await this.parser.getPartialProtos(this.entriesRange, fieldPath);
+    const entries = partialProtos.map((partialProto, index) =>
+      this.getEntryInternal(index, partialProto)
     );
-    return new TraceEntry<T>(
-      this.fullTrace,
-      this.parser,
-      entry,
-      this.getFullTraceTimestamps()[entry],
-      frames
-    );
+    return entries;
   }
 
   getFrame(frame: AbsoluteFrameIndex): Trace<T> {
@@ -365,6 +363,29 @@
     return this.framesRange;
   }
 
+  private getEntryInternal(
+    index: RelativeEntryIndex,
+    prefetchedPartialProto: object | undefined
+  ): TraceEntry<T> {
+    const entry = this.convertToAbsoluteEntryIndex(index) as AbsoluteEntryIndex;
+    if (entry < this.entriesRange.start || entry >= this.entriesRange.end) {
+      throw new Error(
+        `Trace entry's index out of bounds. Input relative index: ${index}. Slice length: ${this.lengthEntries}.`
+      );
+    }
+    const frames = this.clampFramesRangeToSliceBounds(
+      this.frameMap?.getFramesRange({start: entry, end: entry + 1})
+    );
+    return new TraceEntry<T>(
+      this.fullTrace,
+      this.parser,
+      entry,
+      this.getFullTraceTimestamps()[entry],
+      frames,
+      prefetchedPartialProto
+    );
+  }
+
   private getFullTraceTimestamps(): Timestamp[] {
     if (this.timestampType === undefined) {
       throw new Error('Forgot to initialize trace?');
diff --git a/tools/winscope/src/trace/trace_entry_test.ts b/tools/winscope/src/trace/trace_entry_test.ts
index 7859da6..9b4de44 100644
--- a/tools/winscope/src/trace/trace_entry_test.ts
+++ b/tools/winscope/src/trace/trace_entry_test.ts
@@ -15,7 +15,7 @@
  */
 
 import {TraceBuilder} from 'test/unit/trace_builder';
-import {RealTimestamp} from './timestamp';
+import {RealTimestamp} from '../common/time';
 import {Trace} from './trace';
 
 describe('TraceEntry', () => {
diff --git a/tools/winscope/src/trace/trace_position.ts b/tools/winscope/src/trace/trace_position.ts
index f0cd118..9f8889b 100644
--- a/tools/winscope/src/trace/trace_position.ts
+++ b/tools/winscope/src/trace/trace_position.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
+import {Timestamp} from '../common/time';
 import {AbsoluteFrameIndex} from './index_types';
-import {Timestamp} from './timestamp';
 import {TraceEntry} from './trace';
 
 export class TracePosition {
@@ -23,13 +23,14 @@
     return new TracePosition(timestamp);
   }
 
-  static fromTraceEntry(entry: TraceEntry<{}>): TracePosition {
+  static fromTraceEntry(entry: TraceEntry<{}>, explicitTimestamp?: Timestamp): TracePosition {
     let frame: AbsoluteFrameIndex | undefined;
     if (entry.getFullTrace().hasFrameInfo()) {
       const frames = entry.getFramesRange();
       frame = frames && frames.start < frames.end ? frames.start : undefined;
     }
-    return new TracePosition(entry.getTimestamp(), frame, entry);
+    const timestamp = explicitTimestamp ? explicitTimestamp : entry.getTimestamp();
+    return new TracePosition(timestamp, frame, entry);
   }
 
   isEqual(other: TracePosition): boolean {
diff --git a/tools/winscope/src/trace/trace_test.ts b/tools/winscope/src/trace/trace_test.ts
index 3380c8e..591681e 100644
--- a/tools/winscope/src/trace/trace_test.ts
+++ b/tools/winscope/src/trace/trace_test.ts
@@ -16,9 +16,9 @@
 
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {TraceUtils} from 'test/unit/trace_utils';
+import {RealTimestamp} from '../common/time';
 import {FrameMapBuilder} from './frame_map_builder';
 import {AbsoluteFrameIndex} from './index_types';
-import {RealTimestamp} from './timestamp';
 import {Trace} from './trace';
 
 describe('Trace', () => {
@@ -63,6 +63,26 @@
     }).toThrow();
   });
 
+  it('prefetchPartialProtos()', async () => {
+    const trace = new TraceBuilder<object>()
+      .setEntries([{theKey: 0}, {theKey: 10}, {theKey: 20}, {theKey: 30}])
+      .setFrame(0, 0)
+      .setFrame(1, 1)
+      .setFrame(2, 2)
+      .setFrame(3, 3)
+      .build();
+    const slice = trace.sliceEntries(1, -1);
+    const entries = await slice.prefetchPartialProtos('theKey');
+
+    expect(entries.length).toEqual(2);
+
+    expect(entries[0].getPrefetchedPartialProto()).toEqual({theKey: 10});
+    expect(entries[0].getFramesRange()).toEqual({start: 1, end: 2});
+
+    expect(entries[1].getPrefetchedPartialProto()).toEqual({theKey: 20});
+    expect(entries[1].getFramesRange()).toEqual({start: 2, end: 3});
+  });
+
   it('getFrame()', async () => {
     expect(await TraceUtils.extractFrames(trace.getFrame(0))).toEqual(
       new Map<AbsoluteFrameIndex, string[]>([[0, ['entry-0']]])
diff --git a/tools/winscope/src/trace/trace_type.ts b/tools/winscope/src/trace/trace_type.ts
index ee18a44..72efbb4 100644
--- a/tools/winscope/src/trace/trace_type.ts
+++ b/tools/winscope/src/trace/trace_type.ts
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {Cuj, Event, Transition} from 'trace/flickerlib/common';
-import {LayerTraceEntry} from './flickerlib/layers/LayerTraceEntry';
-import {WindowManagerState} from './flickerlib/windows/WindowManagerState';
+import {Cuj, Event, Transition} from 'flickerlib/common';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
 import {LogMessage} from './protolog';
 import {ScreenRecordingTraceEntry} from './screen_recording';
 
@@ -30,7 +30,6 @@
   WAYLAND_DUMP,
   PROTO_LOG,
   SYSTEM_UI,
-  LAUNCHER,
   INPUT_METHOD_CLIENTS,
   INPUT_METHOD_MANAGER_SERVICE,
   INPUT_METHOD_SERVICE,
@@ -44,11 +43,18 @@
   TEST_TRACE_STRING,
   TEST_TRACE_NUMBER,
   VIEW_CAPTURE,
+  VIEW_CAPTURE_LAUNCHER_ACTIVITY,
+  VIEW_CAPTURE_TASKBAR_DRAG_LAYER,
+  VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER,
 }
 
+// view capture types
+export type ViewNode = any;
+export type FrameData = any;
+export type WindowData = any;
+
 export interface TraceEntryTypeMap {
   [TraceType.ACCESSIBILITY]: object;
-  [TraceType.LAUNCHER]: object;
   [TraceType.PROTO_LOG]: LogMessage;
   [TraceType.SURFACE_FLINGER]: LayerTraceEntry;
   [TraceType.SCREEN_RECORDING]: ScreenRecordingTraceEntry;
@@ -71,4 +77,7 @@
   [TraceType.TEST_TRACE_STRING]: string;
   [TraceType.TEST_TRACE_NUMBER]: number;
   [TraceType.VIEW_CAPTURE]: object;
+  [TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY]: FrameData;
+  [TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER]: FrameData;
+  [TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER]: FrameData;
 }
diff --git a/tools/winscope/src/trace/traces.ts b/tools/winscope/src/trace/traces.ts
index 2c18572..5161e7c 100644
--- a/tools/winscope/src/trace/traces.ts
+++ b/tools/winscope/src/trace/traces.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
+import {Timestamp} from '../common/time';
 import {AbsoluteFrameIndex} from './index_types';
-import {Timestamp} from './timestamp';
 import {Trace} from './trace';
 import {TraceEntryTypeMap, TraceType} from './trace_type';
 
diff --git a/tools/winscope/src/trace/traces_test.ts b/tools/winscope/src/trace/traces_test.ts
index ef473cf..64344c3 100644
--- a/tools/winscope/src/trace/traces_test.ts
+++ b/tools/winscope/src/trace/traces_test.ts
@@ -20,9 +20,9 @@
 import {TracesUtils} from 'test/unit/traces_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {TraceUtils} from 'test/unit/trace_utils';
+import {RealTimestamp} from '../common/time';
 import {FrameMapBuilder} from './frame_map_builder';
 import {AbsoluteFrameIndex} from './index_types';
-import {RealTimestamp} from './timestamp';
 import {Traces} from './traces';
 import {TraceType} from './trace_type';
 
diff --git a/tools/winscope/src/trace_collection/connection.ts b/tools/winscope/src/trace_collection/connection.ts
index b91f1d2..5ff46c9 100644
--- a/tools/winscope/src/trace_collection/connection.ts
+++ b/tools/winscope/src/trace_collection/connection.ts
@@ -13,18 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {ProxyClient} from 'trace_collection/proxy_client';
+import {Device, DeviceProperties, ProxyClient} from 'trace_collection/proxy_client';
 import {ConfigMap} from './trace_collection_utils';
 
-export interface Device {
-  [key: string]: DeviceProperties;
-}
-
-export interface DeviceProperties {
-  authorised: boolean;
-  model: string;
-}
-
 export interface Connection {
   adbSuccess: () => boolean;
   setProxyKey?(key: string): any;
diff --git a/tools/winscope/src/trace_collection/proxy_client.ts b/tools/winscope/src/trace_collection/proxy_client.ts
index 02d33f3..f1ccfbc 100644
--- a/tools/winscope/src/trace_collection/proxy_client.ts
+++ b/tools/winscope/src/trace_collection/proxy_client.ts
@@ -16,9 +16,17 @@
 
 import {OnProgressUpdateType} from 'common/function_utils';
 import {PersistentStore} from 'common/persistent_store';
-import {Device} from './connection';
 import {ConfigMap} from './trace_collection_utils';
 
+export interface Device {
+  [key: string]: DeviceProperties;
+}
+
+export interface DeviceProperties {
+  authorised: boolean;
+  model: string;
+}
+
 export enum ProxyState {
   ERROR = 0,
   CONNECTING = 1,
@@ -254,7 +262,7 @@
 // stores all the changing variables from proxy and sets up calls from ProxyRequest
 export class ProxyClient {
   readonly WINSCOPE_PROXY_URL = 'http://localhost:5544';
-  readonly VERSION = '1.0';
+  readonly VERSION = '1.1';
   state: ProxyState = ProxyState.CONNECTING;
   stateChangeListeners: Array<{(param: ProxyState, errorText: string): void}> = [];
   refresh_worker: NodeJS.Timer | null = null;
diff --git a/tools/winscope/src/trace_collection/proxy_connection.ts b/tools/winscope/src/trace_collection/proxy_connection.ts
index 231e0a5..d5a5389 100644
--- a/tools/winscope/src/trace_collection/proxy_connection.ts
+++ b/tools/winscope/src/trace_collection/proxy_connection.ts
@@ -15,8 +15,14 @@
  */
 
 import {FunctionUtils, OnProgressUpdateType} from 'common/function_utils';
-import {proxyClient, ProxyEndpoint, proxyRequest, ProxyState} from 'trace_collection/proxy_client';
-import {Connection, DeviceProperties} from './connection';
+import {
+  DeviceProperties,
+  proxyClient,
+  ProxyEndpoint,
+  proxyRequest,
+  ProxyState,
+} from 'trace_collection/proxy_client';
+import {Connection} from './connection';
 import {ConfigMap} from './trace_collection_utils';
 import {TracingConfig} from './tracing_config';
 
diff --git a/tools/winscope/src/trace_processor/bigint_math.ts b/tools/winscope/src/trace_processor/bigint_math.ts
new file mode 100644
index 0000000..dd1e550
--- /dev/null
+++ b/tools/winscope/src/trace_processor/bigint_math.ts
@@ -0,0 +1,111 @@
+// 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.
+
+export class BigintMath {
+  static INT64_MAX: bigint = (2n ** 63n) - 1n;
+  static INT64_MIN: bigint = -(2n ** 63n);
+
+  // Returns the smallest integral power of 2 that is not smaller than n.
+  // If n is less than or equal to 0, returns 1.
+  static bitCeil(n: bigint): bigint {
+    let result = 1n;
+    while (result < n) {
+      result <<= 1n;
+    }
+    return result;
+  };
+
+  // Returns the largest integral power of 2 which is not greater than n.
+  // If n is less than or equal to 0, returns 1.
+  static bitFloor(n: bigint): bigint {
+    let result = 1n;
+    while ((result << 1n) <= n) {
+      result <<= 1n;
+    }
+    return result;
+  };
+
+  // Returns the largest integral value x where 2^x is not greater than n.
+  static log2(n: bigint): number {
+    let result = 1n;
+    let log2 = 0;
+    while ((result << 1n) <= n) {
+      result <<= 1n;
+      ++log2;
+    }
+    return log2;
+  }
+
+  // Returns the integral multiple of step which is closest to n.
+  // If step is less than or equal to 0, returns n.
+  static quant(n: bigint, step: bigint): bigint {
+    step = BigintMath.max(1n, step);
+    const halfStep = step / 2n;
+    return step * ((n + halfStep) / step);
+  }
+
+  // Returns the largest integral multiple of step which is not larger than n.
+  // If step is less than or equal to 0, returns n.
+  static quantFloor(n: bigint, step: bigint): bigint {
+    step = BigintMath.max(1n, step);
+    return step * (n / step);
+  }
+
+  // Returns the smallest integral multiple of step which is not smaller than n.
+  // If step is less than or equal to 0, returns n.
+  static quantCeil(n: bigint, step: bigint): bigint {
+    step = BigintMath.max(1n, step);
+    const remainder = n % step;
+    if (remainder === 0n) {
+      return n;
+    }
+    const quotient = n / step;
+    return (quotient + 1n) * step;
+  }
+
+  // Returns the greater of a and b.
+  static max(a: bigint, b: bigint): bigint {
+    return a > b ? a : b;
+  }
+
+  // Returns the smaller of a and b.
+  static min(a: bigint, b: bigint): bigint {
+    return a < b ? a : b;
+  }
+
+  // Returns the number of 1 bits in n.
+  static popcount(n: bigint): number {
+    if (n < 0n) {
+      throw Error(`Can\'t get popcount of negative number ${n}`);
+    }
+    let count = 0;
+    while (n) {
+      if (n & 1n) {
+        ++count;
+      }
+      n >>= 1n;
+    }
+    return count;
+  }
+
+  // Return the ratio between two bigints as a number.
+  static ratio(dividend: bigint, divisor: bigint): number {
+    return Number(dividend) / Number(divisor);
+  }
+
+  // Calculates the absolute value of a n.
+  static abs(n: bigint) {
+    return n < 0n ? -1n * n : n;
+  }
+}
diff --git a/tools/winscope/src/trace_processor/deferred.ts b/tools/winscope/src/trace_processor/deferred.ts
new file mode 100644
index 0000000..114fb0c
--- /dev/null
+++ b/tools/winscope/src/trace_processor/deferred.ts
@@ -0,0 +1,27 @@
+// Copyright (C) 2018 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.
+
+// Promise wrapper with exposed resolve and reject callbacks.
+export interface Deferred<T> extends Promise<T> {
+  readonly resolve: (value?: T|PromiseLike<T>) => void;
+  readonly reject: (reason?: any) => void;
+}
+
+// Create a promise with exposed resolve and reject callbacks.
+export function defer<T>(): Deferred<T> {
+  let resolve = null as any;
+  let reject = null as any;
+  const p = new Promise((res, rej) => [resolve, reject] = [res, rej]);
+  return Object.assign(p, {resolve, reject}) as any;
+}
diff --git a/tools/winscope/src/trace_processor/engine.ts b/tools/winscope/src/trace_processor/engine.ts
new file mode 100644
index 0000000..865f37a
--- /dev/null
+++ b/tools/winscope/src/trace_processor/engine.ts
@@ -0,0 +1,438 @@
+// Copyright (C) 2018 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 {defer, Deferred} from './deferred';
+import {assertExists, assertTrue} from './logging';
+import {perfetto} from '../../deps_build/trace_processor/ui/tsc/gen/protos';
+
+import {ProtoRingBuffer} from './proto_ring_buffer';
+import {
+  ComputeMetricArgs,
+  ComputeMetricResult,
+  DisableAndReadMetatraceResult,
+  QueryArgs,
+  ResetTraceProcessorArgs,
+} from './protos';
+import {
+  createQueryResult,
+  LONG,
+  LONG_NULL,
+  NUM,
+  QueryError,
+  QueryResult,
+  STR,
+  WritableQueryResult,
+} from './query_result';
+
+import TraceProcessorRpc = perfetto.protos.TraceProcessorRpc;
+import TraceProcessorRpcStream = perfetto.protos.TraceProcessorRpcStream;
+import TPM = perfetto.protos.TraceProcessorRpc.TraceProcessorMethod;
+
+export interface LoadingTracker {
+  beginLoading(): void;
+  endLoading(): void;
+}
+
+export class NullLoadingTracker implements LoadingTracker {
+  beginLoading(): void {}
+  endLoading(): void {}
+}
+
+
+// This is used to skip the decoding of queryResult from protobufjs and deal
+// with it ourselves. See the comment below around `QueryResult.decode = ...`.
+interface QueryResultBypass {
+  rawQueryResult: Uint8Array;
+}
+
+export interface TraceProcessorConfig {
+  cropTrackEvents: boolean;
+  ingestFtraceInRawTable: boolean;
+  analyzeTraceProtoContent: boolean;
+}
+
+// Abstract interface of a trace proccessor.
+// This is the TypeScript equivalent of src/trace_processor/rpc.h.
+// There are two concrete implementations:
+//   1. WasmEngineProxy: creates a Wasm module and interacts over postMessage().
+//   2. HttpRpcEngine: connects to an external `trace_processor_shell --httpd`.
+//      and interacts via fetch().
+// In both cases, we have a byte-oriented pipe to interact with TraceProcessor.
+// The derived class is only expected to deal with these two functions:
+// 1. Implement the abstract rpcSendRequestBytes() function, sending the
+//    proto-encoded TraceProcessorRpc requests to the TraceProcessor instance.
+// 2. Call onRpcResponseBytes() when response data is received.
+export abstract class Engine {
+  abstract readonly id: string;
+  private _cpus?: number[];
+  private _numGpus?: number;
+  private loadingTracker: LoadingTracker;
+  private txSeqId = 0;
+  private rxSeqId = 0;
+  private rxBuf = new ProtoRingBuffer();
+  private pendingParses = new Array<Deferred<void>>();
+  private pendingEOFs = new Array<Deferred<void>>();
+  private pendingResetTraceProcessors = new Array<Deferred<void>>();
+  private pendingQueries = new Array<WritableQueryResult>();
+  private pendingRestoreTables = new Array<Deferred<void>>();
+  private pendingComputeMetrics = new Array<Deferred<ComputeMetricResult>>();
+  private pendingReadMetatrace?: Deferred<DisableAndReadMetatraceResult>;
+  private _isMetatracingEnabled = false;
+
+  constructor(tracker?: LoadingTracker) {
+    this.loadingTracker = tracker ? tracker : new NullLoadingTracker();
+  }
+
+  // Called to send data to the TraceProcessor instance. This turns into a
+  // postMessage() or a HTTP request, depending on the Engine implementation.
+  abstract rpcSendRequestBytes(data: Uint8Array): void;
+
+  // Called when an inbound message is received by the Engine implementation
+  // (e.g. onmessage for the Wasm case, on when HTTP replies are received for
+  // the HTTP+RPC case).
+  onRpcResponseBytes(dataWillBeRetained: Uint8Array) {
+    // Note: when hitting the fastpath inside ProtoRingBuffer, the |data| buffer
+    // is returned back by readMessage() (% subarray()-ing it) and held onto by
+    // other classes (e.g., QueryResult). For both fetch() and Wasm we are fine
+    // because every response creates a new buffer.
+    this.rxBuf.append(dataWillBeRetained);
+    for (;;) {
+      const msg = this.rxBuf.readMessage();
+      if (msg === undefined) break;
+      this.onRpcResponseMessage(msg);
+    }
+  }
+
+  // Parses a response message.
+  // |rpcMsgEncoded| is a sub-array to to the start of a TraceProcessorRpc
+  // proto-encoded message (without the proto preamble and varint size).
+  private onRpcResponseMessage(rpcMsgEncoded: Uint8Array) {
+    // Here we override the protobufjs-generated code to skip the parsing of the
+    // new streaming QueryResult and instead passing it through like a buffer.
+    // This is the overall problem: All trace processor responses are wrapped
+    // into a perfetto.protos.TraceProcessorRpc proto message. In all cases %
+    // TPM_QUERY_STREAMING, we want protobufjs to decode the proto bytes and
+    // give us a structured object. In the case of TPM_QUERY_STREAMING, instead,
+    // we want to deal with the proto parsing ourselves using the new
+    // QueryResult.appendResultBatch() method, because that handled streaming
+    // results more efficiently and skips several copies.
+    // By overriding the decode method below, we achieve two things:
+    // 1. We avoid protobufjs decoding the TraceProcessorRpc.query_result field.
+    // 2. We stash (a view of) the original buffer into the |rawQueryResult| so
+    //    the `case TPM_QUERY_STREAMING` below can take it.
+    perfetto.protos.QueryResult.decode =
+        (reader: protobuf.Reader, length: number) => {
+          const res =
+              perfetto.protos.QueryResult.create() as {} as QueryResultBypass;
+          res.rawQueryResult =
+              reader.buf.subarray(reader.pos, reader.pos + length);
+          // All this works only if protobufjs returns the original ArrayBuffer
+          // from |rpcMsgEncoded|. It should be always the case given the
+          // current implementation. This check mainly guards against future
+          // behavioral changes of protobufjs. We don't want to accidentally
+          // hold onto some internal protobufjs buffer. We are fine holding
+          // onto |rpcMsgEncoded| because those come from ProtoRingBuffer which
+          // is buffer-retention-friendly.
+          assertTrue(res.rawQueryResult.buffer === rpcMsgEncoded.buffer);
+          reader.pos += length;
+          return res as {} as perfetto.protos.QueryResult;
+        };
+
+    const rpc = TraceProcessorRpc.decode(rpcMsgEncoded);
+
+    if (rpc.fatalError !== undefined && rpc.fatalError.length > 0) {
+      throw new Error(`${rpc.fatalError}`);
+    }
+
+    // Allow restarting sequences from zero (when reloading the browser).
+    if (rpc.seq !== this.rxSeqId + 1 && this.rxSeqId !== 0 && rpc.seq !== 0) {
+      // "(ERR:rpc_seq)" is intercepted by error_dialog.ts to show a more
+      // graceful and actionable error.
+      throw new Error(`RPC sequence id mismatch cur=${rpc.seq} last=${
+          this.rxSeqId} (ERR:rpc_seq)`);
+    }
+
+    this.rxSeqId = rpc.seq;
+
+    let isFinalResponse = true;
+
+    switch (rpc.response) {
+      case TPM.TPM_APPEND_TRACE_DATA:
+        const appendResult = assertExists(rpc.appendResult);
+        const pendingPromise = assertExists(this.pendingParses.shift());
+        if (appendResult.error && appendResult.error.length > 0) {
+          pendingPromise.reject(appendResult.error);
+        } else {
+          pendingPromise.resolve();
+        }
+        break;
+      case TPM.TPM_FINALIZE_TRACE_DATA:
+        assertExists(this.pendingEOFs.shift()).resolve();
+        break;
+      case TPM.TPM_RESET_TRACE_PROCESSOR:
+        assertExists(this.pendingResetTraceProcessors.shift()).resolve();
+        break;
+      case TPM.TPM_RESTORE_INITIAL_TABLES:
+        assertExists(this.pendingRestoreTables.shift()).resolve();
+        break;
+      case TPM.TPM_QUERY_STREAMING:
+        const qRes = assertExists(rpc.queryResult) as {} as QueryResultBypass;
+        const pendingQuery = assertExists(this.pendingQueries[0]);
+        pendingQuery.appendResultBatch(qRes.rawQueryResult);
+        if (pendingQuery.isComplete()) {
+          this.pendingQueries.shift();
+        } else {
+          isFinalResponse = false;
+        }
+        break;
+      case TPM.TPM_COMPUTE_METRIC:
+        const metricRes = assertExists(rpc.metricResult) as ComputeMetricResult;
+        const pendingComputeMetric =
+            assertExists(this.pendingComputeMetrics.shift());
+        if (metricRes.error && metricRes.error.length > 0) {
+          const error =
+              new QueryError(`ComputeMetric() error: ${metricRes.error}`, {
+                query: 'COMPUTE_METRIC',
+              });
+          pendingComputeMetric.reject(error);
+        } else {
+          pendingComputeMetric.resolve(metricRes);
+        }
+        break;
+      case TPM.TPM_DISABLE_AND_READ_METATRACE:
+        const metatraceRes =
+            assertExists(rpc.metatrace) as DisableAndReadMetatraceResult;
+        assertExists(this.pendingReadMetatrace).resolve(metatraceRes);
+        this.pendingReadMetatrace = undefined;
+        break;
+      default:
+        console.log(
+            'Unexpected TraceProcessor response received: ', rpc.response);
+        break;
+    }  // switch(rpc.response);
+
+    if (isFinalResponse) {
+      this.loadingTracker.endLoading();
+    }
+  }
+
+  // TraceProcessor methods below this point.
+  // The methods below are called by the various controllers in the UI and
+  // deal with marshalling / unmarshaling requests to/from TraceProcessor.
+
+
+  // Push trace data into the engine. The engine is supposed to automatically
+  // figure out the type of the trace (JSON vs Protobuf).
+  parse(data: Uint8Array): Promise<void> {
+    const asyncRes = defer<void>();
+    this.pendingParses.push(asyncRes);
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_APPEND_TRACE_DATA;
+    rpc.appendTraceData = data;
+    this.rpcSendRequest(rpc);
+    return asyncRes;  // Linearize with the worker.
+  }
+
+  // Notify the engine that we reached the end of the trace.
+  // Called after the last parse() call.
+  notifyEof(): Promise<void> {
+    const asyncRes = defer<void>();
+    this.pendingEOFs.push(asyncRes);
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_FINALIZE_TRACE_DATA;
+    this.rpcSendRequest(rpc);
+    return asyncRes;  // Linearize with the worker.
+  }
+
+  // Updates the TraceProcessor Config. This method creates a new
+  // TraceProcessor instance, so it should be called before passing any trace
+  // data.
+  resetTraceProcessor(
+      {cropTrackEvents, ingestFtraceInRawTable, analyzeTraceProtoContent}:
+          TraceProcessorConfig): Promise<void> {
+    const asyncRes = defer<void>();
+    this.pendingResetTraceProcessors.push(asyncRes);
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_RESET_TRACE_PROCESSOR;
+    const args = rpc.resetTraceProcessorArgs = new ResetTraceProcessorArgs();
+    args.dropTrackEventDataBefore = cropTrackEvents ?
+        ResetTraceProcessorArgs.DropTrackEventDataBefore
+            .TRACK_EVENT_RANGE_OF_INTEREST :
+        ResetTraceProcessorArgs.DropTrackEventDataBefore.NO_DROP;
+    args.ingestFtraceInRawTable = ingestFtraceInRawTable;
+    args.analyzeTraceProtoContent = analyzeTraceProtoContent;
+    this.rpcSendRequest(rpc);
+    return asyncRes;
+  }
+
+  // Resets the trace processor state by destroying any table/views created by
+  // the UI after loading.
+  restoreInitialTables(): Promise<void> {
+    const asyncRes = defer<void>();
+    this.pendingRestoreTables.push(asyncRes);
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_RESTORE_INITIAL_TABLES;
+    this.rpcSendRequest(rpc);
+    return asyncRes;  // Linearize with the worker.
+  }
+
+  // Shorthand for sending a compute metrics request to the engine.
+  async computeMetric(metrics: string[]): Promise<ComputeMetricResult> {
+    const asyncRes = defer<ComputeMetricResult>();
+    this.pendingComputeMetrics.push(asyncRes);
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_COMPUTE_METRIC;
+    const args = rpc.computeMetricArgs = new ComputeMetricArgs();
+    args.metricNames = metrics;
+    args.format = ComputeMetricArgs.ResultFormat.TEXTPROTO;
+    this.rpcSendRequest(rpc);
+    return asyncRes;
+  }
+
+  // Issues a streaming query and retrieve results in batches.
+  // The returned QueryResult object will be populated over time with batches
+  // of rows (each batch conveys ~128KB of data and a variable number of rows).
+  // The caller can decide whether to wait that all batches have been received
+  // (by awaiting the returned object or calling result.waitAllRows()) or handle
+  // the rows incrementally.
+  //
+  // Example usage:
+  // const res = engine.query('SELECT foo, bar FROM table');
+  // console.log(res.numRows());  // Will print 0 because we didn't await.
+  // await(res.waitAllRows());
+  // console.log(res.numRows());  // Will print the total number of rows.
+  //
+  // for (const it = res.iter({foo: NUM, bar:STR}); it.valid(); it.next()) {
+  //   console.log(it.foo, it.bar);
+  // }
+  //
+  // Optional |tag| (usually a component name) can be provided to allow
+  // attributing trace processor workload to different UI components.
+  query(sqlQuery: string, tag?: string): Promise<QueryResult>&QueryResult {
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_QUERY_STREAMING;
+    rpc.queryArgs = new QueryArgs();
+    rpc.queryArgs.sqlQuery = sqlQuery;
+    if (tag) {
+      rpc.queryArgs.tag = tag;
+    }
+    const result = createQueryResult({
+      query: sqlQuery,
+    });
+    this.pendingQueries.push(result);
+    this.rpcSendRequest(rpc);
+    return result;
+  }
+
+  isMetatracingEnabled(): boolean {
+    return this._isMetatracingEnabled;
+  }
+
+  enableMetatrace(categories?: perfetto.protos.MetatraceCategories) {
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_ENABLE_METATRACE;
+    if (categories) {
+      rpc.enableMetatraceArgs = new perfetto.protos.EnableMetatraceArgs();
+      rpc.enableMetatraceArgs.categories = categories;
+    }
+    this._isMetatracingEnabled = true;
+    this.rpcSendRequest(rpc);
+  }
+
+  stopAndGetMetatrace(): Promise<DisableAndReadMetatraceResult> {
+    // If we are already finalising a metatrace, ignore the request.
+    if (this.pendingReadMetatrace) {
+      return Promise.reject(new Error('Already finalising a metatrace'));
+    }
+
+    const result = defer<DisableAndReadMetatraceResult>();
+
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_DISABLE_AND_READ_METATRACE;
+    this._isMetatracingEnabled = false;
+    this.pendingReadMetatrace = result;
+    this.rpcSendRequest(rpc);
+    return result;
+  }
+
+  // Marshals the TraceProcessorRpc request arguments and sends the request
+  // to the concrete Engine (Wasm or HTTP).
+  private rpcSendRequest(rpc: TraceProcessorRpc) {
+    rpc.seq = this.txSeqId++;
+    // Each message is wrapped in a TraceProcessorRpcStream to add the varint
+    // preamble with the size, which allows tokenization on the other end.
+    const outerProto = TraceProcessorRpcStream.create();
+    outerProto.msg.push(rpc);
+    const buf = TraceProcessorRpcStream.encode(outerProto).finish();
+    this.loadingTracker.beginLoading();
+    this.rpcSendRequestBytes(buf);
+  }
+
+  // TODO(hjd): When streaming must invalidate this somehow.
+  async getCpus(): Promise<number[]> {
+    if (!this._cpus) {
+      const cpus = [];
+      const queryRes = await this.query(
+          'select distinct(cpu) as cpu from sched order by cpu;');
+      for (const it = queryRes.iter({cpu: NUM}); it.valid(); it.next()) {
+        cpus.push(it.cpu);
+      }
+      this._cpus = cpus;
+    }
+    return this._cpus;
+  }
+
+  async getNumberOfGpus(): Promise<number> {
+    if (!this._numGpus) {
+      const result = await this.query(`
+        select count(distinct(gpu_id)) as gpuCount
+        from gpu_counter_track
+        where name = 'gpufreq';
+      `);
+      this._numGpus = result.firstRow({gpuCount: NUM}).gpuCount;
+    }
+    return this._numGpus;
+  }
+
+  // TODO: This should live in code that's more specific to chrome, instead of
+  // in engine.
+  async getNumberOfProcesses(): Promise<number> {
+    const result = await this.query('select count(*) as cnt from process;');
+    return result.firstRow({cnt: NUM}).cnt;
+  }
+
+  getProxy(tag: string): EngineProxy {
+    return new EngineProxy(this, tag);
+  }
+}
+
+// Lightweight wrapper over Engine exposing only `query` method and annotating
+// all queries going through it with a tag.
+export class EngineProxy {
+  private engine: Engine;
+  private tag: string;
+
+  constructor(engine: Engine, tag: string) {
+    this.engine = engine;
+    this.tag = tag;
+  }
+
+  query(sqlQuery: string, tag?: string): Promise<QueryResult>&QueryResult {
+    return this.engine.query(sqlQuery, tag || this.tag);
+  }
+
+  get engineId(): string {
+    return this.engine.id;
+  }
+}
diff --git a/tools/winscope/src/trace_processor/logging.ts b/tools/winscope/src/trace_processor/logging.ts
new file mode 100644
index 0000000..665488f
--- /dev/null
+++ b/tools/winscope/src/trace_processor/logging.ts
@@ -0,0 +1,77 @@
+// Copyright (C) 2018 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 {SCM_REVISION, VERSION} from '../../deps_build/trace_processor/ui/tsc/gen/perfetto_version';
+
+export type ErrorHandler = (err: string) => void;
+
+let errorHandler: ErrorHandler = (_: string) => {};
+
+export function assertExists<A>(value: A | null | undefined): A {
+  if (value === null || value === undefined) {
+    throw new Error('Value doesn\'t exist');
+  }
+  return value;
+}
+
+export function assertTrue(value: boolean, optMsg?: string) {
+  if (!value) {
+    throw new Error(optMsg ?? 'Failed assertion');
+  }
+}
+
+export function assertFalse(value: boolean, optMsg?: string) {
+  assertTrue(!value, optMsg);
+}
+
+export function setErrorHandler(handler: ErrorHandler) {
+  errorHandler = handler;
+}
+
+export function reportError(err: ErrorEvent|PromiseRejectionEvent|{}) {
+  let errLog = '';
+  let errorObj = undefined;
+
+  if (err instanceof ErrorEvent) {
+    errLog = err.message;
+    errorObj = err.error;
+  } else if (err instanceof PromiseRejectionEvent) {
+    errLog = `${err.reason}`;
+    errorObj = err.reason;
+  } else {
+    errLog = `${err}`;
+  }
+  if (errorObj !== undefined && errorObj !== null) {
+    const errStack = (errorObj as {stack?: string}).stack;
+    errLog += '\n';
+    errLog += errStack !== undefined ? errStack : JSON.stringify(errorObj);
+  }
+  errLog += '\n\n';
+  errLog += `${VERSION} ${SCM_REVISION}\n`;
+  errLog += `UA: ${navigator.userAgent}\n`;
+
+  console.error(errLog, err);
+  errorHandler(errLog);
+}
+
+// This function serves two purposes.
+// 1) A runtime check - if we are ever called, we throw an exception.
+// This is useful for checking that code we suspect should never be reached is
+// actually never reached.
+// 2) A compile time check where typescript asserts that the value passed can be
+// cast to the "never" type.
+// This is useful for ensuring we exhastively check union types.
+export function assertUnreachable(_x: never) {
+  throw new Error('This code should not be reachable');
+}
diff --git a/tools/winscope/src/trace_processor/proto_ring_buffer.ts b/tools/winscope/src/trace_processor/proto_ring_buffer.ts
new file mode 100644
index 0000000..da878ce
--- /dev/null
+++ b/tools/winscope/src/trace_processor/proto_ring_buffer.ts
@@ -0,0 +1,156 @@
+// 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 {assertTrue} from './logging';
+
+// This class is the TypeScript equivalent of the identically-named C++ class in
+// //protozero/proto_ring_buffer.h. See comments in that header for a detailed
+// description. The architecture is identical.
+
+const kGrowBytes = 128 * 1024;
+const kMaxMsgSize = 64 * 1024 * 1024;
+
+export class ProtoRingBuffer {
+  private buf = new Uint8Array(kGrowBytes);
+  private fastpath?: Uint8Array;
+  private rd = 0;
+  private wr = 0;
+
+  // The caller must call ReadMessage() after each append() call.
+  // The |data| might be either copied in the internal ring buffer or returned
+  // (% subarray()) to the next ReadMessage() call.
+  append(data: Uint8Array) {
+    assertTrue(this.wr <= this.buf.length);
+    assertTrue(this.rd <= this.wr);
+
+    // If the last call to ReadMessage() consumed all the data in the buffer and
+    // there are no incomplete messages pending, restart from the beginning
+    // rather than keep ringing. This is the most common case.
+    if (this.rd === this.wr) {
+      this.rd = this.wr = 0;
+    }
+
+    // The caller is expected to issue a ReadMessage() after each append().
+    const dataLen = data.length;
+    if (dataLen === 0) return;
+    assertTrue(this.fastpath === undefined);
+    if (this.rd === this.wr) {
+      const msg = ProtoRingBuffer.tryReadMessage(data, 0, dataLen);
+      if (msg !== undefined &&
+          ((msg.byteOffset + msg.length) === (data.byteOffset + dataLen))) {
+        // Fastpath: in many cases, the underlying stream will effectively
+        // preserve the atomicity of messages for most small messages.
+        // In this case we can avoid the extra buffer roundtrip and return the
+        // original array (actually a subarray that skips the proto header).
+        // The next call to ReadMessage() will return this.
+        this.fastpath = msg;
+        return;
+      }
+    }
+
+    let avail = this.buf.length - this.wr;
+    if (dataLen > avail) {
+      // This whole section should be hit extremely rarely.
+
+      // Try first just recompacting the buffer by moving everything to the
+      // left. This can happen if we received "a message and a bit" on each
+      // append() call.
+      this.buf.copyWithin(0, this.rd, this.wr);
+      avail += this.rd;
+      this.wr -= this.rd;
+      this.rd = 0;
+      if (dataLen > avail) {
+        // Still not enough, expand the buffer.
+        let newSize = this.buf.length;
+        while (dataLen > newSize - this.wr) {
+          newSize += kGrowBytes;
+        }
+        assertTrue(newSize <= kMaxMsgSize * 2);
+        const newBuf = new Uint8Array(newSize);
+        newBuf.set(this.buf);
+        this.buf = newBuf;
+        // No need to touch rd / wr.
+      }
+    }
+
+    // Append the received data at the end of the ring buffer.
+    this.buf.set(data, this.wr);
+    this.wr += dataLen;
+  }
+
+  // Tries to extract a message from the ring buffer. If there is no message,
+  // or if the current message is still incomplete, returns undefined.
+  // The caller is expected to call this in a loop until it returns undefined.
+  // Note that a single write to Append() can yield more than one message
+  // (see ProtoRingBufferTest.CoalescingStream in the unittest).
+  readMessage(): Uint8Array|undefined {
+    if (this.fastpath !== undefined) {
+      assertTrue(this.rd === this.wr);
+      const msg = this.fastpath;
+      this.fastpath = undefined;
+      return msg;
+    }
+    assertTrue(this.rd <= this.wr);
+    if (this.rd >= this.wr) {
+      return undefined;  // Completely empty.
+    }
+    const msg = ProtoRingBuffer.tryReadMessage(this.buf, this.rd, this.wr);
+    if (msg === undefined) return undefined;
+    assertTrue(msg.buffer === this.buf.buffer);
+    assertTrue(this.buf.byteOffset === 0);
+    this.rd = msg.byteOffset + msg.length;
+
+    // Deliberately returning a copy of the data with slice(). In various cases
+    // (streaming query response) the caller will hold onto the returned buffer.
+    // If we get to this point, |msg| is a view of the circular buffer that we
+    // will overwrite on the next calls to append().
+    return msg.slice();
+  }
+
+  private static tryReadMessage(
+      data: Uint8Array, dataStart: number, dataEnd: number): Uint8Array
+      |undefined {
+    assertTrue(dataEnd <= data.length);
+    let pos = dataStart;
+    if (pos >= dataEnd) return undefined;
+    const tag = data[pos++];  // Assume one-byte tag.
+    if (tag >= 0x80 || (tag & 0x07) !== 2 /* len delimited */) {
+      throw new Error(
+          `RPC framing error, unexpected tag ${tag} @ offset ${pos - 1}`);
+    }
+
+    let len = 0;
+    for (let shift = 0; /* no check */; shift += 7) {
+      if (pos >= dataEnd) {
+        return undefined;  // Not enough data to read varint.
+      }
+      const val = data[pos++];
+      len |= ((val & 0x7f) << shift) >>> 0;
+      if (val < 0x80) break;
+    }
+
+    if (len >= kMaxMsgSize) {
+      throw new Error(
+          `RPC framing error, message too large (${len} > ${kMaxMsgSize}`);
+    }
+    const end = pos + len;
+    if (end > dataEnd) return undefined;
+
+    // This is a subarray() and not a slice() because in the |fastpath| case
+    // we want to just return the original buffer pushed by append().
+    // In the slow-path (ring-buffer) case, the readMessage() above will create
+    // a copy via slice() before returning it.
+    return data.subarray(pos, end);
+  }
+}
diff --git a/tools/winscope/src/trace_processor/protos.ts b/tools/winscope/src/trace_processor/protos.ts
new file mode 100644
index 0000000..f042bf2
--- /dev/null
+++ b/tools/winscope/src/trace_processor/protos.ts
@@ -0,0 +1,134 @@
+// Copyright (C) 2018 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 protos from '../../deps_build/trace_processor/ui/tsc/gen/protos';
+
+// Aliases protos to avoid the super nested namespaces.
+// See https://www.typescriptlang.org/docs/handbook/namespaces.html#aliases
+import AndroidLogConfig = protos.perfetto.protos.AndroidLogConfig;
+import AndroidPowerConfig = protos.perfetto.protos.AndroidPowerConfig;
+import AndroidLogId = protos.perfetto.protos.AndroidLogId;
+import BatteryCounters =
+    protos.perfetto.protos.AndroidPowerConfig.BatteryCounters;
+import BufferConfig = protos.perfetto.protos.TraceConfig.BufferConfig;
+import ChromeConfig = protos.perfetto.protos.ChromeConfig;
+import TrackEventConfig = protos.perfetto.protos.TrackEventConfig;
+import ConsumerPort = protos.perfetto.protos.ConsumerPort;
+import NetworkPacketTraceConfig =
+    protos.perfetto.protos.NetworkPacketTraceConfig;
+import NativeContinuousDumpConfig =
+    protos.perfetto.protos.HeapprofdConfig.ContinuousDumpConfig;
+import JavaContinuousDumpConfig =
+    protos.perfetto.protos.JavaHprofConfig.ContinuousDumpConfig;
+import DataSourceConfig = protos.perfetto.protos.DataSourceConfig;
+import DataSourceDescriptor = protos.perfetto.protos.DataSourceDescriptor;
+import FtraceConfig = protos.perfetto.protos.FtraceConfig;
+import HeapprofdConfig = protos.perfetto.protos.HeapprofdConfig;
+import JavaHprofConfig = protos.perfetto.protos.JavaHprofConfig;
+import IAndroidPowerConfig = protos.perfetto.protos.IAndroidPowerConfig;
+import IBufferConfig = protos.perfetto.protos.TraceConfig.IBufferConfig;
+import IProcessStatsConfig = protos.perfetto.protos.IProcessStatsConfig;
+import ISysStatsConfig = protos.perfetto.protos.ISysStatsConfig;
+import ITraceConfig = protos.perfetto.protos.ITraceConfig;
+import MeminfoCounters = protos.perfetto.protos.MeminfoCounters;
+import ProcessStatsConfig = protos.perfetto.protos.ProcessStatsConfig;
+import StatCounters = protos.perfetto.protos.SysStatsConfig.StatCounters;
+import SysStatsConfig = protos.perfetto.protos.SysStatsConfig;
+import TraceConfig = protos.perfetto.protos.TraceConfig;
+import VmstatCounters = protos.perfetto.protos.VmstatCounters;
+import IPCFrame = protos.perfetto.protos.IPCFrame;
+import IMethodInfo =
+    protos.perfetto.protos.IPCFrame.BindServiceReply.IMethodInfo;
+import IBufferStats = protos.perfetto.protos.TraceStats.IBufferStats;
+import ISlice = protos.perfetto.protos.ReadBuffersResponse.ISlice;
+import EnableTracingRequest = protos.perfetto.protos.EnableTracingRequest;
+import DisableTracingRequest = protos.perfetto.protos.DisableTracingRequest;
+import GetTraceStatsRequest = protos.perfetto.protos.GetTraceStatsRequest;
+import FreeBuffersRequest = protos.perfetto.protos.FreeBuffersRequest;
+import ReadBuffersRequest = protos.perfetto.protos.ReadBuffersRequest;
+import QueryServiceStateRequest =
+    protos.perfetto.protos.QueryServiceStateRequest;
+import EnableTracingResponse = protos.perfetto.protos.EnableTracingResponse;
+import DisableTracingResponse = protos.perfetto.protos.DisableTracingResponse;
+import GetTraceStatsResponse = protos.perfetto.protos.GetTraceStatsResponse;
+import FreeBuffersResponse = protos.perfetto.protos.FreeBuffersResponse;
+import ReadBuffersResponse = protos.perfetto.protos.ReadBuffersResponse;
+import QueryServiceStateResponse =
+    protos.perfetto.protos.QueryServiceStateResponse;
+// Trace Processor protos.
+import QueryArgs = protos.perfetto.protos.QueryArgs;
+import ResetTraceProcessorArgs = protos.perfetto.protos.ResetTraceProcessorArgs;
+import StatusResult = protos.perfetto.protos.StatusResult;
+import ComputeMetricArgs = protos.perfetto.protos.ComputeMetricArgs;
+import ComputeMetricResult = protos.perfetto.protos.ComputeMetricResult;
+import DisableAndReadMetatraceResult =
+    protos.perfetto.protos.DisableAndReadMetatraceResult;
+import Trace = protos.perfetto.protos.Trace;
+import TracePacket = protos.perfetto.protos.TracePacket;
+import PerfettoMetatrace = protos.perfetto.protos.PerfettoMetatrace;
+
+export {
+  AndroidLogConfig,
+  AndroidLogId,
+  AndroidPowerConfig,
+  BatteryCounters,
+  BufferConfig,
+  ChromeConfig,
+  ConsumerPort,
+  ComputeMetricArgs,
+  ComputeMetricResult,
+  DataSourceConfig,
+  DisableAndReadMetatraceResult,
+  DataSourceDescriptor,
+  DisableTracingRequest,
+  DisableTracingResponse,
+  EnableTracingRequest,
+  EnableTracingResponse,
+  FreeBuffersRequest,
+  FreeBuffersResponse,
+  FtraceConfig,
+  GetTraceStatsRequest,
+  GetTraceStatsResponse,
+  HeapprofdConfig,
+  IAndroidPowerConfig,
+  IBufferConfig,
+  IBufferStats,
+  IMethodInfo,
+  IPCFrame,
+  IProcessStatsConfig,
+  ISlice,
+  ISysStatsConfig,
+  ITraceConfig,
+  JavaContinuousDumpConfig,
+  JavaHprofConfig,
+  MeminfoCounters,
+  NativeContinuousDumpConfig,
+  NetworkPacketTraceConfig,
+  ProcessStatsConfig,
+  PerfettoMetatrace,
+  ReadBuffersRequest,
+  ReadBuffersResponse,
+  QueryServiceStateRequest,
+  QueryServiceStateResponse,
+  QueryArgs,
+  ResetTraceProcessorArgs,
+  StatCounters,
+  StatusResult,
+  SysStatsConfig,
+  Trace,
+  TraceConfig,
+  TrackEventConfig,
+  TracePacket,
+  VmstatCounters,
+};
diff --git a/tools/winscope/src/trace_processor/query_result.ts b/tools/winscope/src/trace_processor/query_result.ts
new file mode 100644
index 0000000..0185cf1
--- /dev/null
+++ b/tools/winscope/src/trace_processor/query_result.ts
@@ -0,0 +1,963 @@
+// 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.
+
+// This file deals with deserialization and iteration of the proto-encoded
+// byte buffer that is returned by TraceProcessor when invoking the
+// TPM_QUERY_STREAMING method. The returned |query_result| buffer is optimized
+// for being moved cheaply across workers and decoded on-the-flight as we step
+// through the iterator.
+// See comments around QueryResult in trace_processor.proto for more details.
+
+// The classes in this file are organized as follows:
+//
+// QueryResultImpl:
+// The object returned by the Engine.query(sql) method.
+// This object is a holder of row data. Batches of raw get appended
+// incrementally as they are received by the remote TraceProcessor instance.
+// QueryResultImpl also deals with asynchronicity of queries and allows callers
+// to obtain a promise that waits for more (or all) rows.
+// At any point in time the following objects hold a reference to QueryResult:
+// - The Engine: for appending row batches.
+// - UI code, typically controllers, who make queries.
+//
+// ResultBatch:
+// Hold the data, returned by the remote TraceProcessor instance, for a number
+// of rows (TP typically chunks the results in batches of 128KB).
+// A QueryResultImpl holds exclusively ResultBatches for a given query.
+// ResultBatch is not exposed externally, it's just an internal representation
+// that helps with proto decoding. ResultBatch is immutable after it gets
+// appended and decoded. The iteration state is held by the RowIteratorImpl.
+//
+// RowIteratorImpl:
+// Decouples the data owned by QueryResultImpl (and its ResultBatch(es)) from
+// the iteration state. The iterator effectively is the union of a ResultBatch
+// and the row number in it. Rows within the batch are decoded as the user calls
+// next(). When getting at the end of the batch, it takes care of switching to
+// the next batch (if any) within the QueryResultImpl.
+// This object is part of the API exposed to tracks / controllers.
+
+// Import below commented out to prevent the protobufjs initialiation with:
+// `protobuf.util.Long = undefined as any;`
+// The Winscope parsers need the 64-bit proto fields to be retrieved as Long instead of number,
+// otherwise data (e.g. state flags) would be lost because of the 53-bit integer limitation.
+// import './static_initializers';
+
+import protobuf from 'protobufjs/minimal';
+
+import {defer, Deferred} from './deferred';
+import {assertExists, assertFalse, assertTrue} from './logging';
+import {utf8Decode} from './string_utils';
+
+export const NUM = 0;
+export const STR = 'str';
+export const NUM_NULL: number|null = 1;
+export const STR_NULL: string|null = 'str_null';
+export const BLOB: Uint8Array = new Uint8Array();
+export const BLOB_NULL: Uint8Array|null = new Uint8Array();
+export const LONG: bigint = 0n;
+export const LONG_NULL: bigint|null = 1n;
+
+export type ColumnType = string|number|bigint|null|Uint8Array;
+export type SqlValue = ColumnType;
+
+const SHIFT_32BITS = 32n;
+
+// Fast decode varint int64 into a bigint
+// Inspired by
+// https://github.com/protobufjs/protobuf.js/blob/56b1e64979dae757b67a21d326e16acee39f2267/src/reader.js#L123
+export function decodeInt64Varint(buf: Uint8Array, pos: number): bigint {
+  let hi: number = 0;
+  let lo: number = 0;
+  let i = 0;
+
+  if (buf.length - pos > 4) {  // fast route (lo)
+    for (; i < 4; ++i) {
+      // 1st..4th
+      lo = (lo | (buf[pos] & 127) << i * 7) >>> 0;
+      if (buf[pos++] < 128) {
+        return BigInt(lo);
+      }
+    }
+    // 5th
+    lo = (lo | (buf[pos] & 127) << 28) >>> 0;
+    hi = (hi | (buf[pos] & 127) >> 4) >>> 0;
+    if (buf[pos++] < 128) {
+      return BigInt(hi) << SHIFT_32BITS | BigInt(lo);
+    }
+    i = 0;
+  } else {
+    for (; i < 3; ++i) {
+      if (pos >= buf.length) {
+        throw Error('Index out of range');
+      }
+      // 1st..3rd
+      lo = (lo | (buf[pos] & 127) << i * 7) >>> 0;
+      if (buf[pos++] < 128) {
+        return BigInt(lo);
+      }
+    }
+    // 4th
+    lo = (lo | (buf[pos++] & 127) << i * 7) >>> 0;
+    return BigInt(hi) << SHIFT_32BITS | BigInt(lo);
+  }
+  if (buf.length - pos > 4) {  // fast route (hi)
+    for (; i < 5; ++i) {
+      // 6th..10th
+      hi = (hi | (buf[pos] & 127) << i * 7 + 3) >>> 0;
+      if (buf[pos++] < 128) {
+        const big = BigInt(hi) << SHIFT_32BITS | BigInt(lo);
+        return BigInt.asIntN(64, big);
+      }
+    }
+  } else {
+    for (; i < 5; ++i) {
+      if (pos >= buf.length) {
+        throw Error('Index out of range');
+      }
+      // 6th..10th
+      hi = (hi | (buf[pos] & 127) << i * 7 + 3) >>> 0;
+      if (buf[pos++] < 128) {
+        const big = BigInt(hi) << SHIFT_32BITS | BigInt(lo);
+        return BigInt.asIntN(64, big);
+      }
+    }
+  }
+  throw Error('invalid varint encoding');
+}
+
+// Info that could help debug a query error. For example the query
+// in question, the stack where the query was issued, the active
+// plugin etc.
+export interface QueryErrorInfo {
+  query: string;
+}
+
+export class QueryError extends Error {
+  readonly query: string;
+
+  constructor(message: string, info: QueryErrorInfo) {
+    super(message);
+    this.query = info.query;
+  }
+
+  override toString() {
+    return `Query: ${this.query}\n` + super.toString();
+  }
+}
+
+// One row extracted from an SQL result:
+export interface Row {
+  [key: string]: ColumnType;
+}
+
+// The methods that any iterator has to implement.
+export interface RowIteratorBase {
+  valid(): boolean;
+  next(): void;
+
+  // Reflection support for cases where the column names are not known upfront
+  // (e.g. the query result table for user-provided SQL queries).
+  // It throws if the passed column name doesn't exist.
+  // Example usage:
+  // for (const it = queryResult.iter({}); it.valid(); it.next()) {
+  //   for (const columnName : queryResult.columns()) {
+  //      console.log(it.get(columnName));
+  get(columnName: string): ColumnType;
+}
+
+// A RowIterator is a type that has all the fields defined in the query spec
+// plus the valid() and next() operators. This is to ultimately allow the
+// clients to do:
+// const result = await engine.query("select name, surname, id from people;");
+// const iter = queryResult.iter({name: STR, surname: STR, id: NUM});
+// for (; iter.valid(); iter.next())
+//  console.log(iter.name, iter.surname);
+export type RowIterator<T extends Row> = RowIteratorBase&T;
+
+function columnTypeToString(t: ColumnType): string {
+  switch (t) {
+    case NUM:
+      return 'NUM';
+    case NUM_NULL:
+      return 'NUM_NULL';
+    case STR:
+      return 'STR';
+    case STR_NULL:
+      return 'STR_NULL';
+    case BLOB:
+      return 'BLOB';
+    case BLOB_NULL:
+      return 'BLOB_NULL';
+    case LONG:
+      return 'LONG';
+    case LONG_NULL:
+      return 'LONG_NULL';
+    default:
+      return `INVALID(${t})`;
+  }
+}
+
+function isCompatible(actual: CellType, expected: ColumnType): boolean {
+  switch (actual) {
+    case CellType.CELL_NULL:
+      return expected === NUM_NULL || expected === STR_NULL ||
+          expected === BLOB_NULL || expected === LONG_NULL;
+    case CellType.CELL_VARINT:
+      return expected === NUM || expected === NUM_NULL || expected === LONG ||
+          expected === LONG_NULL;
+    case CellType.CELL_FLOAT64:
+      return expected === NUM || expected === NUM_NULL;
+    case CellType.CELL_STRING:
+      return expected === STR || expected === STR_NULL;
+    case CellType.CELL_BLOB:
+      return expected === BLOB || expected === BLOB_NULL;
+    default:
+      throw new Error(`Unknown CellType ${actual}`);
+  }
+}
+
+// This has to match CellType in trace_processor.proto.
+enum CellType {
+  CELL_NULL = 1,
+  CELL_VARINT = 2,
+  CELL_FLOAT64 = 3,
+  CELL_STRING = 4,
+  CELL_BLOB = 5,
+}
+
+const CELL_TYPE_NAMES =
+    ['UNKNOWN', 'NULL', 'VARINT', 'FLOAT64', 'STRING', 'BLOB'];
+
+const TAG_LEN_DELIM = 2;
+
+// This is the interface exposed to readers (e.g. tracks). The underlying object
+// (QueryResultImpl) owns the result data. This allows to obtain iterators on
+// that. In future it will allow to wait for incremental updates (new rows being
+// fetched) for streaming queries.
+export interface QueryResult {
+  // Obtains an iterator.
+  // TODO(primiano): this should have an option to destruct data as we read. In
+  // the case of a long query (e.g. `SELECT * FROM sched` in the query prompt)
+  // we don't want to accumulate everything in memory. OTOH UI tracks want to
+  // keep the data around so they can redraw them on each animation frame. For
+  // now we keep everything in memory in the QueryResultImpl object.
+  // iter<T extends Row>(spec: T): RowIterator<T>;
+  iter<T extends Row>(spec: T): RowIterator<T>;
+
+  // Like iter() for queries that expect only one row. It embeds the valid()
+  // check (i.e. throws if no rows are available) and returns directly the
+  // first result.
+  firstRow<T extends Row>(spec: T): T;
+
+  // If != undefined the query errored out and error() contains the message.
+  error(): string|undefined;
+
+  // Returns the number of rows accumulated so far. Note that this number can
+  // change over time as more batches are received. It becomes stable only
+  // when isComplete() returns true or after waitAllRows() is resolved.
+  numRows(): number;
+
+  // If true all rows have been fetched. Calling iter() will iterate through the
+  // last row. If false, iter() will return an iterator which might iterate
+  // through some rows (or none) but will surely not reach the end.
+
+  isComplete(): boolean;
+
+  // Returns a promise that is resolved only when all rows (i.e. all batches)
+  // have been fetched. The promise return value is always the object iself.
+  waitAllRows(): Promise<QueryResult>;
+
+  // Returns a promise that is resolved when either:
+  // - more rows are available
+  // - all rows are available
+  // The promise return value is always the object iself.
+  waitMoreRows(): Promise<QueryResult>;
+
+  // Can return an empty array if called before the first batch is resolved.
+  // This should be called only after having awaited for at least one batch.
+  columns(): string[];
+
+  // Returns the number of SQL statements in the query
+  // (e.g. 2 'if SELECT 1; SELECT 2;')
+  statementCount(): number;
+
+  // Returns the number of SQL statement that produced output rows. This number
+  // is <= statementCount().
+  statementWithOutputCount(): number;
+
+  // Returns the last SQL statement.
+  lastStatementSql(): string;
+}
+
+// Interface exposed to engine.ts to pump in the data as new row batches arrive.
+export interface WritableQueryResult extends QueryResult {
+  // |resBytes| is a proto-encoded trace_processor.QueryResult message.
+  //  The overall flow looks as follows:
+  // - The user calls engine.query('select ...') and gets a QueryResult back.
+  // - The query call posts a message to the worker that runs the SQL engine (
+  //   or sends a HTTP request in case of the RPC+HTTP interface).
+  // - The returned QueryResult object is initially empty.
+  // - Over time, the sql engine will postMessage() back results in batches.
+  // - Each bach will end up calling this appendResultBatch() method.
+  // - If there is any pending promise (e.g. the caller called
+  //   queryResult.waitAllRows()), this call will awake them (if this is the
+  //   last batch).
+  appendResultBatch(resBytes: Uint8Array): void;
+}
+
+// The actual implementation, which bridges together the reader side and the
+// writer side (the one exposed to the Engine). This is the same object so that
+// when the engine pumps new row batches we can resolve pending promises that
+// readers (e.g. track code) are waiting for.
+class QueryResultImpl implements QueryResult, WritableQueryResult {
+  columnNames: string[] = [];
+  private _error?: string;
+  private _numRows = 0;
+  private _isComplete = false;
+  private _errorInfo: QueryErrorInfo;
+  private _statementCount = 0;
+  private _statementWithOutputCount = 0;
+  private _lastStatementSql = '';
+
+  constructor(errorInfo: QueryErrorInfo) {
+    this._errorInfo = errorInfo;
+  }
+
+  // --- QueryResult implementation.
+
+  // TODO(primiano): for the moment new batches are appended but old batches
+  // are never removed. This won't work with abnormally large result sets, as
+  // it will stash all rows in memory. We could switch to a model where the
+  // iterator is destructive and deletes batch objects once iterating past the
+  // end of each batch. If we do that, than we need to assign monotonic IDs to
+  // batches. Also if we do that, we should prevent creating more than one
+  // iterator for a QueryResult.
+  batches: ResultBatch[] = [];
+
+  // Promise awaiting on waitAllRows(). This should be resolved only when the
+  // last result batch has been been retrieved.
+  private allRowsPromise?: Deferred<QueryResult>;
+
+  // Promise awaiting on waitMoreRows(). This resolved when the next
+  // batch is appended via appendResultBatch.
+  private moreRowsPromise?: Deferred<QueryResult>;
+
+  isComplete(): boolean {
+    return this._isComplete;
+  }
+  numRows(): number {
+    return this._numRows;
+  }
+  error(): string|undefined {
+    return this._error;
+  }
+  columns(): string[] {
+    return this.columnNames;
+  }
+  statementCount(): number {
+    return this._statementCount;
+  }
+  statementWithOutputCount(): number {
+    return this._statementWithOutputCount;
+  }
+  lastStatementSql(): string {
+    return this._lastStatementSql;
+  }
+
+  iter<T extends Row>(spec: T): RowIterator<T> {
+    const impl = new RowIteratorImplWithRowData(spec, this);
+    return impl as {} as RowIterator<T>;
+  }
+
+  firstRow<T extends Row>(spec: T): T {
+    const impl = new RowIteratorImplWithRowData(spec, this);
+    assertTrue(impl.valid());
+    return impl as {} as RowIterator<T>as T;
+  }
+
+  // Can be called only once.
+  waitAllRows(): Promise<QueryResult> {
+    assertTrue(this.allRowsPromise === undefined);
+    this.allRowsPromise = defer<QueryResult>();
+    if (this._isComplete) {
+      this.resolveOrReject(this.allRowsPromise, this);
+    }
+    return this.allRowsPromise;
+  }
+
+  waitMoreRows(): Promise<QueryResult> {
+    if (this.moreRowsPromise !== undefined) {
+      return this.moreRowsPromise;
+    }
+
+    const moreRowsPromise = defer<QueryResult>();
+    if (this._isComplete) {
+      this.resolveOrReject(moreRowsPromise, this);
+    } else {
+      this.moreRowsPromise = moreRowsPromise;
+    }
+    return moreRowsPromise;
+  }
+
+  // --- WritableQueryResult implementation.
+
+  // Called by the engine when a new QueryResult is available. Note that a
+  // single Query() call can yield >1 QueryResult due to result batching
+  // if more than ~64K of data are returned, e.g. when returning O(M) rows.
+  // |resBytes| is a proto-encoded trace_processor.QueryResult message.
+  // It is fine to retain the resBytes without slicing a copy, because
+  // ProtoRingBuffer does the slice() for us (or passes through the buffer
+  // coming from postMessage() (Wasm case) of fetch() (HTTP+RPC case).
+  appendResultBatch(resBytes: Uint8Array) {
+    const reader = protobuf.Reader.create(resBytes);
+    assertTrue(reader.pos === 0);
+    const columnNamesEmptyAtStartOfBatch = this.columnNames.length === 0;
+    const columnNamesSet = new Set<string>();
+    while (reader.pos < reader.len) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:  // column_names
+          // Only the first batch should contain the column names. If this fires
+          // something is going wrong in the handling of the batch stream.
+          assertTrue(columnNamesEmptyAtStartOfBatch);
+          const origColName = reader.string();
+          let colName = origColName;
+          // In some rare cases two columns can have the same name (b/194891824)
+          // e.g. `select 1 as x, 2 as x`. These queries don't happen in the
+          // UI code, but they can happen when the user types a query (e.g.
+          // with a join). The most practical thing we can do here is renaming
+          // the columns with a suffix. Keeping the same name will break when
+          // iterating, because column names become iterator object keys.
+          for (let i = 1; columnNamesSet.has(colName); ++i) {
+            colName = `${origColName}_${i}`;
+            assertTrue(i < 100);  // Give up at some point;
+          }
+          columnNamesSet.add(colName);
+          this.columnNames.push(colName);
+          break;
+        case 2:  // error
+          // The query has errored only if the |error| field is non-empty.
+          // In protos, we don't distinguish between non-present and empty.
+          // Make sure we don't propagate ambiguous empty strings to JS.
+          const err = reader.string();
+          this._error = (err !== undefined && err.length) ? err : undefined;
+          break;
+        case 3:  // batch
+          const batchLen = reader.uint32();
+          const batchRaw = resBytes.subarray(reader.pos, reader.pos + batchLen);
+          reader.pos += batchLen;
+
+          // The ResultBatch ctor parses the CellsBatch submessage.
+          const parsedBatch = new ResultBatch(batchRaw);
+          this.batches.push(parsedBatch);
+          this._isComplete = parsedBatch.isLastBatch;
+
+          // In theory one could construct a valid proto serializing the column
+          // names after the cell batches. In practice the QueryResultSerializer
+          // doesn't do that so it's not worth complicating the code.
+          const numColumns = this.columnNames.length;
+          if (numColumns !== 0) {
+            assertTrue(parsedBatch.numCells % numColumns === 0);
+            this._numRows += parsedBatch.numCells / numColumns;
+          } else {
+            // numColumns == 0 is  plausible for queries like CREATE TABLE ... .
+            assertTrue(parsedBatch.numCells === 0);
+          }
+          break;
+
+        case 4:
+          this._statementCount = reader.uint32();
+          break;
+
+        case 5:
+          this._statementWithOutputCount = reader.uint32();
+          break;
+
+        case 6:
+          this._lastStatementSql = reader.string();
+          break;
+
+        default:
+          console.warn(`Unexpected QueryResult field ${tag >>> 3}`);
+          reader.skipType(tag & 7);
+          break;
+      }  // switch (tag)
+    }    // while (pos < end)
+
+    if (this.moreRowsPromise !== undefined) {
+      this.resolveOrReject(this.moreRowsPromise, this);
+      this.moreRowsPromise = undefined;
+    }
+
+    if (this._isComplete && this.allRowsPromise !== undefined) {
+      this.resolveOrReject(this.allRowsPromise, this);
+    }
+  }
+
+  ensureAllRowsPromise(): Promise<QueryResult> {
+    if (this.allRowsPromise === undefined) {
+      this.waitAllRows();  // Will populate |this.allRowsPromise|.
+    }
+    return assertExists(this.allRowsPromise);
+  }
+
+  private resolveOrReject(promise: Deferred<QueryResult>, arg: QueryResult) {
+    if (this._error === undefined) {
+      promise.resolve(arg);
+    } else {
+      promise.reject(new QueryError(this._error, this._errorInfo));
+    }
+  }
+}
+
+// This class holds onto a received result batch (a Uint8Array) and does some
+// partial parsing to tokenize the various cell groups. This parsing mainly
+// consists of identifying and caching the offsets of each cell group and
+// initializing the varint decoders. This half parsing is done to keep the
+// iterator's next() fast, without decoding everything into memory.
+// This is an internal implementation detail and is not exposed outside. The
+// RowIteratorImpl uses this class to iterate through batches (this class takes
+// care of iterating within a batch, RowIteratorImpl takes care of switching
+// batches when needed).
+// Note: at any point in time there can be more than one ResultIterator
+// referencing the same batch. The batch must be immutable.
+class ResultBatch {
+  readonly isLastBatch: boolean = false;
+  readonly batchBytes: Uint8Array;
+  readonly cellTypesOff: number = 0;
+  readonly cellTypesLen: number = 0;
+  readonly varintOff: number = 0;
+  readonly varintLen: number = 0;
+  readonly float64Cells = new Float64Array();
+  readonly blobCells: Uint8Array[] = [];
+  readonly stringCells: string[] = [];
+
+  // batchBytes is a trace_processor.QueryResult.CellsBatch proto.
+  constructor(batchBytes: Uint8Array) {
+    this.batchBytes = batchBytes;
+    const reader = protobuf.Reader.create(batchBytes);
+    assertTrue(reader.pos === 0);
+    const end = reader.len;
+
+    // Here we deconstruct the proto by hand. The CellsBatch is carefully
+    // designed to allow a very fast parsing from the TS side. We pack all cells
+    // of the same types together, so we can do only one call (per batch) to
+    // TextDecoder.decode(), we can overlay a memory-aligned typedarray for
+    // float values and can quickly tell and type-check the cell types.
+    // One row = N cells (we know the number upfront from the outer message).
+    // Each bach contains always an integer multiple of N cells (i.e. rows are
+    // never fragmented across different batches).
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:  // cell types, a packed array containing one CellType per cell.
+          assertTrue((tag & 7) === TAG_LEN_DELIM);  // Must be packed varint.
+          this.cellTypesLen = reader.uint32();
+          this.cellTypesOff = reader.pos;
+          reader.pos += this.cellTypesLen;
+          break;
+
+        case 2:  // varint_cells, a packed varint buffer.
+          assertTrue((tag & 7) === TAG_LEN_DELIM);  // Must be packed varint.
+          const packLen = reader.uint32();
+          this.varintOff = reader.pos;
+          this.varintLen = packLen;
+          assertTrue(reader.buf === batchBytes);
+          assertTrue(
+              this.varintOff + this.varintLen <=
+              batchBytes.byteOffset + batchBytes.byteLength);
+          reader.pos += packLen;
+          break;
+
+        case 3:  // float64_cells, a 64-bit aligned packed fixed64 buffer.
+          assertTrue((tag & 7) === TAG_LEN_DELIM);  // Must be packed varint.
+          const f64Len = reader.uint32();
+          assertTrue(f64Len % 8 === 0);
+          // Float64Array's constructor is evil: the offset is in bytes but the
+          // length is in 8-byte words.
+          const f64Words = f64Len / 8;
+          const f64Off = batchBytes.byteOffset + reader.pos;
+          if (f64Off % 8 === 0) {
+            this.float64Cells =
+                new Float64Array(batchBytes.buffer, f64Off, f64Words);
+          } else {
+            // When using the production code in trace_processor's rpc.cc, the
+            // float64 should be 8-bytes aligned. The slow-path case is only for
+            // tests.
+            const slice = batchBytes.buffer.slice(f64Off, f64Off + f64Len);
+            this.float64Cells = new Float64Array(slice);
+          }
+          reader.pos += f64Len;
+          break;
+
+        case 4:  // blob_cells: one entry per blob.
+          assertTrue((tag & 7) === TAG_LEN_DELIM);
+          // protobufjs's bytes() under the hoods calls slice() and creates
+          // a copy. Fine here as blobs are rare and not a fastpath.
+          this.blobCells.push(new Uint8Array(reader.bytes()));
+          break;
+
+        case 5:  // string_cells: all the string cells concatenated with \0s.
+          assertTrue((tag & 7) === TAG_LEN_DELIM);
+          const strLen = reader.uint32();
+          assertTrue(reader.pos + strLen <= end);
+          const subArr = batchBytes.subarray(reader.pos, reader.pos + strLen);
+          assertTrue(subArr.length === strLen);
+          // The reason why we do this split rather than creating one string
+          // per entry is that utf8 decoding has some non-negligible cost. See
+          // go/postmessage-benchmark .
+          this.stringCells = utf8Decode(subArr).split('\0');
+          reader.pos += strLen;
+          break;
+
+        case 6:  // is_last_batch (boolean).
+          this.isLastBatch = !!reader.bool();
+          break;
+
+        case 7:  // padding for realignment, skip silently.
+          reader.skipType(tag & 7);
+          break;
+
+        default:
+          console.warn(`Unexpected QueryResult.CellsBatch field ${tag >>> 3}`);
+          reader.skipType(tag & 7);
+          break;
+      }  // switch(tag)
+    }    // while (pos < end)
+  }
+
+  get numCells() {
+    return this.cellTypesLen;
+  }
+}
+
+class RowIteratorImpl implements RowIteratorBase {
+  // The spec passed to the iter call containing the expected types, e.g.:
+  // {'colA': NUM, 'colB': NUM_NULL, 'colC': STRING}.
+  // This doesn't ever change.
+  readonly rowSpec: Row;
+
+  // The object that holds the current row. This points to the parent
+  // RowIteratorImplWithRowData instance that created this class.
+  rowData: Row;
+
+  // The QueryResult object we are reading data from. The engine will pump
+  // batches over time into this object.
+  private resultObj: QueryResultImpl;
+
+  // All the member variables in the group below point to the identically-named
+  // members in result.batch[batchIdx]. This is to avoid indirection layers in
+  // the next() hotpath, so we can do this.float64Cells vs
+  // this.resultObj.batch[this.batchIdx].float64Cells.
+  // These are re-set every time tryMoveToNextBatch() is called (and succeeds).
+  private batchIdx = -1;  // The batch index within |result.batches[]|.
+  private batchBytes = new Uint8Array();
+  private columnNames: string[] = [];
+  private numColumns = 0;
+  private cellTypesEnd = -1;  // -1 so the 1st next() hits tryMoveToNextBatch().
+  private float64Cells = new Float64Array();
+  private varIntReader = protobuf.Reader.create(this.batchBytes);
+  private blobCells: Uint8Array[] = [];
+  private stringCells: string[] = [];
+
+  // These members instead are incremented as we read cells from next(). They
+  // are the mutable state of the iterator.
+  private nextCellTypeOff = 0;
+  private nextFloat64Cell = 0;
+  private nextStringCell = 0;
+  private nextBlobCell = 0;
+  private isValid = false;
+
+  constructor(querySpec: Row, rowData: Row, res: QueryResultImpl) {
+    Object.assign(this, querySpec);
+    this.rowData = rowData;
+    this.rowSpec = {...querySpec};  // ... -> Copy all the key/value pairs.
+    this.resultObj = res;
+    this.next();
+  }
+
+  valid(): boolean {
+    return this.isValid;
+  }
+
+
+  get(columnName: string): ColumnType {
+    const res = this.rowData[columnName];
+    if (res === undefined) {
+      throw new Error(
+          `Column '${columnName}' doesn't exist. ` +
+          `Actual columns: [${this.columnNames.join(',')}]`);
+    }
+    return res;
+  }
+
+  // Moves the cursor next by one row and updates |isValid|.
+  // When this fails to move, two cases are possible:
+  // 1. We reached the end of the result set (this is the case if
+  //    QueryResult.isComplete() == true when this fails).
+  // 2. We reached the end of the current batch, but more rows might come later
+  //    (if QueryResult.isComplete() == false).
+  next() {
+    // At some point we might reach the end of the current batch, but the next
+    // batch might be available already. In this case we want next() to
+    // transparently move on to the next batch.
+    while (this.nextCellTypeOff + this.numColumns > this.cellTypesEnd) {
+      // If TraceProcessor is behaving well, we should never end up in a
+      // situation where we have leftover cells. TP is expected to serialize
+      // whole rows in each QueryResult batch and NOT truncate them midway.
+      // If this assert fires the TP RPC logic has a bug.
+      assertTrue(
+          this.nextCellTypeOff === this.cellTypesEnd ||
+          this.cellTypesEnd === -1);
+      if (!this.tryMoveToNextBatch()) {
+        this.isValid = false;
+        return;
+      }
+    }
+
+    const rowData = this.rowData;
+    const numColumns = this.numColumns;
+
+    // Read the current row.
+    for (let i = 0; i < numColumns; i++) {
+      const cellType = this.batchBytes[this.nextCellTypeOff++];
+      const colName = this.columnNames[i];
+      const expType = this.rowSpec[colName];
+
+      switch (cellType) {
+        case CellType.CELL_NULL:
+          rowData[colName] = null;
+          break;
+
+        case CellType.CELL_VARINT:
+          if (expType === NUM || expType === NUM_NULL) {
+            // This is very subtle. The return type of int64 can be either a
+            // number or a Long.js {high:number, low:number} if Long.js is
+            // installed. The default state seems different in node and browser.
+            // We force-disable Long.js support in the top of this source file.
+            const val = this.varIntReader.int64();
+            rowData[colName] = val as {} as number;
+          } else {
+            // LONG, LONG_NULL, or unspecified - return as bigint
+            const value =
+                decodeInt64Varint(this.batchBytes, this.varIntReader.pos);
+            rowData[colName] = value;
+            this.varIntReader.skip();  // Skips a varint
+          }
+          break;
+
+        case CellType.CELL_FLOAT64:
+          rowData[colName] = this.float64Cells[this.nextFloat64Cell++];
+          break;
+
+        case CellType.CELL_STRING:
+          rowData[colName] = this.stringCells[this.nextStringCell++];
+          break;
+
+        case CellType.CELL_BLOB:
+          const blob = this.blobCells[this.nextBlobCell++];
+          rowData[colName] = blob;
+          break;
+
+        default:
+          throw new Error(`Invalid cell type ${cellType}`);
+      }
+    }  // For (cells)
+    this.isValid = true;
+  }
+
+  private tryMoveToNextBatch(): boolean {
+    const nextBatchIdx = this.batchIdx + 1;
+    if (nextBatchIdx >= this.resultObj.batches.length) {
+      return false;
+    }
+
+    this.columnNames = this.resultObj.columnNames;
+    this.numColumns = this.columnNames.length;
+
+    this.batchIdx = nextBatchIdx;
+    const batch = assertExists(this.resultObj.batches[nextBatchIdx]);
+    this.batchBytes = batch.batchBytes;
+    this.nextCellTypeOff = batch.cellTypesOff;
+    this.cellTypesEnd = batch.cellTypesOff + batch.cellTypesLen;
+    this.float64Cells = batch.float64Cells;
+    this.blobCells = batch.blobCells;
+    this.stringCells = batch.stringCells;
+    this.varIntReader = protobuf.Reader.create(batch.batchBytes);
+    this.varIntReader.pos = batch.varintOff;
+    this.varIntReader.len = batch.varintOff + batch.varintLen;
+    this.nextFloat64Cell = 0;
+    this.nextStringCell = 0;
+    this.nextBlobCell = 0;
+
+    // Check that all the expected columns are present.
+    for (const expectedCol of Object.keys(this.rowSpec)) {
+      if (this.columnNames.indexOf(expectedCol) < 0) {
+        throw new Error(
+            `Column ${expectedCol} not found in the SQL result ` +
+            `set {${this.columnNames.join(' ')}}`);
+      }
+    }
+
+    // Check that the cells types are consistent.
+    const numColumns = this.numColumns;
+    if (batch.numCells === 0) {
+      // This can happen if the query result contains just an error. In this
+      // an empty batch with isLastBatch=true is appended as an EOF marker.
+      // In theory TraceProcessor could return an empty batch in the middle and
+      // that would be fine from a protocol viewpoint. In practice, no code path
+      // does that today so it doesn't make sense trying supporting it with a
+      // recursive call to tryMoveToNextBatch().
+      assertTrue(batch.isLastBatch);
+      return false;
+    }
+
+    assertTrue(numColumns > 0);
+    for (let i = this.nextCellTypeOff; i < this.cellTypesEnd; i++) {
+      const col = (i - this.nextCellTypeOff) % numColumns;
+      const colName = this.columnNames[col];
+      const actualType = this.batchBytes[i] as CellType;
+      const expType = this.rowSpec[colName];
+
+      // If undefined, the caller doesn't want to read this column at all, so
+      // it can be whatever.
+      if (expType === undefined) continue;
+
+      let err = '';
+      if (!isCompatible(actualType, expType)) {
+        if (actualType === CellType.CELL_NULL) {
+          err = 'SQL value is NULL but that was not expected' +
+              ` (expected type: ${columnTypeToString(expType)}). ` +
+              'Did you mean NUM_NULL, LONG_NULL, STR_NULL or BLOB_NULL?';
+        } else {
+          err = `Incompatible cell type. Expected: ${
+              columnTypeToString(
+                  expType)} actual: ${CELL_TYPE_NAMES[actualType]}`;
+        }
+      }
+      if (err.length > 0) {
+        throw new Error(
+            `Error @ row: ${Math.floor(i / numColumns)} col: '` +
+            `${colName}': ${err}`);
+      }
+    }
+    return true;
+  }
+}
+
+// This is the object ultimately returned to the client when calling
+// QueryResult.iter(...).
+// The only reason why this is disjoint from RowIteratorImpl is to avoid
+// naming collisions between the members variables required by RowIteratorImpl
+// and the column names returned by the iterator.
+class RowIteratorImplWithRowData implements RowIteratorBase {
+  private _impl: RowIteratorImpl;
+
+  next: () => void;
+  valid: () => boolean;
+  get: (columnName: string) => ColumnType;
+
+  constructor(querySpec: Row, res: QueryResultImpl) {
+    const thisAsRow = this as {} as Row;
+    Object.assign(thisAsRow, querySpec);
+    this._impl = new RowIteratorImpl(querySpec, thisAsRow, res);
+    this.next = this._impl.next.bind(this._impl);
+    this.valid = this._impl.valid.bind(this._impl);
+    this.get = this._impl.get.bind(this._impl);
+  }
+}
+
+// This is a proxy object that wraps QueryResultImpl, adding await-ability.
+// This is so that:
+// 1. Clients that just want to await for the full result set can just call
+//    await engine.query('...') and will get a QueryResult that is guaranteed
+//    to be complete.
+// 2. Clients that know how to handle the streaming can use it straight away.
+class WaitableQueryResultImpl implements QueryResult, WritableQueryResult,
+                                         PromiseLike<QueryResult> {
+  private impl: QueryResultImpl;
+  private thenCalled = false;
+
+  constructor(errorInfo: QueryErrorInfo) {
+    this.impl = new QueryResultImpl(errorInfo);
+  }
+
+  // QueryResult implementation. Proxies all calls to the impl object.
+  iter<T extends Row>(spec: T) {
+    return this.impl.iter(spec);
+  }
+  firstRow<T extends Row>(spec: T) {
+    return this.impl.firstRow(spec);
+  }
+  waitAllRows() {
+    return this.impl.waitAllRows();
+  }
+  waitMoreRows() {
+    return this.impl.waitMoreRows();
+  }
+  isComplete() {
+    return this.impl.isComplete();
+  }
+  numRows() {
+    return this.impl.numRows();
+  }
+  columns() {
+    return this.impl.columns();
+  }
+  error() {
+    return this.impl.error();
+  }
+  statementCount() {
+    return this.impl.statementCount();
+  }
+  statementWithOutputCount() {
+    return this.impl.statementWithOutputCount();
+  }
+  lastStatementSql() {
+    return this.impl.lastStatementSql();
+  }
+
+  // WritableQueryResult implementation.
+  appendResultBatch(resBytes: Uint8Array) {
+    return this.impl.appendResultBatch(resBytes);
+  }
+
+  // PromiseLike<QueryResult> implementaton.
+
+  then(onfulfilled: any, onrejected: any): any {
+    assertFalse(this.thenCalled);
+    this.thenCalled = true;
+    return this.impl.ensureAllRowsPromise().then(onfulfilled, onrejected);
+  }
+
+  catch(error: any): any {
+    return this.impl.ensureAllRowsPromise().catch(error);
+  }
+
+  finally(callback: () => void): any {
+    return this.impl.ensureAllRowsPromise().finally(callback);
+  }
+
+  // eslint and clang-format disagree on how to format get[foo](). Let
+  // clang-format win:
+  // eslint-disable-next-line keyword-spacing
+  get[Symbol.toStringTag](): string {
+    return 'Promise<WaitableQueryResult>';
+  }
+}
+
+export function createQueryResult(errorInfo: QueryErrorInfo): QueryResult&
+    Promise<QueryResult>&WritableQueryResult {
+  return new WaitableQueryResultImpl(errorInfo);
+}
diff --git a/tools/winscope/src/trace_processor/string_utils.ts b/tools/winscope/src/trace_processor/string_utils.ts
new file mode 100644
index 0000000..bc33b70
--- /dev/null
+++ b/tools/winscope/src/trace_processor/string_utils.ts
@@ -0,0 +1,117 @@
+// Copyright (C) 2019 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 {
+  decode as b64Decode,
+  encode as b64Encode,
+  length as b64Len,
+} from '@protobufjs/base64';
+import {
+  length as utf8Len,
+  read as utf8Read,
+  write as utf8Write,
+} from '@protobufjs/utf8';
+
+import {assertTrue} from './logging';
+
+// TextDecoder/Decoder requires the full DOM and isn't available in all types
+// of tests. Use fallback implementation from protbufjs.
+let Utf8Decoder: {decode: (buf: Uint8Array) => string;};
+let Utf8Encoder: {encode: (str: string) => Uint8Array;};
+try {
+  Utf8Decoder = new TextDecoder('utf-8');
+  Utf8Encoder = new TextEncoder();
+} catch (_) {
+  if (typeof process === 'undefined') {
+    // Silence the warning when we know we are running under NodeJS.
+    console.warn(
+        'Using fallback UTF8 Encoder/Decoder, This should happen only in ' +
+        'tests and NodeJS-based environments, not in browsers.');
+  }
+  Utf8Decoder = {decode: (buf: Uint8Array) => utf8Read(buf, 0, buf.length)};
+  Utf8Encoder = {
+    encode: (str: string) => {
+      const arr = new Uint8Array(utf8Len(str));
+      const written = utf8Write(str, arr, 0);
+      assertTrue(written === arr.length);
+      return arr;
+    },
+  };
+}
+
+export function base64Encode(buffer: Uint8Array): string {
+  return b64Encode(buffer, 0, buffer.length);
+}
+
+export function base64Decode(str: string): Uint8Array {
+  // if the string is in base64url format, convert to base64
+  const b64 = str.replace(/-/g, '+').replace(/_/g, '/');
+  const arr = new Uint8Array(b64Len(b64));
+  const written = b64Decode(b64, arr, 0);
+  assertTrue(written === arr.length);
+  return arr;
+}
+
+// encode binary array to hex string
+export function hexEncode(bytes: Uint8Array): string {
+  return bytes.reduce(
+      (prev, cur) => prev + ('0' + cur.toString(16)).slice(-2), '');
+}
+
+export function utf8Encode(str: string): Uint8Array {
+  return Utf8Encoder.encode(str);
+}
+
+// Note: not all byte sequences can be converted to<>from UTF8. This can be
+// used only with valid unicode strings, not arbitrary byte buffers.
+export function utf8Decode(buffer: Uint8Array): string {
+  return Utf8Decoder.decode(buffer);
+}
+
+// The binaryEncode/Decode functions below allow to encode an arbitrary binary
+// buffer into a string that can be JSON-encoded. binaryEncode() applies
+// UTF-16 encoding to each byte individually.
+// Unlike utf8Encode/Decode, any arbitrary byte sequence can be converted into a
+// valid string, and viceversa.
+// This should be only used when a byte array needs to be transmitted over an
+// interface that supports only JSON serialization (e.g., postmessage to a
+// chrome extension).
+
+export function binaryEncode(buf: Uint8Array): string {
+  let str = '';
+  for (let i = 0; i < buf.length; i++) {
+    str += String.fromCharCode(buf[i]);
+  }
+  return str;
+}
+
+export function binaryDecode(str: string): Uint8Array {
+  const buf = new Uint8Array(str.length);
+  const strLen = str.length;
+  for (let i = 0; i < strLen; i++) {
+    buf[i] = str.charCodeAt(i);
+  }
+  return buf;
+}
+
+// A function used to interpolate strings into SQL query. The only replacement
+// is done is that single quote replaced with two single quotes, according to
+// SQLite documentation:
+// https://www.sqlite.org/lang_expr.html#literal_values_constants_
+//
+// The purpose of this function is to use in simple comparisons, to escape
+// strings used in GLOB clauses see escapeQuery function.
+export function sqliteString(str: string): string {
+  return `'${str.replace(/'/g, '\'\'')}'`;
+}
diff --git a/tools/winscope/src/trace_processor/wasm_engine_proxy.ts b/tools/winscope/src/trace_processor/wasm_engine_proxy.ts
new file mode 100644
index 0000000..251eeeb
--- /dev/null
+++ b/tools/winscope/src/trace_processor/wasm_engine_proxy.ts
@@ -0,0 +1,75 @@
+// Copyright (C) 2018 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 {assertExists, assertTrue} from './logging';
+
+import {Engine, LoadingTracker} from './engine';
+import {EngineWorkerInitMessage} from './worker_messages';
+
+let bundlePath: string;
+let idleWasmWorker: Worker;
+let activeWasmWorker: Worker;
+
+export function initWasm(root: string) {
+  bundlePath = root + 'engine_bundle.js';
+  idleWasmWorker = new Worker(bundlePath);
+}
+
+// This method is called trace_controller whenever a new trace is loaded.
+export function resetEngineWorker(): MessagePort {
+  const channel = new MessageChannel();
+  const port = channel.port1;
+
+  // We keep always an idle worker around, the first one is created by the
+  // main() below, so we can hide the latency of the Wasm initialization.
+  if (activeWasmWorker !== undefined) {
+    activeWasmWorker.terminate();
+  }
+
+  // Swap the active worker with the idle one and create a new idle worker
+  // for the next trace.
+  activeWasmWorker = assertExists(idleWasmWorker);
+  const msg: EngineWorkerInitMessage = {enginePort: port};
+  activeWasmWorker.postMessage(msg, [port]);
+  idleWasmWorker = new Worker(bundlePath);
+  return channel.port2;
+}
+
+/**
+ * This implementation of Engine uses a WASM backend hosted in a separate
+ * worker thread.
+ */
+export class WasmEngineProxy extends Engine {
+  readonly id: string;
+  private port: MessagePort;
+
+  constructor(id: string, port: MessagePort, loadingTracker?: LoadingTracker) {
+    super(loadingTracker);
+    this.id = id;
+    this.port = port;
+    this.port.onmessage = this.onMessage.bind(this);
+  }
+
+  onMessage(m: MessageEvent) {
+    assertTrue(m.data instanceof Uint8Array);
+    super.onRpcResponseBytes(m.data as Uint8Array);
+  }
+
+  rpcSendRequestBytes(data: Uint8Array): void {
+    // We deliberately don't use a transfer list because protobufjs reuses the
+    // same buffer when encoding messages (which is good, because creating a new
+    // TypedArray for each decode operation would be too expensive).
+    this.port.postMessage(data);
+  }
+}
diff --git a/tools/winscope/src/trace_processor/worker_messages.ts b/tools/winscope/src/trace_processor/worker_messages.ts
new file mode 100644
index 0000000..166b1cc
--- /dev/null
+++ b/tools/winscope/src/trace_processor/worker_messages.ts
@@ -0,0 +1,30 @@
+// 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.
+
+// This file defines the API of messages exchanged between frontend and
+// {engine, controller} worker when bootstrapping the workers.
+// Those messages are sent only once. The rest of the communication happens
+// over the MessagePort(s) that are sent in the init message.
+
+// This is so we can create all the workers in a central place in the frontend
+// (Safari still doesn't spawning workers from other workers) but then let them
+// communicate by sending the right MessagePort to them.
+
+// Frontend -> Engine initialization message.
+export interface EngineWorkerInitMessage {
+  // The port used to receive engine messages (e.g., query commands).
+  // The controller owns the other end of the MessageChannel
+  // (see resetEngineWorker()).
+  enginePort: MessagePort;
+}
diff --git a/tools/winscope/src/viewers/common/ime_utils.ts b/tools/winscope/src/viewers/common/ime_utils.ts
index 027da4d..e29aee9 100644
--- a/tools/winscope/src/viewers/common/ime_utils.ts
+++ b/tools/winscope/src/viewers/common/ime_utils.ts
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 import {FilterType, TreeUtils} from 'common/tree_utils';
-import {WindowContainer} from 'trace/flickerlib/common';
-import {Layer} from 'trace/flickerlib/layers/Layer';
-import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry';
-import {Activity} from 'trace/flickerlib/windows/Activity';
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
-import {WindowState} from 'trace/flickerlib/windows/WindowState';
+import {WindowContainer} from 'flickerlib/common';
+import {Layer} from 'flickerlib/layers/Layer';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {Activity} from 'flickerlib/windows/Activity';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
+import {WindowState} from 'flickerlib/windows/WindowState';
 
 class ProcessedWindowManagerState {
   constructor(
diff --git a/tools/winscope/src/viewers/common/presenter_input_method.ts b/tools/winscope/src/viewers/common/presenter_input_method.ts
index ce75695..c7834f5 100644
--- a/tools/winscope/src/viewers/common/presenter_input_method.ts
+++ b/tools/winscope/src/viewers/common/presenter_input_method.ts
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
+import {AppEvent, AppEventType} from 'app/app_event';
 import {PersistentStoreProxy} from 'common/persistent_store_proxy';
 import {FilterType, TreeUtils} from 'common/tree_utils';
-import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry';
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
 import {Trace, TraceEntry} from 'trace/trace';
 import {Traces} from 'trace/traces';
 import {TraceEntryFinder} from 'trace/trace_entry_finder';
@@ -101,24 +102,26 @@
     this.notifyViewCallback(this.uiData);
   }
 
-  async onTracePositionUpdate(position: TracePosition) {
-    this.uiData = new ImeUiData(this.dependencies);
-    this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
-    this.uiData.propertiesUserOptions = this.propertiesUserOptions;
+  async onAppEvent(event: AppEvent) {
+    await event.visit(AppEventType.TRACE_POSITION_UPDATE, async (event) => {
+      this.uiData = new ImeUiData(this.dependencies);
+      this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
+      this.uiData.propertiesUserOptions = this.propertiesUserOptions;
 
-    const [imeEntry, sfEntry, wmEntry] = this.findTraceEntries(position);
+      const [imeEntry, sfEntry, wmEntry] = this.findTraceEntries(event.position);
 
-    if (imeEntry) {
-      this.entry = (await imeEntry.getValue()) as TraceTreeNode;
-      this.uiData.highlightedItems = this.highlightedItems;
-      this.uiData.additionalProperties = this.getAdditionalProperties(
-        await wmEntry?.getValue(),
-        await sfEntry?.getValue()
-      );
-      this.uiData.tree = this.generateTree();
-      this.uiData.hierarchyTableProperties = this.updateHierarchyTableProperties();
-    }
-    this.notifyViewCallback(this.uiData);
+      if (imeEntry) {
+        this.entry = (await imeEntry.getValue()) as TraceTreeNode;
+        this.uiData.highlightedItems = this.highlightedItems;
+        this.uiData.additionalProperties = this.getAdditionalProperties(
+          await wmEntry?.getValue(),
+          await sfEntry?.getValue()
+        );
+        this.uiData.tree = this.generateTree();
+        this.uiData.hierarchyTableProperties = this.updateHierarchyTableProperties();
+      }
+      this.notifyViewCallback(this.uiData);
+    });
   }
 
   updatePinnedItems(pinnedItem: HierarchyTreeNode) {
diff --git a/tools/winscope/src/viewers/common/presenter_input_method_test_utils.ts b/tools/winscope/src/viewers/common/presenter_input_method_test_utils.ts
index 62a56ca..022e8ec 100644
--- a/tools/winscope/src/viewers/common/presenter_input_method_test_utils.ts
+++ b/tools/winscope/src/viewers/common/presenter_input_method_test_utils.ts
@@ -14,6 +14,7 @@
  * limitations under the License.d
  */
 
+import {TracePositionUpdate} from 'app/app_event';
 import {assertDefined} from 'common/assert_utils';
 import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
 import {MockStorage} from 'test/unit/mock_storage';
@@ -21,7 +22,6 @@
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {ImeUiData} from 'viewers/common/ime_ui_data';
 import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
@@ -45,7 +45,7 @@
   describe('PresenterInputMethod', () => {
     let presenter: PresenterInputMethod;
     let uiData: ImeUiData;
-    let position: TracePosition;
+    let positionUpdate: TracePositionUpdate;
     let selectedTree: HierarchyTreeNode;
 
     beforeEach(async () => {
@@ -65,7 +65,7 @@
       expect(uiData.propertiesUserOptions).toBeTruthy();
       expect(uiData.tree).toBeFalsy();
 
-      await presenter.onTracePositionUpdate(position);
+      await presenter.onAppEvent(positionUpdate);
       expect(uiData.hierarchyUserOptions).toBeTruthy();
       expect(uiData.propertiesUserOptions).toBeTruthy();
       expect(uiData.tree).toBeFalsy();
@@ -73,7 +73,7 @@
 
     it('is robust to traces without SF', async () => {
       await setUpTestEnvironment([imeTraceType, TraceType.WINDOW_MANAGER]);
-      await presenter.onTracePositionUpdate(position);
+      await presenter.onAppEvent(positionUpdate);
       expect(uiData.hierarchyUserOptions).toBeTruthy();
       expect(uiData.propertiesUserOptions).toBeTruthy();
       expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
@@ -81,7 +81,7 @@
 
     it('is robust to traces without WM', async () => {
       await setUpTestEnvironment([imeTraceType, TraceType.SURFACE_FLINGER]);
-      await presenter.onTracePositionUpdate(position);
+      await presenter.onAppEvent(positionUpdate);
       expect(uiData.hierarchyUserOptions).toBeTruthy();
       expect(uiData.propertiesUserOptions).toBeTruthy();
       expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
@@ -89,14 +89,14 @@
 
     it('is robust to traces without WM and SF', async () => {
       await setUpTestEnvironment([imeTraceType]);
-      await presenter.onTracePositionUpdate(position);
+      await presenter.onAppEvent(positionUpdate);
       expect(uiData.hierarchyUserOptions).toBeTruthy();
       expect(uiData.propertiesUserOptions).toBeTruthy();
       expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
     });
 
     it('processes trace position updates', async () => {
-      await presenter.onTracePositionUpdate(position);
+      await presenter.onAppEvent(positionUpdate);
       expect(uiData.hierarchyUserOptions).toBeTruthy();
       expect(uiData.propertiesUserOptions).toBeTruthy();
       expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
@@ -138,7 +138,7 @@
       };
 
       let expectedChildren = expectHierarchyTreeWithSfSubtree ? 2 : 1;
-      await presenter.onTracePositionUpdate(position);
+      await presenter.onAppEvent(positionUpdate);
       expect(uiData.tree?.children.length).toEqual(expectedChildren);
 
       // Filter out non-visible child
@@ -165,7 +165,7 @@
       };
 
       const expectedChildren = expectHierarchyTreeWithSfSubtree ? 12 : 1;
-      await presenter.onTracePositionUpdate(position);
+      await presenter.onAppEvent(positionUpdate);
       presenter.updateHierarchyTree(userOptions);
       expect(uiData.tree?.children.length).toEqual(expectedChildren);
 
@@ -175,14 +175,14 @@
     });
 
     it('can set new properties tree and associated ui data', async () => {
-      await presenter.onTracePositionUpdate(position);
+      await presenter.onAppEvent(positionUpdate);
       presenter.newPropertiesTree(selectedTree);
       // does not check specific tree values as tree transformation method may change
       expect(uiData.propertiesTree).toBeTruthy();
     });
 
     it('can filter properties tree', async () => {
-      await presenter.onTracePositionUpdate(position);
+      await presenter.onAppEvent(positionUpdate);
       presenter.newPropertiesTree(selectedTree);
       let nonTerminalChildren =
         uiData.propertiesTree?.children?.filter(
@@ -213,9 +213,8 @@
 
       presenter = createPresenter(traces);
 
-      position = TracePosition.fromTraceEntry(
-        assertDefined(traces.getTrace(imeTraceType)).getEntry(0)
-      );
+      const entry = assertDefined(traces.getTrace(imeTraceType)).getEntry(0);
+      positionUpdate = TracePositionUpdate.fromTraceEntry(entry);
     };
 
     const createPresenter = (traces: Traces): PresenterInputMethod => {
diff --git a/tools/winscope/src/viewers/common/surface_flinger_utils.ts b/tools/winscope/src/viewers/common/surface_flinger_utils.ts
new file mode 100644
index 0000000..21b296b
--- /dev/null
+++ b/tools/winscope/src/viewers/common/surface_flinger_utils.ts
@@ -0,0 +1,107 @@
+/*
+ * 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 {Layer, LayerTraceEntry} from 'flickerlib/common';
+import {ParserViewCapture} from 'parsers/parser_view_capture';
+import {Rectangle, TransformMatrix} from 'viewers/components/rects/types2d';
+import {UserOptions} from './user_options';
+
+export class SurfaceFlingerUtils {
+  static makeRects(entry: LayerTraceEntry, hierarchyUserOptions: UserOptions): Rectangle[] {
+    const layerRects = SurfaceFlingerUtils.makeLayerRects(entry, hierarchyUserOptions);
+    const displayRects = SurfaceFlingerUtils.makeDisplayRects(entry);
+    return layerRects.concat(displayRects);
+  }
+
+  private static makeLayerRects(
+    entry: LayerTraceEntry,
+    hierarchyUserOptions: UserOptions
+  ): Rectangle[] {
+    return entry.flattenedLayers
+      .filter((layer: Layer) => {
+        return SurfaceFlingerUtils.isLayerToRenderInRectsComponent(layer, hierarchyUserOptions);
+      })
+      .sort(SurfaceFlingerUtils.compareLayerZ)
+      .map((it: Layer) => {
+        const transform: TransformMatrix = it.rect.transform?.matrix ?? it.rect.transform;
+        const rect: Rectangle = {
+          topLeft: {x: it.rect.left, y: it.rect.top},
+          bottomRight: {x: it.rect.right, y: it.rect.bottom},
+          label: it.rect.label,
+          transform,
+          isVisible: it.isVisible,
+          isDisplay: false,
+          id: it.stableId,
+          displayId: it.stackId,
+          isVirtual: false,
+          isClickable: true,
+          cornerRadius: it.cornerRadius,
+          // TODO(b/291213403): should read this data from the trace instead of a global variable
+          hasContent: ParserViewCapture.packageNames.includes(
+            it.rect.label.substring(0, it.rect.label.indexOf('/'))
+          ),
+        };
+        return rect;
+      });
+  }
+
+  private static makeDisplayRects(entry: LayerTraceEntry): Rectangle[] {
+    if (!entry.displays) {
+      return [];
+    }
+
+    return entry.displays?.map((display: any) => {
+      const transform: TransformMatrix = display.transform?.matrix ?? display.transform;
+      const rect: Rectangle = {
+        topLeft: {x: 0, y: 0},
+        bottomRight: {x: display.size.width, y: display.size.height},
+        label: 'Display',
+        transform,
+        isVisible: false,
+        isDisplay: true,
+        id: `Display - ${display.id}`,
+        displayId: display.layerStackId,
+        isVirtual: display.isVirtual ?? false,
+        isClickable: false,
+        cornerRadius: 0,
+        hasContent: false,
+      };
+      return rect;
+    });
+  }
+
+  private static isLayerToRenderInRectsComponent(
+    layer: Layer,
+    hierarchyUserOptions: UserOptions
+  ): boolean {
+    const onlyVisible = hierarchyUserOptions['onlyVisible']?.enabled ?? false;
+    // Show only visible layers or Visible + Occluded layers. Don't show all layers
+    // (flattenedLayers) because container layers are never meant to be displayed
+    return layer.isVisible || (!onlyVisible && layer.occludedBy.length > 0);
+  }
+
+  static compareLayerZ(a: Layer, b: Layer): number {
+    const zipLength = Math.min(a.zOrderPath.length, b.zOrderPath.length);
+    for (let i = 0; i < zipLength; ++i) {
+      const zOrderA = a.zOrderPath[i];
+      const zOrderB = b.zOrderPath[i];
+      if (zOrderA > zOrderB) return -1;
+      if (zOrderA < zOrderB) return 1;
+    }
+    // When z-order is the same, the layer with larger ID is on top
+    return a.id > b.id ? -1 : 1;
+  }
+}
diff --git a/tools/winscope/src/viewers/common/surface_flinger_utils_test.ts b/tools/winscope/src/viewers/common/surface_flinger_utils_test.ts
new file mode 100644
index 0000000..fb604f2
--- /dev/null
+++ b/tools/winscope/src/viewers/common/surface_flinger_utils_test.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 {Layer} from 'flickerlib/common';
+import {SurfaceFlingerUtils as Utils} from './surface_flinger_utils';
+
+describe('SurfaceFlingerUtils', () => {
+  describe("Layer's z order comparison", () => {
+    it('handles z-order paths with equal lengths', () => {
+      const a: Layer = {
+        zOrderPath: [1],
+      };
+      const b: Layer = {
+        zOrderPath: [0],
+      };
+      expect(Utils.compareLayerZ(a, b)).toEqual(-1);
+      expect(Utils.compareLayerZ(b, a)).toEqual(1);
+    });
+
+    it('handles z-order paths with different lengths', () => {
+      const a: Layer = {
+        zOrderPath: [0, 1],
+      };
+      const b: Layer = {
+        zOrderPath: [0, 0, 0],
+      };
+      expect(Utils.compareLayerZ(a, b)).toEqual(-1);
+      expect(Utils.compareLayerZ(b, a)).toEqual(1);
+    });
+
+    it('handles z-order paths with equal values (fall back to Layer ID comparison)', () => {
+      const a: Layer = {
+        id: 1,
+        zOrderPath: [0, 1],
+      };
+      const b: Layer = {
+        id: 0,
+        zOrderPath: [0, 1, 0],
+      };
+      expect(Utils.compareLayerZ(a, b)).toEqual(-1);
+      expect(Utils.compareLayerZ(b, a)).toEqual(1);
+    });
+  });
+});
diff --git a/tools/winscope/src/viewers/common/tree_generator.ts b/tools/winscope/src/viewers/common/tree_generator.ts
index 51f9207..e5d2fc3 100644
--- a/tools/winscope/src/viewers/common/tree_generator.ts
+++ b/tools/winscope/src/viewers/common/tree_generator.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 import {FilterType, TreeNode} from 'common/tree_utils';
-import {ObjectFormatter} from 'trace/flickerlib/ObjectFormatter';
+import {ObjectFormatter} from 'flickerlib/ObjectFormatter';
 import {TraceTreeNode} from 'trace/trace_tree_node';
 import {
   GPU_CHIP,
diff --git a/tools/winscope/src/viewers/common/tree_transformer.ts b/tools/winscope/src/viewers/common/tree_transformer.ts
index a39eb3e..15b7640 100644
--- a/tools/winscope/src/viewers/common/tree_transformer.ts
+++ b/tools/winscope/src/viewers/common/tree_transformer.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 import {FilterType, TreeNode} from 'common/tree_utils';
-import {ObjectFormatter} from 'trace/flickerlib/ObjectFormatter';
+import {ObjectFormatter} from 'flickerlib/ObjectFormatter';
 import {TraceTreeNode} from 'trace/trace_tree_node';
 
 import {
diff --git a/tools/winscope/src/viewers/common/ui_tree_utils.ts b/tools/winscope/src/viewers/common/ui_tree_utils.ts
index 0039e71..a4ca5bf 100644
--- a/tools/winscope/src/viewers/common/ui_tree_utils.ts
+++ b/tools/winscope/src/viewers/common/ui_tree_utils.ts
@@ -83,15 +83,19 @@
     return diffType ?? '';
   }
 
-  static isHighlighted(item: UiTreeNode, highlightedItems: string[]) {
+  static isHighlighted(item: UiTreeNode, highlightedItems: string[]): boolean {
     return item instanceof HierarchyTreeNode && highlightedItems.includes(`${item.stableId}`);
   }
 
-  static isVisibleNode(kind: string, type?: string) {
-    return kind === 'WindowState' || kind === 'Activity' || type?.includes('Layer');
+  static isVisibleNode(kind: string, type?: string): boolean {
+    return kind === 'WindowState' || kind === 'Activity' || UiTreeUtils.isSFEntryNode(type);
   }
 
-  static isParentNode(kind: string) {
+  private static isSFEntryNode(type?: string): boolean {
+    return type === undefined;
+  }
+
+  static isParentNode(kind: string): boolean {
     return UiTreeUtils.PARENT_NODE_KINDS.includes(kind);
   }
 
diff --git a/tools/winscope/src/viewers/common/user_options.ts b/tools/winscope/src/viewers/common/user_options.ts
index d5c2bdb..54fffa2 100644
--- a/tools/winscope/src/viewers/common/user_options.ts
+++ b/tools/winscope/src/viewers/common/user_options.ts
@@ -18,5 +18,6 @@
     name: string;
     enabled: boolean;
     tooltip?: string;
+    isUnavailable?: boolean;
   };
 }
diff --git a/tools/winscope/src/viewers/common/view_capture_constants.ts b/tools/winscope/src/viewers/common/view_capture_constants.ts
new file mode 100644
index 0000000..b5b42cb
--- /dev/null
+++ b/tools/winscope/src/viewers/common/view_capture_constants.ts
@@ -0,0 +1,17 @@
+/*
+ * 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.
+ */
+
+export const NEXUS_LAUNCHER_PACKAGE_NAME = 'com.google.android.apps.nexuslauncher';
diff --git a/tools/winscope/src/viewers/common/viewer_events.ts b/tools/winscope/src/viewers/common/viewer_events.ts
index fd109da..21420bd 100644
--- a/tools/winscope/src/viewers/common/viewer_events.ts
+++ b/tools/winscope/src/viewers/common/viewer_events.ts
@@ -22,4 +22,10 @@
   PropertiesUserOptionsChange: 'PropertiesUserOptionsChange',
   PropertiesFilterChange: 'PropertiesFilterChange',
   AdditionalPropertySelected: 'AdditionalPropertySelected',
+  RectsDblClick: 'RectsDblClick',
+  MiniRectsDblClick: 'MiniRectsDblClick',
 };
+
+export class RectDblClickDetail {
+  constructor(public clickedRectId: string) {}
+}
diff --git a/tools/winscope/src/viewers/common/viewer_input_method.ts b/tools/winscope/src/viewers/common/viewer_input_method.ts
index 61c3e31..642a3aa 100644
--- a/tools/winscope/src/viewers/common/viewer_input_method.ts
+++ b/tools/winscope/src/viewers/common/viewer_input_method.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
+import {AppEvent} from 'app/app_event';
 import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {ImeUiData} from 'viewers/common/ime_ui_data';
 import {PresenterInputMethod} from 'viewers/common/presenter_input_method';
@@ -23,14 +23,21 @@
 import {View, Viewer} from 'viewers/viewer';
 
 abstract class ViewerInputMethod implements Viewer {
+  protected htmlElement: HTMLElement;
+  protected presenter: PresenterInputMethod;
+
   constructor(traces: Traces, storage: Storage) {
     this.htmlElement = document.createElement('viewer-input-method');
     this.presenter = this.initialisePresenter(traces, storage);
     this.addViewerEventListeners();
   }
 
-  async onTracePositionUpdate(position: TracePosition) {
-    await this.presenter.onTracePositionUpdate(position);
+  async onAppEvent(event: AppEvent) {
+    await this.presenter.onAppEvent(event);
+  }
+
+  setEmitAppEvent() {
+    // do nothing
   }
 
   abstract getViews(): View[];
@@ -72,9 +79,6 @@
   }
 
   protected abstract initialisePresenter(traces: Traces, storage: Storage): PresenterInputMethod;
-
-  protected htmlElement: HTMLElement;
-  protected presenter: PresenterInputMethod;
 }
 
 export {ViewerInputMethod};
diff --git a/tools/winscope/src/viewers/components/hierarchy_component.ts b/tools/winscope/src/viewers/components/hierarchy_component.ts
index dd4ca94..9925e00 100644
--- a/tools/winscope/src/viewers/components/hierarchy_component.ts
+++ b/tools/winscope/src/viewers/components/hierarchy_component.ts
@@ -38,6 +38,7 @@
           *ngFor="let option of objectKeys(userOptions)"
           color="primary"
           [(ngModel)]="userOptions[option].enabled"
+          [disabled]="userOptions[option].isUnavailable ?? false"
           (ngModelChange)="updateTree()"
           >{{ userOptions[option].name }}</mat-checkbox
         >
diff --git a/tools/winscope/src/viewers/components/hierarchy_component_test.ts b/tools/winscope/src/viewers/components/hierarchy_component_test.ts
index 33b8417..8fb2996 100644
--- a/tools/winscope/src/viewers/components/hierarchy_component_test.ts
+++ b/tools/winscope/src/viewers/components/hierarchy_component_test.ts
@@ -64,9 +64,10 @@
 
     component.store = new PersistentStore();
     component.userOptions = {
-      onlyVisible: {
-        name: 'Only visible',
+      showDiff: {
+        name: 'Show diff',
         enabled: false,
+        isUnavailable: true,
       },
     };
     component.pinnedItems = [component.tree];
@@ -94,4 +95,10 @@
     expect(treeView!.innerHTML).toContain('Root node');
     expect(treeView!.innerHTML).toContain('Child node');
   });
+
+  it('disables checkboxes if option unavailable', async () => {
+    const viewControls = htmlElement.querySelector('.view-controls');
+    expect(viewControls).toBeTruthy();
+    expect(viewControls!.innerHTML).toContain('disabled=""');
+  });
 });
diff --git a/tools/winscope/src/viewers/components/properties_component.ts b/tools/winscope/src/viewers/components/properties_component.ts
index 9162c04..7f2952d 100644
--- a/tools/winscope/src/viewers/components/properties_component.ts
+++ b/tools/winscope/src/viewers/components/properties_component.ts
@@ -15,6 +15,7 @@
  */
 import {Component, ElementRef, Inject, Input} from '@angular/core';
 import {TraceTreeNode} from 'trace/trace_tree_node';
+import {TraceType, ViewNode} from 'trace/trace_type';
 import {PropertiesTreeNode, Terminal} from 'viewers/common/ui_tree_utils';
 import {UserOptions} from 'viewers/common/user_options';
 import {ViewerEvents} from 'viewers/common/viewer_events';
@@ -38,16 +39,22 @@
           *ngFor="let option of objectKeys(userOptions)"
           color="primary"
           [(ngModel)]="userOptions[option].enabled"
+          [disabled]="userOptions[option].isUnavailable ?? false"
           (ngModelChange)="updateTree()"
           [matTooltip]="userOptions[option].tooltip ?? ''"
           >{{ userOptions[option].name }}</mat-checkbox
         >
       </div>
 
-      <property-groups
-        *ngIf="itemIsSelected() && displayPropertyGroups"
+      <surface-flinger-property-groups
+        *ngIf="itemIsSelected() && isSurfaceFlinger() && displayPropertyGroups"
         class="property-groups"
-        [item]="selectedFlickerItem"></property-groups>
+        [item]="selectedItem"></surface-flinger-property-groups>
+
+      <view-capture-property-groups
+        *ngIf="showViewCaptureFormat()"
+        class="property-groups"
+        [item]="selectedItem"></view-capture-property-groups>
     </div>
 
     <mat-divider></mat-divider>
@@ -61,7 +68,7 @@
 
       <div class="tree-wrapper">
         <tree-view
-          *ngIf="objectKeys(propertiesTree).length > 0"
+          *ngIf="objectKeys(propertiesTree).length > 0 && !showViewCaptureFormat()"
           [item]="propertiesTree"
           [showNode]="showNode"
           [isLeaf]="isLeaf"
@@ -121,9 +128,10 @@
 
   @Input() userOptions: UserOptions = {};
   @Input() propertiesTree: PropertiesTreeNode = {};
-  @Input() selectedFlickerItem: TraceTreeNode | null = null;
+  @Input() selectedItem: TraceTreeNode | ViewNode | null = null;
   @Input() displayPropertyGroups = false;
   @Input() isProtoDump = false;
+  @Input() traceType: TraceType | undefined;
 
   constructor(@Inject(ElementRef) private elementRef: ElementRef) {}
 
@@ -160,6 +168,20 @@
   }
 
   itemIsSelected() {
-    return this.selectedFlickerItem && Object.keys(this.selectedFlickerItem).length > 0;
+    return this.selectedItem && Object.keys(this.selectedItem).length > 0;
+  }
+
+  showViewCaptureFormat(): boolean {
+    return (
+      this.traceType === TraceType.VIEW_CAPTURE &&
+      this.filterString === '' &&
+      // Todo: Highlight Inline in formatted ViewCapture Properties Component.
+      this.userOptions['showDiff']?.enabled === false &&
+      this.selectedItem
+    );
+  }
+
+  isSurfaceFlinger(): boolean {
+    return this.traceType === TraceType.SURFACE_FLINGER;
   }
 }
diff --git a/tools/winscope/src/viewers/components/properties_component_test.ts b/tools/winscope/src/viewers/components/properties_component_test.ts
index 9098ff3..2a77775 100644
--- a/tools/winscope/src/viewers/components/properties_component_test.ts
+++ b/tools/winscope/src/viewers/components/properties_component_test.ts
@@ -22,7 +22,7 @@
 import {MatInputModule} from '@angular/material/input';
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 import {PropertiesComponent} from './properties_component';
-import {PropertyGroupsComponent} from './property_groups_component';
+import {SurfaceFlingerPropertyGroupsComponent} from './surface_flinger_property_groups_component';
 import {TreeComponent} from './tree_component';
 
 describe('PropertiesComponent', () => {
@@ -33,7 +33,7 @@
   beforeAll(async () => {
     await TestBed.configureTestingModule({
       providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
-      declarations: [PropertiesComponent, PropertyGroupsComponent, TreeComponent],
+      declarations: [PropertiesComponent, SurfaceFlingerPropertyGroupsComponent, TreeComponent],
       imports: [
         CommonModule,
         MatInputModule,
@@ -51,7 +51,7 @@
     component = fixture.componentInstance;
     htmlElement = fixture.nativeElement;
     component.propertiesTree = {};
-    component.selectedFlickerItem = null;
+    component.selectedItem = null;
     component.userOptions = {
       showDefaults: {
         name: 'Show defaults',
diff --git a/tools/winscope/src/viewers/components/rects/canvas.ts b/tools/winscope/src/viewers/components/rects/canvas.ts
index 078c9ee..420abd6 100644
--- a/tools/winscope/src/viewers/components/rects/canvas.ts
+++ b/tools/winscope/src/viewers/components/rects/canvas.ts
@@ -15,13 +15,13 @@
  */
 import * as THREE from 'three';
 import {CSS2DObject, CSS2DRenderer} from 'three/examples/jsm/renderers/CSS2DRenderer';
-import {Rectangle} from 'viewers/common/rectangle';
 import {ViewerEvents} from 'viewers/common/viewer_events';
-import {Circle3D, ColorType, Label3D, Point3D, Rect3D, Scene3D, Transform3D} from './types3d';
+import {Circle3D, ColorType, Label3D, Point3D, Rect3D, Scene3D, TransformMatrix} from './types3d';
 
 export class Canvas {
-  private static readonly TARGET_SCENE_DIAGONAL = 4;
+  static readonly TARGET_SCENE_DIAGONAL = 4;
   private static readonly RECT_COLOR_HIGHLIGHTED = new THREE.Color(0xd2e3fc);
+  private static readonly RECT_COLOR_HAS_CONTENT = new THREE.Color(0xad42f5);
   private static readonly RECT_EDGE_COLOR = 0x000000;
   private static readonly RECT_EDGE_COLOR_ROUNDED = 0x848884;
   private static readonly LABEL_CIRCLE_COLOR = 0x000000;
@@ -31,15 +31,14 @@
   private static readonly OPACITY_OVERSIZED = 0.25;
 
   private canvasRects: HTMLCanvasElement;
-  private canvasLabels: HTMLElement;
+  private canvasLabels?: HTMLElement;
   private camera?: THREE.OrthographicCamera;
   private scene?: THREE.Scene;
   private renderer?: THREE.WebGLRenderer;
   private labelRenderer?: CSS2DRenderer;
-  private rects: Rectangle[] = [];
   private clickableObjects: THREE.Object3D[] = [];
 
-  constructor(canvasRects: HTMLCanvasElement, canvasLabels: HTMLElement) {
+  constructor(canvasRects: HTMLCanvasElement, canvasLabels?: HTMLElement) {
     this.canvasRects = canvasRects;
     this.canvasLabels = canvasLabels;
   }
@@ -98,20 +97,20 @@
       alpha: true,
     });
 
-    this.labelRenderer = new CSS2DRenderer({element: this.canvasLabels});
-
     // set various factors for shading and shifting
-    const numberOfRects = this.rects.length;
     this.drawRects(scene.rects);
-    this.drawLabels(scene.labels);
+    if (this.canvasLabels) {
+      this.drawLabels(scene.labels);
+
+      this.labelRenderer = new CSS2DRenderer({element: this.canvasLabels});
+      this.labelRenderer.setSize(this.canvasRects!.clientWidth, this.canvasRects!.clientHeight);
+      this.labelRenderer.render(this.scene, this.camera);
+    }
 
     this.renderer.setSize(this.canvasRects!.clientWidth, this.canvasRects!.clientHeight);
     this.renderer.setPixelRatio(window.devicePixelRatio);
     this.renderer.compile(this.scene, this.camera);
     this.renderer.render(this.scene, this.camera);
-
-    this.labelRenderer.setSize(this.canvasRects!.clientWidth, this.canvasRects!.clientHeight);
-    this.labelRenderer.render(this.scene, this.camera);
   }
 
   getClickedRectId(x: number, y: number, z: number): undefined | string {
@@ -128,8 +127,8 @@
   private drawRects(rects: Rect3D[]) {
     this.clickableObjects = [];
     rects.forEach((rect) => {
-      const rectMesh = this.makeRectMesh(rect);
-      const transform = this.toMatrix4(rect.transform);
+      const rectMesh = Canvas.makeRectMesh(rect);
+      const transform = Canvas.toMatrix4(rect.transform);
       rectMesh.applyMatrix4(transform);
 
       this.scene?.add(rectMesh);
@@ -195,7 +194,7 @@
     this.scene?.add(labelCss);
   }
 
-  private toMatrix4(transform: Transform3D): THREE.Matrix4 {
+  private static toMatrix4(transform: TransformMatrix): THREE.Matrix4 {
     return new THREE.Matrix4().set(
       transform.dsdx,
       transform.dsdy,
@@ -216,10 +215,10 @@
     );
   }
 
-  private makeRectMesh(rect: Rect3D): THREE.Mesh {
-    const rectShape = this.createRectShape(rect);
+  private static makeRectMesh(rect: Rect3D): THREE.Mesh {
+    const rectShape = Canvas.createRectShape(rect);
     const rectGeometry = new THREE.ShapeGeometry(rectShape);
-    const rectBorders = this.createRectBorders(rect, rectGeometry);
+    const rectBorders = Canvas.createRectBorders(rect, rectGeometry);
 
     let opacity = Canvas.OPACITY_REGULAR;
     if (rect.isOversized) {
@@ -230,7 +229,7 @@
     const mesh = new THREE.Mesh(
       rectGeometry,
       new THREE.MeshBasicMaterial({
-        color: this.getColor(rect),
+        color: Canvas.getColor(rect),
         opacity,
         transparent: true,
       })
@@ -245,7 +244,7 @@
     return mesh;
   }
 
-  private createRectShape(rect: Rect3D): THREE.Shape {
+  private static createRectShape(rect: Rect3D): THREE.Shape {
     const bottomLeft: Point3D = {x: rect.topLeft.x, y: rect.bottomRight.y, z: rect.topLeft.z};
     const topRight: Point3D = {x: rect.bottomRight.x, y: rect.topLeft.y, z: rect.bottomRight.z};
 
@@ -283,7 +282,7 @@
       );
   }
 
-  private getColor(rect: Rect3D): THREE.Color {
+  private static getColor(rect: Rect3D): THREE.Color {
     switch (rect.colorType) {
       case ColorType.VISIBLE: {
         // green (darkness depends on z order)
@@ -302,13 +301,19 @@
       case ColorType.HIGHLIGHTED: {
         return Canvas.RECT_COLOR_HIGHLIGHTED;
       }
+      case ColorType.HAS_CONTENT: {
+        return Canvas.RECT_COLOR_HAS_CONTENT;
+      }
       default: {
         throw new Error(`Unexpected color type: ${rect.colorType}`);
       }
     }
   }
 
-  private createRectBorders(rect: Rect3D, rectGeometry: THREE.ShapeGeometry): THREE.LineSegments {
+  private static createRectBorders(
+    rect: Rect3D,
+    rectGeometry: THREE.ShapeGeometry
+  ): THREE.LineSegments {
     // create line edges for rect
     const edgeGeo = new THREE.EdgesGeometry(rectGeometry);
     let edgeMaterial: THREE.Material;
@@ -346,6 +351,8 @@
   }
 
   private clearLabels() {
-    this.canvasLabels.innerHTML = '';
+    if (this.canvasLabels) {
+      this.canvasLabels.innerHTML = '';
+    }
   }
 }
diff --git a/tools/winscope/src/viewers/components/rects/mapper3d.ts b/tools/winscope/src/viewers/components/rects/mapper3d.ts
index 3e131e1..5750e44 100644
--- a/tools/winscope/src/viewers/components/rects/mapper3d.ts
+++ b/tools/winscope/src/viewers/components/rects/mapper3d.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {Rectangle, Size} from 'viewers/common/rectangle';
+import {Rectangle, Size} from 'viewers/components/rects/types2d';
 import {
   Box3D,
   ColorType,
@@ -23,7 +23,7 @@
   Point3D,
   Rect3D,
   Scene3D,
-  Transform3D,
+  TransformMatrix,
 } from './types3d';
 
 class Mapper3D {
@@ -37,7 +37,7 @@
   private static readonly ZOOM_FACTOR_MIN = 0.1;
   private static readonly ZOOM_FACTOR_MAX = 8.5;
   private static readonly ZOOM_FACTOR_STEP = 0.2;
-  private static readonly IDENTITY_TRANSFORM: Transform3D = {
+  private static readonly IDENTITY_TRANSFORM: TransformMatrix = {
     dsdx: 1,
     dsdy: 0,
     tx: 0,
@@ -80,13 +80,13 @@
     this.zSpacingFactor = Math.min(Math.max(factor, 0), 1);
   }
 
-  increaseZoomFactor() {
-    this.zoomFactor += Mapper3D.ZOOM_FACTOR_STEP;
+  increaseZoomFactor(times: number = 1) {
+    this.zoomFactor += Mapper3D.ZOOM_FACTOR_STEP * times;
     this.zoomFactor = Math.min(this.zoomFactor, Mapper3D.ZOOM_FACTOR_MAX);
   }
 
-  decreaseZoomFactor() {
-    this.zoomFactor -= Mapper3D.ZOOM_FACTOR_STEP;
+  decreaseZoomFactor(times: number = 1) {
+    this.zoomFactor -= Mapper3D.ZOOM_FACTOR_STEP * times;
     this.zoomFactor = Math.max(this.zoomFactor, Mapper3D.ZOOM_FACTOR_MIN);
   }
 
@@ -207,7 +207,7 @@
         darkFactor,
         colorType: this.getColorType(rect2d),
         isClickable: rect2d.isClickable,
-        transform: this.getTransform(rect2d),
+        transform: rect2d.transform ?? Mapper3D.IDENTITY_TRANSFORM,
       };
       return this.cropOversizedRect(rect, maxDisplaySize);
     });
@@ -219,6 +219,8 @@
     let colorType: ColorType;
     if (this.highlightedRectIds.includes(rect2d.id)) {
       colorType = ColorType.HIGHLIGHTED;
+    } else if (rect2d.hasContent === true) {
+      colorType = ColorType.HAS_CONTENT;
     } else if (rect2d.isVisible) {
       colorType = ColorType.VISIBLE;
     } else {
@@ -227,23 +229,6 @@
     return colorType;
   }
 
-  private getTransform(rect2d: Rectangle): Transform3D {
-    let transform: Transform3D;
-    if (rect2d.transform?.matrix) {
-      transform = {
-        dsdx: rect2d.transform.matrix.dsdx,
-        dsdy: rect2d.transform.matrix.dsdy,
-        tx: rect2d.transform.matrix.tx,
-        dtdx: rect2d.transform.matrix.dtdx,
-        dtdy: rect2d.transform.matrix.dtdy,
-        ty: rect2d.transform.matrix.ty,
-      };
-    } else {
-      transform = Mapper3D.IDENTITY_TRANSFORM;
-    }
-    return transform;
-  }
-
   private getMaxDisplaySize(rects2d: Rectangle[]): Size {
     const displays = rects2d.filter((rect2d) => rect2d.isDisplay);
 
@@ -361,7 +346,7 @@
     return labels3d;
   }
 
-  private matMultiply(mat: Transform3D, point: Point3D): Point3D {
+  private matMultiply(mat: TransformMatrix, point: Point3D): Point3D {
     return {
       x: mat.dsdx * point.x + mat.dsdy * point.y + mat.tx,
       y: mat.dtdx * point.x + mat.dtdy * point.y + mat.ty,
diff --git a/tools/winscope/src/viewers/components/rects/rects_component.ts b/tools/winscope/src/viewers/components/rects/rects_component.ts
index 54ce85f..42211cb 100644
--- a/tools/winscope/src/viewers/components/rects/rects_component.ts
+++ b/tools/winscope/src/viewers/components/rects/rects_component.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 import {Component, ElementRef, HostListener, Inject, Input, OnDestroy, OnInit} from '@angular/core';
-import {Rectangle} from 'viewers/common/rectangle';
-import {ViewerEvents} from 'viewers/common/viewer_events';
+import {RectDblClickDetail, ViewerEvents} from 'viewers/common/viewer_events';
+import {Rectangle} from 'viewers/components/rects/types2d';
 import {Canvas} from './canvas';
 import {Mapper3D} from './mapper3d';
 import {Distance2D} from './types3d';
@@ -86,9 +86,16 @@
     <mat-divider></mat-divider>
     <div class="rects-content">
       <div class="canvas-container">
-        <canvas class="canvas-rects" (click)="onRectClick($event)" oncontextmenu="return false">
-        </canvas>
-        <div class="canvas-labels"></div>
+        <canvas
+          class="large-rects-canvas"
+          (click)="onRectClick($event)"
+          (dblclick)="onRectDblClick($event)"
+          oncontextmenu="return false"></canvas>
+        <div class="large-rects-labels"></div>
+        <canvas
+          class="mini-rects-canvas"
+          (dblclick)="onMiniRectDblClick($event)"
+          oncontextmenu="return false"></canvas>
       </div>
       <div *ngIf="internalDisplayIds.length > 1" class="display-button-container">
         <button
@@ -140,7 +147,7 @@
         width: 100%;
         position: relative;
       }
-      .canvas-rects {
+      .large-rects-canvas {
         position: absolute;
         top: 0;
         left: 0;
@@ -148,7 +155,7 @@
         height: 100%;
         cursor: pointer;
       }
-      .canvas-labels {
+      .large-rects-labels {
         position: absolute;
         top: 0;
         left: 0;
@@ -162,47 +169,64 @@
         flex-wrap: wrap;
         column-gap: 10px;
       }
+      .mini-rects-canvas {
+        cursor: pointer;
+        width: 30%;
+        height: 30%;
+        top: 16px;
+        display: block;
+        position: absolute;
+        z-index: 1000;
+      }
     `,
   ],
 })
 export class RectsComponent implements OnInit, OnDestroy {
   @Input() title = 'title';
   @Input() enableShowVirtualButton: boolean = true;
+  @Input() zoomFactor: number = 1;
   @Input() set rects(rects: Rectangle[]) {
     this.internalRects = rects;
-    this.drawScene();
+    this.drawLargeRectsAndLabels();
+  }
+  @Input() set miniRects(rects: Rectangle[] | undefined) {
+    this.internalMiniRects = rects;
+    this.drawMiniRects();
   }
 
   @Input() set displayIds(ids: number[]) {
     this.internalDisplayIds = ids;
     if (!this.internalDisplayIds.includes(this.mapper3d.getCurrentDisplayId())) {
       this.mapper3d.setCurrentDisplayId(this.internalDisplayIds[0]);
-      this.drawScene();
+      this.drawLargeRectsAndLabels();
     }
   }
 
   @Input() set highlightedItems(stableIds: string[]) {
     this.internalHighlightedItems = stableIds;
     this.mapper3d.setHighlightedRectIds(this.internalHighlightedItems);
-    this.drawScene();
+    this.drawLargeRectsAndLabels();
   }
 
   private internalRects: Rectangle[] = [];
+  private internalMiniRects?: Rectangle[];
   private internalDisplayIds: number[] = [];
   private internalHighlightedItems: string[] = [];
 
   private mapper3d: Mapper3D;
-  private canvas?: Canvas;
+  private largeRectsCanvas?: Canvas;
+  private miniRectsCanvas?: Canvas;
   private resizeObserver: ResizeObserver;
-  private canvasRects?: HTMLCanvasElement;
-  private canvasLabels?: HTMLElement;
+  private largeRectsCanvasElement?: HTMLCanvasElement;
+  private miniRectsCanvasElement?: HTMLCanvasElement;
+  private largeRectsLabelsElement?: HTMLElement;
   private mouseMoveListener = (event: MouseEvent) => this.onMouseMove(event);
   private mouseUpListener = (event: MouseEvent) => this.onMouseUp(event);
 
   constructor(@Inject(ElementRef) private elementRef: ElementRef) {
     this.mapper3d = new Mapper3D();
     this.resizeObserver = new ResizeObserver((entries) => {
-      this.drawScene();
+      this.drawLargeRectsAndLabels();
     });
   }
 
@@ -210,14 +234,26 @@
     const canvasContainer = this.elementRef.nativeElement.querySelector('.canvas-container');
     this.resizeObserver.observe(canvasContainer);
 
-    this.canvasRects = canvasContainer.querySelector('.canvas-rects')! as HTMLCanvasElement;
-    this.canvasLabels = canvasContainer.querySelector('.canvas-labels');
-    this.canvas = new Canvas(this.canvasRects, this.canvasLabels!);
-
-    this.canvasRects.addEventListener('mousedown', (event) => this.onCanvasMouseDown(event));
+    this.largeRectsCanvasElement = canvasContainer.querySelector(
+      '.large-rects-canvas'
+    )! as HTMLCanvasElement;
+    this.largeRectsLabelsElement = canvasContainer.querySelector('.large-rects-labels');
+    this.largeRectsCanvas = new Canvas(this.largeRectsCanvasElement, this.largeRectsLabelsElement!);
+    this.largeRectsCanvasElement.addEventListener('mousedown', (event) =>
+      this.onCanvasMouseDown(event)
+    );
 
     this.mapper3d.setCurrentDisplayId(this.internalDisplayIds[0] ?? 0);
-    this.drawScene();
+    this.mapper3d.increaseZoomFactor(this.zoomFactor - 1);
+    this.drawLargeRectsAndLabels();
+
+    this.miniRectsCanvasElement = canvasContainer.querySelector(
+      '.mini-rects-canvas'
+    )! as HTMLCanvasElement;
+    this.miniRectsCanvas = new Canvas(this.miniRectsCanvasElement);
+    if (this.miniRects) {
+      this.drawMiniRects();
+    }
   }
 
   ngOnDestroy() {
@@ -226,17 +262,17 @@
 
   onSeparationSliderChange(factor: number) {
     this.mapper3d.setZSpacingFactor(factor);
-    this.drawScene();
+    this.drawLargeRectsAndLabels();
   }
 
   onRotationSliderChange(factor: number) {
     this.mapper3d.setCameraRotationFactor(factor);
-    this.drawScene();
+    this.drawLargeRectsAndLabels();
   }
 
   resetCamera() {
     this.mapper3d.resetCamera();
-    this.drawScene();
+    this.drawLargeRectsAndLabels();
   }
 
   @HostListener('wheel', ['$event'])
@@ -256,7 +292,7 @@
   onMouseMove(event: MouseEvent) {
     const distance = new Distance2D(event.movementX, event.movementY);
     this.mapper3d.addPanScreenDistance(distance);
-    this.drawScene();
+    this.drawLargeRectsAndLabels();
   }
 
   onMouseUp(event: MouseEvent) {
@@ -274,22 +310,53 @@
 
   onShowOnlyVisibleModeChange(enabled: boolean) {
     this.mapper3d.setShowOnlyVisibleMode(enabled);
-    this.drawScene();
+    this.drawLargeRectsAndLabels();
   }
 
   onShowVirtualModeChange(enabled: boolean) {
     this.mapper3d.setShowVirtualMode(enabled);
-    this.drawScene();
+    this.drawLargeRectsAndLabels();
   }
 
   onDisplayIdChange(id: number) {
     this.mapper3d.setCurrentDisplayId(id);
-    this.drawScene();
+    this.drawLargeRectsAndLabels();
   }
 
   onRectClick(event: MouseEvent) {
     event.preventDefault();
 
+    const id = this.findClickedRectId(event);
+    if (id !== undefined) {
+      this.notifyHighlightedItem(id);
+    }
+  }
+
+  onRectDblClick(event: MouseEvent) {
+    event.preventDefault();
+
+    const clickedRectId = this.findClickedRectId(event);
+    if (clickedRectId === undefined) {
+      return;
+    }
+
+    this.elementRef.nativeElement.dispatchEvent(
+      new CustomEvent(ViewerEvents.RectsDblClick, {
+        bubbles: true,
+        detail: new RectDblClickDetail(clickedRectId),
+      })
+    );
+  }
+
+  onMiniRectDblClick(event: MouseEvent) {
+    event.preventDefault();
+
+    this.elementRef.nativeElement.dispatchEvent(
+      new CustomEvent(ViewerEvents.MiniRectsDblClick, {bubbles: true})
+    );
+  }
+
+  private findClickedRectId(event: MouseEvent): string | undefined {
     const canvas = event.target as Element;
     const canvasOffset = canvas.getBoundingClientRect();
 
@@ -297,29 +364,45 @@
     const y = -((event.clientY - canvasOffset.top) / canvas.clientHeight) * 2 + 1;
     const z = 0;
 
-    const id = this.canvas?.getClickedRectId(x, y, z);
-    if (id !== undefined) {
-      this.notifyHighlightedItem(id);
-    }
+    return this.largeRectsCanvas?.getClickedRectId(x, y, z);
   }
 
   private doZoomIn() {
     this.mapper3d.increaseZoomFactor();
-    this.drawScene();
+    this.drawLargeRectsAndLabels();
   }
 
   private doZoomOut() {
     this.mapper3d.decreaseZoomFactor();
-    this.drawScene();
+    this.drawLargeRectsAndLabels();
   }
 
-  private drawScene() {
-    // TODO: Re-create scene only when input rects change. With the other input events
-    //  (rotation, spacing, ...) we can just update the camera and/or update the mesh positions.
-    //  We'd probably need to get rid of the intermediate layer (Scene3D, Rect3D, ... types) and
-    //  work directly with three.js's meshes.
+  private drawLargeRectsAndLabels() {
+    // TODO(b/258593034): Re-create scene only when input rects change. With the other input events
+    // (rotation, spacing, ...) we can just update the camera and/or update the mesh positions.
+    // We'd probably need to get rid of the intermediate layer (Scene3D, Rect3D, ... types) and
+    // work directly with three.js's meshes.
     this.mapper3d.setRects(this.internalRects);
-    this.canvas?.draw(this.mapper3d.computeScene());
+    this.largeRectsCanvas?.draw(this.mapper3d.computeScene());
+  }
+
+  private drawMiniRects() {
+    // TODO(b/258593034): Re-create scene only when input rects change. With the other input events
+    // (rotation, spacing, ...) we can just update the camera and/or update the mesh positions.
+    // We'd probably need to get rid of the intermediate layer (Scene3D, Rect3D, ... types) and
+    // work directly with three.js's meshes.
+    if (this.internalMiniRects) {
+      this.mapper3d.setRects(this.internalMiniRects);
+      this.mapper3d.decreaseZoomFactor(this.zoomFactor - 1);
+      this.miniRectsCanvas?.draw(this.mapper3d.computeScene());
+      this.mapper3d.increaseZoomFactor(this.zoomFactor - 1);
+
+      // Mapper internally sets these values to 100%. They need to be reset afterwards
+      if (this.miniRectsCanvasElement) {
+        this.miniRectsCanvasElement.style.width = '25%';
+        this.miniRectsCanvasElement.style.height = '25%';
+      }
+    }
   }
 
   private notifyHighlightedItem(id: string) {
diff --git a/tools/winscope/src/viewers/components/rects/rects_component_test.ts b/tools/winscope/src/viewers/components/rects/rects_component_test.ts
index 0196e99..feda1dd 100644
--- a/tools/winscope/src/viewers/components/rects/rects_component_test.ts
+++ b/tools/winscope/src/viewers/components/rects/rects_component_test.ts
@@ -20,8 +20,8 @@
 import {MatDividerModule} from '@angular/material/divider';
 import {MatRadioModule} from '@angular/material/radio';
 import {MatSliderModule} from '@angular/material/slider';
-import {Rectangle} from 'viewers/common/rectangle';
 import {RectsComponent} from 'viewers/components/rects/rects_component';
+import {Rectangle} from 'viewers/components/rects/types2d';
 import {Canvas} from './canvas';
 
 describe('RectsComponent', () => {
@@ -57,7 +57,7 @@
   });
 
   it('renders canvas', () => {
-    const rectsCanvas = htmlElement.querySelector('.canvas-rects');
+    const rectsCanvas = htmlElement.querySelector('.large-rects-canvas');
     expect(rectsCanvas).toBeTruthy();
   });
 
@@ -69,18 +69,15 @@
       bottomRight: {x: 1, y: -1},
       label: 'rectangle1',
       transform: {
-        matrix: {
-          dsdx: 1,
-          dsdy: 0,
-          dtdx: 0,
-          dtdy: 1,
-          tx: 0,
-          ty: 0,
-        },
+        dsdx: 1,
+        dsdy: 0,
+        dtdx: 0,
+        dtdy: 1,
+        tx: 0,
+        ty: 0,
       },
       isVisible: true,
       isDisplay: false,
-      ref: null,
       id: 'test-id-1234',
       displayId: 0,
       isVirtual: false,
diff --git a/tools/winscope/src/viewers/common/rectangle.ts b/tools/winscope/src/viewers/components/rects/types2d.ts
similarity index 81%
rename from tools/winscope/src/viewers/common/rectangle.ts
rename to tools/winscope/src/viewers/components/rects/types2d.ts
index 5f6b858..badb59c 100644
--- a/tools/winscope/src/viewers/common/rectangle.ts
+++ b/tools/winscope/src/viewers/components/rects/types2d.ts
@@ -18,16 +18,16 @@
   topLeft: Point;
   bottomRight: Point;
   label: string;
-  transform: RectTransform | null;
+  transform?: TransformMatrix;
   isVisible: boolean;
   isDisplay: boolean;
-  ref: any;
   id: string;
   displayId: number;
   isVirtual: boolean;
   isClickable: boolean;
   cornerRadius: number;
   depth?: number;
+  hasContent?: boolean;
 }
 
 export interface Point {
@@ -40,17 +40,7 @@
   height: number;
 }
 
-export interface RectTransform {
-  matrix?: RectMatrix;
-  dsdx?: number;
-  dsdy?: number;
-  dtdx?: number;
-  dtdy?: number;
-  tx?: number;
-  ty?: number;
-}
-
-export interface RectMatrix {
+export interface TransformMatrix {
   dsdx: number;
   dsdy: number;
   dtdx: number;
diff --git a/tools/winscope/src/viewers/components/rects/types3d.ts b/tools/winscope/src/viewers/components/rects/types3d.ts
index cb30635..efb239c 100644
--- a/tools/winscope/src/viewers/components/rects/types3d.ts
+++ b/tools/winscope/src/viewers/components/rects/types3d.ts
@@ -14,10 +14,15 @@
  * limitations under the License.
  */
 
+import {TransformMatrix} from 'viewers/components/rects/types2d';
+
+export {TransformMatrix};
+
 export enum ColorType {
   VISIBLE,
   NOT_VISIBLE,
   HIGHLIGHTED,
+  HAS_CONTENT,
 }
 
 export class Distance2D {
@@ -40,19 +45,10 @@
   darkFactor: number;
   colorType: ColorType;
   isClickable: boolean;
-  transform: Transform3D;
+  transform: TransformMatrix;
   isOversized: boolean;
 }
 
-export interface Transform3D {
-  dsdx: number;
-  dsdy: number;
-  tx: number;
-  dtdx: number;
-  dtdy: number;
-  ty: number;
-}
-
 export interface Point3D {
   x: number;
   y: number;
diff --git a/tools/winscope/src/viewers/components/property_groups_component.ts b/tools/winscope/src/viewers/components/surface_flinger_property_groups_component.ts
similarity index 98%
rename from tools/winscope/src/viewers/components/property_groups_component.ts
rename to tools/winscope/src/viewers/components/surface_flinger_property_groups_component.ts
index 5193de1..faf176d 100644
--- a/tools/winscope/src/viewers/components/property_groups_component.ts
+++ b/tools/winscope/src/viewers/components/surface_flinger_property_groups_component.ts
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 import {Component, Input} from '@angular/core';
-import {Layer} from 'trace/flickerlib/common';
+import {Layer} from 'flickerlib/common';
 
 @Component({
-  selector: 'property-groups',
+  selector: 'surface-flinger-property-groups',
   template: `
     <div class="group">
       <h3 class="group-header mat-subheading-2">Visibility</h3>
@@ -277,7 +277,7 @@
     `,
   ],
 })
-export class PropertyGroupsComponent {
+export class SurfaceFlingerPropertyGroupsComponent {
   @Input() item!: Layer;
 
   hasInputChannel() {
diff --git a/tools/winscope/src/viewers/components/property_groups_component_test.ts b/tools/winscope/src/viewers/components/surface_flinger_property_groups_component_test.ts
similarity index 78%
rename from tools/winscope/src/viewers/components/property_groups_component_test.ts
rename to tools/winscope/src/viewers/components/surface_flinger_property_groups_component_test.ts
index 1186061..9d59a34 100644
--- a/tools/winscope/src/viewers/components/property_groups_component_test.ts
+++ b/tools/winscope/src/viewers/components/surface_flinger_property_groups_component_test.ts
@@ -17,21 +17,21 @@
 import {MatDividerModule} from '@angular/material/divider';
 import {MatTooltipModule} from '@angular/material/tooltip';
 import {LayerBuilder} from 'test/unit/layer_builder';
-import {PropertyGroupsComponent} from './property_groups_component';
+import {SurfaceFlingerPropertyGroupsComponent} from './surface_flinger_property_groups_component';
 import {TransformMatrixComponent} from './transform_matrix_component';
 
 describe('PropertyGroupsComponent', () => {
-  let fixture: ComponentFixture<PropertyGroupsComponent>;
-  let component: PropertyGroupsComponent;
+  let fixture: ComponentFixture<SurfaceFlingerPropertyGroupsComponent>;
+  let component: SurfaceFlingerPropertyGroupsComponent;
   let htmlElement: HTMLElement;
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
       imports: [MatDividerModule, MatTooltipModule],
-      declarations: [PropertyGroupsComponent, TransformMatrixComponent],
+      declarations: [SurfaceFlingerPropertyGroupsComponent, TransformMatrixComponent],
       schemas: [],
     }).compileComponents();
-    fixture = TestBed.createComponent(PropertyGroupsComponent);
+    fixture = TestBed.createComponent(SurfaceFlingerPropertyGroupsComponent);
     component = fixture.componentInstance;
     htmlElement = fixture.nativeElement;
   });
@@ -40,7 +40,7 @@
     expect(component).toBeTruthy();
   });
 
-  it('it renders verbose flags if available', async () => {
+  it('renders verbose flags if available', async () => {
     const layer = new LayerBuilder().setFlags(3).build();
     component.item = layer;
     fixture.detectChanges();
@@ -50,7 +50,7 @@
     expect(flags!.innerHTML).toMatch('Flags:.*HIDDEN|OPAQUE \\(0x3\\)');
   });
 
-  it('it renders numeric flags if verbose flags not available', async () => {
+  it('renders numeric flags if verbose flags not available', async () => {
     const layer = new LayerBuilder().setFlags(0).build();
     component.item = layer;
     fixture.detectChanges();
diff --git a/tools/winscope/src/viewers/components/transform_matrix_component.ts b/tools/winscope/src/viewers/components/transform_matrix_component.ts
index 36959cc..6a8513f 100644
--- a/tools/winscope/src/viewers/components/transform_matrix_component.ts
+++ b/tools/winscope/src/viewers/components/transform_matrix_component.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 import {Component, Input} from '@angular/core';
-import {Transform} from 'trace/flickerlib/common';
+import {Transform} from 'flickerlib/common';
 
 @Component({
   selector: 'transform-matrix',
diff --git a/tools/winscope/src/viewers/components/view_capture_property_groups_component.ts b/tools/winscope/src/viewers/components/view_capture_property_groups_component.ts
new file mode 100644
index 0000000..36a8a74
--- /dev/null
+++ b/tools/winscope/src/viewers/components/view_capture_property_groups_component.ts
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2022 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 {Component, Input} from '@angular/core';
+import {ViewNode} from 'trace/trace_type';
+
+@Component({
+  selector: 'view-capture-property-groups',
+  template: `
+    <div class="group">
+      <h3 class="group-header mat-subheading-2">View</h3>
+      <div class="left-column">
+        <p class="mat-body-1 flags">
+          <span class="mat-body-2">Class: </span>
+          &ngsp;
+          {{ item.className }}
+        </p>
+        <p class="mat-body-1 flags">
+          <span class="mat-body-2">Hashcode: </span>
+          &ngsp;
+          {{ item.hashcode }}
+        </p>
+      </div>
+    </div>
+    <mat-divider></mat-divider>
+    <div class="group">
+      <h3 class="group-header mat-subheading-2">Geometry</h3>
+      <div class="left-column">
+        <p class="column-header mat-small">Coordinates</p>
+        <p class="mat-body-1">
+          <span class="mat-body-2">Left: </span>
+          &ngsp;
+          {{ item.left }}
+        </p>
+        <p class="mat-body-1">
+          <span class="mat-body-2">Top: </span>
+          &ngsp;
+          {{ item.top }}
+        </p>
+        <p class="mat-body-1">
+          <span class="mat-body-2">Elevation: </span>
+          &ngsp;
+          {{ item.elevation }}
+        </p>
+      </div>
+      <div class="right-column">
+        <p class="column-header mat-small">Size</p>
+        <p class="mat-body-1">
+          <span class="mat-body-2">Height: </span>
+          &ngsp;
+          {{ item.height }}
+        </p>
+        <p class="mat-body-1">
+          <span class="mat-body-2">Width: </span>
+          &ngsp;
+          {{ item.width }}
+        </p>
+      </div>
+    </div>
+    <div class="group">
+      <h3 class="group-header mat-subheading-2"></h3>
+      <div class="left-column">
+        <p class="column-header mat-small">Translation</p>
+        <p class="mat-body-1">
+          <span class="mat-body-2">Translation X: </span>
+          &ngsp;
+          {{ item.translationX }}
+        </p>
+        <p class="mat-body-1">
+          <span class="mat-body-2">Translation Y: </span>
+          &ngsp;
+          {{ item.translationY }}
+        </p>
+      </div>
+      <div class="right-column">
+        <p class="column-header mat-small">Scroll</p>
+        <p class="mat-body-1">
+          <span class="mat-body-2">Scroll X: </span>
+          &ngsp;
+          {{ item.scrollX }}
+        </p>
+        <p class="mat-body-1">
+          <span class="mat-body-2">Scroll Y: </span>
+          &ngsp;
+          {{ item.scrollY }}
+        </p>
+      </div>
+    </div>
+    <div class="group">
+      <h3 class="group-header mat-subheading-2"></h3>
+      <div class="left-column">
+        <p class="column-header mat-small">Scale</p>
+        <p class="mat-body-1">
+          <span class="mat-body-2">Scale X: </span>
+          &ngsp;
+          {{ item.scaleX }}
+        </p>
+        <p class="mat-body-1">
+          <span class="mat-body-2">Scale Y: </span>
+          &ngsp;
+          {{ item.scaleY }}
+        </p>
+      </div>
+    </div>
+    <mat-divider></mat-divider>
+    <div class="group">
+      <h3 class="group-header mat-subheading-2">Effects</h3>
+      <div class="left-column">
+        <p class="column-header mat-small">Translation</p>
+        <p class="mat-body-1">
+          <span class="mat-body-2">Visibility: </span>
+          &ngsp;
+          {{ item.visibility }}
+        </p>
+        <p class="mat-body-1">
+          <span class="mat-body-2">Alpha: </span>
+          &ngsp;
+          {{ item.alpha }}
+        </p>
+        <p class="mat-body-1">
+          <span class="mat-body-2">Will Not Draw: </span>
+          &ngsp;
+          {{ item.willNotDraw }}
+        </p>
+      </div>
+      <div class="right-column">
+        <p class="column-header mat-small">Miscellaneous</p>
+        <p class="mat-body-1">
+          <span class="mat-body-2">Clip Children: </span>
+          &ngsp;
+          {{ item.clipChildren }}
+        </p>
+      </div>
+    </div>
+  `,
+  styles: [
+    `
+      .group {
+        display: flex;
+        flex-direction: row;
+        padding: 8px;
+      }
+
+      .group-header {
+        width: 80px;
+        color: gray;
+      }
+
+      .left-column {
+        flex: 1;
+        padding: 0 5px;
+      }
+
+      .right-column {
+        flex: 1;
+        border: 1px solid var(--border-color);
+        border-left-width: 5px;
+        padding: 0 5px;
+      }
+
+      .column-header {
+        color: gray;
+      }
+    `,
+  ],
+})
+export class ViewCapturePropertyGroupsComponent {
+  @Input() item: ViewNode;
+}
diff --git a/tools/winscope/src/viewers/viewer.ts b/tools/winscope/src/viewers/viewer.ts
index c6ccb7b..c19ff04 100644
--- a/tools/winscope/src/viewers/viewer.ts
+++ b/tools/winscope/src/viewers/viewer.ts
@@ -13,8 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-import {TracePosition} from 'trace/trace_position';
+import {AppEvent} from 'app/app_event';
+import {AppEventEmitter, EmitAppEvent} from 'interfaces/app_event_emitter';
+import {AppEventListener} from 'interfaces/app_event_listener';
 import {TraceType} from 'trace/trace_type';
 
 enum ViewType {
@@ -32,8 +33,9 @@
   ) {}
 }
 
-interface Viewer {
-  onTracePositionUpdate(position: TracePosition): Promise<void>;
+interface Viewer extends AppEventListener, AppEventEmitter {
+  onAppEvent(event: AppEvent): Promise<void>;
+  setEmitAppEvent(callback: EmitAppEvent): void;
   getViews(): View[];
   getDependencies(): TraceType[];
 }
diff --git a/tools/winscope/src/viewers/viewer_factory.ts b/tools/winscope/src/viewers/viewer_factory.ts
index 001302f..dae30ca 100644
--- a/tools/winscope/src/viewers/viewer_factory.ts
+++ b/tools/winscope/src/viewers/viewer_factory.ts
@@ -25,7 +25,11 @@
 import {ViewerSurfaceFlinger} from './viewer_surface_flinger/viewer_surface_flinger';
 import {ViewerTransactions} from './viewer_transactions/viewer_transactions';
 import {ViewerTransitions} from './viewer_transitions/viewer_transitions';
-import {ViewerViewCapture} from './viewer_view_capture/viewer_view_capture';
+import {
+  ViewerViewCaptureLauncherActivity,
+  ViewerViewCaptureTaskbarDragLayer,
+  ViewerViewCaptureTaskbarOverlayDragLayer,
+} from './viewer_view_capture/viewer_view_capture';
 import {ViewerWindowManager} from './viewer_window_manager/viewer_window_manager';
 
 class ViewerFactory {
@@ -42,10 +46,13 @@
     ViewerProtoLog,
     ViewerScreenRecording,
     ViewerTransitions,
-    ViewerViewCapture,
+    ViewerViewCaptureLauncherActivity,
+    ViewerViewCaptureTaskbarDragLayer,
+    ViewerViewCaptureTaskbarOverlayDragLayer,
   ];
 
-  createViewers(activeTraceTypes: Set<TraceType>, traces: Traces, storage: Storage): Viewer[] {
+  createViewers(traces: Traces, storage: Storage): Viewer[] {
+    const activeTraceTypes = new Set(traces.mapTrace((trace) => trace.type));
     const viewers: Viewer[] = [];
 
     for (const Viewer of ViewerFactory.VIEWERS) {
diff --git a/tools/winscope/src/viewers/viewer_protolog/presenter.ts b/tools/winscope/src/viewers/viewer_protolog/presenter.ts
index 87d5fb5..66dd1e9 100644
--- a/tools/winscope/src/viewers/viewer_protolog/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/presenter.ts
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
+import {AppEvent, AppEventType} from 'app/app_event';
 import {ArrayUtils} from 'common/array_utils';
 import {assertDefined} from 'common/assert_utils';
 import {LogMessage} from 'trace/protolog';
 import {Trace, TraceEntry} from 'trace/trace';
 import {Traces} from 'trace/traces';
 import {TraceEntryFinder} from 'trace/trace_entry_finder';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {UiData, UiDataMessage} from './ui_data';
 
@@ -48,11 +48,13 @@
     this.notifyUiDataCallback(this.uiData);
   }
 
-  async onTracePositionUpdate(position: TracePosition) {
-    await this.initializeIfNeeded();
-    this.entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position);
-    this.computeUiDataCurrentMessageIndex();
-    this.notifyUiDataCallback(this.uiData);
+  async onAppEvent(event: AppEvent) {
+    await event.visit(AppEventType.TRACE_POSITION_UPDATE, async (event) => {
+      await this.initializeIfNeeded();
+      this.entry = TraceEntryFinder.findCorrespondingEntry(this.trace, event.position);
+      this.computeUiDataCurrentMessageIndex();
+      this.notifyUiDataCallback(this.uiData);
+    });
   }
 
   onLogLevelsFilterChanged(levels: string[]) {
diff --git a/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts b/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
index a2de977..7de121c 100644
--- a/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
+import {TracePositionUpdate} from 'app/app_event';
 import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp} from 'common/time';
 import {TracesBuilder} from 'test/unit/traces_builder';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {LogMessage} from 'trace/protolog';
-import {RealTimestamp} from 'trace/timestamp';
 import {Trace} from 'trace/trace';
 import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {Presenter} from './presenter';
 import {UiData, UiDataMessage} from './ui_data';
@@ -30,9 +30,9 @@
   let presenter: Presenter;
   let inputMessages: UiDataMessage[];
   let trace: Trace<LogMessage>;
-  let position10: TracePosition;
-  let position11: TracePosition;
-  let position12: TracePosition;
+  let positionUpdate10: TracePositionUpdate;
+  let positionUpdate11: TracePositionUpdate;
+  let positionUpdate12: TracePositionUpdate;
   let outputUiData: undefined | UiData;
 
   beforeEach(async () => {
@@ -54,9 +54,9 @@
       .setTimestamps([time10, time11, time12])
       .build();
 
-    position10 = TracePosition.fromTimestamp(time10);
-    position11 = TracePosition.fromTimestamp(time11);
-    position12 = TracePosition.fromTimestamp(time12);
+    positionUpdate10 = TracePositionUpdate.fromTimestamp(time10);
+    positionUpdate11 = TracePositionUpdate.fromTimestamp(time11);
+    positionUpdate12 = TracePositionUpdate.fromTimestamp(time12);
 
     outputUiData = undefined;
 
@@ -65,7 +65,7 @@
     presenter = new Presenter(traces, (data: UiData) => {
       outputUiData = data;
     });
-    await presenter.onTracePositionUpdate(position10); // trigger initialization
+    await presenter.onAppEvent(positionUpdate10); // trigger initialization
   });
 
   it('is robust to empty trace', async () => {
@@ -77,13 +77,13 @@
     expect(assertDefined(outputUiData).messages).toEqual([]);
     expect(assertDefined(outputUiData).currentMessageIndex).toBeUndefined();
 
-    await presenter.onTracePositionUpdate(position10);
+    await presenter.onAppEvent(positionUpdate10);
     expect(assertDefined(outputUiData).messages).toEqual([]);
     expect(assertDefined(outputUiData).currentMessageIndex).toBeUndefined();
   });
 
   it('processes trace position updates', async () => {
-    await presenter.onTracePositionUpdate(position10);
+    await presenter.onAppEvent(positionUpdate10);
 
     expect(assertDefined(outputUiData).allLogLevels).toEqual(['level0', 'level1', 'level2']);
     expect(assertDefined(outputUiData).allTags).toEqual(['tag0', 'tag1', 'tag2']);
@@ -153,7 +153,7 @@
 
   it('computes current message index', async () => {
     // Position -> entry #0
-    await presenter.onTracePositionUpdate(position10);
+    await presenter.onAppEvent(positionUpdate10);
     presenter.onLogLevelsFilterChanged([]);
     expect(assertDefined(outputUiData).currentMessageIndex).toEqual(0);
 
@@ -164,7 +164,7 @@
     expect(assertDefined(outputUiData).currentMessageIndex).toEqual(0);
 
     // Position -> entry #1
-    await presenter.onTracePositionUpdate(position11);
+    await presenter.onAppEvent(positionUpdate11);
     presenter.onLogLevelsFilterChanged([]);
     expect(assertDefined(outputUiData).currentMessageIndex).toEqual(1);
 
@@ -178,7 +178,7 @@
     expect(assertDefined(outputUiData).currentMessageIndex).toEqual(1);
 
     // Position -> entry #2
-    await presenter.onTracePositionUpdate(position12);
+    await presenter.onAppEvent(positionUpdate12);
     presenter.onLogLevelsFilterChanged([]);
     expect(assertDefined(outputUiData).currentMessageIndex).toEqual(2);
   });
diff --git a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts
index 5a6e3c4..ea507e5 100644
--- a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
+import {AppEvent} from 'app/app_event';
 import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {View, Viewer, ViewType} from 'viewers/viewer';
 import {Events} from './events';
@@ -44,8 +44,12 @@
     });
   }
 
-  async onTracePositionUpdate(position: TracePosition) {
-    await this.presenter.onTracePositionUpdate(position);
+  async onAppEvent(event: AppEvent) {
+    await this.presenter.onAppEvent(event);
+  }
+
+  setEmitAppEvent() {
+    // do nothing
   }
 
   getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_screen_recording/viewer_screen_recording.ts b/tools/winscope/src/viewers/viewer_screen_recording/viewer_screen_recording.ts
index 65cfc33..99b96df 100644
--- a/tools/winscope/src/viewers/viewer_screen_recording/viewer_screen_recording.ts
+++ b/tools/winscope/src/viewers/viewer_screen_recording/viewer_screen_recording.ts
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
+import {AppEvent, AppEventType} from 'app/app_event';
 import {assertDefined} from 'common/assert_utils';
 import {ScreenRecordingTraceEntry} from 'trace/screen_recording';
 import {Trace} from 'trace/trace';
 import {Traces} from 'trace/traces';
 import {TraceEntryFinder} from 'trace/trace_entry_finder';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {View, Viewer, ViewType} from 'viewers/viewer';
 import {ViewerScreenRecordingComponent} from './viewer_screen_recording_component';
@@ -34,10 +34,16 @@
     this.htmlElement = document.createElement('viewer-screen-recording');
   }
 
-  async onTracePositionUpdate(position: TracePosition) {
-    const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position);
-    (this.htmlElement as unknown as ViewerScreenRecordingComponent).currentTraceEntry =
-      await entry?.getValue();
+  async onAppEvent(event: AppEvent) {
+    await event.visit(AppEventType.TRACE_POSITION_UPDATE, async (event) => {
+      const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, event.position);
+      (this.htmlElement as unknown as ViewerScreenRecordingComponent).currentTraceEntry =
+        await entry?.getValue();
+    });
+  }
+
+  setEmitAppEvent() {
+    // do nothing
   }
 
   getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_stub.ts b/tools/winscope/src/viewers/viewer_stub.ts
index d5f76e2..df6efc8 100644
--- a/tools/winscope/src/viewers/viewer_stub.ts
+++ b/tools/winscope/src/viewers/viewer_stub.ts
@@ -14,12 +14,20 @@
  * limitations under the License.
  */
 
-import {TracePosition} from 'trace/trace_position';
+import {AppEvent} from 'app/app_event';
+import {FunctionUtils} from 'common/function_utils';
+import {EmitAppEvent} from 'interfaces/app_event_emitter';
 import {TraceType} from 'trace/trace_type';
 import {View, Viewer, ViewType} from './viewer';
 
 class ViewerStub implements Viewer {
-  constructor(title: string, viewContent?: string) {
+  private htmlElement: HTMLElement;
+  private title: string;
+  private view: View;
+  private dependencies: TraceType[];
+  private emitAppEvent: EmitAppEvent = FunctionUtils.DO_NOTHING_ASYNC;
+
+  constructor(title: string, viewContent?: string, dependencies?: TraceType[]) {
     this.title = title;
 
     if (viewContent !== undefined) {
@@ -28,30 +36,37 @@
     } else {
       this.htmlElement = undefined as unknown as HTMLElement;
     }
+
+    this.dependencies = dependencies ?? [TraceType.WINDOW_MANAGER];
+
+    this.view = new View(
+      ViewType.TAB,
+      this.getDependencies(),
+      this.htmlElement,
+      this.title,
+      this.getDependencies()[0]
+    );
   }
 
-  onTracePositionUpdate(position: TracePosition): Promise<void> {
+  onAppEvent(event: AppEvent): Promise<void> {
     return Promise.resolve();
   }
 
+  setEmitAppEvent(callback: EmitAppEvent) {
+    this.emitAppEvent = callback;
+  }
+
+  async emitAppEventForTesting(event: AppEvent) {
+    await this.emitAppEvent(event);
+  }
+
   getViews(): View[] {
-    return [
-      new View(
-        ViewType.TAB,
-        this.getDependencies(),
-        this.htmlElement,
-        this.title,
-        this.getDependencies()[0]
-      ),
-    ];
+    return [this.view];
   }
 
   getDependencies(): TraceType[] {
-    return [TraceType.WINDOW_MANAGER];
+    return this.dependencies;
   }
-
-  private htmlElement: HTMLElement;
-  private title: string;
 }
 
 export {ViewerStub};
diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts b/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts
index 49594fe..f471913 100644
--- a/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts
@@ -14,17 +14,17 @@
  * limitations under the License.
  */
 
+import {AppEvent, AppEventType} from 'app/app_event';
 import {assertDefined} from 'common/assert_utils';
 import {PersistentStoreProxy} from 'common/persistent_store_proxy';
 import {FilterType, TreeUtils} from 'common/tree_utils';
-import {Layer} from 'trace/flickerlib/layers/Layer';
-import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry';
+import {Layer} from 'flickerlib/layers/Layer';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
 import {Trace} from 'trace/trace';
 import {Traces} from 'trace/traces';
 import {TraceEntryFinder} from 'trace/trace_entry_finder';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
-import {Rectangle, RectMatrix, RectTransform} from 'viewers/common/rectangle';
+import {SurfaceFlingerUtils} from 'viewers/common/surface_flinger_utils';
 import {TreeGenerator} from 'viewers/common/tree_generator';
 import {TreeTransformer} from 'viewers/common/tree_transformer';
 import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
@@ -40,7 +40,6 @@
   private hierarchyFilter: FilterType = TreeUtils.makeNodeFilter('');
   private propertiesFilter: FilterType = TreeUtils.makeNodeFilter('');
   private highlightedItems: string[] = [];
-  private displayIds: number[] = [];
   private pinnedItems: HierarchyTreeNode[] = [];
   private pinnedIds: string[] = [];
   private selectedHierarchyTree: HierarchyTreeNode | null = null;
@@ -53,6 +52,7 @@
       showDiff: {
         name: 'Show diff', // TODO: PersistentStoreObject.Ignored("Show diff") or something like that to instruct to not store this info
         enabled: false,
+        isUnavailable: false,
       },
       simplifyNames: {
         name: 'Simplify names',
@@ -76,6 +76,7 @@
       showDiff: {
         name: 'Show diff',
         enabled: false,
+        isUnavailable: false,
       },
       showDefaults: {
         name: 'Show defaults',
@@ -101,25 +102,33 @@
     this.copyUiDataAndNotifyView();
   }
 
-  async onTracePositionUpdate(position: TracePosition) {
-    this.uiData = new UiData();
-    this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
-    this.uiData.propertiesUserOptions = this.propertiesUserOptions;
+  async onAppEvent(event: AppEvent) {
+    await event.visit(AppEventType.TRACE_POSITION_UPDATE, async (event) => {
+      const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, event.position);
+      const prevEntry =
+        entry && entry.getIndex() > 0 ? this.trace.getEntry(entry.getIndex() - 1) : undefined;
 
-    const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position);
-    const prevEntry =
-      entry && entry.getIndex() > 0 ? this.trace.getEntry(entry.getIndex() - 1) : undefined;
+      this.entry = (await entry?.getValue()) ?? null;
+      this.previousEntry = (await prevEntry?.getValue()) ?? null;
+      if (this.hierarchyUserOptions['showDiff'].isUnavailable !== undefined) {
+        this.hierarchyUserOptions['showDiff'].isUnavailable = this.previousEntry == null;
+      }
+      if (this.propertiesUserOptions['showDiff'].isUnavailable !== undefined) {
+        this.propertiesUserOptions['showDiff'].isUnavailable = this.previousEntry == null;
+      }
 
-    this.entry = (await entry?.getValue()) ?? null;
-    this.previousEntry = (await prevEntry?.getValue()) ?? null;
-    if (this.entry) {
-      this.uiData.highlightedItems = this.highlightedItems;
-      this.uiData.rects = this.generateRects();
-      this.uiData.displayIds = this.displayIds;
-      this.uiData.tree = this.generateTree();
-    }
+      this.uiData = new UiData();
+      this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
+      this.uiData.propertiesUserOptions = this.propertiesUserOptions;
 
-    this.copyUiDataAndNotifyView();
+      if (this.entry) {
+        this.uiData.highlightedItems = this.highlightedItems;
+        this.uiData.rects = SurfaceFlingerUtils.makeRects(this.entry, this.hierarchyUserOptions);
+        this.uiData.displayIds = this.getDisplayIds(this.entry);
+        this.uiData.tree = this.generateTree();
+      }
+      this.copyUiDataAndNotifyView();
+    });
   }
 
   updatePinnedItems(pinnedItem: HierarchyTreeNode) {
@@ -174,62 +183,17 @@
     this.updateSelectedTreeUiData();
   }
 
-  private generateRects(): Rectangle[] {
-    const displayRects =
-      this.entry.displays.map((display: any) => {
-        const rect = display.layerStackSpace;
-        rect.label = 'Display';
-        if (display.name) {
-          rect.label += ` - ${display.name}`;
-        }
-        rect.stableId = `Display - ${display.id}`;
-        rect.displayId = display.layerStackId;
-        rect.isDisplay = true;
-        rect.cornerRadius = 0;
-        rect.isVirtual = display.isVirtual ?? false;
-        rect.transform = {
-          matrix: display.transform.matrix,
-        };
-        return rect;
-      }) ?? [];
-    this.displayIds = this.entry.displays.map((it: any) => it.layerStackId);
-    this.displayIds.sort();
-    const rects = this.getLayersForRectsView()
-      .sort(this.compareLayerZ)
-      .map((it: any) => {
-        const rect = it.rect;
-        rect.displayId = it.stackId;
-        rect.cornerRadius = it.cornerRadius;
-        if (!this.displayIds.includes(it.stackId)) {
-          this.displayIds.push(it.stackId);
-        }
-        rect.transform = {
-          matrix: rect.transform.matrix,
-        };
-        return rect;
-      });
-
-    return this.rectsToUiData(rects.concat(displayRects));
-  }
-
-  private getLayersForRectsView(): Layer[] {
-    const onlyVisible = this.hierarchyUserOptions['onlyVisible']?.enabled ?? false;
-    // Show only visible layers or Visible + Occluded layers. Don't show all layers
-    // (flattenedLayers) because container layers are never meant to be displayed
-    return this.entry.flattenedLayers.filter(
-      (it: any) => it.isVisible || (!onlyVisible && it.occludedBy.length > 0)
-    );
-  }
-
-  private compareLayerZ(a: Layer, b: Layer): number {
-    const zipLength = Math.min(a.zOrderPath.length, b.zOrderPath.length);
-    for (let i = 0; i < zipLength; ++i) {
-      const zOrderA = a.zOrderPath[i];
-      const zOrderB = b.zOrderPath[i];
-      if (zOrderA > zOrderB) return -1;
-      if (zOrderA < zOrderB) return 1;
-    }
-    return b.zOrderPath.length - a.zOrderPath.length;
+  private getDisplayIds(entry: LayerTraceEntry): number[] {
+    const ids = new Set<number>();
+    entry.displays.forEach((display: any) => {
+      ids.add(display.layerStackId);
+    });
+    entry.flattenedLayers.forEach((layer: Layer) => {
+      ids.add(layer.stackId);
+    });
+    return Array.from(ids.values()).sort((a, b) => {
+      return a - b;
+    });
   }
 
   private updateSelectedTreeUiData() {
@@ -254,7 +218,10 @@
       .setIsFlatView(this.hierarchyUserOptions['flat']?.enabled)
       .withUniqueNodeId();
     let tree: HierarchyTreeNode | null;
-    if (!this.hierarchyUserOptions['showDiff']?.enabled) {
+    if (
+      !this.hierarchyUserOptions['showDiff']?.enabled ||
+      this.hierarchyUserOptions['showDiff']?.isUnavailable
+    ) {
       tree = generator.generateTree();
     } else {
       tree = generator
@@ -267,49 +234,6 @@
     return tree;
   }
 
-  private rectsToUiData(rects: any[]): Rectangle[] {
-    const uiRects: Rectangle[] = [];
-    rects.forEach((rect: any) => {
-      let t = null;
-      if (rect.transform && rect.transform.matrix) {
-        t = rect.transform.matrix;
-      } else if (rect.transform) {
-        t = rect.transform;
-      }
-      let transform: RectTransform | null = null;
-      if (t !== null) {
-        const matrix: RectMatrix = {
-          dsdx: t.dsdx,
-          dsdy: t.dsdy,
-          dtdx: t.dtdx,
-          dtdy: t.dtdy,
-          tx: t.tx,
-          ty: t.ty,
-        };
-        transform = {
-          matrix,
-        };
-      }
-
-      const newRect: Rectangle = {
-        topLeft: {x: rect.left, y: rect.top},
-        bottomRight: {x: rect.right, y: rect.bottom},
-        label: rect.label,
-        transform,
-        isVisible: rect.ref?.isVisible ?? false,
-        isDisplay: rect.isDisplay ?? false,
-        ref: rect.ref,
-        id: rect.stableId ?? rect.ref.stableId,
-        displayId: rect.displayId ?? rect.ref.stackId,
-        isVirtual: rect.isVirtual ?? false,
-        isClickable: !(rect.isDisplay ?? false),
-        cornerRadius: rect.cornerRadius,
-      };
-      uiRects.push(newRect);
-    });
-    return uiRects;
-  }
-
   private updatePinnedIds(newId: string) {
     if (this.pinnedIds.includes(newId)) {
       this.pinnedIds = this.pinnedIds.filter((pinned) => pinned !== newId);
@@ -322,7 +246,10 @@
     const transformer = new TreeTransformer(selectedTree, this.propertiesFilter)
       .setOnlyProtoDump(true)
       .setIsShowDefaults(this.propertiesUserOptions['showDefaults']?.enabled)
-      .setIsShowDiff(this.propertiesUserOptions['showDiff']?.enabled)
+      .setIsShowDiff(
+        this.propertiesUserOptions['showDiff']?.enabled &&
+          !this.propertiesUserOptions['showDiff']?.isUnavailable
+      )
       .setTransformerOptions({skip: selectedTree.skip})
       .setProperties(this.entry)
       .setDiffProperties(this.previousEntry);
diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts b/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts
index 2cf363f..75f4cd8 100644
--- a/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts
@@ -14,15 +14,15 @@
  * limitations under the License.
  */
 
+import {TracePositionUpdate} from 'app/app_event';
+import {RealTimestamp} from 'common/time';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
 import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
 import {MockStorage} from 'test/unit/mock_storage';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {UnitTestUtils} from 'test/unit/utils';
-import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry';
-import {RealTimestamp} from 'trace/timestamp';
 import {Trace} from 'trace/trace';
 import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
 import {UserOptions} from 'viewers/common/user_options';
@@ -31,8 +31,8 @@
 
 describe('PresenterSurfaceFlinger', () => {
   let trace: Trace<LayerTraceEntry>;
-  let position: TracePosition;
-  let positionMultiDisplayEntry: TracePosition;
+  let positionUpdate: TracePositionUpdate;
+  let positionUpdateMultiDisplayEntry: TracePositionUpdate;
   let presenter: Presenter;
   let uiData: UiData;
   let selectedTree: HierarchyTreeNode;
@@ -45,15 +45,14 @@
       ])
       .build();
 
-    position = TracePosition.fromTraceEntry(trace.getEntry(0));
-    positionMultiDisplayEntry = TracePosition.fromTraceEntry(trace.getEntry(1));
+    positionUpdate = TracePositionUpdate.fromTraceEntry(trace.getEntry(0));
+    positionUpdateMultiDisplayEntry = TracePositionUpdate.fromTraceEntry(trace.getEntry(1));
 
     selectedTree = new HierarchyTreeBuilder()
       .setName('Dim layer#53')
-      .setStableId('EffectLayer 53 Dim layer#53')
+      .setStableId('53 Dim layer#53')
       .setFilteredView(true)
       .setKind('53')
-      .setDiffType('EffectLayer')
       .setId(53)
       .build();
   });
@@ -66,14 +65,16 @@
     const emptyTrace = new TraceBuilder<LayerTraceEntry>().setEntries([]).build();
     const presenter = createPresenter(emptyTrace);
 
-    const positionWithoutTraceEntry = TracePosition.fromTimestamp(new RealTimestamp(0n));
-    await presenter.onTracePositionUpdate(positionWithoutTraceEntry);
+    const positionUpdateWithoutTraceEntry = TracePositionUpdate.fromTimestamp(
+      new RealTimestamp(0n)
+    );
+    await presenter.onAppEvent(positionUpdateWithoutTraceEntry);
     expect(uiData.hierarchyUserOptions).toBeTruthy();
     expect(uiData.tree).toBeFalsy();
   });
 
   it('processes trace position updates', async () => {
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
 
     expect(uiData.rects.length).toBeGreaterThan(0);
     expect(uiData.highlightedItems?.length).toEqual(0);
@@ -89,11 +90,25 @@
     expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
   });
 
+  it('disables show diff and generates non-diff tree if no prev entry available', async () => {
+    await presenter.onAppEvent(positionUpdate);
+
+    const hierarchyOpts = uiData.hierarchyUserOptions ?? null;
+    expect(hierarchyOpts).toBeTruthy();
+    expect(hierarchyOpts!['showDiff'].isUnavailable).toBeTrue();
+
+    const propertyOpts = uiData.propertiesUserOptions ?? null;
+    expect(propertyOpts).toBeTruthy();
+    expect(propertyOpts!['showDiff'].isUnavailable).toBeTrue();
+
+    expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
+  });
+
   it('creates input data for rects view', async () => {
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     expect(uiData.rects.length).toBeGreaterThan(0);
     expect(uiData.rects[0].topLeft).toEqual({x: 0, y: 0});
-    expect(uiData.rects[0].bottomRight).toEqual({x: 1080, y: 118});
+    expect(uiData.rects[0].bottomRight).toEqual({x: 1080, y: 74});
   });
 
   it('updates pinned items', () => {
@@ -137,7 +152,7 @@
       },
     };
 
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     expect(uiData.tree?.children.length).toEqual(3);
 
     presenter.updateHierarchyTree(userOptions);
@@ -165,7 +180,7 @@
         enabled: true,
       },
     };
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     presenter.updateHierarchyTree(userOptions);
     expect(uiData.tree?.children.length).toEqual(94);
     presenter.filterHierarchyTree('Wallpaper');
@@ -174,7 +189,7 @@
   });
 
   it('sets properties tree and associated ui data', async () => {
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     presenter.newPropertiesTree(selectedTree);
     // does not check specific tree values as tree transformation method may change
     expect(uiData.propertiesTree).toBeTruthy();
@@ -198,7 +213,7 @@
       },
     };
 
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     presenter.newPropertiesTree(selectedTree);
     expect(uiData.propertiesTree?.diffType).toBeFalsy();
 
@@ -208,7 +223,7 @@
   });
 
   it('filters properties tree', async () => {
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     presenter.newPropertiesTree(selectedTree);
     let nonTerminalChildren =
       uiData.propertiesTree?.children?.filter(
@@ -226,7 +241,7 @@
   });
 
   it('handles displays with no visible layers', async () => {
-    await presenter.onTracePositionUpdate(positionMultiDisplayEntry);
+    await presenter.onAppEvent(positionUpdateMultiDisplayEntry);
     expect(uiData.displayIds.length).toEqual(5);
     // we want the ids to be sorted
     expect(uiData.displayIds).toEqual([0, 2, 3, 4, 5]);
diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/ui_data.ts b/tools/winscope/src/viewers/viewer_surface_flinger/ui_data.ts
index 319bf96..35e31a8 100644
--- a/tools/winscope/src/viewers/viewer_surface_flinger/ui_data.ts
+++ b/tools/winscope/src/viewers/viewer_surface_flinger/ui_data.ts
@@ -13,11 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {Layer} from 'trace/flickerlib/common';
+import {Layer} from 'flickerlib/common';
 import {TraceType} from 'trace/trace_type';
-import {Rectangle} from 'viewers/common/rectangle';
 import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
 import {UserOptions} from 'viewers/common/user_options';
+import {Rectangle} from 'viewers/components/rects/types2d';
 
 export class UiData {
   dependencies: TraceType[];
diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts b/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts
index 6868f55..8055002 100644
--- a/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts
+++ b/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts
@@ -14,16 +14,20 @@
  * limitations under the License.
  */
 
+import {AppEvent, TabbedViewSwitchRequest} from 'app/app_event';
+import {FunctionUtils} from 'common/function_utils';
+import {EmitAppEvent} from 'interfaces/app_event_emitter';
 import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {ViewerEvents} from 'viewers/common/viewer_events';
+import {NEXUS_LAUNCHER_PACKAGE_NAME} from 'viewers/common/view_capture_constants';
 import {View, Viewer, ViewType} from 'viewers/viewer';
 import {Presenter} from './presenter';
 import {UiData} from './ui_data';
 
 class ViewerSurfaceFlinger implements Viewer {
   static readonly DEPENDENCIES: TraceType[] = [TraceType.SURFACE_FLINGER];
+  private emitAppEvent: EmitAppEvent = FunctionUtils.DO_NOTHING_ASYNC;
   private readonly htmlElement: HTMLElement;
   private readonly presenter: Presenter;
 
@@ -55,10 +59,24 @@
     this.htmlElement.addEventListener(ViewerEvents.SelectedTreeChange, (event) =>
       this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem)
     );
+    this.htmlElement.addEventListener(ViewerEvents.RectsDblClick, (event) => {
+      if ((event as CustomEvent).detail.clickedRectId.includes(NEXUS_LAUNCHER_PACKAGE_NAME)) {
+        this.switchToNexusLauncherViewer();
+      }
+    });
   }
 
-  async onTracePositionUpdate(position: TracePosition) {
-    await this.presenter.onTracePositionUpdate(position);
+  async onAppEvent(event: AppEvent) {
+    await this.presenter.onAppEvent(event);
+  }
+
+  setEmitAppEvent(callback: EmitAppEvent) {
+    this.emitAppEvent = callback;
+  }
+
+  // TODO: Make this generic by package name once TraceType is not explicitly defined
+  async switchToNexusLauncherViewer() {
+    await this.emitAppEvent(new TabbedViewSwitchRequest(TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY));
   }
 
   getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger_component.ts b/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger_component.ts
index 5613126..d724a2d 100644
--- a/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger_component.ts
+++ b/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger_component.ts
@@ -44,7 +44,8 @@
         class="properties-view"
         [userOptions]="inputData?.propertiesUserOptions ?? {}"
         [propertiesTree]="inputData?.propertiesTree ?? {}"
-        [selectedFlickerItem]="inputData?.selectedLayer ?? {}"
+        [selectedItem]="inputData?.selectedLayer ?? {}"
+        [traceType]="${TraceType.SURFACE_FLINGER}"
         [displayPropertyGroups]="inputData?.displayPropertyGroups"
         [isProtoDump]="true"></properties-view>
     </div>
diff --git a/tools/winscope/src/viewers/viewer_transactions/presenter.ts b/tools/winscope/src/viewers/viewer_transactions/presenter.ts
index 50a5293..050962e 100644
--- a/tools/winscope/src/viewers/viewer_transactions/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/presenter.ts
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
+import {AppEvent, AppEventType} from 'app/app_event';
 import {ArrayUtils} from 'common/array_utils';
 import {assertDefined} from 'common/assert_utils';
 import {TimeUtils} from 'common/time_utils';
-import {ObjectFormatter} from 'trace/flickerlib/ObjectFormatter';
+import {ObjectFormatter} from 'flickerlib/ObjectFormatter';
 import {Trace, TraceEntry} from 'trace/trace';
 import {Traces} from 'trace/traces';
 import {TraceEntryFinder} from 'trace/trace_entry_finder';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {PropertiesTreeGenerator} from 'viewers/common/properties_tree_generator';
 import {PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
@@ -60,21 +60,21 @@
     this.notifyUiDataCallback(this.uiData);
   }
 
-  async onTracePositionUpdate(position: TracePosition) {
-    await this.initializeIfNeeded();
+  async onAppEvent(event: AppEvent) {
+    await event.visit(AppEventType.TRACE_POSITION_UPDATE, async (event) => {
+      await this.initializeIfNeeded();
+      this.entry = TraceEntryFinder.findCorrespondingEntry(this.trace, event.position);
+      this.uiData.currentEntryIndex = this.computeCurrentEntryIndex();
+      this.uiData.selectedEntryIndex = undefined;
+      this.uiData.scrollToIndex = this.uiData.currentEntryIndex;
+      this.uiData.currentPropertiesTree = this.computeCurrentPropertiesTree(
+        this.uiData.entries,
+        this.uiData.currentEntryIndex,
+        this.uiData.selectedEntryIndex
+      );
 
-    this.entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position);
-
-    this.uiData.currentEntryIndex = this.computeCurrentEntryIndex();
-    this.uiData.selectedEntryIndex = undefined;
-    this.uiData.scrollToIndex = this.uiData.currentEntryIndex;
-    this.uiData.currentPropertiesTree = this.computeCurrentPropertiesTree(
-      this.uiData.entries,
-      this.uiData.currentEntryIndex,
-      this.uiData.selectedEntryIndex
-    );
-
-    this.notifyUiDataCallback(this.uiData);
+      this.notifyUiDataCallback(this.uiData);
+    });
   }
 
   onVSyncIdFilterChanged(vsyncIds: string[]) {
@@ -278,9 +278,15 @@
     const formattingOptions = ObjectFormatter.displayDefaults;
     ObjectFormatter.displayDefaults = true;
 
+    const entryProtos = await Promise.all(
+      this.trace.mapEntry(async (entry) => {
+        return await entry.getValue();
+      })
+    );
+
     for (let originalIndex = 0; originalIndex < this.trace.lengthEntries; ++originalIndex) {
       const entry = this.trace.getEntry(originalIndex);
-      const entryProto = (await entry.getValue()) as any;
+      const entryProto = entryProtos[originalIndex] as any;
 
       for (const transactionStateProto of entryProto.transactions) {
         for (const layerStateProto of transactionStateProto.layerChanges) {
diff --git a/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts b/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts
index c04bb91..81b3161 100644
--- a/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts
@@ -14,15 +14,15 @@
  * limitations under the License.
  */
 
+import {TracePositionUpdate} from 'app/app_event';
 import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp, TimestampType} from 'common/time';
 import {TracesBuilder} from 'test/unit/traces_builder';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
-import {RealTimestamp, TimestampType} from 'trace/timestamp';
 import {Trace} from 'trace/trace';
 import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {Presenter} from './presenter';
 import {UiData, UiDataEntryType} from './ui_data';
@@ -52,12 +52,12 @@
 
     expect(outputUiData).toEqual(UiData.EMPTY);
 
-    await presenter.onTracePositionUpdate(TracePosition.fromTimestamp(new RealTimestamp(10n)));
+    await presenter.onAppEvent(TracePositionUpdate.fromTimestamp(new RealTimestamp(10n)));
     expect(outputUiData).toEqual(UiData.EMPTY);
   });
 
   it('processes trace position update and computes output UI data', async () => {
-    await presenter.onTracePositionUpdate(createTracePosition(0));
+    await presenter.onAppEvent(createTracePositionUpdate(0));
 
     expect(assertDefined(outputUiData).allPids).toEqual([
       'N/A',
@@ -98,11 +98,11 @@
   });
 
   it('processes trace position update and updates current entry and scroll position', async () => {
-    await presenter.onTracePositionUpdate(createTracePosition(0));
+    await presenter.onAppEvent(createTracePositionUpdate(0));
     expect(assertDefined(outputUiData).currentEntryIndex).toEqual(0);
     expect(assertDefined(outputUiData).scrollToIndex).toEqual(0);
 
-    await presenter.onTracePositionUpdate(createTracePosition(10));
+    await presenter.onAppEvent(createTracePositionUpdate(10));
     expect(assertDefined(outputUiData).currentEntryIndex).toEqual(13);
     expect(assertDefined(outputUiData).scrollToIndex).toEqual(13);
   });
@@ -234,7 +234,7 @@
   });
 
   it('updates selected entry and properties tree when entry is clicked', async () => {
-    await presenter.onTracePositionUpdate(createTracePosition(0));
+    await presenter.onAppEvent(createTracePositionUpdate(0));
     expect(assertDefined(outputUiData).currentEntryIndex).toEqual(0);
     expect(assertDefined(outputUiData).selectedEntryIndex).toBeUndefined();
     expect(assertDefined(outputUiData).scrollToIndex).toEqual(0);
@@ -261,15 +261,15 @@
   });
 
   it('computes current entry index', async () => {
-    await presenter.onTracePositionUpdate(createTracePosition(0));
+    await presenter.onAppEvent(createTracePositionUpdate(0));
     expect(assertDefined(outputUiData).currentEntryIndex).toEqual(0);
 
-    await presenter.onTracePositionUpdate(createTracePosition(10));
+    await presenter.onAppEvent(createTracePositionUpdate(10));
     expect(assertDefined(outputUiData).currentEntryIndex).toEqual(13);
   });
 
   it('updates current entry index when filters change', async () => {
-    await presenter.onTracePositionUpdate(createTracePosition(10));
+    await presenter.onAppEvent(createTracePositionUpdate(10));
 
     presenter.onPidFilterChanged([]);
     expect(assertDefined(outputUiData).currentEntryIndex).toEqual(13);
@@ -304,10 +304,11 @@
       outputUiData = data;
     });
 
-    await presenter.onTracePositionUpdate(createTracePosition(0)); // trigger initialization
+    await presenter.onAppEvent(createTracePositionUpdate(0)); // trigger initialization
   };
 
-  const createTracePosition = (entryIndex: number): TracePosition => {
-    return TracePosition.fromTraceEntry(trace.getEntry(entryIndex));
+  const createTracePositionUpdate = (entryIndex: number): TracePositionUpdate => {
+    const entry = trace.getEntry(entryIndex);
+    return TracePositionUpdate.fromTraceEntry(entry);
   };
 });
diff --git a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts
index f10a75c..109df9a 100644
--- a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
+import {AppEvent} from 'app/app_event';
 import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {View, Viewer, ViewType} from 'viewers/viewer';
 import {Events} from './events';
@@ -63,8 +63,12 @@
     });
   }
 
-  async onTracePositionUpdate(position: TracePosition) {
-    await this.presenter.onTracePositionUpdate(position);
+  async onAppEvent(event: AppEvent) {
+    await this.presenter.onAppEvent(event);
+  }
+
+  setEmitAppEvent() {
+    // do nothing
   }
 
   getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_transitions/presenter.ts b/tools/winscope/src/viewers/viewer_transitions/presenter.ts
index 21e1ad1..dcef3e4 100644
--- a/tools/winscope/src/viewers/viewer_transitions/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/presenter.ts
@@ -14,9 +14,11 @@
  * limitations under the License.
  */
 
+import {AppEvent, AppEventType} from 'app/app_event';
 import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp} from 'common/time';
 import {TimeUtils} from 'common/time_utils';
-import {LayerTraceEntry, Transition, WindowManagerState} from 'trace/flickerlib/common';
+import {LayerTraceEntry, Transition, WindowManagerState} from 'flickerlib/common';
 import {Trace} from 'trace/trace';
 import {Traces} from 'trace/traces';
 import {TraceEntryFinder} from 'trace/trace_entry_finder';
@@ -39,22 +41,28 @@
     this.notifyUiDataCallback = notifyUiDataCallback;
   }
 
-  async onTracePositionUpdate(position: TracePosition) {
-    if (this.uiData === UiData.EMPTY) {
-      this.uiData = await this.computeUiData();
-    }
+  async onAppEvent(event: AppEvent) {
+    await event.visit(AppEventType.TRACE_POSITION_UPDATE, async (event) => {
+      if (this.uiData === UiData.EMPTY) {
+        this.uiData = await this.computeUiData();
+      }
 
-    const entry = TraceEntryFinder.findCorrespondingEntry(this.transitionTrace, position);
+      const entry = TraceEntryFinder.findCorrespondingEntry(this.transitionTrace, event.position);
 
-    this.uiData.selectedTransition = await entry?.getValue();
+      this.uiData.selectedTransition = await entry?.getValue();
+      if (this.uiData.selectedTransition !== undefined) {
+        await this.onTransitionSelected(this.uiData.selectedTransition);
+      }
 
-    this.notifyUiDataCallback(this.uiData);
+      this.notifyUiDataCallback(this.uiData);
+    });
   }
 
-  onTransitionSelected(transition: Transition): void {
+  async onTransitionSelected(transition: Transition): Promise<void> {
     this.uiData.selectedTransition = transition;
-    this.uiData.selectedTransitionPropertiesTree =
-      this.makeSelectedTransitionPropertiesTree(transition);
+    this.uiData.selectedTransitionPropertiesTree = await this.makeSelectedTransitionPropertiesTree(
+      transition
+    );
     this.notifyUiDataCallback(this.uiData);
   }
 
@@ -80,39 +88,78 @@
     );
   }
 
-  private makeSelectedTransitionPropertiesTree(transition: Transition): PropertiesTreeNode {
+  private async makeSelectedTransitionPropertiesTree(
+    transition: Transition
+  ): Promise<PropertiesTreeNode> {
     const changes: PropertiesTreeNode[] = [];
 
+    const createTime = BigInt(transition.createTime.unixNanos.toString());
+    const finishTime = BigInt(transition.finishTime.unixNanos.toString());
+    const middleOfTransitionTimestamp = new RealTimestamp((createTime + finishTime) / 2n);
+    const middleOfTransitionPosition = TracePosition.fromTimestamp(middleOfTransitionTimestamp);
+
     for (const change of transition.changes) {
       let layerName: string | undefined = undefined;
       let windowName: string | undefined = undefined;
 
       if (this.surfaceFlingerTrace) {
-        this.surfaceFlingerTrace.forEachEntry((entry, originalIndex) => {
-          if (layerName !== undefined) {
-            return;
-          }
-          const layerTraceEntry = entry.getValue() as LayerTraceEntry;
+        const entry = TraceEntryFinder.findCorrespondingEntry(
+          this.surfaceFlingerTrace,
+          middleOfTransitionPosition
+        );
+        if (entry !== undefined) {
+          const layerTraceEntry = (await entry.getValue()) as LayerTraceEntry;
           for (const layer of layerTraceEntry.flattenedLayers) {
             if (layer.id === change.layerId) {
               layerName = layer.name;
             }
           }
-        });
+        } else {
+          // Fallback
+          await Promise.all(
+            this.surfaceFlingerTrace.mapEntry(async (entry, originalIndex) => {
+              if (layerName !== undefined) {
+                return;
+              }
+              const layerTraceEntry = (await entry.getValue()) as LayerTraceEntry;
+              for (const layer of layerTraceEntry.flattenedLayers) {
+                if (layer.id === change.layerId) {
+                  layerName = layer.name;
+                }
+              }
+            })
+          );
+        }
       }
 
       if (this.windowManagerTrace) {
-        this.windowManagerTrace.forEachEntry((entry, originalIndex) => {
-          if (windowName !== undefined) {
-            return;
-          }
-          const wmState = entry.getValue() as WindowManagerState;
+        const entry = TraceEntryFinder.findCorrespondingEntry(
+          this.windowManagerTrace,
+          middleOfTransitionPosition
+        );
+        if (entry !== undefined) {
+          const wmState = (await entry.getValue()) as WindowManagerState;
           for (const window of wmState.windowContainers) {
             if (window.token.toLowerCase() === change.windowId.toString(16).toLowerCase()) {
               windowName = window.title;
             }
           }
-        });
+        } else {
+          // Fallback
+          await Promise.all(
+            this.windowManagerTrace.mapEntry(async (entry, originalIndex) => {
+              if (windowName !== undefined) {
+                return;
+              }
+              const wmState = (await entry.getValue()) as WindowManagerState;
+              for (const window of wmState.windowContainers) {
+                if (window.token.toLowerCase() === change.windowId.toString(16).toLowerCase()) {
+                  windowName = window.title;
+                }
+              }
+            })
+          );
+        }
       }
 
       const layerIdValue = layerName ? `${change.layerId} (${layerName})` : change.layerId;
diff --git a/tools/winscope/src/viewers/viewer_transitions/ui_data.ts b/tools/winscope/src/viewers/viewer_transitions/ui_data.ts
index 8676b99..da0cbc1 100644
--- a/tools/winscope/src/viewers/viewer_transitions/ui_data.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/ui_data.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-import {Transition} from 'trace/flickerlib/common';
-import {TimestampType} from 'trace/timestamp';
+import {TimestampType} from 'common/time';
+import {Transition} from 'flickerlib/common';
 import {PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
 
 export class UiData {
diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts
index a66bbc6..65aff78 100644
--- a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts
@@ -13,8 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {AppEvent} from 'app/app_event';
 import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {View, Viewer, ViewType} from 'viewers/viewer';
 import {Events} from './events';
@@ -34,8 +34,12 @@
     });
   }
 
-  async onTracePositionUpdate(position: TracePosition) {
-    await this.presenter.onTracePositionUpdate(position);
+  async onAppEvent(event: AppEvent) {
+    await this.presenter.onAppEvent(event);
+  }
+
+  setEmitAppEvent() {
+    // do nothing
   }
 
   getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts
index f642223..b751572 100644
--- a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts
@@ -15,9 +15,9 @@
  */
 
 import {Component, ElementRef, Inject, Input} from '@angular/core';
+import {ElapsedTimestamp, TimestampType} from 'common/time';
 import {TimeUtils} from 'common/time_utils';
-import {Transition} from 'trace/flickerlib/common';
-import {ElapsedTimestamp, TimestampType} from 'trace/timestamp';
+import {Transition} from 'flickerlib/common';
 import {Terminal} from 'viewers/common/ui_tree_utils';
 import {Events} from './events';
 import {UiData} from './ui_data';
@@ -26,118 +26,90 @@
   selector: 'viewer-transitions',
   template: `
     <div class="card-grid container">
-      <div class="top-viewer">
-        <div class="entries">
-          <div class="table-header table-row">
-            <div class="id">Id</div>
-            <div class="type">Type</div>
-            <div class="send-time">Send Time</div>
-            <div class="duration">Duration</div>
-            <div class="status">Status</div>
-          </div>
-          <cdk-virtual-scroll-viewport itemSize="53" class="scroll">
-            <div
-              *cdkVirtualFor="let transition of uiData.entries; let i = index"
-              class="entry table-row"
-              [class.current]="isCurrentTransition(transition)"
-              (click)="onTransitionClicked(transition)">
-              <div class="id">
-                <span class="mat-body-1">{{ transition.id }}</span>
+      <div class="entries">
+        <div class="table-header table-row">
+          <div class="id">Id</div>
+          <div class="type">Type</div>
+          <div class="send-time">Send Time</div>
+          <div class="duration">Duration</div>
+          <div class="status">Status</div>
+        </div>
+        <cdk-virtual-scroll-viewport itemSize="53" class="scroll">
+          <div
+            *cdkVirtualFor="let transition of uiData.entries; let i = index"
+            class="entry table-row"
+            [class.current]="isCurrentTransition(transition)"
+            (click)="onTransitionClicked(transition)">
+            <div class="id">
+              <span class="mat-body-1">{{ transition.id }}</span>
+            </div>
+            <div class="type">
+              <span class="mat-body-1">{{ transition.type }}</span>
+            </div>
+            <div class="send-time">
+              <span *ngIf="!transition.sendTime.isMin" class="mat-body-1">{{
+                formattedTime(transition.sendTime, uiData.timestampType)
+              }}</span>
+              <span *ngIf="transition.sendTime.isMin"> n/a </span>
+            </div>
+            <div class="duration">
+              <span
+                *ngIf="!transition.sendTime.isMin && !transition.finishTime.isMax"
+                class="mat-body-1"
+                >{{
+                  formattedTimeDiff(
+                    transition.sendTime,
+                    transition.finishTime,
+                    uiData.timestampType
+                  )
+                }}</span
+              >
+              <span *ngIf="transition.sendTime.isMin || transition.finishTime.isMax">n/a</span>
+            </div>
+            <div class="status">
+              <div *ngIf="transition.mergedInto">
+                <span>MERGED</span>
+                <mat-icon aria-hidden="false" fontIcon="merge" matTooltip="merged" icon-gray>
+                </mat-icon>
               </div>
-              <div class="type">
-                <span class="mat-body-1">{{ transition.type }}</span>
-              </div>
-              <div class="send-time">
-                <span *ngIf="!transition.sendTime.isMin" class="mat-body-1">{{
-                  formattedTime(transition.sendTime, uiData.timestampType)
-                }}</span>
-                <span *ngIf="transition.sendTime.isMin"> n/a </span>
-              </div>
-              <div class="duration">
-                <span
-                  *ngIf="!transition.sendTime.isMin && !transition.finishTime.isMax"
-                  class="mat-body-1"
-                  >{{
-                    formattedTimeDiff(
-                      transition.sendTime,
-                      transition.finishTime,
-                      uiData.timestampType
-                    )
-                  }}</span
-                >
-                <span *ngIf="transition.sendTime.isMin || transition.finishTime.isMax">n/a</span>
-              </div>
-              <div class="status">
-                <div *ngIf="transition.mergedInto">
-                  <span>MERGED</span>
-                  <mat-icon aria-hidden="false" fontIcon="merge" matTooltip="merged" icon-gray>
-                  </mat-icon>
-                </div>
 
-                <div *ngIf="transition.aborted && !transition.mergedInto">
-                  <span>ABORTED</span>
-                  <mat-icon
-                    aria-hidden="false"
-                    fontIcon="close"
-                    matTooltip="aborted"
-                    style="color: red"
-                    icon-red></mat-icon>
-                </div>
+              <div *ngIf="transition.aborted && !transition.mergedInto">
+                <span>ABORTED</span>
+                <mat-icon
+                  aria-hidden="false"
+                  fontIcon="close"
+                  matTooltip="aborted"
+                  style="color: red"
+                  icon-red></mat-icon>
+              </div>
 
-                <div *ngIf="transition.played && !transition.aborted && !transition.mergedInto">
-                  <span>PLAYED</span>
-                  <mat-icon
-                    aria-hidden="false"
-                    fontIcon="check"
-                    matTooltip="played"
-                    style="color: green"
-                    *ngIf="
-                      transition.played && !transition.aborted && !transition.mergedInto
-                    "></mat-icon>
-                </div>
+              <div *ngIf="transition.played && !transition.aborted && !transition.mergedInto">
+                <span>PLAYED</span>
+                <mat-icon
+                  aria-hidden="false"
+                  fontIcon="check"
+                  matTooltip="played"
+                  style="color: green"
+                  *ngIf="
+                    transition.played && !transition.aborted && !transition.mergedInto
+                  "></mat-icon>
               </div>
             </div>
-          </cdk-virtual-scroll-viewport>
-        </div>
-
-        <mat-divider [vertical]="true"></mat-divider>
-
-        <div class="container-properties">
-          <h3 class="properties-title mat-title">Selected Transition</h3>
-          <tree-view
-            [item]="uiData.selectedTransitionPropertiesTree"
-            [showNode]="showNode"
-            [isLeaf]="isLeaf"
-            [isAlwaysCollapsed]="true">
-          </tree-view>
-          <div *ngIf="!uiData.selectedTransitionPropertiesTree">
-            No selected transition.<br />
-            Select the tranitions below.
           </div>
-        </div>
+        </cdk-virtual-scroll-viewport>
       </div>
 
-      <div class="bottom-viewer">
-        <div class="transition-timeline">
-          <div *ngFor="let row of timelineRows()" class="row">
-            <svg width="100%" [attr.height]="transitionHeight">
-              <rect
-                *ngFor="let transition of transitionsOnRow(row)"
-                [attr.width]="widthOf(transition)"
-                [attr.height]="transitionHeight"
-                [attr.style]="transitionRectStyle(transition)"
-                rx="5"
-                [attr.x]="startOf(transition)"
-                (click)="onTransitionClicked(transition)" />
-              <rect
-                *ngFor="let transition of transitionsOnRow(row)"
-                [attr.width]="transitionDividerWidth"
-                [attr.height]="transitionHeight"
-                [attr.style]="transitionDividerRectStyle(transition)"
-                [attr.x]="sendOf(transition)" />
-            </svg>
-          </div>
-        </div>
+      <mat-divider [vertical]="true"></mat-divider>
+
+      <div class="container-properties">
+        <h3 class="properties-title mat-title">Selected Transition</h3>
+        <tree-view
+          [item]="uiData.selectedTransitionPropertiesTree"
+          [showNode]="showNode"
+          [isLeaf]="isLeaf"
+          [isAlwaysCollapsed]="true">
+        </tree-view>
+        <div *ngIf="!uiData.selectedTransitionPropertiesTree">No selected transition.</div>
       </div>
     </div>
   `,
@@ -146,24 +118,7 @@
       .container {
         display: flex;
         flex-grow: 1;
-        flex-direction: column;
-      }
-
-      .top-viewer {
-        display: flex;
-        flex-grow: 1;
-        flex: 3;
-        border-bottom: solid 1px rgba(0, 0, 0, 0.12);
-      }
-
-      .bottom-viewer {
-        display: flex;
-        flex-shrink: 1;
-      }
-
-      .transition-timeline {
-        flex-grow: 1;
-        padding: 1.5rem 1rem;
+        flex-direction: row;
       }
 
       .entries {
@@ -176,6 +131,7 @@
       .container-properties {
         flex: 1;
         padding: 16px;
+        overflow-y: scroll;
       }
 
       .entries .scroll {
@@ -450,7 +406,7 @@
     return Math.max(...this.assignRowsToTransitions().values());
   }
 
-  private emitEvent(event: string, data: any) {
+  emitEvent(event: string, data: any) {
     const customEvent = new CustomEvent(event, {
       bubbles: true,
       detail: data,
diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts
index feb61c5..fa27224 100644
--- a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts
@@ -18,6 +18,9 @@
 import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA} from '@angular/core';
 import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing';
 import {MatDividerModule} from '@angular/material/divider';
+import {TracePositionUpdate} from 'app/app_event';
+import {assertDefined} from 'common/assert_utils';
+import {TimestampType} from 'common/time';
 import {
   CrossPlatform,
   ShellTransitionData,
@@ -25,8 +28,22 @@
   TransitionChange,
   TransitionType,
   WmTransitionData,
-} from 'trace/flickerlib/common';
-import {TimestampType} from 'trace/timestamp';
+} from 'flickerlib/common';
+import {ParserTransitionsShell} from 'parsers/parser_transitions_shell';
+import {ParserTransitionsWm} from 'parsers/parser_transitions_wm';
+import {TracesParserTransitions} from 'parsers/traces_parser_transitions';
+import {UnitTestUtils} from 'test/unit/utils';
+import {Trace} from 'trace/trace';
+import {Traces} from 'trace/traces';
+import {TraceFile} from 'trace/trace_file';
+import {TracePosition} from 'trace/trace_position';
+import {TraceType} from 'trace/trace_type';
+import {TreeComponent} from 'viewers/components/tree_component';
+import {TreeNodeComponent} from 'viewers/components/tree_node_component';
+import {TreeNodeDataViewComponent} from 'viewers/components/tree_node_data_view_component';
+import {TreeNodePropertiesDataViewComponent} from 'viewers/components/tree_node_properties_data_view_component';
+import {Events} from './events';
+import {Presenter} from './presenter';
 import {UiData} from './ui_data';
 import {ViewerTransitionsComponent} from './viewer_transitions_component';
 
@@ -35,11 +52,17 @@
   let component: ViewerTransitionsComponent;
   let htmlElement: HTMLElement;
 
-  beforeAll(async () => {
+  beforeEach(async () => {
     await TestBed.configureTestingModule({
       providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
       imports: [MatDividerModule, ScrollingModule],
-      declarations: [ViewerTransitionsComponent],
+      declarations: [
+        ViewerTransitionsComponent,
+        TreeComponent,
+        TreeNodeComponent,
+        TreeNodeDataViewComponent,
+        TreeNodePropertiesDataViewComponent,
+      ],
       schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
     }).compileComponents();
 
@@ -69,14 +92,99 @@
       'No selected transition'
     );
   });
+
+  it('emits TransitionSelected event on transition clicked', () => {
+    const emitEventSpy = spyOn(component, 'emitEvent');
+
+    const entries = htmlElement.querySelectorAll('.entry.table-row');
+    const entry1 = assertDefined(entries[0]) as HTMLElement;
+    const entry2 = assertDefined(entries[1]) as HTMLElement;
+    const treeView = assertDefined(
+      htmlElement.querySelector('.container-properties')
+    ) as HTMLElement;
+    expect(treeView.textContent).toContain('No selected transition');
+
+    expect(emitEventSpy).not.toHaveBeenCalled();
+
+    const id0 = assertDefined(entry1.querySelector('.id')).textContent;
+    expect(id0).toBe('0');
+    entry1.click();
+    fixture.detectChanges();
+
+    expect(emitEventSpy).toHaveBeenCalled();
+    expect(emitEventSpy).toHaveBeenCalledWith(Events.TransitionSelected, jasmine.any(Object));
+    expect(emitEventSpy.calls.mostRecent().args[1].id).toBe(0);
+
+    const id1 = assertDefined(entry2.querySelector('.id')).textContent;
+    expect(id1).toBe('1');
+    entry2.click();
+    fixture.detectChanges();
+
+    expect(emitEventSpy).toHaveBeenCalled();
+    expect(emitEventSpy).toHaveBeenCalledWith(Events.TransitionSelected, jasmine.any(Object));
+    expect(emitEventSpy.calls.mostRecent().args[1].id).toBe(1);
+  });
+
+  it('updates tree view on TracePositionUpdate event', async () => {
+    const wmTransFile = new TraceFile(
+      await UnitTestUtils.getFixtureFile('traces/elapsed_and_real_timestamp/wm_transition_trace.pb')
+    );
+    const shellTransFile = new TraceFile(
+      await UnitTestUtils.getFixtureFile(
+        'traces/elapsed_and_real_timestamp/shell_transition_trace.pb'
+      )
+    );
+
+    const wmTrans = new ParserTransitionsWm(wmTransFile);
+    await wmTrans.parse();
+    const shellTrans = new ParserTransitionsShell(shellTransFile);
+    await shellTrans.parse();
+
+    const transitionsTraceParser = new TracesParserTransitions([wmTrans, shellTrans]);
+    await transitionsTraceParser.parse();
+
+    const traces = new Traces();
+    const trace = Trace.newUninitializedTrace(transitionsTraceParser);
+    trace.init(TimestampType.REAL);
+    traces.setTrace(TraceType.TRANSITION, trace);
+
+    let treeView = assertDefined(
+      htmlElement.querySelector('.container-properties')
+    ) as any as HTMLElement;
+    expect(treeView.textContent).toContain('No selected transition');
+
+    const presenter = new Presenter(traces, (data) => {
+      component.inputData = data;
+    });
+    const selectedTransitionEntry = assertDefined(
+      traces.getTrace(TraceType.TRANSITION)?.getEntry(2)
+    );
+    const selectedTransition = (await selectedTransitionEntry.getValue()) as Transition;
+    await presenter.onAppEvent(
+      new TracePositionUpdate(TracePosition.fromTraceEntry(selectedTransitionEntry))
+    );
+
+    expect(component.uiData.selectedTransition.id).toBe(selectedTransition.id);
+    expect(component.uiData.selectedTransitionPropertiesTree).toBeTruthy();
+
+    fixture.detectChanges();
+
+    treeView = assertDefined(
+      fixture.nativeElement.querySelector('.container-properties')
+    ) as any as HTMLElement;
+    const textContentWithoutWhitespaces = treeView.textContent?.replace(/(\s|\t|\n)*/g, '');
+    expect(textContentWithoutWhitespaces).toContain(`id:${selectedTransition.id}`);
+  });
 });
 
 function makeUiData(): UiData {
+  let mockTransitionIdCounter = 0;
+
   const transitions = [
-    createMockTransition(10, 20, 30),
-    createMockTransition(40, 42, 50),
-    createMockTransition(45, 46, 49),
-    createMockTransition(55, 58, 70),
+    createMockTransition(10, 20, 30, mockTransitionIdCounter++),
+    createMockTransition(40, 42, 50, mockTransitionIdCounter++),
+    createMockTransition(45, 46, 49, mockTransitionIdCounter++),
+    createMockTransition(55, 58, 70, mockTransitionIdCounter++),
   ];
 
   const selectedTransition = undefined;
@@ -94,12 +202,14 @@
 function createMockTransition(
   createTimeNanos: number,
   sendTimeNanos: number,
-  finishTimeNanos: number
+  finishTimeNanos: number,
+  id: number
 ): Transition {
   const createTime = CrossPlatform.timestamp.fromString(createTimeNanos.toString(), null, null);
   const sendTime = CrossPlatform.timestamp.fromString(sendTimeNanos.toString(), null, null);
   const abortTime = null;
   const finishTime = CrossPlatform.timestamp.fromString(finishTimeNanos.toString(), null, null);
+  const startingWindowRemoveTime = null;
 
   const startTransactionId = '-1';
   const finishTransactionId = '-1';
@@ -107,12 +217,13 @@
   const changes: TransitionChange[] = [];
 
   return new Transition(
-    id++,
+    id,
     new WmTransitionData(
       createTime,
       sendTime,
       abortTime,
       finishTime,
+      startingWindowRemoveTime,
       startTransactionId,
       finishTransactionId,
       type,
@@ -121,5 +232,3 @@
     new ShellTransitionData()
   );
 }
-
-let id = 0;
diff --git a/tools/winscope/src/viewers/viewer_view_capture/presenter.ts b/tools/winscope/src/viewers/viewer_view_capture/presenter.ts
index a8ddf70..935df94 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/presenter.ts
@@ -14,27 +14,28 @@
  * limitations under the License.
  */
 
+import {AppEvent, AppEventType} from 'app/app_event';
 import {assertDefined} from 'common/assert_utils';
 import {PersistentStoreProxy} from 'common/persistent_store_proxy';
 import {FilterType, TreeUtils} from 'common/tree_utils';
-import {Point} from 'trace/flickerlib/common';
 import {Trace} from 'trace/trace';
 import {Traces} from 'trace/traces';
 import {TraceEntryFinder} from 'trace/trace_entry_finder';
-import {TracePosition} from 'trace/trace_position';
-import {TraceType} from 'trace/trace_type';
-import {Rectangle} from 'viewers/common/rectangle';
+import {FrameData, TraceType, ViewNode} from 'trace/trace_type';
+import {SurfaceFlingerUtils} from 'viewers/common/surface_flinger_utils';
 import {TreeGenerator} from 'viewers/common/tree_generator';
 import {TreeTransformer} from 'viewers/common/tree_transformer';
 import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
 import {UserOptions} from 'viewers/common/user_options';
+import {Rectangle} from 'viewers/components/rects/types2d';
 import {UiData} from './ui_data';
 
 export class Presenter {
   private viewCaptureTrace: Trace<object>;
+  private surfaceFlingerTrace: Trace<object> | undefined;
 
-  private selectedFrameData: any | undefined;
-  private previousFrameData: any | undefined;
+  private selectedFrameData: FrameData | undefined;
+  private previousFrameData: FrameData | undefined;
   private selectedHierarchyTree: HierarchyTreeNode | undefined;
 
   private uiData: UiData | undefined;
@@ -87,25 +88,43 @@
   );
 
   constructor(
+    traceType: TraceType,
     traces: Traces,
     private readonly storage: Storage,
     private readonly notifyUiDataCallback: (data: UiData) => void
   ) {
-    this.viewCaptureTrace = assertDefined(traces.getTrace(TraceType.VIEW_CAPTURE));
+    this.viewCaptureTrace = assertDefined(traces.getTrace(traceType));
+    this.surfaceFlingerTrace = traces.getTrace(TraceType.SURFACE_FLINGER);
   }
 
-  async onTracePositionUpdate(position: TracePosition) {
-    const entry = TraceEntryFinder.findCorrespondingEntry(this.viewCaptureTrace, position);
+  async onAppEvent(event: AppEvent) {
+    await event.visit(AppEventType.TRACE_POSITION_UPDATE, async (event) => {
+      const vcEntry = TraceEntryFinder.findCorrespondingEntry(
+        this.viewCaptureTrace,
+        event.position
+      );
+      let prevVcEntry: typeof vcEntry;
+      if (vcEntry && vcEntry.getIndex() > 0) {
+        prevVcEntry = this.viewCaptureTrace.getEntry(vcEntry.getIndex() - 1);
+      }
 
-    let prevEntry: typeof entry;
-    if (entry && entry.getIndex() > 0) {
-      prevEntry = this.viewCaptureTrace.getEntry(entry.getIndex() - 1);
-    }
+      this.selectedFrameData = await vcEntry?.getValue();
+      this.previousFrameData = await prevVcEntry?.getValue();
 
-    this.selectedFrameData = await entry?.getValue();
-    this.previousFrameData = await prevEntry?.getValue();
-
-    this.refreshUI();
+      if (this.uiData && this.surfaceFlingerTrace) {
+        const surfaceFlingerEntry = await TraceEntryFinder.findCorrespondingEntry(
+          this.surfaceFlingerTrace,
+          event.position
+        )?.getValue();
+        if (surfaceFlingerEntry) {
+          this.uiData.sfRects = SurfaceFlingerUtils.makeRects(
+            surfaceFlingerEntry,
+            this.hierarchyUserOptions
+          );
+        }
+      }
+      this.refreshUI();
+    });
   }
 
   private refreshUI() {
@@ -114,40 +133,53 @@
     if (!this.selectedHierarchyTree && tree) {
       this.selectedHierarchyTree = tree;
     }
+    let selectedViewNode: any | null = null;
+    if (this.selectedHierarchyTree?.name && this.selectedFrameData?.node) {
+      selectedViewNode = this.findViewNode(
+        this.selectedHierarchyTree.name,
+        this.selectedFrameData.node
+      );
+    }
 
     this.uiData = new UiData(
-      this.generateRectangles(),
+      this.generateViewCaptureRectangles(),
+      this.uiData?.sfRects,
       tree,
       this.hierarchyUserOptions,
       this.propertiesUserOptions,
       this.pinnedItems,
       this.highlightedItems,
-      this.getTreeWithTransformedProperties(this.selectedHierarchyTree)
+      this.getTreeWithTransformedProperties(this.selectedHierarchyTree),
+      selectedViewNode
     );
+
     this.notifyUiDataCallback(this.uiData);
   }
 
-  private generateRectangles(): Rectangle[] {
+  private generateViewCaptureRectangles(): Rectangle[] {
     const rectangles: Rectangle[] = [];
 
     function inner(node: any /* ViewNode */) {
       const aRectangle: Rectangle = {
-        topLeft: new Point(node.boxPos.left, node.boxPos.top),
-        bottomRight: new Point(
-          node.boxPos.left + node.boxPos.width,
-          node.boxPos.top + node.boxPos.height
-        ),
+        topLeft: {
+          x: node.boxPos.left,
+          y: node.boxPos.top,
+        },
+        bottomRight: {
+          x: node.boxPos.left + node.boxPos.width,
+          y: node.boxPos.top + node.boxPos.height,
+        },
         label: '',
-        transform: null,
+        transform: undefined,
         isVisible: node.isVisible,
         isDisplay: false,
-        ref: {},
         id: node.id,
         displayId: 0,
         isVirtual: false,
         isClickable: true,
         cornerRadius: 0,
         depth: node.depth,
+        hasContent: node.isVisible,
       };
       rectangles.push(aRectangle);
       node.children.forEach((it: any) /* ViewNode */ => inner(it));
@@ -215,7 +247,7 @@
     this.copyUiDataAndNotifyView();
   }
 
-  updateHierarchyTree(userOptions: any) {
+  updateHierarchyTree(userOptions: UserOptions) {
     this.hierarchyUserOptions = userOptions;
     this.uiData!!.hierarchyUserOptions = this.hierarchyUserOptions;
     this.uiData!!.tree = this.generateTree();
@@ -241,6 +273,10 @@
 
   newPropertiesTree(selectedItem: HierarchyTreeNode) {
     this.selectedHierarchyTree = selectedItem;
+    this.uiData!!.selectedViewNode = this.findViewNode(
+      selectedItem.name,
+      this.selectedFrameData.node
+    );
     this.updateSelectedTreeUiData();
   }
 
@@ -251,7 +287,23 @@
     this.copyUiDataAndNotifyView();
   }
 
-  private getTreeWithTransformedProperties(selectedTree: any): PropertiesTreeNode | null {
+  private findViewNode(name: string, root: ViewNode): ViewNode | null {
+    if (name === root.name) {
+      return root;
+    } else {
+      for (let i = 0; i < root.children.length; i++) {
+        const subTreeSearch = this.findViewNode(name, root.children[i]);
+        if (subTreeSearch != null) {
+          return subTreeSearch;
+        }
+      }
+    }
+    return null;
+  }
+
+  private getTreeWithTransformedProperties(
+    selectedTree: HierarchyTreeNode | undefined
+  ): PropertiesTreeNode | null {
     if (!selectedTree) {
       return null;
     }
diff --git a/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts b/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts
index 0a7e14a..a1bc843 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
+import {TracePositionUpdate} from 'app/app_event';
+import {RealTimestamp, TimestampType} from 'common/time';
 import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
 import {MockStorage} from 'test/unit/mock_storage';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {UnitTestUtils} from 'test/unit/utils';
-import {Point} from 'trace/flickerlib/common';
 import {Parser} from 'trace/parser';
-import {RealTimestamp, TimestampType} from 'trace/timestamp';
 import {Trace} from 'trace/trace';
 import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
-import {TraceType} from 'trace/trace_type';
 import {HierarchyTreeNode} from 'viewers/common/ui_tree_utils';
 import {UserOptions} from 'viewers/common/user_options';
 import {Presenter} from 'viewers/viewer_view_capture/presenter';
@@ -35,7 +33,7 @@
   let trace: Trace<object>;
   let uiData: UiData;
   let presenter: Presenter;
-  let position: TracePosition;
+  let positionUpdate: TracePositionUpdate;
   let selectedTree: HierarchyTreeNode;
 
   beforeAll(async () => {
@@ -49,7 +47,7 @@
         parser.getEntry(2, TimestampType.REAL),
       ])
       .build();
-    position = TracePosition.fromTraceEntry(trace.getEntry(0));
+    positionUpdate = TracePositionUpdate.fromTraceEntry(trace.getEntry(0));
     selectedTree = new HierarchyTreeBuilder()
       .setName('Name@Id')
       .setStableId('stableId')
@@ -59,7 +57,7 @@
       .build();
   });
 
-  beforeEach(async () => {
+  beforeEach(() => {
     presenter = createPresenter(trace);
   });
 
@@ -67,14 +65,16 @@
     const emptyTrace = new TraceBuilder<object>().setEntries([]).build();
     const presenter = createPresenter(emptyTrace);
 
-    const positionWithoutTraceEntry = TracePosition.fromTimestamp(new RealTimestamp(0n));
-    await presenter.onTracePositionUpdate(positionWithoutTraceEntry);
+    const positionUpdateWithoutTraceEntry = TracePositionUpdate.fromTimestamp(
+      new RealTimestamp(0n)
+    );
+    await presenter.onAppEvent(positionUpdateWithoutTraceEntry);
     expect(uiData.hierarchyUserOptions).toBeTruthy();
     expect(uiData.tree).toBeFalsy();
   });
 
   it('processes trace position updates', async () => {
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
 
     expect(uiData.rects.length).toBeGreaterThan(0);
     expect(uiData.highlightedItems?.length).toEqual(0);
@@ -86,40 +86,40 @@
   });
 
   it('creates input data for rects view', async () => {
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     expect(uiData.rects.length).toBeGreaterThan(0);
-    expect(uiData.rects[0].topLeft).toEqual(new Point(0, 0));
-    expect(uiData.rects[0].bottomRight).toEqual(new Point(1080, 2340));
+    expect(uiData.rects[0].topLeft).toEqual({x: 0, y: 0});
+    expect(uiData.rects[0].bottomRight).toEqual({x: 1080, y: 249});
   });
 
   it('updates pinned items', async () => {
     const pinnedItem = new HierarchyTreeBuilder().setName('FirstPinnedItem').setId('id').build();
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     presenter.updatePinnedItems(pinnedItem);
     expect(uiData.pinnedItems).toContain(pinnedItem);
   });
 
   it('updates highlighted items', async () => {
+    await presenter.onAppEvent(positionUpdate);
     expect(uiData.highlightedItems).toEqual([]);
 
     const id = '4';
-    await presenter.onTracePositionUpdate(position);
     presenter.updateHighlightedItems(id);
     expect(uiData.highlightedItems).toContain(id);
   });
 
   it('updates hierarchy tree', async () => {
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
 
     expect(
-      // DecorView -> LinearLayout -> FrameLayout -> LauncherRootView -> DragLayer -> Workspace
-      uiData.tree?.children[0].children[1].children[0].children[0].children[1].id
-    ).toEqual('com.android.launcher3.Workspace@251960479');
+      // TaskbarDragLayer -> TaskbarView
+      uiData.tree?.children[0].id
+    ).toEqual('com.android.launcher3.taskbar.TaskbarView@80213537');
 
     const userOptions: UserOptions = {
       showDiff: {
         name: 'Show diff',
-        enabled: true,
+        enabled: false,
       },
       simplifyNames: {
         name: 'Simplify names',
@@ -133,9 +133,9 @@
     presenter.updateHierarchyTree(userOptions);
     expect(uiData.hierarchyUserOptions).toEqual(userOptions);
     expect(
-      // DecorView -> LinearLayout -> FrameLayout (before, this was the 2nd child) -> LauncherRootView -> DragLayer -> Workspace if filter works as expected
-      uiData.tree?.children[0].children[0].children[0].children[0].children[1].id
-    ).toEqual('com.android.launcher3.Workspace@251960479');
+      // TaskbarDragLayer -> TaskbarScrimView
+      uiData.tree?.children[0].id
+    ).toEqual('com.android.launcher3.taskbar.TaskbarScrimView@114418695');
   });
 
   it('filters hierarchy tree', async () => {
@@ -152,23 +152,19 @@
         name: 'Only visible',
         enabled: false,
       },
-      flat: {
-        name: 'Flat',
-        enabled: true,
-      },
     };
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     presenter.updateHierarchyTree(userOptions);
-    presenter.filterHierarchyTree('Workspace');
+    presenter.filterHierarchyTree('BubbleBarView');
 
     expect(
-      // DecorView -> LinearLayout -> FrameLayout -> LauncherRootView -> DragLayer -> Workspace if filter works as expected
-      uiData.tree?.children[0].children[0].children[0].children[0].children[0].id
-    ).toEqual('com.android.launcher3.Workspace@251960479');
+      // TaskbarDragLayer -> BubbleBarView if filter works as expected
+      uiData.tree?.children[0].id
+    ).toEqual('com.android.launcher3.taskbar.bubbles.BubbleBarView@256010548');
   });
 
   it('sets properties tree and associated ui data', async () => {
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     presenter.newPropertiesTree(selectedTree);
     expect(uiData.propertiesTree).toBeTruthy();
   });
@@ -190,7 +186,7 @@
       },
     };
 
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     presenter.newPropertiesTree(selectedTree);
     expect(uiData.propertiesTree?.diffType).toBeFalsy();
 
@@ -200,7 +196,7 @@
   });
 
   it('filters properties tree', async () => {
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
 
     const userOptions: UserOptions = {
       showDiff: {
@@ -221,7 +217,7 @@
     let nonTerminalChildren = uiData.propertiesTree?.children?.filter(
       (it) => typeof it.propertyKey === 'string'
     );
-    expect(nonTerminalChildren?.length).toEqual(24);
+    expect(nonTerminalChildren?.length).toEqual(25);
     presenter.filterPropertiesTree('alpha');
 
     nonTerminalChildren = uiData.propertiesTree?.children?.filter(
@@ -232,8 +228,8 @@
 
   const createPresenter = (trace: Trace<object>): Presenter => {
     const traces = new Traces();
-    traces.setTrace(TraceType.VIEW_CAPTURE, trace);
-    return new Presenter(traces, new MockStorage(), (newData: UiData) => {
+    traces.setTrace(parser.getTraceType(), trace);
+    return new Presenter(parser.getTraceType(), traces, new MockStorage(), (newData: UiData) => {
       uiData = newData;
     });
   };
diff --git a/tools/winscope/src/viewers/viewer_view_capture/ui_data.ts b/tools/winscope/src/viewers/viewer_view_capture/ui_data.ts
index 8d1a138..89b66ae 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/ui_data.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/ui_data.ts
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-import {TraceType} from 'trace/trace_type';
-import {Rectangle} from 'viewers/common/rectangle';
+import {TraceType, ViewNode} from 'trace/trace_type';
 import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
 import {UserOptions} from 'viewers/common/user_options';
+import {Rectangle} from 'viewers/components/rects/types2d';
 
 export class UiData {
   readonly dependencies: TraceType[] = [TraceType.VIEW_CAPTURE];
@@ -25,11 +25,13 @@
 
   constructor(
     readonly rects: Rectangle[],
+    public sfRects: Rectangle[] | undefined,
     public tree: HierarchyTreeNode | null,
     public hierarchyUserOptions: UserOptions,
     public propertiesUserOptions: UserOptions,
     public pinnedItems: HierarchyTreeNode[],
     public highlightedItems: string[],
-    public propertiesTree: PropertiesTreeNode | null
+    public propertiesTree: PropertiesTreeNode | null,
+    public selectedViewNode: ViewNode
   ) {}
 }
diff --git a/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture.ts b/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture.ts
index 2a8671a..7761736 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture.ts
@@ -14,24 +14,25 @@
  * limitations under the License.
  */
 
+import {AppEvent, TabbedViewSwitchRequest} from 'app/app_event';
+import {FunctionUtils} from 'common/function_utils';
+import {EmitAppEvent} from 'interfaces/app_event_emitter';
 import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {ViewerEvents} from 'viewers/common/viewer_events';
 import {View, Viewer, ViewType} from 'viewers/viewer';
 import {Presenter} from './presenter';
 import {UiData} from './ui_data';
 
-// TODO: Fix "flatten tree hierarchy view" behavior.
 export class ViewerViewCapture implements Viewer {
   static readonly DEPENDENCIES: TraceType[] = [TraceType.VIEW_CAPTURE];
+  private emitAppEvent: EmitAppEvent = FunctionUtils.DO_NOTHING_ASYNC;
   private htmlElement: HTMLElement;
   private presenter: Presenter;
 
   constructor(traces: Traces, storage: Storage) {
     this.htmlElement = document.createElement('viewer-view-capture');
-
-    this.presenter = new Presenter(traces, storage, (data: UiData) => {
+    this.presenter = new Presenter(this.getDependencies()[0], traces, storage, (data: UiData) => {
       (this.htmlElement as any).inputData = data;
     });
 
@@ -56,10 +57,21 @@
     this.htmlElement.addEventListener(ViewerEvents.SelectedTreeChange, (event) =>
       this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem)
     );
+    this.htmlElement.addEventListener(ViewerEvents.MiniRectsDblClick, (event) => {
+      this.switchToSurfaceFlingerView();
+    });
   }
 
-  async onTracePositionUpdate(position: TracePosition) {
-    await this.presenter.onTracePositionUpdate(position);
+  async onAppEvent(event: AppEvent) {
+    await this.presenter.onAppEvent(event);
+  }
+
+  setEmitAppEvent(callback: EmitAppEvent) {
+    this.emitAppEvent = callback;
+  }
+
+  async switchToSurfaceFlingerView() {
+    await this.emitAppEvent(new TabbedViewSwitchRequest(TraceType.SURFACE_FLINGER));
   }
 
   getViews(): View[] {
@@ -68,8 +80,8 @@
         ViewType.TAB,
         this.getDependencies(),
         this.htmlElement,
-        'View Capture',
-        TraceType.VIEW_CAPTURE
+        this.getTitle(),
+        this.getDependencies()[0]
       ),
     ];
   }
@@ -77,4 +89,38 @@
   getDependencies(): TraceType[] {
     return ViewerViewCapture.DEPENDENCIES;
   }
+
+  private getTitle(): string {
+    switch (this.getDependencies()[0]) {
+      case TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER:
+        return 'View Capture - Taskbar';
+      case TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER:
+        return 'View Capture - Taskbar Overlay';
+      default:
+        return 'View Capture - Nexuslauncher';
+    }
+  }
+}
+
+export class ViewerViewCaptureLauncherActivity extends ViewerViewCapture {
+  static override readonly DEPENDENCIES: TraceType[] = [TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY];
+  override getDependencies(): TraceType[] {
+    return ViewerViewCaptureLauncherActivity.DEPENDENCIES;
+  }
+}
+
+export class ViewerViewCaptureTaskbarDragLayer extends ViewerViewCapture {
+  static override readonly DEPENDENCIES: TraceType[] = [TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER];
+  override getDependencies(): TraceType[] {
+    return ViewerViewCaptureTaskbarDragLayer.DEPENDENCIES;
+  }
+}
+
+export class ViewerViewCaptureTaskbarOverlayDragLayer extends ViewerViewCapture {
+  static override readonly DEPENDENCIES: TraceType[] = [
+    TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER,
+  ];
+  override getDependencies(): TraceType[] {
+    return ViewerViewCaptureTaskbarOverlayDragLayer.DEPENDENCIES;
+  }
 }
diff --git a/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component.ts b/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component.ts
index 174bb60..6ff95da 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component.ts
@@ -16,6 +16,7 @@
 
 import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
 import {PersistentStore} from 'common/persistent_store';
+import {TraceType} from 'trace/trace_type';
 import {UiData} from './ui_data';
 
 /**
@@ -31,6 +32,8 @@
         title="View Hierarchy Sketch"
         [enableShowVirtualButton]="false"
         [rects]="inputData?.rects ?? []"
+        [zoomFactor]="4"
+        [miniRects]="inputData?.sfRects ?? []"
         [highlightedItems]="inputData?.highlightedItems ?? []"
         [displayIds]="[0]"></rects-view>
       <mat-divider [vertical]="true"></mat-divider>
@@ -47,8 +50,9 @@
         class="properties-view"
         [userOptions]="inputData?.propertiesUserOptions ?? {}"
         [propertiesTree]="inputData?.propertiesTree ?? {}"
-        [displayPropertyGroups]="inputData?.displayPropertyGroups"
-        [isProtoDump]="true">
+        [selectedItem]="inputData?.selectedViewNode ?? null"
+        [traceType]="${TraceType.VIEW_CAPTURE}"
+        [isProtoDump]="false">
       </properties-view>
     </div>
   `,
diff --git a/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component_test.ts b/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component_test.ts
index 170acc8..bd267bc 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component_test.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component_test.ts
@@ -22,7 +22,7 @@
 import {RectsComponent} from 'viewers/components/rects/rects_component';
 import {ViewerViewCaptureComponent} from './viewer_view_capture_component';
 
-describe('ViewerSurfaceFlingerComponent', () => {
+describe('ViewerViewCaptureComponent', () => {
   let fixture: ComponentFixture<ViewerViewCaptureComponent>;
   let component: ViewerViewCaptureComponent;
   let htmlElement: HTMLElement;
diff --git a/tools/winscope/src/viewers/viewer_window_manager/presenter.ts b/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
index 6d43714..5416925 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
@@ -14,22 +14,22 @@
  * limitations under the License.
  */
 
+import {AppEvent, AppEventType} from 'app/app_event';
 import {assertDefined} from 'common/assert_utils';
 import {PersistentStoreProxy} from 'common/persistent_store_proxy';
 import {FilterType, TreeUtils} from 'common/tree_utils';
-import {DisplayContent} from 'trace/flickerlib/windows/DisplayContent';
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
+import {DisplayContent} from 'flickerlib/windows/DisplayContent';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
 import {Trace} from 'trace/trace';
 import {Traces} from 'trace/traces';
 import {TraceEntryFinder} from 'trace/trace_entry_finder';
-import {TracePosition} from 'trace/trace_position';
 import {TraceTreeNode} from 'trace/trace_tree_node';
 import {TraceType} from 'trace/trace_type';
-import {Rectangle, RectMatrix, RectTransform} from 'viewers/common/rectangle';
 import {TreeGenerator} from 'viewers/common/tree_generator';
 import {TreeTransformer} from 'viewers/common/tree_transformer';
 import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
 import {UserOptions} from 'viewers/common/user_options';
+import {Rectangle, TransformMatrix} from 'viewers/components/rects/types2d';
 import {UiData} from './ui_data';
 
 type NotifyViewCallbackType = (uiData: UiData) => void;
@@ -53,6 +53,7 @@
       showDiff: {
         name: 'Show diff',
         enabled: false,
+        isUnavailable: false,
       },
       simplifyNames: {
         name: 'Simplify names',
@@ -75,6 +76,7 @@
       showDiff: {
         name: 'Show diff',
         enabled: false,
+        isUnavailable: false,
       },
       showDefaults: {
         name: 'Show defaults',
@@ -152,55 +154,93 @@
     this.updateSelectedTreeUiData();
   }
 
-  async onTracePositionUpdate(position: TracePosition) {
-    this.uiData = new UiData();
-    this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
-    this.uiData.propertiesUserOptions = this.propertiesUserOptions;
+  async onAppEvent(event: AppEvent) {
+    await event.visit(AppEventType.TRACE_POSITION_UPDATE, async (event) => {
+      const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, event.position);
+      const prevEntry =
+        entry && entry.getIndex() > 0 ? this.trace.getEntry(entry.getIndex() - 1) : undefined;
 
-    const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position);
-    const prevEntry =
-      entry && entry.getIndex() > 0 ? this.trace.getEntry(entry.getIndex() - 1) : undefined;
+      this.entry = (await entry?.getValue()) ?? null;
+      this.previousEntry = (await prevEntry?.getValue()) ?? null;
+      if (this.hierarchyUserOptions['showDiff'].isUnavailable !== undefined) {
+        this.hierarchyUserOptions['showDiff'].isUnavailable = this.previousEntry == null;
+      }
+      if (this.propertiesUserOptions['showDiff'].isUnavailable !== undefined) {
+        this.propertiesUserOptions['showDiff'].isUnavailable = this.previousEntry == null;
+      }
 
-    this.entry = (await entry?.getValue()) ?? null;
-    this.previousEntry = (await prevEntry?.getValue()) ?? null;
-    if (this.entry) {
-      this.uiData.highlightedItems = this.highlightedItems;
-      this.uiData.rects = this.generateRects();
-      this.uiData.displayIds = this.displayIds;
-      this.uiData.tree = this.generateTree();
-    }
+      this.uiData = new UiData();
+      this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
+      this.uiData.propertiesUserOptions = this.propertiesUserOptions;
 
-    this.notifyViewCallback(this.uiData);
+      if (this.entry) {
+        this.uiData.highlightedItems = this.highlightedItems;
+        this.uiData.rects = this.generateRects(this.entry);
+        this.uiData.displayIds = this.getDisplayIds(this.entry);
+        this.uiData.tree = this.generateTree();
+      }
+      this.notifyViewCallback(this.uiData);
+    });
   }
 
-  private generateRects(): Rectangle[] {
+  private generateRects(entry: TraceTreeNode): Rectangle[] {
+    const identityMatrix: TransformMatrix = {
+      dsdx: 1,
+      dsdy: 0,
+      tx: 0,
+      dtdx: 0,
+      dtdy: 1,
+      ty: 0,
+    };
     const displayRects: Rectangle[] =
-      this.entry?.displays?.map((display: DisplayContent) => {
-        const rect = display.displayRect;
-        rect.label = `Display - ${display.title}`;
-        rect.stableId = display.stableId;
-        rect.displayId = display.id;
-        rect.isDisplay = true;
-        rect.cornerRadius = 0;
-        rect.isVirtual = false;
+      entry.displays?.map((display: DisplayContent) => {
+        const rect: Rectangle = {
+          topLeft: {x: display.displayRect.left, y: display.displayRect.top},
+          bottomRight: {x: display.displayRect.right, y: display.displayRect.bottom},
+          label: `Display - ${display.title}`,
+          transform: identityMatrix,
+          isVisible: false, //TODO: check if displayRect.ref.isVisible exists
+          isDisplay: true,
+          id: display.stableId,
+          displayId: display.id,
+          isVirtual: false,
+          isClickable: false,
+          cornerRadius: 0,
+        };
         return rect;
       }) ?? [];
-    this.displayIds = [];
-    const rects: Rectangle[] =
-      this.entry?.windowStates
+
+    const windowRects: Rectangle[] =
+      entry.windowStates
         ?.sort((a: any, b: any) => b.computedZ - a.computedZ)
         .map((it: any) => {
-          const rect = it.rect;
-          rect.id = it.layerId;
-          rect.displayId = it.displayId;
-          rect.cornerRadius = 0;
-          if (!this.displayIds.includes(it.displayId)) {
-            this.displayIds.push(it.displayId);
-          }
+          const rect: Rectangle = {
+            topLeft: {x: it.rect.left, y: it.rect.top},
+            bottomRight: {x: it.rect.right, y: it.rect.bottom},
+            label: it.rect.label,
+            transform: identityMatrix,
+            isVisible: it.isVisible,
+            isDisplay: false,
+            id: it.stableId,
+            displayId: it.displayId,
+            isVirtual: false, //TODO: is this correct?
+            isClickable: true,
+            cornerRadius: 0,
+          };
           return rect;
         }) ?? [];
-    this.displayIds.sort();
-    return this.rectsToUiData(rects.concat(displayRects));
+
+    return windowRects.concat(displayRects);
+  }
+
+  private getDisplayIds(entry: TraceTreeNode): number[] {
+    const ids = new Set<number>();
+    entry.windowStates?.map((it: any) => {
+      ids.add(it.displayId);
+    });
+    return Array.from(ids.values()).sort((a, b) => {
+      return a - b;
+    });
   }
 
   private updateSelectedTreeUiData() {
@@ -223,7 +263,10 @@
       .setIsFlatView(this.hierarchyUserOptions['flat']?.enabled)
       .withUniqueNodeId();
     let tree: HierarchyTreeNode | null;
-    if (!this.hierarchyUserOptions['showDiff']?.enabled) {
+    if (
+      !this.hierarchyUserOptions['showDiff']?.enabled ||
+      this.hierarchyUserOptions['showDiff']?.isUnavailable
+    ) {
       tree = generator.generateTree();
     } else {
       tree = generator
@@ -236,40 +279,6 @@
     return tree;
   }
 
-  private rectsToUiData(rects: any[]): Rectangle[] {
-    const uiRects: Rectangle[] = [];
-    const identityMatrix: RectMatrix = {
-      dsdx: 1,
-      dsdy: 0,
-      tx: 0,
-      dtdx: 0,
-      dtdy: 1,
-      ty: 0,
-    };
-    rects.forEach((rect: any) => {
-      const transform: RectTransform = {
-        matrix: identityMatrix,
-      };
-
-      const newRect: Rectangle = {
-        topLeft: {x: rect.left, y: rect.top},
-        bottomRight: {x: rect.right, y: rect.bottom},
-        label: rect.label,
-        transform,
-        isVisible: rect.ref?.isVisible ?? false,
-        isDisplay: rect.isDisplay ?? false,
-        ref: rect.ref,
-        id: rect.stableId ?? rect.ref.stableId,
-        displayId: rect.displayId ?? rect.ref.stackId,
-        isVirtual: rect.isVirtual ?? false,
-        isClickable: !rect.isDisplay,
-        cornerRadius: rect.cornerRadius,
-      };
-      uiRects.push(newRect);
-    });
-    return uiRects;
-  }
-
   private updatePinnedIds(newId: string) {
     if (this.pinnedIds.includes(newId)) {
       this.pinnedIds = this.pinnedIds.filter((pinned) => pinned !== newId);
@@ -285,7 +294,10 @@
     const transformer = new TreeTransformer(selectedTree, this.propertiesFilter)
       .setOnlyProtoDump(true)
       .setIsShowDefaults(this.propertiesUserOptions['showDefaults']?.enabled)
-      .setIsShowDiff(this.propertiesUserOptions['showDiff']?.enabled)
+      .setIsShowDiff(
+        this.propertiesUserOptions['showDiff']?.enabled &&
+          !this.propertiesUserOptions['showDiff']?.isUnavailable
+      )
       .setTransformerOptions({skip: selectedTree.skip})
       .setProperties(this.entry)
       .setDiffProperties(this.previousEntry);
diff --git a/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts b/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts
index 326e7e6..799a68d 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts
@@ -14,15 +14,15 @@
  * limitations under the License.
  */
 
+import {TracePositionUpdate} from 'app/app_event';
+import {RealTimestamp} from 'common/time';
+import {WindowManagerState} from 'flickerlib/common';
 import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
 import {MockStorage} from 'test/unit/mock_storage';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {UnitTestUtils} from 'test/unit/utils';
-import {WindowManagerState} from 'trace/flickerlib/common';
-import {RealTimestamp} from 'trace/timestamp';
 import {Trace} from 'trace/trace';
 import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {VISIBLE_CHIP} from 'viewers/common/chip';
 import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
@@ -32,7 +32,7 @@
 
 describe('PresenterWindowManager', () => {
   let trace: Trace<WindowManagerState>;
-  let position: TracePosition;
+  let positionUpdate: TracePositionUpdate;
   let presenter: Presenter;
   let uiData: UiData;
   let selectedTree: HierarchyTreeNode;
@@ -42,7 +42,7 @@
       .setEntries([await UnitTestUtils.getWindowManagerState()])
       .build();
 
-    position = TracePosition.fromTraceEntry(trace.getEntry(0));
+    positionUpdate = TracePositionUpdate.fromTraceEntry(trace.getEntry(0));
 
     selectedTree = new HierarchyTreeBuilder()
       .setName('ScreenDecorOverlayBottom')
@@ -67,14 +67,16 @@
     expect(uiData.hierarchyUserOptions).toBeTruthy();
     expect(uiData.tree).toBeFalsy();
 
-    const positionWithoutTraceEntry = TracePosition.fromTimestamp(new RealTimestamp(0n));
-    await presenter.onTracePositionUpdate(positionWithoutTraceEntry);
+    const positionUpdateWithoutTraceEntry = TracePositionUpdate.fromTimestamp(
+      new RealTimestamp(0n)
+    );
+    await presenter.onAppEvent(positionUpdateWithoutTraceEntry);
     expect(uiData.hierarchyUserOptions).toBeTruthy();
     expect(uiData.tree).toBeFalsy();
   });
 
   it('processes trace position update', async () => {
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     const filteredUiDataRectLabels = uiData.rects
       ?.filter((rect) => rect.isVisible !== undefined)
       .map((rect) => rect.label);
@@ -94,15 +96,29 @@
     expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
   });
 
+  it('disables show diff and generates non-diff tree if no prev entry available', async () => {
+    await presenter.onAppEvent(positionUpdate);
+
+    const hierarchyOpts = uiData.hierarchyUserOptions ?? null;
+    expect(hierarchyOpts).toBeTruthy();
+    expect(hierarchyOpts!['showDiff'].isUnavailable).toBeTrue();
+
+    const propertyOpts = uiData.propertiesUserOptions ?? null;
+    expect(propertyOpts).toBeTruthy();
+    expect(propertyOpts!['showDiff'].isUnavailable).toBeTrue();
+
+    expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
+  });
+
   it('creates input data for rects view', async () => {
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     expect(uiData.rects.length).toBeGreaterThan(0);
     expect(uiData.rects[0].topLeft).toEqual({x: 0, y: 2326});
     expect(uiData.rects[0].bottomRight).toEqual({x: 1080, y: 2400});
   });
 
   it('updates pinned items', async () => {
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     expect(uiData.pinnedItems).toEqual([]);
 
     const pinnedItem = new HierarchyTreeBuilder()
@@ -143,7 +159,7 @@
       },
     };
 
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     expect(uiData.tree?.children.length).toEqual(1);
     presenter.updateHierarchyTree(userOptions);
     expect(uiData.hierarchyUserOptions).toEqual(userOptions);
@@ -170,7 +186,7 @@
         enabled: true,
       },
     };
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     presenter.updateHierarchyTree(userOptions);
     expect(uiData.tree?.children.length).toEqual(72);
     presenter.filterHierarchyTree('ScreenDecor');
@@ -179,7 +195,7 @@
   });
 
   it('sets properties tree and associated ui data', async () => {
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     presenter.newPropertiesTree(selectedTree);
     // does not check specific tree values as tree transformation method may change
     expect(uiData.propertiesTree).toBeTruthy();
@@ -203,7 +219,7 @@
       },
     };
 
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     presenter.newPropertiesTree(selectedTree);
     expect(uiData.propertiesTree?.diffType).toBeFalsy();
     presenter.updatePropertiesTree(userOptions);
@@ -213,7 +229,7 @@
   });
 
   it('filters properties tree', async () => {
-    await presenter.onTracePositionUpdate(position);
+    await presenter.onAppEvent(positionUpdate);
     presenter.newPropertiesTree(selectedTree);
 
     let nonTerminalChildren =
diff --git a/tools/winscope/src/viewers/viewer_window_manager/ui_data.ts b/tools/winscope/src/viewers/viewer_window_manager/ui_data.ts
index 35bcaf7..afdfc6e 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/ui_data.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/ui_data.ts
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 import {TraceType} from 'trace/trace_type';
-import {Rectangle} from 'viewers/common/rectangle';
 import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
 import {UserOptions} from 'viewers/common/user_options';
+import {Rectangle} from 'viewers/components/rects/types2d';
 
 export class UiData {
   dependencies: TraceType[];
diff --git a/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager.ts b/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager.ts
index f44a8b7..4d3edc3 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
+import {AppEvent} from 'app/app_event';
 import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {ViewerEvents} from 'viewers/common/viewer_events';
 import {View, Viewer, ViewType} from 'viewers/viewer';
@@ -51,8 +51,12 @@
     );
   }
 
-  async onTracePositionUpdate(position: TracePosition) {
-    await this.presenter.onTracePositionUpdate(position);
+  async onAppEvent(event: AppEvent) {
+    await this.presenter.onAppEvent(event);
+  }
+
+  setEmitAppEvent() {
+    // do nothing
   }
 
   getViews(): View[] {
diff --git a/tools/winscope/webpack.config.common.js b/tools/winscope/webpack.config.common.js
index 8a3a00c..9580f48 100644
--- a/tools/winscope/webpack.config.common.js
+++ b/tools/winscope/webpack.config.common.js
@@ -19,7 +19,7 @@
 module.exports = {
   resolve: {
     extensions: ['.ts', '.js', '.css'],
-    modules: ['node_modules', 'src', 'kotlin_build', path.resolve(__dirname, '../../..')],
+    modules: ['node_modules', 'src', 'deps_build', __dirname, path.resolve(__dirname, '../../..')],
   },
 
   resolveLoader: {
@@ -49,7 +49,9 @@
         loader: 'proto-loader',
         options: {
           paths: [
+            __dirname,
             path.resolve(__dirname, '../../..'),
+            path.resolve(__dirname, '../../../external/perfetto'),
             path.resolve(__dirname, '../../../external/protobuf/src'),
           ],
         },
diff --git a/tools/winscope/webpack.config.dev.js b/tools/winscope/webpack.config.dev.js
index 5857a8e..9c6d873 100644
--- a/tools/winscope/webpack.config.dev.js
+++ b/tools/winscope/webpack.config.dev.js
@@ -16,6 +16,7 @@
 const {merge} = require('webpack-merge');
 const configCommon = require('./webpack.config.common');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
+const CopyPlugin = require('copy-webpack-plugin');
 
 const configDev = {
   mode: 'development',
@@ -25,12 +26,31 @@
     app: './src/main_dev.ts',
   },
   devtool: 'source-map',
+
+  externals: {
+    fs: 'fs',
+    path: 'path',
+    crypto: 'crypto',
+  },
+
+  node: {
+    global: false,
+    __filename: false,
+    __dirname: false,
+  },
+
   plugins: [
     new HtmlWebpackPlugin({
       template: 'src/index.html',
       inject: 'body',
       inlineSource: '.(css|js)$',
     }),
+    new CopyPlugin({
+      patterns: [
+        'deps_build/trace_processor/to_be_served/trace_processor.wasm',
+        'deps_build/trace_processor/to_be_served/engine_bundle.js',
+      ],
+    }),
   ],
 };
 
diff --git a/tools/winscope/webpack.config.unit_test.js b/tools/winscope/webpack.config.unit_test.js
deleted file mode 100644
index 15b4ed4..0000000
--- a/tools/winscope/webpack.config.unit_test.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2022 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.
- */
-const path = require('path');
-const glob = require('glob');
-
-const config = require('./webpack.config.common');
-
-config['mode'] = 'development';
-
-const allTestFiles = [...glob.sync('./src/**/*_test.js'), ...glob.sync('./src/**/*_test.ts')];
-const unitTestFiles = allTestFiles
-  .filter((file) => !file.match('.*_component_test\\.(js|ts)$'))
-  .filter((file) => !file.match('.*e2e.*'));
-config['entry'] = {
-  tests: unitTestFiles,
-};
-
-config['output'] = {
-  path: path.resolve(__dirname, 'dist/unit_test'),
-  filename: 'bundle.js',
-};
-
-config['target'] = 'node';
-config['node'] = {
-  __dirname: false,
-};
-
-module.exports = config;