Add OS access interfaces and test impl for wear tethering
Bug: 245971639
Change-Id: I750c2e9bbe8c9a3992ae91d9fea4958bfce60828
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index c9b3d07..bcf89b3 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -36,6 +36,7 @@
"libnanohttpd",
"net-tests-utils-host-device-common",
"net-utils-device-common",
+ "net-utils-device-common-async",
"net-utils-device-common-netlink",
"modules-utils-build_system",
],
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java b/staticlibs/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java
new file mode 100644
index 0000000..1b8e26b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java
@@ -0,0 +1,568 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import android.os.ParcelFileDescriptor;
+import android.system.StructPollfd;
+import android.util.Log;
+
+import com.android.net.module.util.async.CircularByteBuffer;
+import com.android.net.module.util.async.OsAccess;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.concurrent.TimeUnit;
+
+public class FakeOsAccess extends OsAccess {
+ public static final boolean ENABLE_FINE_DEBUG = true;
+
+ public static final int DEFAULT_FILE_DATA_QUEUE_SIZE = 8 * 1024;
+
+ private enum FileType { PAIR, PIPE }
+
+ // Common poll() constants:
+ private static final short POLLIN = 0x0001;
+ private static final short POLLOUT = 0x0004;
+ private static final short POLLERR = 0x0008;
+ private static final short POLLHUP = 0x0010;
+
+ private static final Constructor<FileDescriptor> FD_CONSTRUCTOR;
+ private static final Field FD_FIELD_DESCRIPTOR;
+ private static final Field PFD_FIELD_DESCRIPTOR;
+ private static final Field PFD_FIELD_GUARD;
+ private static final Method CLOSE_GUARD_METHOD_CLOSE;
+
+ private final int mReadQueueSize = DEFAULT_FILE_DATA_QUEUE_SIZE;
+ private final int mWriteQueueSize = DEFAULT_FILE_DATA_QUEUE_SIZE;
+ private final HashMap<Integer, File> mFiles = new HashMap<>();
+ private final byte[] mTmpBuffer = new byte[1024];
+ private final long mStartTime;
+ private final String mLogTag;
+ private int mFileNumberGen = 3;
+ private boolean mHasRateLimitedData;
+
+ public FakeOsAccess(String logTag) {
+ mLogTag = logTag;
+ mStartTime = monotonicTimeMillis();
+ }
+
+ @Override
+ public long monotonicTimeMillis() {
+ return System.nanoTime() / 1000000;
+ }
+
+ @Override
+ public FileDescriptor getInnerFileDescriptor(ParcelFileDescriptor fd) {
+ try {
+ return (FileDescriptor) PFD_FIELD_DESCRIPTOR.get(fd);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void close(ParcelFileDescriptor fd) {
+ if (fd != null) {
+ close(getInnerFileDescriptor(fd));
+
+ try {
+ // Reduce CloseGuard warnings.
+ Object guard = PFD_FIELD_GUARD.get(fd);
+ CLOSE_GUARD_METHOD_CLOSE.invoke(guard);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ public synchronized void close(FileDescriptor fd) {
+ if (fd != null) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.decreaseRefCount();
+ mFiles.remove(getFileDescriptorNumber(fd));
+ setFileDescriptorNumber(fd, -1);
+ notifyAll();
+ }
+ }
+ }
+
+ private File getFile(String func, FileDescriptor fd) throws IOException {
+ File file = getFileOrNull(fd);
+ if (file == null) {
+ throw newIOException(func, "Unknown file descriptor: " + getFileDebugName(fd));
+ }
+ return file;
+ }
+
+ private File getFileOrNull(FileDescriptor fd) {
+ return mFiles.get(getFileDescriptorNumber(fd));
+ }
+
+ @Override
+ public String getFileDebugName(ParcelFileDescriptor fd) {
+ return (fd != null ? getFileDebugName(getInnerFileDescriptor(fd)) : "null");
+ }
+
+ public String getFileDebugName(FileDescriptor fd) {
+ if (fd == null) {
+ return "null";
+ }
+
+ final int fdNumber = getFileDescriptorNumber(fd);
+ File file = mFiles.get(fdNumber);
+
+ StringBuilder sb = new StringBuilder();
+ if (file != null) {
+ if (file.name != null) {
+ sb.append(file.name);
+ sb.append("/");
+ }
+ sb.append(file.type);
+ sb.append("/");
+ } else {
+ sb.append("BADFD/");
+ }
+ sb.append(fdNumber);
+ return sb.toString();
+ }
+
+ public synchronized void setFileName(FileDescriptor fd, String name) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.name = name;
+ }
+ }
+
+ @Override
+ public synchronized void setNonBlocking(FileDescriptor fd) throws IOException {
+ File file = getFile("fcntl", fd);
+ file.isBlocking = false;
+ }
+
+ @Override
+ public synchronized int read(FileDescriptor fd, byte[] buffer, int pos, int len)
+ throws IOException {
+ checkBoundaries("read", buffer, pos, len);
+
+ File file = getFile("read", fd);
+ if (file.readQueue == null) {
+ throw newIOException("read", "File not readable");
+ }
+ file.checkNonBlocking("read");
+
+ if (len == 0) {
+ return 0;
+ }
+
+ final int availSize = file.readQueue.size();
+ if (availSize == 0) {
+ if (file.isEndOfStream) {
+ // Java convention uses -1 to indicate end of stream.
+ return -1;
+ }
+ return 0; // EAGAIN
+ }
+
+ final int readCount = Math.min(len, availSize);
+ file.readQueue.readBytes(buffer, pos, readCount);
+ maybeTransferData(file);
+ return readCount;
+ }
+
+ @Override
+ public synchronized int write(FileDescriptor fd, byte[] buffer, int pos, int len)
+ throws IOException {
+ checkBoundaries("write", buffer, pos, len);
+
+ File file = getFile("write", fd);
+ if (file.writeQueue == null) {
+ throw newIOException("read", "File not writable");
+ }
+ if (file.type == FileType.PIPE && file.sink.openCount == 0) {
+ throw newIOException("write", "The other end of pipe is closed");
+ }
+ file.checkNonBlocking("write");
+
+ if (len == 0) {
+ return 0;
+ }
+
+ final int originalFreeSize = file.writeQueue.freeSize();
+ if (originalFreeSize == 0) {
+ return 0; // EAGAIN
+ }
+
+ final int writeCount = Math.min(len, originalFreeSize);
+ file.writeQueue.writeBytes(buffer, pos, writeCount);
+ maybeTransferData(file);
+
+ if (file.writeQueue.freeSize() < originalFreeSize) {
+ final int additionalQueuedCount = originalFreeSize - file.writeQueue.freeSize();
+ Log.i(mLogTag, logStr("Delaying transfer of " + additionalQueuedCount
+ + " bytes, queued=" + file.writeQueue.size() + ", type=" + file.type
+ + ", src_red=" + file.outboundLimiter + ", dst_red=" + file.sink.inboundLimiter));
+ }
+
+ return writeCount;
+ }
+
+ private void maybeTransferData(File file) {
+ boolean hasChanges = copyFileBuffers(file, file.sink);
+ hasChanges = copyFileBuffers(file.source, file) || hasChanges;
+
+ if (hasChanges) {
+ // TODO(b/245971639): Avoid notifying if no-one is polling.
+ notifyAll();
+ }
+ }
+
+ private boolean copyFileBuffers(File src, File dst) {
+ if (src.writeQueue == null || dst.readQueue == null) {
+ return false;
+ }
+
+ final int originalCopyCount = Math.min(mTmpBuffer.length,
+ Math.min(src.writeQueue.size(), dst.readQueue.freeSize()));
+
+ final int allowedCopyCount = RateLimiter.limit(
+ src.outboundLimiter, dst.inboundLimiter, originalCopyCount);
+
+ if (allowedCopyCount < originalCopyCount) {
+ if (ENABLE_FINE_DEBUG) {
+ Log.i(mLogTag, logStr("Delaying transfer of "
+ + (originalCopyCount - allowedCopyCount) + " bytes, original="
+ + originalCopyCount + ", allowed=" + allowedCopyCount
+ + ", type=" + src.type));
+ }
+ if (originalCopyCount > 0) {
+ mHasRateLimitedData = true;
+ }
+ if (allowedCopyCount == 0) {
+ return false;
+ }
+ }
+
+ boolean hasChanges = false;
+ if (allowedCopyCount > 0) {
+ if (dst.readQueue.size() == 0 || src.writeQueue.freeSize() == 0) {
+ hasChanges = true; // Read queue had no data, or write queue was full.
+ }
+ src.writeQueue.readBytes(mTmpBuffer, 0, allowedCopyCount);
+ dst.readQueue.writeBytes(mTmpBuffer, 0, allowedCopyCount);
+ }
+
+ if (!dst.isEndOfStream && src.openCount == 0
+ && src.writeQueue.size() == 0 && dst.readQueue.size() == 0) {
+ dst.isEndOfStream = true;
+ hasChanges = true;
+ }
+
+ return hasChanges;
+ }
+
+ public void clearInboundRateLimit(FileDescriptor fd) {
+ setInboundRateLimit(fd, Integer.MAX_VALUE);
+ }
+
+ public void clearOutboundRateLimit(FileDescriptor fd) {
+ setOutboundRateLimit(fd, Integer.MAX_VALUE);
+ }
+
+ public synchronized void setInboundRateLimit(FileDescriptor fd, int bytesPerSecond) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.inboundLimiter.setBytesPerSecond(bytesPerSecond);
+ maybeTransferData(file);
+ }
+ }
+
+ public synchronized void setOutboundRateLimit(FileDescriptor fd, int bytesPerSecond) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.outboundLimiter.setBytesPerSecond(bytesPerSecond);
+ maybeTransferData(file);
+ }
+ }
+
+ public synchronized ParcelFileDescriptor[] socketpair() throws IOException {
+ int fdNumber1 = getNextFd("socketpair");
+ int fdNumber2 = getNextFd("socketpair");
+
+ File file1 = new File(FileType.PAIR, mReadQueueSize, mWriteQueueSize);
+ File file2 = new File(FileType.PAIR, mReadQueueSize, mWriteQueueSize);
+
+ return registerFilePair(fdNumber1, file1, fdNumber2, file2);
+ }
+
+ @Override
+ public synchronized ParcelFileDescriptor[] pipe() throws IOException {
+ int fdNumber1 = getNextFd("pipe");
+ int fdNumber2 = getNextFd("pipe");
+
+ File file1 = new File(FileType.PIPE, mReadQueueSize, 0);
+ File file2 = new File(FileType.PIPE, 0, mWriteQueueSize);
+
+ return registerFilePair(fdNumber1, file1, fdNumber2, file2);
+ }
+
+ private ParcelFileDescriptor[] registerFilePair(
+ int fdNumber1, File file1, int fdNumber2, File file2) {
+ file1.sink = file2;
+ file1.source = file2;
+ file2.sink = file1;
+ file2.source = file1;
+
+ mFiles.put(fdNumber1, file1);
+ mFiles.put(fdNumber2, file2);
+ return new ParcelFileDescriptor[] {
+ newParcelFileDescriptor(fdNumber1), newParcelFileDescriptor(fdNumber2)};
+ }
+
+ @Override
+ public short getPollInMask() {
+ return POLLIN;
+ }
+
+ @Override
+ public short getPollOutMask() {
+ return POLLOUT;
+ }
+
+ @Override
+ public synchronized int poll(StructPollfd[] fds, int timeoutMs) throws IOException {
+ if (timeoutMs < 0) {
+ timeoutMs = (int) TimeUnit.HOURS.toMillis(1); // Make "infinite" equal to 1 hour.
+ }
+
+ if (fds == null || fds.length > 1000) {
+ throw newIOException("poll", "Invalid fds param");
+ }
+ for (StructPollfd pollFd : fds) {
+ getFile("poll", pollFd.fd);
+ }
+
+ int waitCallCount = 0;
+ final long deadline = monotonicTimeMillis() + timeoutMs;
+ while (true) {
+ if (mHasRateLimitedData) {
+ mHasRateLimitedData = false;
+ for (File file : mFiles.values()) {
+ if (file.inboundLimiter.getLastRequestReduction() != 0) {
+ copyFileBuffers(file.source, file);
+ }
+ if (file.outboundLimiter.getLastRequestReduction() != 0) {
+ copyFileBuffers(file, file.sink);
+ }
+ }
+ }
+
+ final int readyCount = calculateReadyCount(fds);
+ if (readyCount > 0) {
+ if (ENABLE_FINE_DEBUG) {
+ Log.v(mLogTag, logStr("Poll returns " + readyCount
+ + " after " + waitCallCount + " wait calls"));
+ }
+ return readyCount;
+ }
+
+ long remainingTimeoutMs = deadline - monotonicTimeMillis();
+ if (remainingTimeoutMs <= 0) {
+ if (ENABLE_FINE_DEBUG) {
+ Log.v(mLogTag, logStr("Poll timeout " + timeoutMs
+ + "ms after " + waitCallCount + " wait calls"));
+ }
+ return 0;
+ }
+
+ if (mHasRateLimitedData) {
+ remainingTimeoutMs = Math.min(RateLimiter.BUCKET_DURATION_MS, remainingTimeoutMs);
+ }
+
+ try {
+ wait(remainingTimeoutMs);
+ } catch (InterruptedException e) {
+ // Ignore and retry
+ }
+ waitCallCount++;
+ }
+ }
+
+ private int calculateReadyCount(StructPollfd[] fds) {
+ int fdCount = 0;
+ for (StructPollfd pollFd : fds) {
+ pollFd.revents = 0;
+
+ File file = getFileOrNull(pollFd.fd);
+ if (file == null) {
+ Log.w(mLogTag, logStr("Ignoring FD concurrently closed by a buggy app: "
+ + getFileDebugName(pollFd.fd)));
+ continue;
+ }
+
+ if (ENABLE_FINE_DEBUG) {
+ Log.v(mLogTag, logStr("calculateReadyCount fd=" + getFileDebugName(pollFd.fd)
+ + ", events=" + pollFd.events + ", eof=" + file.isEndOfStream
+ + ", r=" + (file.readQueue != null ? file.readQueue.size() : -1)
+ + ", w=" + (file.writeQueue != null ? file.writeQueue.freeSize() : -1)));
+ }
+
+ if ((pollFd.events & POLLIN) != 0) {
+ if (file.readQueue != null && file.readQueue.size() != 0) {
+ pollFd.revents |= POLLIN;
+ }
+ if (file.isEndOfStream) {
+ pollFd.revents |= POLLHUP;
+ }
+ }
+
+ if ((pollFd.events & POLLOUT) != 0) {
+ if (file.type == FileType.PIPE && file.sink.openCount == 0) {
+ pollFd.revents |= POLLERR;
+ }
+ if (file.writeQueue != null && file.writeQueue.freeSize() != 0) {
+ pollFd.revents |= POLLOUT;
+ }
+ }
+
+ if (pollFd.revents != 0) {
+ fdCount++;
+ }
+ }
+ return fdCount;
+ }
+
+ private int getNextFd(String func) throws IOException {
+ if (mFileNumberGen > 100000) {
+ throw newIOException(func, "Too many files open");
+ }
+
+ return mFileNumberGen++;
+ }
+
+ private static IOException newIOException(String func, String message) {
+ return new IOException(message + ", func=" + func);
+ }
+
+ public static void checkBoundaries(String func, byte[] buffer, int pos, int len)
+ throws IOException {
+ if (((buffer.length | pos | len) < 0 || pos > buffer.length - len)) {
+ throw newIOException(func, "Invalid array bounds");
+ }
+ }
+
+ private ParcelFileDescriptor newParcelFileDescriptor(int fdNumber) {
+ try {
+ return new ParcelFileDescriptor(newFileDescriptor(fdNumber));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private FileDescriptor newFileDescriptor(int fdNumber) {
+ try {
+ return FD_CONSTRUCTOR.newInstance(Integer.valueOf(fdNumber));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public int getFileDescriptorNumber(FileDescriptor fd) {
+ try {
+ return (Integer) FD_FIELD_DESCRIPTOR.get(fd);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void setFileDescriptorNumber(FileDescriptor fd, int fdNumber) {
+ try {
+ FD_FIELD_DESCRIPTOR.set(fd, Integer.valueOf(fdNumber));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String logStr(String message) {
+ return "[FakeOs " + (monotonicTimeMillis() - mStartTime) + "] " + message;
+ }
+
+ private class File {
+ final FileType type;
+ final CircularByteBuffer readQueue;
+ final CircularByteBuffer writeQueue;
+ final RateLimiter inboundLimiter = new RateLimiter(FakeOsAccess.this, Integer.MAX_VALUE);
+ final RateLimiter outboundLimiter = new RateLimiter(FakeOsAccess.this, Integer.MAX_VALUE);
+ String name;
+ int openCount = 1;
+ boolean isBlocking = true;
+ File sink;
+ File source;
+ boolean isEndOfStream;
+
+ File(FileType type, int readQueueSize, int writeQueueSize) {
+ this.type = type;
+ readQueue = (readQueueSize > 0 ? new CircularByteBuffer(readQueueSize) : null);
+ writeQueue = (writeQueueSize > 0 ? new CircularByteBuffer(writeQueueSize) : null);
+ }
+
+ void decreaseRefCount() {
+ if (openCount <= 0) {
+ throw new IllegalStateException();
+ }
+ openCount--;
+ }
+
+ void checkNonBlocking(String func) throws IOException {
+ if (isBlocking) {
+ throw newIOException(func, "File in blocking mode");
+ }
+ }
+ }
+
+ static {
+ try {
+ FD_CONSTRUCTOR = FileDescriptor.class.getDeclaredConstructor(int.class);
+ FD_CONSTRUCTOR.setAccessible(true);
+
+ Field descriptorIntField;
+ try {
+ descriptorIntField = FileDescriptor.class.getDeclaredField("descriptor");
+ } catch (NoSuchFieldException e) {
+ descriptorIntField = FileDescriptor.class.getDeclaredField("fd");
+ }
+ FD_FIELD_DESCRIPTOR = descriptorIntField;
+ FD_FIELD_DESCRIPTOR.setAccessible(true);
+
+ PFD_FIELD_DESCRIPTOR = ParcelFileDescriptor.class.getDeclaredField("mFd");
+ PFD_FIELD_DESCRIPTOR.setAccessible(true);
+
+ PFD_FIELD_GUARD = ParcelFileDescriptor.class.getDeclaredField("mGuard");
+ PFD_FIELD_GUARD.setAccessible(true);
+
+ CLOSE_GUARD_METHOD_CLOSE = Class.forName("dalvik.system.CloseGuard")
+ .getDeclaredMethod("close");
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/async/RateLimiter.java b/staticlibs/testutils/devicetests/com/android/testutils/async/RateLimiter.java
new file mode 100644
index 0000000..137873d
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/async/RateLimiter.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import com.android.net.module.util.async.OsAccess;
+
+import java.util.Arrays;
+
+/**
+ * Limits the number of bytes processed to the given maximum of bytes per second.
+ *
+ * The limiter tracks the total for the past second, along with sums for each 10ms
+ * in the past second, allowing the total to be adjusted as the time passes.
+ */
+public final class RateLimiter {
+ private static final int PERIOD_DURATION_MS = 1000;
+ private static final int BUCKET_COUNT = 100;
+
+ public static final int BUCKET_DURATION_MS = PERIOD_DURATION_MS / BUCKET_COUNT;
+
+ private final OsAccess mOsAccess;
+ private final int[] mStatBuckets = new int[BUCKET_COUNT];
+ private int mMaxPerPeriodBytes;
+ private int mMaxPerBucketBytes;
+ private int mRecordedPeriodBytes;
+ private long mLastLimitTimestamp;
+ private int mLastRequestReduction;
+
+ public RateLimiter(OsAccess osAccess, int bytesPerSecond) {
+ mOsAccess = osAccess;
+ setBytesPerSecond(bytesPerSecond);
+ clear();
+ }
+
+ public int getBytesPerSecond() {
+ return mMaxPerPeriodBytes;
+ }
+
+ public void setBytesPerSecond(int bytesPerSecond) {
+ mMaxPerPeriodBytes = bytesPerSecond;
+ mMaxPerBucketBytes = Math.max(1, (mMaxPerPeriodBytes / BUCKET_COUNT) * 2);
+ }
+
+ public void clear() {
+ mLastLimitTimestamp = mOsAccess.monotonicTimeMillis();
+ mRecordedPeriodBytes = 0;
+ Arrays.fill(mStatBuckets, 0);
+ }
+
+ public static int limit(RateLimiter limiter1, RateLimiter limiter2, int requestedBytes) {
+ final long now = limiter1.mOsAccess.monotonicTimeMillis();
+ final int allowedCount = Math.min(limiter1.calculateLimit(now, requestedBytes),
+ limiter2.calculateLimit(now, requestedBytes));
+ limiter1.recordBytes(now, requestedBytes, allowedCount);
+ limiter2.recordBytes(now, requestedBytes, allowedCount);
+ return allowedCount;
+ }
+
+ public int limit(int requestedBytes) {
+ final long now = mOsAccess.monotonicTimeMillis();
+ final int allowedCount = calculateLimit(now, requestedBytes);
+ recordBytes(now, requestedBytes, allowedCount);
+ return allowedCount;
+ }
+
+ public int getLastRequestReduction() {
+ return mLastRequestReduction;
+ }
+
+ public boolean acceptAllOrNone(int requestedBytes) {
+ final long now = mOsAccess.monotonicTimeMillis();
+ final int allowedCount = calculateLimit(now, requestedBytes);
+ if (allowedCount < requestedBytes) {
+ return false;
+ }
+ recordBytes(now, requestedBytes, allowedCount);
+ return true;
+ }
+
+ private int calculateLimit(long now, int requestedBytes) {
+ // First remove all stale bucket data and adjust the total.
+ final long currentBucketAbsIdx = now / BUCKET_DURATION_MS;
+ final long staleCutoffIdx = currentBucketAbsIdx - BUCKET_COUNT;
+ for (long i = mLastLimitTimestamp / BUCKET_DURATION_MS; i < staleCutoffIdx; i++) {
+ final int idx = (int) (i % BUCKET_COUNT);
+ mRecordedPeriodBytes -= mStatBuckets[idx];
+ mStatBuckets[idx] = 0;
+ }
+
+ final int bucketIdx = (int) (currentBucketAbsIdx % BUCKET_COUNT);
+ final int maxAllowed = Math.min(mMaxPerPeriodBytes - mRecordedPeriodBytes,
+ Math.min(mMaxPerBucketBytes - mStatBuckets[bucketIdx], requestedBytes));
+ return Math.max(0, maxAllowed);
+ }
+
+ private void recordBytes(long now, int requestedBytes, int actualBytes) {
+ mStatBuckets[(int) ((now / BUCKET_DURATION_MS) % BUCKET_COUNT)] += actualBytes;
+ mRecordedPeriodBytes += actualBytes;
+ mLastRequestReduction = requestedBytes - actualBytes;
+ mLastLimitTimestamp = now;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{max=");
+ sb.append(mMaxPerPeriodBytes);
+ sb.append(",max_bucket=");
+ sb.append(mMaxPerBucketBytes);
+ sb.append(",total=");
+ sb.append(mRecordedPeriodBytes);
+ sb.append(",last_red=");
+ sb.append(mLastRequestReduction);
+ sb.append('}');
+ return sb.toString();
+ }
+}