Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 189f115e authored by Vadim Tryshev's avatar Vadim Tryshev
Browse files

Detecting multiple view animation anomalies.

Now detecting all anomalies that were detected during the test. This helps to avoid rerunning the test multiple times and adding anomalies to the ignore-list one by one.

Generating a file with all detected anomalies and instructions how to suppress them.

Flag: N/A
Test: presubmit, local runs
Bug: 286251603
Change-Id: I0c34d228f91976451b518fd44873218b80178d0e
parent fd35baa3
Loading
Loading
Loading
Loading
+33 −5
Original line number Diff line number Diff line
@@ -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."
            )
        }
    }
}
+10 −8
Original line number Diff line number Diff line
@@ -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(
            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, view: %s",
                            alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo));
                            + ", threshold: %s, %s", // ----------- no need to include view?
                    alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo);
        }
        return null;
    }
}
+52 −21
Original line number Diff line number Diff line
@@ -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(
                            detector ->
                                    detectAnomaly(
                                            detector,
                                            frameN,
                                            /* oldInfo = */ info,
                                            /* newInfo = */ null,
                                    frameN));
                                            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();