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;