Increase size limit for pulled StatsEvent.
Increase StatsEvent max byte size to 50 KB when not using pooled buffer.
- Set the default max size to 50 KB
- The starting buffer size is still ~4 KB for pushed and pulled events.
- If a write would exceed the buffer bounds, double buffer size until
write fits or 50 KB limit is exceeded in which case the overflow bit is set to true.
- If usePooledBuffer() is called, max size is set to ~4 KB but buffer
isn't resized. And if the current payload exceeds this limit, set
overflow bit to true.
- Only "recycle" Buffer object if its size is <= ~4 KB.
Bug: 158214941
Test: atest FrameworkStatsdTest
Change-Id: I9b6f73688b0bccb5a70f4ef51750464ec9c87fb9
diff --git a/apex/framework/java/android/util/StatsEvent.java b/apex/framework/java/android/util/StatsEvent.java
index 8bd36a5..8be5c63 100644
--- a/apex/framework/java/android/util/StatsEvent.java
+++ b/apex/framework/java/android/util/StatsEvent.java
@@ -26,6 +26,8 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import java.util.Arrays;
+
/**
* StatsEvent builds and stores the buffer sent over the statsd socket.
* This class defines and encapsulates the socket protocol.
@@ -224,7 +226,9 @@
// Max payload size is 4 bytes less as 4 bytes are reserved for statsEventTag.
// See android_util_StatsLog.cpp.
- private static final int MAX_PAYLOAD_SIZE = LOGGER_ENTRY_MAX_PAYLOAD - 4;
+ private static final int MAX_PUSH_PAYLOAD_SIZE = LOGGER_ENTRY_MAX_PAYLOAD - 4;
+
+ private static final int MAX_PULL_PAYLOAD_SIZE = 50 * 1024; // 50 KB
private final int mAtomId;
private final byte[] mPayload;
@@ -619,6 +623,7 @@
@NonNull
public Builder usePooledBuffer() {
mUsePooledBuffer = true;
+ mBuffer.setMaxSize(MAX_PUSH_PAYLOAD_SIZE, mPos);
return this;
}
@@ -694,8 +699,9 @@
@GuardedBy("sLock")
private static Buffer sPool;
- private final byte[] mBytes = new byte[MAX_PAYLOAD_SIZE];
+ private byte[] mBytes = new byte[MAX_PUSH_PAYLOAD_SIZE];
private boolean mOverflow = false;
+ private int mMaxSize = MAX_PULL_PAYLOAD_SIZE;
@NonNull
private static Buffer obtain() {
@@ -717,15 +723,26 @@
}
private void release() {
- synchronized (sLock) {
- if (null == sPool) {
- sPool = this;
+ // Recycle this Buffer if its size is MAX_PUSH_PAYLOAD_SIZE or under.
+ if (mBytes.length <= MAX_PUSH_PAYLOAD_SIZE) {
+ synchronized (sLock) {
+ if (null == sPool) {
+ sPool = this;
+ }
}
}
}
private void reset() {
mOverflow = false;
+ mMaxSize = MAX_PULL_PAYLOAD_SIZE;
+ }
+
+ private void setMaxSize(final int maxSize, final int numBytesWritten) {
+ mMaxSize = maxSize;
+ if (numBytesWritten > maxSize) {
+ mOverflow = true;
+ }
}
private boolean hasOverflowed() {
@@ -740,11 +757,28 @@
* @return true if space is available, false otherwise.
**/
private boolean hasEnoughSpace(final int index, final int numBytes) {
- final boolean result = index + numBytes < MAX_PAYLOAD_SIZE;
- if (!result) {
+ final int totalBytesNeeded = index + numBytes;
+
+ if (totalBytesNeeded > mMaxSize) {
mOverflow = true;
+ return false;
}
- return result;
+
+ // Expand buffer if needed.
+ if (mBytes.length < mMaxSize && totalBytesNeeded > mBytes.length) {
+ int newSize = mBytes.length;
+ do {
+ newSize *= 2;
+ } while (newSize <= totalBytesNeeded);
+
+ if (newSize > mMaxSize) {
+ newSize = mMaxSize;
+ }
+
+ mBytes = Arrays.copyOf(mBytes, newSize);
+ }
+
+ return true;
}
/**
diff --git a/apex/framework/test/src/android/util/StatsEventTest.java b/apex/framework/test/src/android/util/StatsEventTest.java
index 7b51155..8d26369 100644
--- a/apex/framework/test/src/android/util/StatsEventTest.java
+++ b/apex/framework/test/src/android/util/StatsEventTest.java
@@ -33,6 +33,7 @@
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.Random;
/**
* Internal tests for {@link StatsEvent}.
@@ -644,6 +645,165 @@
statsEvent.release();
}
+ @Test
+ public void testLargePulledEvent() {
+ final int expectedAtomId = 10_020;
+ byte[] field1 = new byte[10 * 1024];
+ new Random().nextBytes(field1);
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent =
+ StatsEvent.newBuilder().setAtomId(expectedAtomId).writeByteArray(field1).build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get())
+ .isEqualTo(3);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong())
+ .isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id").that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("Third element is not byte array")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_BYTE_ARRAY);
+
+ final byte[] field1Actual = getByteArrayFromByteBuffer(buffer);
+ assertWithMessage("Incorrect field 1").that(field1Actual).isEqualTo(field1);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testPulledEventOverflow() {
+ final int expectedAtomId = 10_020;
+ byte[] field1 = new byte[50 * 1024];
+ new Random().nextBytes(field1);
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent =
+ StatsEvent.newBuilder().setAtomId(expectedAtomId).writeByteArray(field1).build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get())
+ .isEqualTo(3);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong())
+ .isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id").that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("Third element is not errors type")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_ERRORS);
+
+ final int errorMask = buffer.getInt();
+
+ assertWithMessage("ERROR_OVERFLOW should be the only error in the error mask")
+ .that(errorMask)
+ .isEqualTo(StatsEvent.ERROR_OVERFLOW);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testPushedEventOverflow() {
+ final int expectedAtomId = 10_020;
+ byte[] field1 = new byte[10 * 1024];
+ new Random().nextBytes(field1);
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .writeByteArray(field1)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get())
+ .isEqualTo(3);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong())
+ .isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id").that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("Third element is not errors type")
+ .that(buffer.get())
+ .isEqualTo(StatsEvent.TYPE_ERRORS);
+
+ final int errorMask = buffer.getInt();
+
+ assertWithMessage("ERROR_OVERFLOW should be the only error in the error mask")
+ .that(errorMask)
+ .isEqualTo(StatsEvent.ERROR_OVERFLOW);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
private static byte[] getByteArrayFromByteBuffer(final ByteBuffer buffer) {
final int numBytes = buffer.getInt();
byte[] bytes = new byte[numBytes];