/* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.imagepipeline.producers; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import java.io.Closeable; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArraySet; import android.util.Pair; import com.facebook.common.internal.Preconditions; import com.facebook.common.internal.Sets; import com.facebook.common.internal.VisibleForTesting; import com.facebook.imagepipeline.common.Priority; /** * Producer for combining multiple identical requests into a single request. * * <p>Requests using the same key will be combined into a single request. This request is only * cancelled when all underlying requests are cancelled, and returns values to all underlying * consumers. If the request has already return one or more results but has not finished, then * any requests with the same key will have the most recent result returned to them immediately. * * @param <K> type of the key * @param <T> type of the closeable reference result that is returned to this producer */ @ThreadSafe public abstract class MultiplexProducer<K, T extends Closeable> implements Producer<T> { /** * Map of multiplexers guarded by "this" lock. The lock should be used only to synchronize * accesses to this map. In particular, no callbacks or third party code should be run under * "this" lock. * * <p> The map might contain entries in progress, entries in progress for which cancellation * has been requested and ignored, or cancelled entries for which onCancellation has not been * called yet. */ @GuardedBy("this") @VisibleForTesting final Map<K, Multiplexer> mMultiplexers; private final Producer<T> mInputProducer; protected MultiplexProducer(Producer<T> inputProducer) { mInputProducer = inputProducer; mMultiplexers = new HashMap<>(); } @Override public void produceResults(Consumer<T> consumer, ProducerContext context) { K key = getKey(context); Multiplexer multiplexer; boolean createdNewMultiplexer; // We do want to limit scope of this lock to guard only accesses to mMultiplexers map. // However what we would like to do here is to atomically lookup mMultiplexers, add new // consumer to consumers set associated with the map's entry and call consumer's callback with // last intermediate result. We should not do all of those things under this lock. do { createdNewMultiplexer = false; synchronized (this) { multiplexer = getExistingMultiplexer(key); if (multiplexer == null) { multiplexer = createAndPutNewMultiplexer(key); createdNewMultiplexer = true; } } // addNewConsumer may call consumer's onNewResult method immediately. For this reason // we release "this" lock. If multiplexer is removed from mMultiplexers in the meantime, // which is not very probable, then addNewConsumer will fail and we will be able to retry. } while (!multiplexer.addNewConsumer(consumer, context)); if (createdNewMultiplexer) { multiplexer.startInputProducerIfHasAttachedConsumers(); } } private synchronized Multiplexer getExistingMultiplexer(K key) { return mMultiplexers.get(key); } private synchronized Multiplexer createAndPutNewMultiplexer(K key) { Multiplexer multiplexer = new Multiplexer(key); mMultiplexers.put(key, multiplexer); return multiplexer; } private synchronized void removeMultiplexer(K key, Multiplexer multiplexer) { if (mMultiplexers.get(key) == multiplexer) { mMultiplexers.remove(key); } } protected abstract K getKey(ProducerContext producerContext); protected abstract T cloneOrNull(T object); /** * Multiplexes same requests - passes the same result to multiple consumers, manages cancellation * and maintains last intermediate result. * * <p> Multiplexed computation might be in one of 3 states: * <ul> * <li> in progress </li> * <li> in progress after requesting cancellation (cancellation has been denied) </li> * <li> cancelled, but without onCancellation method being called yet </li> * </ul> * * <p> In last case new consumers may be added before onCancellation is called. When it is, the * Multiplexer has to check if it is the case and start next producer once again if so. */ @VisibleForTesting class Multiplexer { private final K mKey; /** * Set of consumer-context pairs participating in multiplexing. Cancelled pairs * are removed from the set. * * <p> Following invariant is maintained: if mConsumerContextPairs is not empty, then this * instance of Multiplexer is present in mMultiplexers map. This way all ongoing multiplexed * requests might be attached to by other requests * * <p> A Multiplexer is removed from the map only if * <ul> * <li> final result is received </li> * <li> error is received </li> * <li> cancellation notification is received and mConsumerContextPairs is empty </li> * </ul> */ private final CopyOnWriteArraySet<Pair<Consumer<T>, ProducerContext>> mConsumerContextPairs; @GuardedBy("Multiplexer.this") @Nullable private T mLastIntermediateResult; @GuardedBy("Multiplexer.this") private float mLastProgress; /** * Producer context used for cancelling producers below MultiplexProducers, and for setting * whether the request is a prefetch or not. * * <p> If not null, then underlying computation has been started, and no onCancellation callback * has been received yet. */ @GuardedBy("Multiplexer.this") @Nullable private BaseProducerContext mMultiplexProducerContext; /** * Currently used consumer of next producer. * * <p> The same Multiplexer might call mInputProducer.produceResults multiple times when * cancellation happens. This field is used to guard against late callbacks. * * <p> If not null, then underlying computation has been started, and no onCancellation * callback has been received yet. */ @GuardedBy("Multiplexer.this") @Nullable private ForwardingConsumer mForwardingConsumer; public Multiplexer(K key) { mConsumerContextPairs = Sets.newCopyOnWriteArraySet(); mKey = key; } /** * Tries to add consumer to set of consumers participating in multiplexing. If successful and * appropriate intermediate result is already known, then it will be passed to the consumer. * * <p> This function will fail and return false if the multiplexer is not present in * mMultiplexers map. * * @return true if consumer was added successfully */ public boolean addNewConsumer( final Consumer<T> consumer, final ProducerContext producerContext) { final Pair<Consumer<T>, ProducerContext> consumerContextPair = Pair.create(consumer, producerContext); T lastIntermediateResult; final List<ProducerContextCallbacks> prefetchCallbacks; final List<ProducerContextCallbacks> priorityCallbacks; final List<ProducerContextCallbacks> intermediateResultsCallbacks; final float lastProgress; // Check if Multiplexer is still in mMultiplexers map, and if so add new consumer. // Also store current intermediate result - we will notify consumer after acquiring // appropriate lock. synchronized (Multiplexer.this) { if (getExistingMultiplexer(mKey) != this) { return false; } mConsumerContextPairs.add(consumerContextPair); prefetchCallbacks = updateIsPrefetch(); priorityCallbacks = updatePriority(); intermediateResultsCallbacks = updateIsIntermediateResultExpected(); lastIntermediateResult = mLastIntermediateResult; lastProgress = mLastProgress; } BaseProducerContext.callOnIsPrefetchChanged(prefetchCallbacks); BaseProducerContext.callOnPriorityChanged(priorityCallbacks); BaseProducerContext.callOnIsIntermediateResultExpectedChanged(intermediateResultsCallbacks); synchronized (consumerContextPair) { // check if last result changed in the mean time. In such case we should not propagate it synchronized (Multiplexer.this) { if (lastIntermediateResult != mLastIntermediateResult) { lastIntermediateResult = null; } else if (lastIntermediateResult != null) { lastIntermediateResult = cloneOrNull(lastIntermediateResult); } } if (lastIntermediateResult != null) { if (lastProgress > 0) { consumer.onProgressUpdate(lastProgress); } consumer.onNewResult(lastIntermediateResult, false); closeSafely(lastIntermediateResult); } } addCallbacks(consumerContextPair, producerContext); return true; } /** * Register callbacks to be called when cancellation of consumer is requested, or if the * prefetch status of the consumer changes. */ private void addCallbacks( final Pair<Consumer<T>, ProducerContext> consumerContextPair, final ProducerContext producerContext) { producerContext.addCallbacks( new BaseProducerContextCallbacks() { @Override public void onCancellationRequested() { BaseProducerContext contextToCancel = null; List<ProducerContextCallbacks> isPrefetchCallbacks = null; List<ProducerContextCallbacks> priorityCallbacks = null; List<ProducerContextCallbacks> isIntermediateResultExpectedCallbacks = null; final boolean pairWasRemoved; synchronized (Multiplexer.this) { pairWasRemoved = mConsumerContextPairs.remove(consumerContextPair); if (pairWasRemoved) { if (mConsumerContextPairs.isEmpty()) { contextToCancel = mMultiplexProducerContext; } else { isPrefetchCallbacks = updateIsPrefetch(); priorityCallbacks = updatePriority(); isIntermediateResultExpectedCallbacks = updateIsIntermediateResultExpected(); } } } BaseProducerContext.callOnIsPrefetchChanged(isPrefetchCallbacks); BaseProducerContext.callOnPriorityChanged(priorityCallbacks); BaseProducerContext.callOnIsIntermediateResultExpectedChanged( isIntermediateResultExpectedCallbacks); if (contextToCancel != null) { contextToCancel.cancel(); } if (pairWasRemoved) { consumerContextPair.first.onCancellation(); } } @Override public void onIsPrefetchChanged() { BaseProducerContext.callOnIsPrefetchChanged(updateIsPrefetch()); } @Override public void onIsIntermediateResultExpectedChanged() { BaseProducerContext.callOnIsIntermediateResultExpectedChanged( updateIsIntermediateResultExpected()); } @Override public void onPriorityChanged() { BaseProducerContext.callOnPriorityChanged(updatePriority()); } }); } /** * Starts next producer if it is not started yet and there is at least one Consumer waiting for * the data. If all consumers are cancelled, then this multiplexer is removed from mRequest * map to clean up. */ private void startInputProducerIfHasAttachedConsumers() { BaseProducerContext multiplexProducerContext; ForwardingConsumer forwardingConsumer; synchronized (Multiplexer.this) { Preconditions.checkArgument(mMultiplexProducerContext == null); Preconditions.checkArgument(mForwardingConsumer == null); // Cleanup if all consumers have been cancelled before this method was called if (mConsumerContextPairs.isEmpty()) { removeMultiplexer(mKey, this); return; } ProducerContext producerContext = mConsumerContextPairs.iterator().next().second; mMultiplexProducerContext = new BaseProducerContext( producerContext.getImageRequest(), producerContext.getId(), producerContext.getListener(), producerContext.getCallerContext(), producerContext.getLowestPermittedRequestLevel(), computeIsPrefetch(), computeIsIntermediateResultExpected(), computePriority()); mForwardingConsumer = new ForwardingConsumer(); multiplexProducerContext = mMultiplexProducerContext; forwardingConsumer = mForwardingConsumer; } mInputProducer.produceResults( forwardingConsumer, multiplexProducerContext); } @Nullable private synchronized List<ProducerContextCallbacks> updateIsPrefetch() { if (mMultiplexProducerContext == null) { return null; } return mMultiplexProducerContext.setIsPrefetchNoCallbacks(computeIsPrefetch()); } private synchronized boolean computeIsPrefetch() { for (Pair<Consumer<T>, ProducerContext> pair : mConsumerContextPairs) { if (!pair.second.isPrefetch()) { return false; } } return true; } @Nullable private synchronized List<ProducerContextCallbacks> updateIsIntermediateResultExpected() { if (mMultiplexProducerContext == null) { return null; } return mMultiplexProducerContext.setIsIntermediateResultExpectedNoCallbacks( computeIsIntermediateResultExpected()); } private synchronized boolean computeIsIntermediateResultExpected() { for (Pair<Consumer<T>, ProducerContext> pair : mConsumerContextPairs) { if (pair.second.isIntermediateResultExpected()) { return true; } } return false; } @Nullable private synchronized List<ProducerContextCallbacks> updatePriority() { if (mMultiplexProducerContext == null) { return null; } return mMultiplexProducerContext.setPriorityNoCallbacks(computePriority()); } private synchronized Priority computePriority() { Priority priority = Priority.LOW; for (Pair<Consumer<T>, ProducerContext> pair : mConsumerContextPairs) { priority = Priority.getHigherPriority(priority, pair.second.getPriority()); } return priority; } public void onFailure(final ForwardingConsumer consumer, final Throwable t) { Iterator<Pair<Consumer<T>, ProducerContext>> iterator; synchronized (Multiplexer.this) { // check for late callbacks if (mForwardingConsumer != consumer) { return; } iterator = mConsumerContextPairs.iterator(); mConsumerContextPairs.clear(); removeMultiplexer(mKey, this); closeSafely(mLastIntermediateResult); mLastIntermediateResult = null; } while (iterator.hasNext()) { Pair<Consumer<T>, ProducerContext> pair = iterator.next(); synchronized (pair) { pair.first.onFailure(t); } } } public void onNextResult( final ForwardingConsumer consumer, final T closeableObject, final boolean isFinal) { Iterator<Pair<Consumer<T>, ProducerContext>> iterator; synchronized (Multiplexer.this) { // check for late callbacks if (mForwardingConsumer != consumer) { return; } closeSafely(mLastIntermediateResult); mLastIntermediateResult = null; iterator = mConsumerContextPairs.iterator(); if (!isFinal) { mLastIntermediateResult = cloneOrNull(closeableObject); } else { mConsumerContextPairs.clear(); removeMultiplexer(mKey, this); } } while (iterator.hasNext()) { Pair<Consumer<T>, ProducerContext> pair = iterator.next(); synchronized (pair) { pair.first.onNewResult(closeableObject, isFinal); } } } public void onCancelled(final ForwardingConsumer forwardingConsumer) { synchronized (Multiplexer.this) { // check for late callbacks if (mForwardingConsumer != forwardingConsumer) { return; } mForwardingConsumer = null; mMultiplexProducerContext = null; closeSafely(mLastIntermediateResult); mLastIntermediateResult = null; } startInputProducerIfHasAttachedConsumers(); } public void onProgressUpdate(ForwardingConsumer forwardingConsumer, float progress) { Iterator<Pair<Consumer<T>, ProducerContext>> iterator; synchronized (Multiplexer.this) { // check for late callbacks if (mForwardingConsumer != forwardingConsumer) { return; } mLastProgress = progress; iterator = mConsumerContextPairs.iterator(); } while (iterator.hasNext()) { Pair<Consumer<T>, ProducerContext> pair = iterator.next(); synchronized (pair) { pair.first.onProgressUpdate(progress); } } } private void closeSafely(Closeable obj) { try { if (obj != null) { obj.close(); } } catch (IOException ioe) { throw new RuntimeException(ioe); } } /** * Forwards {@link Consumer} methods to Multiplexer. */ private class ForwardingConsumer extends BaseConsumer<T> { @Override protected void onNewResultImpl(T newResult, boolean isLast) { Multiplexer.this.onNextResult(this, newResult, isLast); } @Override protected void onFailureImpl(Throwable t) { Multiplexer.this.onFailure(this, t); } @Override protected void onCancellationImpl() { Multiplexer.this.onCancelled(this); } @Override protected void onProgressUpdateImpl(float progress) { Multiplexer.this.onProgressUpdate(this, progress); } } } }