Merge "Detecting multiple view animation anomalies." into udc-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
1e6daed881
@@ -26,8 +26,13 @@ import com.android.app.viewcapture.data.ExportedData
|
||||
import com.android.launcher3.tapl.TestHelpers
|
||||
import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter
|
||||
import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer
|
||||
import org.junit.Assert.assertTrue
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStreamWriter
|
||||
import java.util.function.Supplier
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
@@ -81,7 +86,7 @@ class ViewCaptureRule(var alreadyOpenActivitySupplier: Supplier<Activity?>) : Te
|
||||
MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) }
|
||||
}
|
||||
|
||||
analyzeViewCapture()
|
||||
analyzeViewCapture(description)
|
||||
}
|
||||
|
||||
private fun startCapturingExistingActivity(
|
||||
@@ -107,16 +112,39 @@ class ViewCaptureRule(var alreadyOpenActivitySupplier: Supplier<Activity?>) : Te
|
||||
}
|
||||
}
|
||||
|
||||
private fun analyzeViewCapture() {
|
||||
private fun analyzeViewCapture(description: Description) {
|
||||
// OOP tests don't produce ViewCapture data
|
||||
if (!TestHelpers.isInLauncherProcess()) return
|
||||
|
||||
ViewCaptureAnalyzer.assertNoAnomalies(viewCaptureData)
|
||||
|
||||
var frameCount = 0
|
||||
for (i in 0 until viewCaptureData!!.windowDataCount) {
|
||||
frameCount += viewCaptureData!!.getWindowData(i).frameDataCount
|
||||
}
|
||||
assertTrue("Empty ViewCapture data", frameCount > 0)
|
||||
|
||||
val anomalies: Map<String, String> = ViewCaptureAnalyzer.getAnomalies(viewCaptureData)
|
||||
if (!anomalies.isEmpty()) {
|
||||
val diagFile = FailureWatcher.diagFile(description, "ViewAnomalies", "txt")
|
||||
try {
|
||||
OutputStreamWriter(BufferedOutputStream(FileOutputStream(diagFile))).use { writer ->
|
||||
writer.write("View animation anomalies detected.\r\n")
|
||||
writer.write(
|
||||
"To suppress an anomaly for a view, add its full path to the PATHS_TO_IGNORE list in the corresponding AnomalyDetector.\r\n"
|
||||
)
|
||||
writer.write("List of views with animation anomalies:\r\n")
|
||||
|
||||
for ((viewPath, message) in anomalies) {
|
||||
writer.write("View: $viewPath\r\n $message\r\n")
|
||||
}
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
throw RuntimeException(ex)
|
||||
}
|
||||
|
||||
val (viewPath, message) = anomalies.entries.first()
|
||||
fail(
|
||||
"${anomalies.size} view(s) had animation anomalies during the test, including view: $viewPath: $message\r\nSee ${diagFile.name} for details."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,24 +212,26 @@ final class AlphaJumpDetector extends AnomalyDetector {
|
||||
}
|
||||
|
||||
@Override
|
||||
void detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN) {
|
||||
String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN) {
|
||||
// If the view was previously seen, proceed with analysis only if it was present in the
|
||||
// view hierarchy in the previous frame.
|
||||
if (oldInfo != null && oldInfo.frameN != frameN) return;
|
||||
if (oldInfo != null && oldInfo.frameN != frameN) return null;
|
||||
|
||||
final AnalysisNode latestInfo = newInfo != null ? newInfo : oldInfo;
|
||||
if (getNodeData(latestInfo).ignoreAlphaJumps) return;
|
||||
final NodeData nodeData = getNodeData(latestInfo);
|
||||
if (nodeData.ignoreAlphaJumps) return null;
|
||||
|
||||
final float oldAlpha = oldInfo != null ? oldInfo.alpha : 0;
|
||||
final float newAlpha = newInfo != null ? newInfo.alpha : 0;
|
||||
final float alphaDeltaAbs = Math.abs(newAlpha - oldAlpha);
|
||||
|
||||
if (alphaDeltaAbs >= ALPHA_JUMP_THRESHOLD) {
|
||||
throw new AssertionError(
|
||||
String.format(
|
||||
"Alpha jump detected in ViewCapture data: alpha change: %s (%s -> %s)"
|
||||
+ ", threshold: %s, view: %s",
|
||||
alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo));
|
||||
nodeData.ignoreAlphaJumps = true; // No need to report alpha jump in children.
|
||||
return String.format(
|
||||
"Alpha jump detected in ViewCapture data: alpha change: %s (%s -> %s)"
|
||||
+ ", threshold: %s, %s", // ----------- no need to include view?
|
||||
alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+52
-21
@@ -61,8 +61,9 @@ public class ViewCaptureAnalyzer {
|
||||
* the view is not present in the 'currentFrame', but was present in earlier
|
||||
* frames.
|
||||
* @param frameN number of the current frame.
|
||||
* @return Anomaly diagnostic message if an anomaly has been detected; null otherwise.
|
||||
*/
|
||||
abstract void detectAnomalies(
|
||||
abstract String detectAnomalies(
|
||||
@Nullable AnalysisNode oldInfo, @Nullable AnalysisNode newInfo, int frameN);
|
||||
}
|
||||
|
||||
@@ -101,25 +102,31 @@ public class ViewCaptureAnalyzer {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("window coordinates: (%s, %s), class path from the root: %s",
|
||||
left, top, diagPathFromRoot(this));
|
||||
return String.format("view window coordinates: (%s, %s)", left, top);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans a view capture record and throws an error if an anomaly is found.
|
||||
* Scans a view capture record and searches for view animation anomalies. Can find anomalies for
|
||||
* multiple views.
|
||||
* Returns a map from the view path to the anomaly message for the view. Non-empty map means
|
||||
* that anomalies were detected.
|
||||
*/
|
||||
public static void assertNoAnomalies(ExportedData viewCaptureData) {
|
||||
public static Map<String, String> getAnomalies(ExportedData viewCaptureData) {
|
||||
final Map<String, String> anomalies = new HashMap<>();
|
||||
|
||||
final int scrimClassIndex = viewCaptureData.getClassnameList().indexOf(SCRIM_VIEW_CLASS);
|
||||
|
||||
final int windowDataCount = viewCaptureData.getWindowDataCount();
|
||||
for (int i = 0; i < windowDataCount; ++i) {
|
||||
analyzeWindowData(viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex);
|
||||
analyzeWindowData(
|
||||
viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex, anomalies);
|
||||
}
|
||||
return anomalies;
|
||||
}
|
||||
|
||||
private static void analyzeWindowData(ExportedData viewCaptureData, WindowData windowData,
|
||||
int scrimClassIndex) {
|
||||
int scrimClassIndex, Map<String, String> anomalies) {
|
||||
// View hash code => Last seen node with this hash code.
|
||||
// The view is added when we analyze the first frame where it's visible.
|
||||
// After that, it gets updated for every frame where it's visible.
|
||||
@@ -128,12 +135,13 @@ public class ViewCaptureAnalyzer {
|
||||
|
||||
for (int frameN = 0; frameN < windowData.getFrameDataCount(); ++frameN) {
|
||||
analyzeFrame(frameN, windowData.getFrameData(frameN), viewCaptureData, lastSeenNodes,
|
||||
scrimClassIndex);
|
||||
scrimClassIndex, anomalies);
|
||||
}
|
||||
}
|
||||
|
||||
private static void analyzeFrame(int frameN, FrameData frame, ExportedData viewCaptureData,
|
||||
Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex) {
|
||||
Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex,
|
||||
Map<String, String> anomalies) {
|
||||
// Analyze the node tree starting from the root.
|
||||
analyzeView(
|
||||
frame.getNode(),
|
||||
@@ -143,7 +151,8 @@ public class ViewCaptureAnalyzer {
|
||||
/* topShift = */ 0,
|
||||
viewCaptureData,
|
||||
lastSeenNodes,
|
||||
scrimClassIndex);
|
||||
scrimClassIndex,
|
||||
anomalies);
|
||||
|
||||
// Analyze transitions when a view visible in the last frame become invisible in the
|
||||
// current one.
|
||||
@@ -151,10 +160,14 @@ public class ViewCaptureAnalyzer {
|
||||
if (info.frameN == frameN - 1) {
|
||||
if (!info.viewCaptureNode.getWillNotDraw()) {
|
||||
Arrays.stream(ANOMALY_DETECTORS).forEach(
|
||||
detector -> detector.detectAnomalies(
|
||||
/* oldInfo = */ info,
|
||||
/* newInfo = */ null,
|
||||
frameN));
|
||||
detector ->
|
||||
detectAnomaly(
|
||||
detector,
|
||||
frameN,
|
||||
/* oldInfo = */ info,
|
||||
/* newInfo = */ null,
|
||||
anomalies)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +175,8 @@ public class ViewCaptureAnalyzer {
|
||||
|
||||
private static void analyzeView(ViewNode viewCaptureNode, AnalysisNode parent, int frameN,
|
||||
float leftShift, float topShift, ExportedData viewCaptureData,
|
||||
Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex) {
|
||||
Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex,
|
||||
Map<String, String> anomalies) {
|
||||
// Skip analysis of invisible views
|
||||
final float parentAlpha = parent != null ? parent.alpha : 1;
|
||||
final float alpha = getVisibleAlpha(viewCaptureNode, parentAlpha);
|
||||
@@ -205,7 +219,10 @@ public class ViewCaptureAnalyzer {
|
||||
final AnalysisNode oldAnalysisNode = lastSeenNodes.get(hashcode); // may be null
|
||||
if (frameN != 0 && !viewCaptureNode.getWillNotDraw()) {
|
||||
Arrays.stream(ANOMALY_DETECTORS).forEach(
|
||||
detector -> detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN));
|
||||
detector ->
|
||||
detectAnomaly(detector, frameN, oldAnalysisNode, newAnalysisNode,
|
||||
anomalies)
|
||||
);
|
||||
}
|
||||
lastSeenNodes.put(hashcode, newAnalysisNode);
|
||||
|
||||
@@ -220,8 +237,20 @@ public class ViewCaptureAnalyzer {
|
||||
if (child.getClassnameIndex() == scrimClassIndex) break;
|
||||
|
||||
analyzeView(child, newAnalysisNode, frameN, leftShiftForChildren, topShiftForChildren,
|
||||
viewCaptureData, lastSeenNodes,
|
||||
scrimClassIndex);
|
||||
viewCaptureData, lastSeenNodes, scrimClassIndex, anomalies);
|
||||
}
|
||||
}
|
||||
|
||||
private static void detectAnomaly(AnomalyDetector detector, int frameN,
|
||||
AnalysisNode oldAnalysisNode, AnalysisNode newAnalysisNode,
|
||||
Map<String, String> anomalies) {
|
||||
final String maybeAnomaly =
|
||||
detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN);
|
||||
if (maybeAnomaly != null) {
|
||||
final String viewDiagPath = diagPathFromRoot(newAnalysisNode);
|
||||
if (!anomalies.containsKey(viewDiagPath)) {
|
||||
anomalies.put(viewDiagPath, maybeAnomaly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,9 +264,11 @@ public class ViewCaptureAnalyzer {
|
||||
return className.substring(className.lastIndexOf(".") + 1);
|
||||
}
|
||||
|
||||
private static String diagPathFromRoot(AnalysisNode nodeBox) {
|
||||
final StringBuilder path = new StringBuilder(nodeBox.nodeIdentity);
|
||||
for (AnalysisNode ancestor = nodeBox.parent; ancestor != null; ancestor = ancestor.parent) {
|
||||
private static String diagPathFromRoot(AnalysisNode analysisNode) {
|
||||
final StringBuilder path = new StringBuilder(analysisNode.nodeIdentity);
|
||||
for (AnalysisNode ancestor = analysisNode.parent;
|
||||
ancestor != null;
|
||||
ancestor = ancestor.parent) {
|
||||
path.insert(0, ancestor.nodeIdentity + "|");
|
||||
}
|
||||
return path.toString();
|
||||
|
||||
Reference in New Issue
Block a user