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
This commit is contained in:
Vadim Tryshev
2023-07-28 15:03:08 -07:00
parent fd35baa3f8
commit 189f115e83
3 changed files with 95 additions and 34 deletions
@@ -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();