1 /* 2 * Copyright 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.cts.util; 17 18 import android.content.Context; 19 import android.content.res.AssetFileDescriptor; 20 import android.media.MediaCodec; 21 import android.media.MediaCodecInfo; 22 import android.media.MediaCodecInfo.CodecCapabilities; 23 import android.media.MediaCodecInfo.VideoCapabilities; 24 import android.media.MediaCodecList; 25 import android.media.MediaExtractor; 26 import android.media.MediaFormat; 27 import android.net.Uri; 28 import android.util.Log; 29 import android.util.Range; 30 31 import com.android.compatibility.common.util.DeviceReportLog; 32 import com.android.compatibility.common.util.ResultType; 33 import com.android.compatibility.common.util.ResultUnit; 34 35 import java.lang.reflect.Method; 36 import static java.lang.reflect.Modifier.isPublic; 37 import static java.lang.reflect.Modifier.isStatic; 38 import java.util.ArrayList; 39 import java.util.Arrays; 40 import java.util.Map; 41 42 import static junit.framework.Assert.assertTrue; 43 44 import java.io.IOException; 45 46 public class MediaUtils { 47 private static final String TAG = "MediaUtils"; 48 49 /* 50 * ----------------------- HELPER METHODS FOR SKIPPING TESTS ----------------------- 51 */ 52 private static final int ALL_AV_TRACKS = -1; 53 54 private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS); 55 56 /** 57 * Returns the test name (heuristically). 58 * 59 * Since it uses heuristics, this method has only been verified for media 60 * tests. This centralizes the way to signal errors during a test. 61 */ 62 public static String getTestName() { 63 return getTestName(false /* withClass */); 64 } 65 66 /** 67 * Returns the test name with the full class (heuristically). 68 * 69 * Since it uses heuristics, this method has only been verified for media 70 * tests. This centralizes the way to signal errors during a test. 71 */ 72 public static String getTestNameWithClass() { 73 return getTestName(true /* withClass */); 74 } 75 76 private static String getTestName(boolean withClass) { 77 int bestScore = -1; 78 String testName = "test???"; 79 Map<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces(); 80 for (Map.Entry<Thread, StackTraceElement[]> entry : traces.entrySet()) { 81 StackTraceElement[] stack = entry.getValue(); 82 for (int index = 0; index < stack.length; ++index) { 83 // method name must start with "test" 84 String methodName = stack[index].getMethodName(); 85 if (!methodName.startsWith("test")) { 86 continue; 87 } 88 89 int score = 0; 90 // see if there is a public non-static void method that takes no argument 91 Class<?> clazz; 92 try { 93 clazz = Class.forName(stack[index].getClassName()); 94 ++score; 95 for (final Method method : clazz.getDeclaredMethods()) { 96 if (method.getName().equals(methodName) 97 && isPublic(method.getModifiers()) 98 && !isStatic(method.getModifiers()) 99 && method.getParameterTypes().length == 0 100 && method.getReturnType().equals(Void.TYPE)) { 101 ++score; 102 break; 103 } 104 } 105 if (score == 1) { 106 // if we could read the class, but method is not public void, it is 107 // not a candidate 108 continue; 109 } 110 } catch (ClassNotFoundException e) { 111 } 112 113 // even if we cannot verify the method signature, there are signals in the stack 114 115 // usually test method is invoked by reflection 116 int depth = 1; 117 while (index + depth < stack.length 118 && stack[index + depth].getMethodName().equals("invoke") 119 && stack[index + depth].getClassName().equals( 120 "java.lang.reflect.Method")) { 121 ++depth; 122 } 123 if (depth > 1) { 124 ++score; 125 // and usually test method is run by runMethod method in android.test package 126 if (index + depth < stack.length) { 127 if (stack[index + depth].getClassName().startsWith("android.test.")) { 128 ++score; 129 } 130 if (stack[index + depth].getMethodName().equals("runMethod")) { 131 ++score; 132 } 133 } 134 } 135 136 if (score > bestScore) { 137 bestScore = score; 138 testName = methodName; 139 if (withClass) { 140 testName = stack[index].getClassName() + "." + testName; 141 } 142 } 143 } 144 } 145 return testName; 146 } 147 148 /** 149 * Finds test name (heuristically) and prints out standard skip message. 150 * 151 * Since it uses heuristics, this method has only been verified for media 152 * tests. This centralizes the way to signal a skipped test. 153 */ 154 public static void skipTest(String tag, String reason) { 155 Log.i(tag, "SKIPPING " + getTestName() + "(): " + reason); 156 DeviceReportLog log = new DeviceReportLog("CtsMediaSkippedTests", "test_skipped"); 157 log.addValue("reason", reason, ResultType.NEUTRAL, ResultUnit.NONE); 158 log.addValue( 159 "test", getTestNameWithClass(), ResultType.NEUTRAL, ResultUnit.NONE); 160 // TODO: replace with submit() when it is added to DeviceReportLog 161 try { 162 log.submit(null); 163 } catch (NullPointerException e) { } 164 } 165 166 /** 167 * Finds test name (heuristically) and prints out standard skip message. 168 * 169 * Since it uses heuristics, this method has only been verified for media 170 * tests. This centralizes the way to signal a skipped test. 171 */ 172 public static void skipTest(String reason) { 173 skipTest(TAG, reason); 174 } 175 176 public static boolean check(boolean result, String message) { 177 if (!result) { 178 skipTest(message); 179 } 180 return result; 181 } 182 183 /* 184 * ------------------- HELPER METHODS FOR CHECKING CODEC SUPPORT ------------------- 185 */ 186 187 // returns the list of codecs that support any one of the formats 188 private static String[] getCodecNames( 189 boolean isEncoder, Boolean isGoog, MediaFormat... formats) { 190 MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS); 191 ArrayList<String> result = new ArrayList<>(); 192 for (MediaCodecInfo info : mcl.getCodecInfos()) { 193 if (info.isEncoder() != isEncoder) { 194 continue; 195 } 196 if (isGoog != null 197 && info.getName().toLowerCase().startsWith("omx.google.") != isGoog) { 198 continue; 199 } 200 201 for (MediaFormat format : formats) { 202 String mime = format.getString(MediaFormat.KEY_MIME); 203 204 CodecCapabilities caps = null; 205 try { 206 caps = info.getCapabilitiesForType(mime); 207 } catch (IllegalArgumentException e) { // mime is not supported 208 continue; 209 } 210 if (caps.isFormatSupported(format)) { 211 result.add(info.getName()); 212 break; 213 } 214 } 215 } 216 return result.toArray(new String[result.size()]); 217 } 218 219 /* Use isGoog = null to query all decoders */ 220 public static String[] getDecoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) { 221 return getCodecNames(false /* isEncoder */, isGoog, formats); 222 } 223 224 public static String[] getDecoderNames(MediaFormat... formats) { 225 return getCodecNames(false /* isEncoder */, null /* isGoog */, formats); 226 } 227 228 /* Use isGoog = null to query all decoders */ 229 public static String[] getEncoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) { 230 return getCodecNames(true /* isEncoder */, isGoog, formats); 231 } 232 233 public static String[] getEncoderNames(MediaFormat... formats) { 234 return getCodecNames(true /* isEncoder */, null /* isGoog */, formats); 235 } 236 237 public static void verifyNumCodecs( 238 int count, boolean isEncoder, Boolean isGoog, MediaFormat... formats) { 239 String desc = (isEncoder ? "encoders" : "decoders") + " for " 240 + (formats.length == 1 ? formats[0].toString() : Arrays.toString(formats)); 241 if (isGoog != null) { 242 desc = (isGoog ? "Google " : "non-Google ") + desc; 243 } 244 245 String[] codecs = getCodecNames(isEncoder, isGoog, formats); 246 assertTrue("test can only verify " + count + " " + desc + "; found " + codecs.length + ": " 247 + Arrays.toString(codecs), codecs.length <= count); 248 } 249 250 public static MediaCodec getDecoder(MediaFormat format) { 251 String decoder = sMCL.findDecoderForFormat(format); 252 if (decoder != null) { 253 try { 254 return MediaCodec.createByCodecName(decoder); 255 } catch (IOException e) { 256 } 257 } 258 return null; 259 } 260 261 public static boolean canEncode(MediaFormat format) { 262 if (sMCL.findEncoderForFormat(format) == null) { 263 Log.i(TAG, "no encoder for " + format); 264 return false; 265 } 266 return true; 267 } 268 269 public static boolean canDecode(MediaFormat format) { 270 if (sMCL.findDecoderForFormat(format) == null) { 271 Log.i(TAG, "no decoder for " + format); 272 return false; 273 } 274 return true; 275 } 276 277 public static boolean supports(String codecName, String mime, int w, int h) { 278 // While this could be simply written as such, give more graceful feedback. 279 // MediaFormat format = MediaFormat.createVideoFormat(mime, w, h); 280 // return supports(codecName, format); 281 282 VideoCapabilities vidCap = getVideoCapabilities(codecName, mime); 283 if (vidCap == null) { 284 return false; 285 } else if (vidCap.isSizeSupported(w, h)) { 286 return true; 287 } 288 289 Log.w(TAG, "unsupported size " + w + "x" + h); 290 return false; 291 } 292 293 public static boolean supports(String codecName, MediaFormat format) { 294 MediaCodec codec; 295 try { 296 codec = MediaCodec.createByCodecName(codecName); 297 } catch (IOException e) { 298 Log.w(TAG, "codec not found: " + codecName); 299 return false; 300 } 301 302 String mime = format.getString(MediaFormat.KEY_MIME); 303 CodecCapabilities cap = null; 304 try { 305 cap = codec.getCodecInfo().getCapabilitiesForType(mime); 306 } catch (IllegalArgumentException e) { 307 Log.w(TAG, "not supported mime: " + mime); 308 codec.release(); 309 return false; 310 } 311 312 return cap.isFormatSupported(format); 313 } 314 315 public static boolean hasCodecForTrack(MediaExtractor ex, int track) { 316 int count = ex.getTrackCount(); 317 if (track < 0 || track >= count) { 318 throw new IndexOutOfBoundsException(track + " not in [0.." + (count - 1) + "]"); 319 } 320 return canDecode(ex.getTrackFormat(track)); 321 } 322 323 /** 324 * return true iff all audio and video tracks are supported 325 */ 326 public static boolean hasCodecsForMedia(MediaExtractor ex) { 327 for (int i = 0; i < ex.getTrackCount(); ++i) { 328 MediaFormat format = ex.getTrackFormat(i); 329 // only check for audio and video codecs 330 String mime = format.getString(MediaFormat.KEY_MIME).toLowerCase(); 331 if (!mime.startsWith("audio/") && !mime.startsWith("video/")) { 332 continue; 333 } 334 if (!canDecode(format)) { 335 return false; 336 } 337 } 338 return true; 339 } 340 341 /** 342 * return true iff any track starting with mimePrefix is supported 343 */ 344 public static boolean hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix) { 345 mimePrefix = mimePrefix.toLowerCase(); 346 for (int i = 0; i < ex.getTrackCount(); ++i) { 347 MediaFormat format = ex.getTrackFormat(i); 348 String mime = format.getString(MediaFormat.KEY_MIME); 349 if (mime.toLowerCase().startsWith(mimePrefix)) { 350 if (canDecode(format)) { 351 return true; 352 } 353 Log.i(TAG, "no decoder for " + format); 354 } 355 } 356 return false; 357 } 358 359 private static boolean hasCodecsForResourceCombo( 360 Context context, int resourceId, int track, String mimePrefix) { 361 try { 362 AssetFileDescriptor afd = null; 363 MediaExtractor ex = null; 364 try { 365 afd = context.getResources().openRawResourceFd(resourceId); 366 ex = new MediaExtractor(); 367 ex.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); 368 if (mimePrefix != null) { 369 return hasCodecForMediaAndDomain(ex, mimePrefix); 370 } else if (track == ALL_AV_TRACKS) { 371 return hasCodecsForMedia(ex); 372 } else { 373 return hasCodecForTrack(ex, track); 374 } 375 } finally { 376 if (ex != null) { 377 ex.release(); 378 } 379 if (afd != null) { 380 afd.close(); 381 } 382 } 383 } catch (IOException e) { 384 Log.i(TAG, "could not open resource"); 385 } 386 return false; 387 } 388 389 /** 390 * return true iff all audio and video tracks are supported 391 */ 392 public static boolean hasCodecsForResource(Context context, int resourceId) { 393 return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, null /* mimePrefix */); 394 } 395 396 public static boolean checkCodecsForResource(Context context, int resourceId) { 397 return check(hasCodecsForResource(context, resourceId), "no decoder found"); 398 } 399 400 /** 401 * return true iff track is supported. 402 */ 403 public static boolean hasCodecForResource(Context context, int resourceId, int track) { 404 return hasCodecsForResourceCombo(context, resourceId, track, null /* mimePrefix */); 405 } 406 407 public static boolean checkCodecForResource(Context context, int resourceId, int track) { 408 return check(hasCodecForResource(context, resourceId, track), "no decoder found"); 409 } 410 411 /** 412 * return true iff any track starting with mimePrefix is supported 413 */ 414 public static boolean hasCodecForResourceAndDomain( 415 Context context, int resourceId, String mimePrefix) { 416 return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, mimePrefix); 417 } 418 419 /** 420 * return true iff all audio and video tracks are supported 421 */ 422 public static boolean hasCodecsForPath(Context context, String path) { 423 MediaExtractor ex = null; 424 try { 425 ex = new MediaExtractor(); 426 Uri uri = Uri.parse(path); 427 String scheme = uri.getScheme(); 428 if (scheme == null) { // file 429 ex.setDataSource(path); 430 } else if (scheme.equalsIgnoreCase("file")) { 431 ex.setDataSource(uri.getPath()); 432 } else { 433 ex.setDataSource(context, uri, null); 434 } 435 return hasCodecsForMedia(ex); 436 } catch (IOException e) { 437 Log.i(TAG, "could not open path " + path); 438 } finally { 439 if (ex != null) { 440 ex.release(); 441 } 442 } 443 return true; 444 } 445 446 public static boolean checkCodecsForPath(Context context, String path) { 447 return check(hasCodecsForPath(context, path), "no decoder found"); 448 } 449 450 public static boolean hasCodecForDomain(boolean encoder, String domain) { 451 for (MediaCodecInfo info : sMCL.getCodecInfos()) { 452 if (encoder != info.isEncoder()) { 453 continue; 454 } 455 456 for (String type : info.getSupportedTypes()) { 457 if (type.toLowerCase().startsWith(domain.toLowerCase() + "/")) { 458 Log.i(TAG, "found codec " + info.getName() + " for mime " + type); 459 return true; 460 } 461 } 462 } 463 return false; 464 } 465 466 public static boolean checkCodecForDomain(boolean encoder, String domain) { 467 return check(hasCodecForDomain(encoder, domain), 468 "no " + domain + (encoder ? " encoder" : " decoder") + " found"); 469 } 470 471 private static boolean hasCodecForMime(boolean encoder, String mime) { 472 for (MediaCodecInfo info : sMCL.getCodecInfos()) { 473 if (encoder != info.isEncoder()) { 474 continue; 475 } 476 477 for (String type : info.getSupportedTypes()) { 478 if (type.equalsIgnoreCase(mime)) { 479 Log.i(TAG, "found codec " + info.getName() + " for mime " + mime); 480 return true; 481 } 482 } 483 } 484 return false; 485 } 486 487 private static boolean hasCodecForMimes(boolean encoder, String[] mimes) { 488 for (String mime : mimes) { 489 if (!hasCodecForMime(encoder, mime)) { 490 Log.i(TAG, "no " + (encoder ? "encoder" : "decoder") + " for mime " + mime); 491 return false; 492 } 493 } 494 return true; 495 } 496 497 498 public static boolean hasEncoder(String... mimes) { 499 return hasCodecForMimes(true /* encoder */, mimes); 500 } 501 502 public static boolean hasDecoder(String... mimes) { 503 return hasCodecForMimes(false /* encoder */, mimes); 504 } 505 506 public static boolean checkDecoder(String... mimes) { 507 return check(hasCodecForMimes(false /* encoder */, mimes), "no decoder found"); 508 } 509 510 public static boolean checkEncoder(String... mimes) { 511 return check(hasCodecForMimes(true /* encoder */, mimes), "no encoder found"); 512 } 513 514 public static boolean canDecodeVideo(String mime, int width, int height, float rate) { 515 MediaFormat format = MediaFormat.createVideoFormat(mime, width, height); 516 format.setFloat(MediaFormat.KEY_FRAME_RATE, rate); 517 return canDecode(format); 518 } 519 520 public static boolean canDecodeVideo( 521 String mime, int width, int height, float rate, 522 Integer profile, Integer level, Integer bitrate) { 523 MediaFormat format = MediaFormat.createVideoFormat(mime, width, height); 524 format.setFloat(MediaFormat.KEY_FRAME_RATE, rate); 525 if (profile != null) { 526 format.setInteger(MediaFormat.KEY_PROFILE, profile); 527 if (level != null) { 528 format.setInteger(MediaFormat.KEY_LEVEL, level); 529 } 530 } 531 if (bitrate != null) { 532 format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); 533 } 534 return canDecode(format); 535 } 536 537 public static boolean checkEncoderForFormat(MediaFormat format) { 538 return check(canEncode(format), "no encoder for " + format); 539 } 540 541 public static boolean checkDecoderForFormat(MediaFormat format) { 542 return check(canDecode(format), "no decoder for " + format); 543 } 544 545 /* 546 * ----------------------- HELPER METHODS FOR MEDIA HANDLING ----------------------- 547 */ 548 549 public static VideoCapabilities getVideoCapabilities(String codecName, String mime) { 550 for (MediaCodecInfo info : sMCL.getCodecInfos()) { 551 if (!info.getName().equalsIgnoreCase(codecName)) { 552 continue; 553 } 554 CodecCapabilities caps; 555 try { 556 caps = info.getCapabilitiesForType(mime); 557 } catch (IllegalArgumentException e) { 558 // mime is not supported 559 Log.w(TAG, "not supported mime: " + mime); 560 return null; 561 } 562 VideoCapabilities vidCaps = caps.getVideoCapabilities(); 563 if (vidCaps == null) { 564 Log.w(TAG, "not a video codec: " + codecName); 565 } 566 return vidCaps; 567 } 568 Log.w(TAG, "codec not found: " + codecName); 569 return null; 570 } 571 572 public static MediaFormat getTrackFormatForResource( 573 Context context, int resourceId, String mimeTypePrefix) 574 throws IOException { 575 MediaFormat format = null; 576 MediaExtractor extractor = new MediaExtractor(); 577 AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId); 578 try { 579 extractor.setDataSource( 580 afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); 581 } finally { 582 afd.close(); 583 } 584 int trackIndex; 585 for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) { 586 MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex); 587 if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) { 588 format = trackMediaFormat; 589 break; 590 } 591 } 592 extractor.release(); 593 afd.close(); 594 if (format == null) { 595 throw new RuntimeException("couldn't get a track for " + mimeTypePrefix); 596 } 597 598 return format; 599 } 600 601 public static MediaExtractor createMediaExtractorForMimeType( 602 Context context, int resourceId, String mimeTypePrefix) 603 throws IOException { 604 MediaExtractor extractor = new MediaExtractor(); 605 AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId); 606 try { 607 extractor.setDataSource( 608 afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); 609 } finally { 610 afd.close(); 611 } 612 int trackIndex; 613 for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) { 614 MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex); 615 if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) { 616 extractor.selectTrack(trackIndex); 617 break; 618 } 619 } 620 if (trackIndex == extractor.getTrackCount()) { 621 extractor.release(); 622 throw new IllegalStateException("couldn't get a track for " + mimeTypePrefix); 623 } 624 625 return extractor; 626 } 627 628 /* 629 * ---------------------- HELPER METHODS FOR CODEC CONFIGURATION 630 */ 631 632 /** Format must contain mime, width and height. 633 * Throws Exception if encoder does not support this width and height */ 634 public static void setMaxEncoderFrameAndBitrates( 635 MediaCodec encoder, MediaFormat format, int maxFps) { 636 String mime = format.getString(MediaFormat.KEY_MIME); 637 638 VideoCapabilities vidCaps = 639 encoder.getCodecInfo().getCapabilitiesForType(mime).getVideoCapabilities(); 640 setMaxEncoderFrameAndBitrates(vidCaps, format, maxFps); 641 } 642 643 public static void setMaxEncoderFrameAndBitrates( 644 VideoCapabilities vidCaps, MediaFormat format, int maxFps) { 645 int width = format.getInteger(MediaFormat.KEY_WIDTH); 646 int height = format.getInteger(MediaFormat.KEY_HEIGHT); 647 648 int maxWidth = vidCaps.getSupportedWidths().getUpper(); 649 int maxHeight = vidCaps.getSupportedHeightsFor(maxWidth).getUpper(); 650 int frameRate = Math.min( 651 maxFps, vidCaps.getSupportedFrameRatesFor(width, height).getUpper().intValue()); 652 format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); 653 654 int bitrate = vidCaps.getBitrateRange().clamp( 655 (int)(vidCaps.getBitrateRange().getUpper() / 656 Math.sqrt((double)maxWidth * maxHeight / width / height))); 657 format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); 658 } 659 660 /* 661 * ------------------ HELPER METHODS FOR STATISTICS AND REPORTING ------------------ 662 */ 663 664 // TODO: migrate this into com.android.compatibility.common.util.Stat 665 public static class Stats { 666 /** does not support NaN or Inf in |data| */ 667 public Stats(double[] data) { 668 mData = data; 669 if (mData != null) { 670 mNum = mData.length; 671 } 672 } 673 674 public int getNum() { 675 return mNum; 676 } 677 678 /** calculate mSumX and mSumXX */ 679 private void analyze() { 680 if (mAnalyzed) { 681 return; 682 } 683 684 if (mData != null) { 685 for (double x : mData) { 686 if (!(x >= mMinX)) { // mMinX may be NaN 687 mMinX = x; 688 } 689 if (!(x <= mMaxX)) { // mMaxX may be NaN 690 mMaxX = x; 691 } 692 mSumX += x; 693 mSumXX += x * x; 694 } 695 } 696 mAnalyzed = true; 697 } 698 699 /** returns the maximum or NaN if it does not exist */ 700 public double getMin() { 701 analyze(); 702 return mMinX; 703 } 704 705 /** returns the minimum or NaN if it does not exist */ 706 public double getMax() { 707 analyze(); 708 return mMaxX; 709 } 710 711 /** returns the average or NaN if it does not exist. */ 712 public double getAverage() { 713 analyze(); 714 if (mNum == 0) { 715 return Double.NaN; 716 } else { 717 return mSumX / mNum; 718 } 719 } 720 721 /** returns the standard deviation or NaN if it does not exist. */ 722 public double getStdev() { 723 analyze(); 724 if (mNum == 0) { 725 return Double.NaN; 726 } else { 727 double average = mSumX / mNum; 728 return Math.sqrt(mSumXX / mNum - average * average); 729 } 730 } 731 732 /** returns the statistics for the moving average over n values */ 733 public Stats movingAverage(int n) { 734 if (n < 1 || mNum < n) { 735 return new Stats(null); 736 } else if (n == 1) { 737 return this; 738 } 739 740 double[] avgs = new double[mNum - n + 1]; 741 double sum = 0; 742 for (int i = 0; i < mNum; ++i) { 743 sum += mData[i]; 744 if (i >= n - 1) { 745 avgs[i - n + 1] = sum / n; 746 sum -= mData[i - n + 1]; 747 } 748 } 749 return new Stats(avgs); 750 } 751 752 /** returns the statistics for the moving average over a window over the 753 * cumulative sum. Basically, moves a window from: [0, window] to 754 * [sum - window, sum] over the cumulative sum, over ((sum - window) / average) 755 * steps, and returns the average value over each window. 756 * This method is used to average time-diff data over a window of a constant time. 757 */ 758 public Stats movingAverageOverSum(double window) { 759 if (window <= 0 || mNum < 1) { 760 return new Stats(null); 761 } 762 763 analyze(); 764 double average = mSumX / mNum; 765 if (window >= mSumX) { 766 return new Stats(new double[] { average }); 767 } 768 int samples = (int)Math.ceil((mSumX - window) / average); 769 double[] avgs = new double[samples]; 770 771 // A somewhat brute force approach to calculating the moving average. 772 // TODO: add support for weights in Stats, so we can do a more refined approach. 773 double sum = 0; // sum of elements in the window 774 int num = 0; // number of elements in the moving window 775 int bi = 0; // index of the first element in the moving window 776 int ei = 0; // index of the last element in the moving window 777 double space = window; // space at the end of the window 778 double foot = 0; // space at the beginning of the window 779 780 // invariants: foot + sum + space == window 781 // bi + num == ei 782 // 783 // window: |-------------------------------| 784 // | <-----sum------> | 785 // <foot> <---space--> 786 // | | 787 // intervals: |-----------|-------|-------|--------------------|--------| 788 // ^bi ^ei 789 790 int ix = 0; // index in the result 791 while (ix < samples) { 792 // add intervals while there is space in the window 793 while (ei < mData.length && mData[ei] <= space) { 794 space -= mData[ei]; 795 sum += mData[ei]; 796 num++; 797 ei++; 798 } 799 800 // calculate average over window and deal with odds and ends (e.g. if there are no 801 // intervals in the current window: pick whichever element overlaps the window 802 // most. 803 if (num > 0) { 804 avgs[ix++] = sum / num; 805 } else if (bi > 0 && foot > space) { 806 // consider previous 807 avgs[ix++] = mData[bi - 1]; 808 } else if (ei == mData.length) { 809 break; 810 } else { 811 avgs[ix++] = mData[ei]; 812 } 813 814 // move the window to the next position 815 foot -= average; 816 space += average; 817 818 // remove intervals that are now partially or wholly outside of the window 819 while (bi < ei && foot < 0) { 820 foot += mData[bi]; 821 sum -= mData[bi]; 822 num--; 823 bi++; 824 } 825 } 826 return new Stats(Arrays.copyOf(avgs, ix)); 827 } 828 829 /** calculate mSortedData */ 830 private void sort() { 831 if (mSorted || mNum == 0) { 832 return; 833 } 834 mSortedData = Arrays.copyOf(mData, mNum); 835 Arrays.sort(mSortedData); 836 mSorted = true; 837 } 838 839 /** returns an array of percentiles for the points using nearest rank */ 840 public double[] getPercentiles(double... points) { 841 sort(); 842 double[] res = new double[points.length]; 843 for (int i = 0; i < points.length; ++i) { 844 if (mNum < 1 || points[i] < 0 || points[i] > 100) { 845 res[i] = Double.NaN; 846 } else { 847 res[i] = mSortedData[(int)Math.round(points[i] / 100 * (mNum - 1))]; 848 } 849 } 850 return res; 851 } 852 853 @Override 854 public boolean equals(Object o) { 855 if (o instanceof Stats) { 856 Stats other = (Stats)o; 857 if (other.mNum != mNum) { 858 return false; 859 } else if (mNum == 0) { 860 return true; 861 } 862 return Arrays.equals(mData, other.mData); 863 } 864 return false; 865 } 866 867 private double[] mData; 868 private double mSumX = 0; 869 private double mSumXX = 0; 870 private double mMinX = Double.NaN; 871 private double mMaxX = Double.NaN; 872 private int mNum = 0; 873 private boolean mAnalyzed = false; 874 private double[] mSortedData; 875 private boolean mSorted = false; 876 } 877 878 /* 879 * -------------------------------------- END -------------------------------------- 880 */ 881 } 882