Replace Queue<Long> with a queue for long primitives

Replace Queue<Long> with LongArrayQueue which provides queue semantics
for long primitives. LongArrayQueue is forked from IntArrayQueue which
in turn was forked from Androidx CircularIntArray.

IntArrayQueue is deleted and we now use CircularIntArray directly from
Androidx Collection.

PiperOrigin-RevId: 559129744
This commit is contained in:
christosts 2023-08-22 17:09:01 +01:00 committed by Ian Baker
parent 36084eef05
commit 398809e4e2
8 changed files with 212 additions and 182 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 The Android Open Source Project
* Copyright 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.
@ -13,39 +13,57 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.mediacodec;
package androidx.media3.common.util;
import androidx.media3.common.util.UnstableApi;
import static androidx.media3.common.util.Assertions.checkArgument;
import androidx.annotation.VisibleForTesting;
import java.util.NoSuchElementException;
/**
* Array-based unbounded queue for int primitives with amortized O(1) add and remove.
* Array-based unbounded queue for long primitives with amortized O(1) add and remove.
*
* <p>Use this class instead of a {@link java.util.Deque} to avoid boxing int primitives to {@link
* Integer} instances.
* <p>Use this class instead of a {@link java.util.Deque} to avoid boxing long primitives to {@link
* Long} instances.
*/
@UnstableApi
/* package */ final class IntArrayQueue {
public final class LongArrayQueue {
/** Default capacity needs to be a power of 2. */
private static final int DEFAULT_INITIAL_CAPACITY = 16;
/** Default initial capacity. */
public static final int DEFAULT_INITIAL_CAPACITY = 16;
private int headIndex;
private int tailIndex;
private int size;
private int[] data;
private long[] data;
private int wrapAroundMask;
public IntArrayQueue() {
/** Creates a queue with an initial capacity of {@link #DEFAULT_INITIAL_CAPACITY}. */
public LongArrayQueue() {
this(DEFAULT_INITIAL_CAPACITY);
}
/**
* Creates a queue with capacity for at least {@code minCapacity}
*
* @param minCapacity minCapacity the minimum capacity, between 1 and 2^30 inclusive
*/
public LongArrayQueue(int minCapacity) {
checkArgument(minCapacity >= 0 && minCapacity <= (1 << 30));
minCapacity = minCapacity == 0 ? 1 : minCapacity;
// If capacity isn't a power of 2, round up to the next highest power of 2.
if (Integer.bitCount(minCapacity) != 1) {
minCapacity = Integer.highestOneBit(minCapacity - 1) << 1;
}
headIndex = 0;
tailIndex = -1;
size = 0;
data = new int[DEFAULT_INITIAL_CAPACITY];
data = new long[minCapacity];
wrapAroundMask = data.length - 1;
}
/** Add a new item to the queue. */
public void add(int value) {
public void add(long value) {
if (size == data.length) {
doubleArraySize();
}
@ -60,18 +78,31 @@ import java.util.NoSuchElementException;
*
* @throws NoSuchElementException if the queue is empty.
*/
public int remove() {
public long remove() {
if (size == 0) {
throw new NoSuchElementException();
}
int value = data[headIndex];
long value = data[headIndex];
headIndex = (headIndex + 1) & wrapAroundMask;
size--;
return value;
}
/**
* Retrieves, but does not remove, the head of the queue.
*
* @throws NoSuchElementException if the queue is empty.
*/
public long element() {
if (size == 0) {
throw new NoSuchElementException();
}
return data[headIndex];
}
/** Returns the number of items in the queue. */
public int size() {
return size;
@ -90,7 +121,8 @@ import java.util.NoSuchElementException;
}
/** Returns the length of the backing array. */
public int capacity() {
@VisibleForTesting
/* package */ int capacity() {
return data.length;
}
@ -100,7 +132,7 @@ import java.util.NoSuchElementException;
throw new IllegalStateException();
}
int[] newData = new int[newCapacity];
long[] newData = new long[newCapacity];
int itemsToRight = data.length - headIndex;
int itemsToLeft = headIndex;
System.arraycopy(data, headIndex, newData, 0, itemsToRight);

View File

@ -0,0 +1,136 @@
/*
* Copyright 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 androidx.media3.common.util;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.NoSuchElementException;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link LongArrayQueue}. */
@RunWith(AndroidJUnit4.class)
public class LongArrayQueueTest {
@Test
public void capacity() {
LongArrayQueue queue = new LongArrayQueue(2);
assertThat(queue.capacity()).isEqualTo(2);
}
@Test
public void capacity_setTo0_increasedTo1() {
LongArrayQueue queue = new LongArrayQueue(0);
assertThat(queue.capacity()).isEqualTo(1);
}
@Test
public void capacity_setToNextPowerOf2() {
LongArrayQueue queue = new LongArrayQueue(6);
assertThat(queue.capacity()).isEqualTo(8);
}
@Test
public void capacity_invalidMinCapacity_throws() {
assertThrows(IllegalArgumentException.class, () -> new LongArrayQueue(-1));
}
@Test
public void add_beyondInitialCapacity_doublesCapacity() {
LongArrayQueue queue = new LongArrayQueue(2);
queue.add(0);
queue.add(1);
queue.add(2);
assertThat(queue.size()).isEqualTo(3);
assertThat(queue.capacity()).isEqualTo(4);
}
@Test
public void isEmpty_afterConstruction_returnsTrue() {
LongArrayQueue queue = new LongArrayQueue();
assertThat(queue.isEmpty()).isTrue();
}
@Test
public void isEmpty_afterAddition_returnsFalse() {
LongArrayQueue queue = new LongArrayQueue();
queue.add(0);
assertThat(queue.isEmpty()).isFalse();
}
@Test
public void isEmpty_afterRemoval_returnsTrue() {
LongArrayQueue queue = new LongArrayQueue();
queue.add(0);
queue.remove();
assertThat(queue.isEmpty()).isTrue();
}
@Test
public void remove_onEmptyQueue_throwsException() {
LongArrayQueue queue = new LongArrayQueue();
assertThrows(NoSuchElementException.class, queue::remove);
}
@Test
public void remove_returnsCorrectItem() {
LongArrayQueue queue = new LongArrayQueue();
queue.add(20);
assertThat(queue.remove()).isEqualTo(20);
}
@Test
public void element_withEmptyQueue_throws() {
LongArrayQueue queue = new LongArrayQueue();
assertThrows(NoSuchElementException.class, queue::element);
}
@Test
public void element_returnsQueueHead() {
LongArrayQueue queue = new LongArrayQueue();
queue.add(5);
assertThat(queue.element()).isEqualTo(5);
}
@Test
public void clear_resetsQueue() {
LongArrayQueue queue = new LongArrayQueue();
queue.add(123);
queue.clear();
assertThat(queue.size()).isEqualTo(0);
assertThat(queue.isEmpty()).isTrue();
}
}

View File

@ -35,6 +35,7 @@ import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.LongArrayQueue;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
@ -81,8 +82,8 @@ public final class DefaultVideoCompositor implements VideoCompositor {
private boolean allInputsEnded; // Whether all inputSources have signaled end of input.
private final TexturePool outputTexturePool;
private final Queue<Long> outputTextureTimestamps; // Synchronized with outputTexturePool.
private final Queue<Long> syncObjects; // Synchronized with outputTexturePool.
private final LongArrayQueue outputTextureTimestamps; // Synchronized with outputTexturePool.
private final LongArrayQueue syncObjects; // Synchronized with outputTexturePool.
// Only used on the GL Thread.
private @MonotonicNonNull EGLContext eglContext;
@ -111,8 +112,8 @@ public final class DefaultVideoCompositor implements VideoCompositor {
inputSources = new ArrayList<>();
outputTexturePool =
new TexturePool(/* useHighPrecisionColorComponents= */ false, textureOutputCapacity);
outputTextureTimestamps = new ArrayDeque<>(textureOutputCapacity);
syncObjects = new ArrayDeque<>(textureOutputCapacity);
outputTextureTimestamps = new LongArrayQueue(textureOutputCapacity);
syncObjects = new LongArrayQueue(textureOutputCapacity);
boolean ownsExecutor = executorService == null;
ExecutorService instanceExecutorService =
@ -374,7 +375,7 @@ public final class DefaultVideoCompositor implements VideoCompositor {
private synchronized void releaseOutputFrameInternal(long presentationTimeUs)
throws VideoFrameProcessingException, GlUtil.GlException {
while (outputTexturePool.freeTextureCount() < outputTexturePool.capacity()
&& checkNotNull(outputTextureTimestamps.peek()) <= presentationTimeUs) {
&& outputTextureTimestamps.element() <= presentationTimeUs) {
outputTexturePool.freeTexture();
outputTextureTimestamps.remove();
GlUtil.deleteSyncObject(syncObjects.remove());

View File

@ -41,10 +41,10 @@ import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.LongArrayQueue;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
@ -88,8 +88,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final VideoFrameProcessor.Listener videoFrameProcessorListener;
private final Queue<Pair<GlTextureInfo, Long>> availableFrames;
private final TexturePool outputTexturePool;
private final Queue<Long> outputTextureTimestamps; // Synchronized with outputTexturePool.
private final Queue<Long> syncObjects;
private final LongArrayQueue outputTextureTimestamps; // Synchronized with outputTexturePool.
private final LongArrayQueue syncObjects;
@Nullable private final DefaultVideoFrameProcessor.TextureOutputListener textureOutputListener;
private int inputWidth;
@ -148,8 +148,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
boolean useHighPrecisionColorComponents = ColorInfo.isTransferHdr(outputColorInfo);
outputTexturePool = new TexturePool(useHighPrecisionColorComponents, textureOutputCapacity);
outputTextureTimestamps = new ArrayDeque<>(textureOutputCapacity);
syncObjects = new ArrayDeque<>(textureOutputCapacity);
outputTextureTimestamps = new LongArrayQueue(textureOutputCapacity);
syncObjects = new LongArrayQueue(textureOutputCapacity);
}
@Override
@ -227,7 +227,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private void releaseOutputFrameInternal(long presentationTimeUs) throws GlUtil.GlException {
checkState(textureOutputListener != null);
while (outputTexturePool.freeTextureCount() < outputTexturePool.capacity()
&& checkNotNull(outputTextureTimestamps.peek()) <= presentationTimeUs) {
&& outputTextureTimestamps.element() <= presentationTimeUs) {
outputTexturePool.freeTexture();
outputTextureTimestamps.remove();
GlUtil.deleteSyncObject(syncObjects.remove());

View File

@ -50,6 +50,7 @@ dependencies {
api project(modulePrefix + 'lib-extractor')
api project(modulePrefix + 'lib-database')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.collection:collection:' + androidxCollectionVersion
implementation 'androidx.core:core:' + androidxCoreVersion
implementation 'androidx.exifinterface:exifinterface:1.3.6'
compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version

View File

@ -26,6 +26,7 @@ import android.os.HandlerThread;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.collection.CircularIntArray;
import androidx.media3.common.util.Util;
import java.util.ArrayDeque;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -39,10 +40,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private @MonotonicNonNull Handler handler;
@GuardedBy("lock")
private final IntArrayQueue availableInputBuffers;
private final CircularIntArray availableInputBuffers;
@GuardedBy("lock")
private final IntArrayQueue availableOutputBuffers;
private final CircularIntArray availableOutputBuffers;
@GuardedBy("lock")
private final ArrayDeque<MediaCodec.BufferInfo> bufferInfos;
@ -81,8 +82,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* package */ AsynchronousMediaCodecCallback(HandlerThread callbackThread) {
this.lock = new Object();
this.callbackThread = callbackThread;
this.availableInputBuffers = new IntArrayQueue();
this.availableOutputBuffers = new IntArrayQueue();
this.availableInputBuffers = new CircularIntArray();
this.availableOutputBuffers = new CircularIntArray();
this.bufferInfos = new ArrayDeque<>();
this.formats = new ArrayDeque<>();
}
@ -132,7 +133,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} else {
return availableInputBuffers.isEmpty()
? MediaCodec.INFO_TRY_AGAIN_LATER
: availableInputBuffers.remove();
: availableInputBuffers.popFirst();
}
}
}
@ -152,7 +153,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
if (availableOutputBuffers.isEmpty()) {
return MediaCodec.INFO_TRY_AGAIN_LATER;
} else {
int bufferIndex = availableOutputBuffers.remove();
int bufferIndex = availableOutputBuffers.popFirst();
if (bufferIndex >= 0) {
checkStateNotNull(currentFormat);
MediaCodec.BufferInfo nextBufferInfo = bufferInfos.remove();
@ -204,7 +205,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
synchronized (lock) {
availableInputBuffers.add(index);
availableInputBuffers.addLast(index);
}
}
@ -215,7 +216,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
addOutputFormat(pendingOutputFormat);
pendingOutputFormat = null;
}
availableOutputBuffers.add(index);
availableOutputBuffers.addLast(index);
bufferInfos.add(info);
}
}
@ -278,7 +279,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@GuardedBy("lock")
private void addOutputFormat(MediaFormat mediaFormat) {
availableOutputBuffers.add(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
availableOutputBuffers.addLast(MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
formats.add(mediaFormat);
}

View File

@ -37,13 +37,13 @@ import androidx.media3.common.SurfaceInfo;
import androidx.media3.common.VideoFrameProcessingException;
import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.VideoSize;
import androidx.media3.common.util.LongArrayQueue;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.TimedValueQueue;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
@ -168,8 +168,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final Context context;
private final RenderControl renderControl;
private final VideoFrameProcessor videoFrameProcessor;
// TODO b/293447478 - Use a queue for primitive longs to avoid the cost of boxing to Long.
private final ArrayDeque<Long> processedFramesBufferTimestampsUs;
private final LongArrayQueue processedFramesBufferTimestampsUs;
private final TimedValueQueue<Long> streamOffsets;
private final TimedValueQueue<VideoSize> videoSizeChanges;
private final Handler handler;
@ -219,7 +218,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
throws VideoFrameProcessingException {
this.context = context;
this.renderControl = renderControl;
processedFramesBufferTimestampsUs = new ArrayDeque<>();
processedFramesBufferTimestampsUs = new LongArrayQueue();
streamOffsets = new TimedValueQueue<>();
videoSizeChanges = new TimedValueQueue<>();
// TODO b/226330223 - Investigate increasing frame count when frame dropping is
@ -362,7 +361,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override
public void render(long positionUs, long elapsedRealtimeUs) {
while (!processedFramesBufferTimestampsUs.isEmpty()) {
long bufferPresentationTimeUs = checkNotNull(processedFramesBufferTimestampsUs.peek());
long bufferPresentationTimeUs = processedFramesBufferTimestampsUs.element();
// check whether this buffer comes with a new stream offset.
if (maybeUpdateOutputStreamOffset(bufferPresentationTimeUs)) {
renderedFirstFrame = false;

View File

@ -1,140 +0,0 @@
/*
* Copyright (C) 2019 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 androidx.media3.exoplayer.mediacodec;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.NoSuchElementException;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link IntArrayQueue}. */
@RunWith(AndroidJUnit4.class)
public class IntArrayQueueTest {
@Test
public void add_willDoubleCapacity() {
IntArrayQueue queue = new IntArrayQueue();
int capacity = queue.capacity();
for (int i = 0; i <= capacity; i++) {
queue.add(i);
}
assertThat(queue.capacity()).isEqualTo(2 * capacity);
assertThat(queue.size()).isEqualTo(capacity + 1);
}
@Test
public void isEmpty_returnsTrueAfterConstruction() {
IntArrayQueue queue = new IntArrayQueue();
assertThat(queue.isEmpty()).isTrue();
}
@Test
public void isEmpty_returnsFalseAfterAddition() {
IntArrayQueue queue = new IntArrayQueue();
queue.add(0);
assertThat(queue.isEmpty()).isFalse();
}
@Test
public void isEmpty_returnsFalseAfterRemoval() {
IntArrayQueue queue = new IntArrayQueue();
queue.add(0);
queue.remove();
assertThat(queue.isEmpty()).isTrue();
}
@Test
public void remove_onEmptyQueue_throwsException() {
IntArrayQueue queue = new IntArrayQueue();
try {
queue.remove();
fail();
} catch (NoSuchElementException expected) {
// expected
}
}
@Test
public void remove_returnsCorrectItem() {
IntArrayQueue queue = new IntArrayQueue();
int value = 20;
queue.add(value);
assertThat(queue.remove()).isEqualTo(value);
}
@Test
public void remove_untilIsEmpty() {
IntArrayQueue queue = new IntArrayQueue();
for (int i = 0; i < 1024; i++) {
queue.add(i);
}
int expectedRemoved = 0;
while (!queue.isEmpty()) {
if (expectedRemoved == 15) {
System.out.println("foo");
}
int removed = queue.remove();
assertThat(removed).isEqualTo(expectedRemoved++);
}
}
@Test
public void remove_withResize_returnsCorrectItem() {
IntArrayQueue queue = new IntArrayQueue();
int nextToAdd = 0;
while (queue.size() < queue.capacity()) {
queue.add(nextToAdd++);
}
queue.remove();
queue.remove();
// This will force the queue to wrap-around and then resize
int howManyToResize = queue.capacity() - queue.size() + 1;
for (int i = 0; i < howManyToResize; i++) {
queue.add(nextToAdd++);
}
assertThat(queue.remove()).isEqualTo(2);
}
@Test
public void clear_resetsQueue() {
IntArrayQueue queue = new IntArrayQueue();
// Add items until array re-sizes twice (capacity grows by 4)
for (int i = 0; i < 1024; i++) {
queue.add(i);
}
queue.clear();
assertThat(queue.size()).isEqualTo(0);
assertThat(queue.isEmpty()).isTrue();
}
}