Add histogram metrics for video playback freezes
Bug: 234833109
Test: atest VideoRenderQualityTracker_test
Change-Id: I5e3a8b02336bbcbe639aad37f0914ba0f98d8d44
diff --git a/media/libmediametrics/MediaMetrics.cpp b/media/libmediametrics/MediaMetrics.cpp
index a3c2f1a..2240223 100644
--- a/media/libmediametrics/MediaMetrics.cpp
+++ b/media/libmediametrics/MediaMetrics.cpp
@@ -86,6 +86,11 @@
if (item != NULL) item->setRate(attr, count, duration);
}
+void mediametrics_setString(mediametrics_handle_t handle, attr_t attr,
+ const std::string &string) {
+ mediametrics_setCString(handle, attr, string.c_str());
+}
+
void mediametrics_setCString(mediametrics_handle_t handle, attr_t attr,
const char *value) {
Item *item = (Item *) handle;
@@ -152,6 +157,14 @@
return item->getRate(attr, count, duration, rate);
}
+bool mediametrics_getString(mediametrics_handle_t handle, attr_t attr,
+ std::string *string) {
+ Item *item = (Item *) handle;
+ if (item == NULL) return false;
+
+ return item->getString(attr, string);
+}
+
// NB: caller owns the string that comes back, is responsible for freeing it
bool mediametrics_getCString(mediametrics_handle_t handle, attr_t attr,
char **value) {
diff --git a/media/libmediametrics/include/media/MediaMetrics.h b/media/libmediametrics/include/media/MediaMetrics.h
index 76abe86..58612a3 100644
--- a/media/libmediametrics/include/media/MediaMetrics.h
+++ b/media/libmediametrics/include/media/MediaMetrics.h
@@ -50,7 +50,7 @@
void mediametrics_setRate(mediametrics_handle_t handle, attr_t attr,
int64_t count, int64_t duration);
void mediametrics_setCString(mediametrics_handle_t handle, attr_t attr,
- const char * value);
+ const char * value);
// fused get/add/set; if attr wasn't there, it's a simple set.
// these do not provide atomicity or mutual exclusion, only simpler code sequences.
@@ -95,4 +95,11 @@
__END_DECLS
+#ifdef __cplusplus
+#include <string>
+void mediametrics_setString(mediametrics_handle_t handle, attr_t attr,
+ const std::string &value);
+bool mediametrics_getString(mediametrics_handle_t handle, attr_t attr, std::string *value);
+#endif // __cplusplus
+
#endif
diff --git a/media/libmediametrics/include/media/MediaMetricsItem.h b/media/libmediametrics/include/media/MediaMetricsItem.h
index de56665..03834d4 100644
--- a/media/libmediametrics/include/media/MediaMetricsItem.h
+++ b/media/libmediametrics/include/media/MediaMetricsItem.h
@@ -1048,6 +1048,9 @@
}
return true;
}
+ bool getString(const char *key, std::string *value) const {
+ return get(key, value);
+ }
// Caller owns the returned string
bool getCString(const char *key, char **value) const {
std::string s;
@@ -1057,9 +1060,6 @@
}
return false;
}
- bool getString(const char *key, std::string *value) const {
- return get(key, value);
- }
const Prop::Elem* get(const char *key) const {
const Prop *prop = findProp(key);
diff --git a/media/libstagefright/MediaCodec.cpp b/media/libstagefright/MediaCodec.cpp
index 435f547..939f6d6 100644
--- a/media/libstagefright/MediaCodec.cpp
+++ b/media/libstagefright/MediaCodec.cpp
@@ -206,6 +206,15 @@
static const char *kCodecFramerateDesired = "android.media.mediacodec.framerate.desired";
static const char *kCodecFramerateActual = "android.media.mediacodec.framerate.actual";
+static const char *kCodecFreezeCount = "android.media.mediacodec.freeze.count";
+static const char *kCodecFreezeDurationAverage = "android.media.mediacodec.freeze.duration.average";
+static const char *kCodecFreezeDurationMax = "android.media.mediacodec.freeze.duration.max";
+static const char *kCodecFreezeDurationHistogram =
+ "android.media.mediacodec.freeze.duration.histogram";
+static const char *kCodecFreezeDistanceAverage = "android.media.mediacodec.freeze.distance.average";
+static const char *kCodecFreezeDistanceHistogram =
+ "android.media.mediacodec.freeze.distance.histogram";
+
/* -1: shaper disabled
>=0: number of fields changed */
static const char *kCodecShapingEnhanced = "android.media.mediacodec.shaped";
@@ -1114,6 +1123,18 @@
mediametrics_setDouble(mMetricsHandle, kCodecFramerateDesired, m.desiredFrameRate);
mediametrics_setDouble(mMetricsHandle, kCodecFramerateActual, m.actualFrameRate);
}
+ if (m.freezeDurationMsHistogram.getCount() >= 1) {
+ const MediaHistogram &histogram = m.freezeDurationMsHistogram;
+ mediametrics_setInt64(mMetricsHandle, kCodecFreezeCount, histogram.getCount());
+ mediametrics_setInt64(mMetricsHandle, kCodecFreezeDurationAverage, histogram.getAvg());
+ mediametrics_setInt64(mMetricsHandle, kCodecFreezeDurationMax, histogram.getMax());
+ mediametrics_setString(mMetricsHandle, kCodecFreezeDurationHistogram, histogram.emit());
+ }
+ if (m.freezeDistanceMsHistogram.getCount() >= 1) {
+ const MediaHistogram &histogram = m.freezeDistanceMsHistogram;
+ mediametrics_setInt64(mMetricsHandle, kCodecFreezeDistanceAverage, histogram.getAvg());
+ mediametrics_setString(mMetricsHandle, kCodecFreezeDistanceHistogram, histogram.emit());
+ }
}
if (mLatencyHist.getCount() != 0 ) {
diff --git a/media/libstagefright/MediaHistogram.cpp b/media/libstagefright/MediaHistogram.cpp
index 396d90e..3dd67b8 100644
--- a/media/libstagefright/MediaHistogram.cpp
+++ b/media/libstagefright/MediaHistogram.cpp
@@ -21,57 +21,104 @@
#include <assert.h>
#include <inttypes.h>
-#include <stdint.h>
+#include <sstream>
+#include <stdio.h>
namespace android {
+
+MediaHistogram::MediaHistogram() {
+ mBuckets = nullptr;
+ mBucketLimits = nullptr;
+ mBucketCount = 0;
+ mFloor = mCeiling = mWidth = 0;
+ mBelow = 0;
+ mAbove = 0;
+ mSum = 0;
+ mCount = 0;
+ mMin = INT64_MAX;
+ mMax = INT64_MIN;
+}
+
+void MediaHistogram::clear() {
+ if (mBuckets != nullptr) {
+ free(mBuckets);
+ mBuckets = nullptr;
+ }
+ if (mBucketLimits != nullptr) {
+ free(mBucketLimits);
+ mBucketLimits = nullptr;
+ }
+ mBucketCount = 0;
+}
+
bool MediaHistogram::setup(int bucketCount, int64_t width, int64_t floor)
{
if (bucketCount <= 0 || width <= 0) {
return false;
}
-
- // get histogram buckets
- if (bucketCount == mBucketCount && mBuckets != NULL) {
- // reuse our existing buffer
- memset(mBuckets, 0, sizeof(*mBuckets) * mBucketCount);
- } else {
- // get a new pre-zeroed buffer
- int64_t *newbuckets = (int64_t *)calloc(bucketCount, sizeof (*mBuckets));
- if (newbuckets == NULL) {
- goto bad;
- }
- if (mBuckets != NULL)
- free(mBuckets);
- mBuckets = newbuckets;
+ if (!allocate(bucketCount, false)) {
+ return false;
}
-
mWidth = width;
mFloor = floor;
mCeiling = floor + bucketCount * width;
- mBucketCount = bucketCount;
-
mMin = INT64_MAX;
mMax = INT64_MIN;
mSum = 0;
mCount = 0;
mBelow = mAbove = 0;
-
return true;
+}
- bad:
- if (mBuckets != NULL) {
- free(mBuckets);
- mBuckets = NULL;
+bool MediaHistogram::setup(const std::vector<int64_t> &bucketLimits) {
+ if (bucketLimits.size() <= 1) {
+ return false;
+ }
+ int bucketCount = bucketLimits.size() - 1;
+ if (!allocate(bucketCount, true)) {
+ return false;
}
- return false;
+ mWidth = -1;
+ mFloor = bucketLimits[0];
+ for (int i = 0; i < bucketCount; ++i) {
+ mBucketLimits[i] = bucketLimits[i + 1];
+ }
+ mCeiling = bucketLimits[bucketCount];
+ mMin = INT64_MAX;
+ mMax = INT64_MIN;
+ mSum = 0;
+ mCount = 0;
+ mBelow = mAbove = 0;
+ return true;
+}
+
+bool MediaHistogram::allocate(int bucketCount, bool withBucketLimits) {
+ assert(bucketCount > 0);
+ if (bucketCount != mBucketCount) {
+ clear();
+ mBuckets = (int64_t *) calloc(bucketCount, sizeof(*mBuckets));
+ if (mBuckets == nullptr) {
+ return false;
+ }
+ }
+ if (withBucketLimits && mBucketLimits == nullptr) {
+ mBucketLimits = (int64_t *) calloc(bucketCount, sizeof(*mBucketLimits));
+ if (mBucketLimits == nullptr) {
+ clear();
+ return false;
+ }
+ }
+ mBucketCount = bucketCount;
+ memset(mBuckets, 0, sizeof(*mBuckets) * mBucketCount);
+ return true;
}
void MediaHistogram::insert(int64_t sample)
{
// histogram is not set up
- if (mBuckets == NULL) {
+ if (mBuckets == nullptr) {
return;
}
@@ -84,35 +131,43 @@
mBelow++;
} else if (sample >= mCeiling) {
mAbove++;
- } else {
+ } else if (mBucketLimits == nullptr) {
int64_t slot = (sample - mFloor) / mWidth;
assert(slot < mBucketCount);
mBuckets[slot]++;
+ } else {
+ // A binary search might be more efficient for large number of buckets, but it is expected
+ // that there will never be a large amount of buckets, so keep the code simple.
+ for (int slot = 0; slot < mBucketCount; ++slot) {
+ if (sample < mBucketLimits[slot]) {
+ mBuckets[slot]++;
+ break;
+ }
+ }
}
return;
}
-std::string MediaHistogram::emit()
+std::string MediaHistogram::emit() const
{
- std::string value;
- char buffer[64];
-
- // emits: width,Below{bucket0,bucket1,...., bucketN}above
- // unconfigured will emit: 0,0{}0
+ // emits: floor,width,below{bucket0,bucket1,...., bucketN}above
+ // or.. emits: below{bucket0,bucket1,...., bucketN}above
+ // unconfigured will emit: 0,0,0{}0
// XXX: is this best representation?
- snprintf(buffer, sizeof(buffer), "%" PRId64 ",%" PRId64 ",%" PRId64 "{",
- mFloor, mWidth, mBelow);
- value = buffer;
+ std::stringstream ss;
+ if (mBucketLimits == nullptr) {
+ ss << mFloor << "," << mWidth << "," << mBelow << "{";
+ } else {
+ ss << mBelow << "{";
+ }
for (int i = 0; i < mBucketCount; i++) {
if (i != 0) {
- value = value + ",";
+ ss << ",";
}
- snprintf(buffer, sizeof(buffer), "%" PRId64, mBuckets[i]);
- value = value + buffer;
+ ss << mBuckets[i];
}
- snprintf(buffer, sizeof(buffer), "}%" PRId64 , mAbove);
- value = value + buffer;
- return value;
+ ss << "}" << mAbove;
+ return ss.str();
}
} // android
diff --git a/media/libstagefright/VideoRenderQualityTracker.cpp b/media/libstagefright/VideoRenderQualityTracker.cpp
index 7224269..5f0e969 100644
--- a/media/libstagefright/VideoRenderQualityTracker.cpp
+++ b/media/libstagefright/VideoRenderQualityTracker.cpp
@@ -50,15 +50,21 @@
// Allow for a tolerance of 200 milliseconds for determining if we moved forward in content time
// because of frame drops for live content, or because the user is seeking.
contentTimeAdvancedForLiveContentToleranceUs = 200 * 1000;
+
+ // Freeze configuration
+ freezeDurationMsHistogramBuckets = {1, 20, 40, 60, 80, 100, 120, 150, 175, 225, 300, 400, 500};
+ freezeDistanceMsHistogramBuckets = {0, 20, 100, 400, 1000, 2000, 3000, 4000, 8000, 15000, 30000,
+ 60000};
}
-VideoRenderQualityTracker::VideoRenderQualityTracker() :
- mConfiguration(Configuration()) {
+VideoRenderQualityTracker::VideoRenderQualityTracker() : mConfiguration(Configuration()) {
+ configureHistograms(mMetrics, mConfiguration);
resetForDiscontinuity();
}
VideoRenderQualityTracker::VideoRenderQualityTracker(const Configuration &configuration) :
mConfiguration(configuration) {
+ configureHistograms(mMetrics, mConfiguration);
resetForDiscontinuity();
}
@@ -128,6 +134,7 @@
void VideoRenderQualityTracker::resetForDiscontinuity() {
mLastContentTimeUs = -1;
mLastRenderTimeUs = -1;
+ mLastFreezeEndTimeUs = -1;
// Don't worry about tracking frame rendering times from now up until playback catches up to the
// discontinuity. While stuttering or freezing could be found in the next few frames, the impact
@@ -226,6 +233,29 @@
updateFrameRate(mMetrics.contentFrameRate, mContentFrameDurationUs, mConfiguration);
updateFrameRate(mMetrics.desiredFrameRate, mDesiredFrameDurationUs, mConfiguration);
updateFrameRate(mMetrics.actualFrameRate, mActualFrameDurationUs, mConfiguration);
+
+ // If the previous frame was dropped, there was a freeze if we've already rendered a frame
+ if (mActualFrameDurationUs[1] == -1 && mLastRenderTimeUs != -1) {
+ processFreeze(actualRenderTimeUs, mLastRenderTimeUs, mLastFreezeEndTimeUs, mMetrics);
+ mLastFreezeEndTimeUs = actualRenderTimeUs;
+ }
+}
+
+void VideoRenderQualityTracker::processFreeze(int64_t actualRenderTimeUs, int64_t lastRenderTimeUs,
+ int64_t lastFreezeEndTimeUs,
+ VideoRenderQualityMetrics &m) {
+ int64_t freezeDurationMs = (actualRenderTimeUs - lastRenderTimeUs) / 1000;
+ m.freezeDurationMsHistogram.insert(freezeDurationMs);
+ if (lastFreezeEndTimeUs != -1) {
+ int64_t distanceSinceLastFreezeMs = (lastRenderTimeUs - lastFreezeEndTimeUs) / 1000;
+ m.freezeDistanceMsHistogram.insert(distanceSinceLastFreezeMs);
+ }
+}
+
+void VideoRenderQualityTracker::configureHistograms(VideoRenderQualityMetrics &m,
+ const Configuration &c) {
+ m.freezeDurationMsHistogram.setup(c.freezeDurationMsHistogramBuckets);
+ m.freezeDistanceMsHistogram.setup(c.freezeDistanceMsHistogramBuckets);
}
int64_t VideoRenderQualityTracker::nowUs() {
diff --git a/media/libstagefright/include/media/stagefright/MediaHistogram.h b/media/libstagefright/include/media/stagefright/MediaHistogram.h
index 4883203..3c12901 100644
--- a/media/libstagefright/include/media/stagefright/MediaHistogram.h
+++ b/media/libstagefright/include/media/stagefright/MediaHistogram.h
@@ -18,31 +18,36 @@
#define MEDIA_HISTOGRAM_H_
#include <string>
+#include <vector>
namespace android {
class MediaHistogram {
public:
- MediaHistogram() : mFloor(0), mWidth(0), mBelow(0), mAbove(0),
- mMin(INT64_MAX), mMax(INT64_MIN), mSum(0), mCount(0),
- mBucketCount(0), mBuckets(NULL) {};
+ MediaHistogram();
~MediaHistogram() { clear(); };
- void clear() { if (mBuckets != NULL) free(mBuckets); mBuckets = NULL; };
+ void clear();
bool setup(int bucketCount, int64_t width, int64_t floor = 0);
+ bool setup(const std::vector<int64_t> &bucketLimits);
void insert(int64_t sample);
int64_t getMin() const { return mMin; }
int64_t getMax() const { return mMax; }
int64_t getCount() const { return mCount; }
int64_t getSum() const { return mSum; }
int64_t getAvg() const { return mSum / (mCount == 0 ? 1 : mCount); }
- std::string emit();
+ std::string emit() const;
private:
+ MediaHistogram(const MediaHistogram &); // disallow
+
+ bool allocate(int bucketCount, bool withBucketLimits);
+
int64_t mFloor, mCeiling, mWidth;
int64_t mBelow, mAbove;
int64_t mMin, mMax, mSum, mCount;
int mBucketCount;
int64_t *mBuckets;
+ int64_t *mBucketLimits;
};
} // android
diff --git a/media/libstagefright/include/media/stagefright/VideoRenderQualityTracker.h b/media/libstagefright/include/media/stagefright/VideoRenderQualityTracker.h
index bcec783..45c822a 100644
--- a/media/libstagefright/include/media/stagefright/VideoRenderQualityTracker.h
+++ b/media/libstagefright/include/media/stagefright/VideoRenderQualityTracker.h
@@ -22,6 +22,8 @@
#include <list>
#include <queue>
+#include <media/stagefright/MediaHistogram.h>
+
namespace android {
static const float FRAME_RATE_UNDETERMINED = -1.0f;
@@ -55,6 +57,12 @@
// The frame rate as detected by looking at the actual render time, as returned by the system
// post-render.
float actualFrameRate;
+
+ // A histogram of the durations of freezes due to dropped/skipped frames.
+ MediaHistogram freezeDurationMsHistogram;
+
+ // A histogram of the durations between each freeze.
+ MediaHistogram freezeDistanceMsHistogram;
};
///////////////////////////////////////////////////////
@@ -101,6 +109,10 @@
// short, but the content frame duration is large, it is assumed the app is intentionally
// seeking forward.
int32_t contentTimeAdvancedForLiveContentToleranceUs;
+
+ // Freeze configuration
+ std::vector<int64_t> freezeDurationMsHistogramBuckets;
+ std::vector<int64_t> freezeDistanceMsHistogramBuckets;
};
VideoRenderQualityTracker();
@@ -160,6 +172,9 @@
int64_t priorTimestampUs;
};
+ // Configure histograms for the metrics.
+ static void configureHistograms(VideoRenderQualityMetrics &m, const Configuration &c);
+
// The current time in microseconds.
static int64_t nowUs();
@@ -178,6 +193,10 @@
// occurring.
static bool is32pulldown(const FrameDurationUs &durationUs, const Configuration &c);
+ // Process a frame freeze.
+ static void processFreeze(int64_t actualRenderTimeUs, int64_t lastRenderTimeUs,
+ int64_t lastFreezeEndTimeUs, VideoRenderQualityMetrics &m);
+
// Check to see if a discontinuity has occurred by examining the content time and the
// app-desired render time. If so, reset some internal state.
bool resetIfDiscontinuity(int64_t contentTimeUs, int64_t desiredRenderTimeUs);
@@ -204,6 +223,9 @@
// The most recently processed timestamp referring to the wall clock time a frame was rendered.
int64_t mLastRenderTimeUs;
+ // The most recent timestamp of the first frame rendered after the freeze.
+ int64_t mLastFreezeEndTimeUs;
+
// Frames skipped at the end of playback shouldn't really be considered skipped, therefore keep
// a list of the frames, and process them as skipped frames the next time a frame is rendered.
std::list<int64_t> mPendingSkippedFrameContentTimeUsList;
diff --git a/media/libstagefright/tests/VideoRenderQualityTracker_test.cpp b/media/libstagefright/tests/VideoRenderQualityTracker_test.cpp
index 56ec788..2249a8b 100644
--- a/media/libstagefright/tests/VideoRenderQualityTracker_test.cpp
+++ b/media/libstagefright/tests/VideoRenderQualityTracker_test.cpp
@@ -50,6 +50,15 @@
}
}
+ void render(int numFrames) {
+ for (int i = 0; i < numFrames; ++i) {
+ mVideoRenderQualityTracker.onFrameReleased(mMediaTimeUs);
+ mVideoRenderQualityTracker.onFrameRendered(mMediaTimeUs, mClockTimeNs);
+ mMediaTimeUs += mContentFrameDurationUs;
+ mClockTimeNs += mContentFrameDurationUs * 1000;
+ }
+ }
+
void skip(int numFrames) {
for (int i = 0; i < numFrames; ++i) {
mVideoRenderQualityTracker.onFrameSkipped(mMediaTimeUs);
@@ -218,4 +227,68 @@
EXPECT_EQ(h.getMetrics().actualFrameRate, FRAME_RATE_UNDETERMINED);
}
+TEST_F(VideoRenderQualityTrackerTest, capturesFreezeDurationHistogram) {
+ Configuration c;
+ // +17 because freeze durations include the render time of the previous frame
+ c.freezeDurationMsHistogramBuckets = {2 * 17 + 17, 3 * 17 + 17, 6 * 17 + 17};
+ Helper h(17, c);
+ h.render(1);
+ h.drop(1); // below
+ h.render(1);
+ h.drop(3); // bucket 1
+ h.render(1);
+ h.drop(2); // bucket 0
+ h.render(1);
+ h.drop(4); // bucket 1
+ h.render(1);
+ h.drop(2); // bucket 0
+ h.render(1);
+ h.drop(5); // bucket 1
+ h.render(1);
+ h.drop(10); // above
+ h.render(1);
+ h.drop(15); // above
+ h.render(1);
+ EXPECT_EQ(h.getMetrics().freezeDurationMsHistogram.emit(), "1{2,3}2");
+ EXPECT_EQ(h.getMetrics().freezeDurationMsHistogram.getCount(), 8);
+ // the smallest frame drop was 1, +17 because it includes the previous frame render time
+ EXPECT_EQ(h.getMetrics().freezeDurationMsHistogram.getMin(), 1 * 17 + 17);
+ // the largest frame drop was 10, +17 because it includes the previous frame render time
+ EXPECT_EQ(h.getMetrics().freezeDurationMsHistogram.getMax(), 15 * 17 + 17);
+ // total frame drop count, multiplied by 17, plus 17 for each occurrence, divided by occurrences
+ EXPECT_EQ(h.getMetrics().freezeDurationMsHistogram.getAvg(), ((1 + 3 + 2 + 4 + 2 + 5 + 10 + 15)
+ * 17 + 8 * 17) / 8);
+}
+
+TEST_F(VideoRenderQualityTrackerTest, capturesFreezeDistanceHistogram) {
+ Configuration c;
+ c.freezeDistanceMsHistogramBuckets = {1 * 17, 5 * 17, 6 * 17};
+ Helper h(17, c);
+ h.render(1);
+ h.drop(1);
+ h.render(5); // bucket 0
+ h.drop(3);
+ h.render(3); // bucket 0
+ h.drop(2);
+ h.render(9); // above
+ h.drop(5);
+ h.render(1); // below
+ h.drop(2);
+ h.render(6); // bucket 1
+ h.drop(4);
+ h.render(12); // above
+ h.drop(2);
+ h.render(1);
+ EXPECT_EQ(h.getMetrics().freezeDistanceMsHistogram.emit(), "1{2,1}2");
+ EXPECT_EQ(h.getMetrics().freezeDistanceMsHistogram.getCount(), 6);
+ // the smallest render between drops was 1, -17 because the last frame rendered also froze
+ EXPECT_EQ(h.getMetrics().freezeDistanceMsHistogram.getMin(), 1 * 17 - 17);
+ // the largest render between drops was 12, -17 because the last frame rendered also froze
+ EXPECT_EQ(h.getMetrics().freezeDistanceMsHistogram.getMax(), 12 * 17 - 17);
+ // total render count between, multiplied by 17, minus 17 for each occurrence, divided by
+ // occurrences
+ EXPECT_EQ(h.getMetrics().freezeDistanceMsHistogram.getAvg(), ((5 + 3 + 9 + 1 + 6 + 12) * 17 -
+ 6 * 17) / 6);
+}
+
} // android