1 package com.android.game.qualification.metric; 2 3 import com.android.annotations.Nullable; 4 import com.android.game.qualification.CertificationRequirements; 5 import com.android.tradefed.device.metric.DeviceMetricData; 6 import com.android.tradefed.invoker.IInvocationContext; 7 import com.android.tradefed.metrics.proto.MetricMeasurement; 8 import com.android.tradefed.metrics.proto.MetricMeasurement.DataType; 9 import com.android.tradefed.metrics.proto.MetricMeasurement.Directionality; 10 import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements; 11 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 12 13 import java.util.ArrayList; 14 import java.util.Collections; 15 import java.util.Comparator; 16 import java.util.HashMap; 17 import java.util.List; 18 import java.util.Locale; 19 import java.util.Objects; 20 21 /** 22 * Summary of frame time metrics for a single loop. 23 */ 24 public class LoopSummary { 25 private long count; 26 private long totalTimeNs; 27 private double jankRate; 28 private long minFrameTime; 29 private long maxFrameTime; 30 private double avgFrameTime; 31 private long percentile90; 32 private long percentile95; 33 private long percentile99; 34 private double targetPercentile; 35 parseRunMetrics( IInvocationContext context, MetricSummary.TimeType type, int runIndex, HashMap<String, Metric> runMetrics)36 public static LoopSummary parseRunMetrics( 37 IInvocationContext context, 38 MetricSummary.TimeType type, 39 int runIndex, 40 HashMap<String, Metric> runMetrics) { 41 return new LoopSummary( 42 getMetricLongValue(context, type, runIndex, "frame_count", runMetrics), 43 getMetricLongValue(context, type, runIndex, "duration", runMetrics), 44 getMetricDoubleValue(context, type, runIndex, "jank_rate", runMetrics), 45 getMetricLongValue(context, type, runIndex, "min_frametime", runMetrics), 46 getMetricLongValue(context, type, runIndex, "max_frametime", runMetrics), 47 getMetricDoubleValue(context, type, runIndex, "frametime", runMetrics), 48 getMetricLongValue(context, type, runIndex, "90th_percentile", runMetrics), 49 getMetricLongValue(context, type, runIndex, "95th_percentile", runMetrics), 50 getMetricLongValue(context, type, runIndex, "99th_percentile", runMetrics), 51 getMetricDoubleValue(context, type, runIndex, "target_percentile", runMetrics)); 52 } 53 addToMetricData(DeviceMetricData runData, int index, MetricSummary.TimeType type)54 void addToMetricData(DeviceMetricData runData, int index, MetricSummary.TimeType type) { 55 runData.addMetric( 56 getMetricKey(type, index, "frame_count"), 57 Metric.newBuilder() 58 .setType(MetricMeasurement.DataType.PROCESSED) 59 .setMeasurements( 60 MetricMeasurement.Measurements.newBuilder() 61 .setSingleInt(getCount()))); 62 runData.addMetric( 63 getMetricKey(type, index, "duration"), 64 getNsMetric(getDuration())); 65 runData.addMetric( 66 getMetricKey(type, index, "jank_rate"), 67 Metric.newBuilder() 68 .setType(DataType.PROCESSED) 69 .setMeasurements(Measurements.newBuilder().setSingleDouble(getJankRate()))); 70 runData.addMetric( 71 getMetricKey(type, index, "min_frametime"), 72 getNsMetric(getMinFrameTime())); 73 runData.addMetric( 74 getMetricKey(type, index, "max_frametime"), 75 getNsMetric(getMaxFrameTime())); 76 runData.addMetric( 77 getMetricKey(type, index, "frametime"), 78 getNsMetric(getAvgFrameTime())); 79 runData.addMetric( 80 getMetricKey(type, index, "90th_percentile"), 81 getNsMetric(get90thPercentile())); 82 runData.addMetric( 83 getMetricKey(type, index, "95th_percentile"), 84 getNsMetric(get95thPercentile())); 85 runData.addMetric( 86 getMetricKey(type, index, "99th_percentile"), 87 getNsMetric(get99thPercentile())); 88 runData.addMetric( 89 getMetricKey(type, index, "target_percentile"), 90 Metric.newBuilder() 91 .setType(DataType.PROCESSED) 92 .setMeasurements( 93 Measurements.newBuilder().setSingleDouble(getTargetPercentile()))); 94 } 95 LoopSummary( long count, long totalTimeNs, double jankRate, long minFrameTime, long maxFrameTime, double avgFrameTime, long percentile90, long percentile95, long percentile99, double targetPercentile)96 private LoopSummary( 97 long count, 98 long totalTimeNs, 99 double jankRate, 100 long minFrameTime, 101 long maxFrameTime, 102 double avgFrameTime, 103 long percentile90, 104 long percentile95, 105 long percentile99, 106 double targetPercentile) { 107 this.count = count; 108 this.totalTimeNs = totalTimeNs; 109 this.jankRate = jankRate; 110 this.minFrameTime = minFrameTime; 111 this.maxFrameTime = maxFrameTime; 112 this.avgFrameTime = avgFrameTime; 113 this.percentile90 = percentile90; 114 this.percentile95 = percentile95; 115 this.percentile99 = percentile99; 116 this.targetPercentile = targetPercentile; 117 } 118 getCount()119 public long getCount() { 120 return count; 121 } 122 getDuration()123 public long getDuration() { 124 return totalTimeNs; 125 } 126 getJankRate()127 public double getJankRate() { 128 return jankRate; 129 } 130 getMinFrameTime()131 public long getMinFrameTime() { 132 return minFrameTime; 133 } 134 getMaxFrameTime()135 public long getMaxFrameTime() { 136 return maxFrameTime; 137 } 138 getAvgFrameTime()139 public double getAvgFrameTime() { 140 return avgFrameTime; 141 } 142 getMinFPS()143 public double getMinFPS() { 144 return 1.0e9 / maxFrameTime; 145 } 146 getMaxFPS()147 public double getMaxFPS() { 148 return 1.0e9 / minFrameTime; 149 } 150 getAvgFPS()151 public double getAvgFPS() { 152 return 1.0e9 / avgFrameTime; 153 } 154 get90thPercentile()155 public long get90thPercentile() { 156 return percentile90; 157 } 158 get95thPercentile()159 public long get95thPercentile() { 160 return percentile95; 161 } 162 get99thPercentile()163 public long get99thPercentile() { 164 return percentile99; 165 } 166 getTargetPercentile()167 public double getTargetPercentile() { 168 return targetPercentile; 169 } 170 171 @Override equals(Object o)172 public boolean equals(Object o) { 173 if (this == o) return true; 174 if (o == null || getClass() != o.getClass()) return false; 175 LoopSummary that = (LoopSummary) o; 176 return count == that.count && 177 totalTimeNs == that.totalTimeNs && 178 Double.compare(that.jankRate, jankRate) == 0 && 179 minFrameTime == that.minFrameTime && 180 maxFrameTime == that.maxFrameTime && 181 Double.compare(that.avgFrameTime, avgFrameTime) == 0 && 182 percentile90 == that.percentile90 && 183 percentile95 == that.percentile95 && 184 percentile99 == that.percentile99 && 185 Double.compare(that.targetPercentile, targetPercentile) == 0; 186 } 187 188 @Override hashCode()189 public int hashCode() { 190 return Objects.hash( 191 count, 192 totalTimeNs, 193 jankRate, 194 minFrameTime, 195 maxFrameTime, 196 avgFrameTime, 197 percentile90, 198 percentile95, 199 percentile99, 200 targetPercentile); 201 } 202 toString()203 public String toString() { 204 return String.format( 205 "duration: %.3f ms\n" 206 + "Jank Rate: %7.3f/s\n" 207 + "avg Frame Time: %7.3f ms\t\tavg FPS = %.3f fps\n" 208 + "max Frame Time: %7.3f ms\n" 209 + "min Frame Time: %7.3f ms\n" 210 + "90th Percentile Frame Time: %7.3f ms\n" 211 + "95th Percentile Frame Time: %7.3f ms\n" 212 + "99th Percentile Frame Time: %7.3f ms\n" 213 + "Percentile below target: %7.3f\n", 214 nsToMs(getDuration()), 215 getJankRate(), 216 nsToMs(getAvgFrameTime()), getAvgFPS(), 217 nsToMs(getMaxFrameTime()), 218 nsToMs(getMinFrameTime()), 219 nsToMs(get90thPercentile()), 220 nsToMs(get95thPercentile()), 221 nsToMs(get99thPercentile()), 222 targetPercentile * 100); 223 } 224 225 static class Builder { 226 @Nullable 227 private final CertificationRequirements mRequirements; 228 private final long mVSyncPeriodNs; 229 private long totalTimeNs = 0; 230 private double jankScore; 231 private List<Long> frameTimes = new ArrayList<>(); 232 Builder(@ullable CertificationRequirements requirements, long VSyncPeriodNs)233 public Builder(@Nullable CertificationRequirements requirements, long VSyncPeriodNs) { 234 mRequirements = requirements; 235 mVSyncPeriodNs = VSyncPeriodNs; 236 } 237 build()238 public LoopSummary build() { 239 if (frameTimes.isEmpty()) { 240 return new LoopSummary(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); 241 } 242 frameTimes.sort(Comparator.naturalOrder()); 243 int size = frameTimes.size(); 244 long targetFrameTime = mRequirements == null ? 0 : msToNs(mRequirements.getFrameTime()); 245 246 // Find the percentage of frames below the target. 247 // Allow for small amount of slack because frame times have some variability. Frames 248 // that misses the target should be off by at least 1 VSYNC period, so frames that are 249 // just slightly above the target is still considered to be within target. 250 final long slack = (long)(0.1 * mVSyncPeriodNs); 251 int index = Collections.binarySearch(frameTimes, targetFrameTime + slack); 252 // binarySearch return a negative number if exact match is not found. 253 index = index < 0 ? -index - 1: index + 1; 254 255 return new LoopSummary( 256 frameTimes.size(), 257 totalTimeNs, 258 jankScore * 1000000000 / totalTimeNs, 259 frameTimes.get(0), 260 frameTimes.get(frameTimes.size() - 1), 261 (double)totalTimeNs / frameTimes.size(), 262 frameTimes.get((int)Math.ceil(size * 0.90) - 1), 263 frameTimes.get((int)Math.ceil(size * 0.95) - 1), 264 frameTimes.get((int)Math.ceil(size * 0.99) - 1), 265 (double)index / size); 266 } 267 268 public void addFrameTime(long frameTimeNs) { 269 if (mRequirements != null) { 270 long targetFrameTime = msToNs(mRequirements.getFrameTime()); 271 long roundedFrameTimeNs = 272 Math.round(frameTimeNs / (double)mVSyncPeriodNs) * mVSyncPeriodNs; 273 if (roundedFrameTimeNs > targetFrameTime) { 274 double score = (roundedFrameTimeNs - targetFrameTime) / targetFrameTime; 275 jankScore += score; 276 } 277 } 278 totalTimeNs = totalTimeNs + frameTimeNs; 279 frameTimes.add(frameTimeNs); 280 } 281 msToNs(float value)282 private static long msToNs(float value) { 283 return (long) (value * 1e6f); 284 } 285 } 286 getMetricDoubleValue( IInvocationContext context, MetricSummary.TimeType type, int runIndex, String metric, HashMap<String, Metric> runMetrics)287 private static double getMetricDoubleValue( 288 IInvocationContext context, 289 MetricSummary.TimeType type, 290 int runIndex, 291 String metric, 292 HashMap<String, Metric> runMetrics) { 293 Metric m = runMetrics.get( 294 getActualMetricKey(context, type, runIndex, metric)); 295 if (!m.hasMeasurements()) { 296 throw new RuntimeException(); 297 } 298 return m.getMeasurements().getSingleDouble(); 299 } 300 getMetricLongValue( IInvocationContext context, MetricSummary.TimeType type, int runIndex, String metric, HashMap<String, Metric> runMetrics)301 private static long getMetricLongValue( 302 IInvocationContext context, 303 MetricSummary.TimeType type, 304 int runIndex, 305 String metric, 306 HashMap<String, Metric> runMetrics) { 307 Metric m = runMetrics.get( 308 getActualMetricKey(context, type, runIndex, metric)); 309 if (!m.hasMeasurements()) { 310 throw new RuntimeException(); 311 } 312 return m.getMeasurements().getSingleInt(); 313 } 314 getNsMetric(long value)315 private static Metric.Builder getNsMetric(long value) { 316 return Metric.newBuilder() 317 .setUnit("ns") 318 .setDirection(MetricMeasurement.Directionality.DOWN_BETTER) 319 .setType(MetricMeasurement.DataType.PROCESSED) 320 .setMeasurements(MetricMeasurement.Measurements.newBuilder().setSingleInt(value)); 321 } 322 getNsMetric(double value)323 private static Metric.Builder getNsMetric(double value) { 324 return Metric.newBuilder() 325 .setUnit("ns") 326 .setDirection(Directionality.DOWN_BETTER) 327 .setType(DataType.PROCESSED) 328 .setMeasurements(Measurements.newBuilder().setSingleDouble(value)); 329 } 330 getActualMetricKey( IInvocationContext context, MetricSummary.TimeType type, int loopIndex, String label)331 private static String getActualMetricKey( 332 IInvocationContext context, MetricSummary.TimeType type, int loopIndex, String label) { 333 // DeviceMetricData automatically add the deviceName to the metric key if there are more 334 // than one devices. We don't really want or care about the device in the metric data, but 335 // we need to get the actual key that was added in order to parse it correctly. 336 if (context.getDevices().size() > 1) { 337 String deviceName = context.getDeviceName(context.getDevices().get(0)); 338 return String.format("{%s}:%s", deviceName, getMetricKey(type, loopIndex, label)); 339 } 340 return getMetricKey(type, loopIndex, label); 341 } 342 getMetricKey(MetricSummary.TimeType type, int loopIndex, String label)343 private static String getMetricKey(MetricSummary.TimeType type, int loopIndex, String label) { 344 return "run_" + loopIndex + "." + type.name().toLowerCase(Locale.US) + "_" + label; 345 } 346 nsToMs(long value)347 private static double nsToMs(long value) { 348 return value / 1e6; 349 } 350 nsToMs(double value)351 private static double nsToMs(double value) { 352 return value / 1e6; 353 } 354 } 355