From b145a9b35e5409a3421549e9d65d23eff48fc47b Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 9 Jul 2024 10:22:04 -0700 Subject: [PATCH] Limit maximum number of parallel metadata retrievals. The operation almost always acquires resources straight-away, for example a new thread or file access. This means starting many metadata retrievals in parallel easily causes resource contention. This problem can be alleviated by limiting the maximum number of parallel retrievals. Local testing showed a 20% speedup for local file retrievals when limited to 5 in parallel. Any more parallel retrievals did not show any improvement or started getting slower again the more operations are allowed. PiperOrigin-RevId: 650674685 --- .../media3/exoplayer/MetadataRetriever.java | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetriever.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetriever.java index 1600ab01fe..7b7d4c0047 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetriever.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetriever.java @@ -16,6 +16,7 @@ package androidx.media3.exoplayer; +import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; @@ -24,6 +25,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.media3.common.C; @@ -44,12 +46,18 @@ import androidx.media3.extractor.ExtractorsFactory; import androidx.media3.extractor.mp4.Mp4Extractor; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.concurrent.atomic.AtomicInteger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Retrieves the static metadata of {@link MediaItem MediaItems}. */ @UnstableApi public final class MetadataRetriever { + /** The default number of maximum parallel retrievals. */ + public static final int DEFAULT_MAXIMUM_PARALLEL_RETRIEVALS = 5; + private MetadataRetriever() {} /** @@ -97,7 +105,19 @@ public final class MetadataRetriever { private static ListenableFuture retrieveMetadata( MediaSource.Factory mediaSourceFactory, MediaItem mediaItem, Clock clock) { - return new MetadataRetrieverInternal(mediaSourceFactory, clock).retrieveMetadata(mediaItem); + return new MetadataRetrieverInternal(mediaSourceFactory, mediaItem, clock).retrieveMetadata(); + } + + /** + * Sets the maximum number of metadata retrievals run in parallel. + * + *

The default is {@link #DEFAULT_MAXIMUM_PARALLEL_RETRIEVALS}. + * + * @param maximumParallelRetrievals The maximum number of parallel retrievals. + */ + public static void setMaximumParallelRetrievals(int maximumParallelRetrievals) { + checkArgument(maximumParallelRetrievals >= 1); + SharedWorkerThread.MAX_PARALLEL_RETRIEVALS.set(maximumParallelRetrievals); } private static final class MetadataRetrieverInternal { @@ -110,22 +130,29 @@ public final class MetadataRetriever { private static final SharedWorkerThread SHARED_WORKER_THREAD = new SharedWorkerThread(); private final MediaSource.Factory mediaSourceFactory; + private final MediaItem mediaItem; private final HandlerWrapper mediaSourceHandler; private final SettableFuture trackGroupsFuture; - public MetadataRetrieverInternal(MediaSource.Factory mediaSourceFactory, Clock clock) { + public MetadataRetrieverInternal( + MediaSource.Factory mediaSourceFactory, MediaItem mediaItem, Clock clock) { this.mediaSourceFactory = mediaSourceFactory; + this.mediaItem = mediaItem; Looper workerThreadLooper = SHARED_WORKER_THREAD.addWorker(); mediaSourceHandler = clock.createHandler(workerThreadLooper, new MediaSourceHandlerCallback()); trackGroupsFuture = SettableFuture.create(); } - public ListenableFuture retrieveMetadata(MediaItem mediaItem) { - mediaSourceHandler.obtainMessage(MESSAGE_PREPARE_SOURCE, mediaItem).sendToTarget(); + public ListenableFuture retrieveMetadata() { + SHARED_WORKER_THREAD.startRetrieval(this); return trackGroupsFuture; } + public void start() { + mediaSourceHandler.obtainMessage(MESSAGE_PREPARE_SOURCE, mediaItem).sendToTarget(); + } + private final class MediaSourceHandlerCallback implements Handler.Callback { private static final int ERROR_POLL_INTERVAL_MS = 100; @@ -229,9 +256,18 @@ public final class MetadataRetriever { private static final class SharedWorkerThread { + public static final AtomicInteger MAX_PARALLEL_RETRIEVALS = + new AtomicInteger(DEFAULT_MAXIMUM_PARALLEL_RETRIEVALS); + + private final Deque pendingRetrievals; + @Nullable private HandlerThread mediaSourceThread; private int referenceCount; + public SharedWorkerThread() { + pendingRetrievals = new ArrayDeque<>(); + } + public synchronized Looper addWorker() { if (mediaSourceThread == null) { checkState(referenceCount == 0); @@ -242,10 +278,29 @@ public final class MetadataRetriever { return mediaSourceThread.getLooper(); } + public synchronized void startRetrieval(MetadataRetrieverInternal retrieval) { + pendingRetrievals.addLast(retrieval); + maybeStartNewRetrieval(); + } + public synchronized void removeWorker() { if (--referenceCount == 0) { checkNotNull(mediaSourceThread).quit(); mediaSourceThread = null; + } else { + maybeStartNewRetrieval(); + } + } + + @GuardedBy("this") + private void maybeStartNewRetrieval() { + if (pendingRetrievals.isEmpty()) { + return; + } + int activeRetrievals = referenceCount - pendingRetrievals.size(); + if (activeRetrievals < MAX_PARALLEL_RETRIEVALS.get()) { + MetadataRetrieverInternal retrieval = pendingRetrievals.removeFirst(); + retrieval.start(); } } }