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

Commit 1e6daed8 authored by Vadim Tryshev's avatar Vadim Tryshev Committed by Android (Google) Code Review
Browse files

Merge "Detecting multiple view animation anomalies." into udc-qpr-dev

parents baf96f74 189f115e
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();