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];