All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.bumptech.glide.integration.cronet.ChromiumRequestSerializer Maven / Gradle / Ivy

There is a newer version: 5.0.0-rc01
Show newest version
package com.bumptech.glide.integration.cronet;

import android.util.Log;
import androidx.annotation.Nullable;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.HttpException;
import com.bumptech.glide.load.engine.executor.GlideExecutor;
import com.bumptech.glide.load.engine.executor.GlideExecutor.UncaughtThrowableStrategy;
import com.bumptech.glide.load.model.GlideUrl;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import org.chromium.net.CronetException;
import org.chromium.net.UrlRequest;
import org.chromium.net.UrlRequest.Callback;
import org.chromium.net.UrlResponseInfo;

/**
 * Ensures that two simultaneous requests for exactly the same url make only a single http request.
 *
 * 

Requests are started by Glide on multiple threads in a thread pool. An arbitrary number of * threads may attempt to start or cancel requests for one or more urls at once. Our goal is to * ensure: *

  • * *
      * A new request is made to cronet if a url is requested and no cronet request for that url is * in progress *
    * *
      * Subsequent requests for in progress urls do not make new requests to cronet, but are notified * when the existing cronet request completes. *
    * *
      * Cancelling a single request does not cancel the cronet request if multiple requests for the url * have been made, but cancelling all requests for a url does cancel the cronet request. *
    */ final class ChromiumRequestSerializer { private static final String TAG = "ChromiumSerializer"; private static final Map GLIDE_TO_CHROMIUM_PRIORITY = new EnumMap<>(Priority.class); // Memoized so that all callers can share an instance. // Suppliers.memoize() is thread safe. See google3/java/com/google/common/base/Suppliers.java private static final Supplier GLIDE_EXECUTOR_SUPPLIER = Suppliers.memoize( new Supplier() { @Override public GlideExecutor get() { // Allow network operations, but use a single thread. See b/37684357. return GlideExecutor.newSourceExecutor( 1 /*threadCount*/, "chromium-serializer", UncaughtThrowableStrategy.DEFAULT); } }); private abstract static class PriorityRunnable implements Runnable, Comparable { private final int priority; private PriorityRunnable(Priority priority) { this.priority = priority.ordinal(); } @Override public final int compareTo(PriorityRunnable another) { if (another.priority > this.priority) { return -1; } else if (another.priority < this.priority) { return 1; } return 0; } } static { GLIDE_TO_CHROMIUM_PRIORITY.put(Priority.IMMEDIATE, UrlRequest.Builder.REQUEST_PRIORITY_HIGHEST); GLIDE_TO_CHROMIUM_PRIORITY.put(Priority.HIGH, UrlRequest.Builder.REQUEST_PRIORITY_MEDIUM); GLIDE_TO_CHROMIUM_PRIORITY.put(Priority.NORMAL, UrlRequest.Builder.REQUEST_PRIORITY_LOW); GLIDE_TO_CHROMIUM_PRIORITY.put(Priority.LOW, UrlRequest.Builder.REQUEST_PRIORITY_LOWEST); } private final JobPool jobPool = new JobPool(); private final Map jobs = new HashMap<>(); private final CronetRequestFactory requestFactory; @Nullable private final DataLogger dataLogger; ChromiumRequestSerializer(CronetRequestFactory requestFactory, @Nullable DataLogger dataLogger) { this.requestFactory = requestFactory; this.dataLogger = dataLogger; } void startRequest(Priority priority, GlideUrl glideUrl, Listener listener) { boolean startNewRequest = false; Job job; synchronized (this) { job = jobs.get(glideUrl); if (job == null) { startNewRequest = true; job = jobPool.get(glideUrl); jobs.put(glideUrl, job); } job.addListener(listener); } if (startNewRequest) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Fetching image url using cronet" + " url: " + glideUrl); } job.priority = priority; job.request = requestFactory .newRequest( glideUrl.toStringUrl(), GLIDE_TO_CHROMIUM_PRIORITY.get(priority), glideUrl.getHeaders(), job) .build(); job.request.start(); // It's possible we will be cancelled between adding the job to the job list and starting the // corresponding request. We don't want to hold a lock while starting the request, because // starting the request may block for a while and we need cancellation to happen quickly (it // happens on the main thread). if (job.isCancelled) { job.request.cancel(); } } } void cancelRequest(GlideUrl glideUrl, Listener listener) { final Job job; synchronized (this) { job = jobs.get(glideUrl); } // Jobs may be cancelled before they are started. if (job != null) { job.removeListener(listener); } } private static IOException getExceptionIfFailed( UrlResponseInfo info, IOException e, boolean wasCancelled) { if (wasCancelled) { return null; } else if (e != null) { return e; } else if (info.getHttpStatusCode() != HttpURLConnection.HTTP_OK) { return new HttpException(info.getHttpStatusCode()); } return null; } /** * Manages a single cronet request for a single url with one or more active listeners. * *

    Cronet requests are cancelled when all listeners are removed. */ private class Job extends Callback { private final List listeners = new ArrayList<>(2); private GlideUrl glideUrl; private Priority priority; private long startTime; private UrlRequest request; private long endTimeMs; private long responseStartTimeMs; private volatile boolean isCancelled; private BufferQueue.Builder builder; void init(GlideUrl glideUrl) { startTime = System.currentTimeMillis(); this.glideUrl = glideUrl; } void addListener(Listener listener) { synchronized (ChromiumRequestSerializer.this) { listeners.add(listener); } } void removeListener(Listener listener) { synchronized (ChromiumRequestSerializer.this) { // Note: multiple cancellation calls + a subsequent request for a url may mean we fail to // remove the listener here because that listener is actually for a previous request. Since // that race is harmless, we simply ignore it. listeners.remove(listener); if (listeners.isEmpty()) { isCancelled = true; jobs.remove(glideUrl); } } // The request may not have started yet, so request may be null. if (isCancelled) { UrlRequest localRequest = request; if (localRequest != null) { localRequest.cancel(); } } } @Override public void onRedirectReceived(UrlRequest urlRequest, UrlResponseInfo urlResponseInfo, String s) throws Exception { urlRequest.followRedirect(); } @Override public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { responseStartTimeMs = System.currentTimeMillis(); builder = BufferQueue.builder(); request.read(builder.getFirstBuffer(info)); } @Override public void onReadCompleted( UrlRequest urlRequest, UrlResponseInfo urlResponseInfo, ByteBuffer byteBuffer) throws Exception { request.read(builder.getNextBuffer(byteBuffer)); } @Override public void onSucceeded(UrlRequest request, final UrlResponseInfo info) { GLIDE_EXECUTOR_SUPPLIER .get() .execute( new PriorityRunnable(priority) { @Override public void run() { onRequestFinished( info, null /*exception*/, false /*wasCancelled*/, builder.build().coalesceToBuffer()); } }); } @Override public void onFailed( UrlRequest urlRequest, final UrlResponseInfo urlResponseInfo, final CronetException e) { GLIDE_EXECUTOR_SUPPLIER .get() .execute( new PriorityRunnable(priority) { @Override public void run() { onRequestFinished(urlResponseInfo, e, false /*wasCancelled*/, null /*buffer*/); } }); } @Override public void onCanceled(UrlRequest urlRequest, @Nullable final UrlResponseInfo urlResponseInfo) { GLIDE_EXECUTOR_SUPPLIER .get() .execute( new PriorityRunnable(priority) { @Override public void run() { onRequestFinished( urlResponseInfo, null /*exception*/, true /*wasCancelled*/, null /*buffer*/); } }); } private void onRequestFinished( UrlResponseInfo info, @Nullable CronetException e, boolean wasCancelled, ByteBuffer buffer) { synchronized (ChromiumRequestSerializer.this) { jobs.remove(glideUrl); } Exception exception = getExceptionIfFailed(info, e, wasCancelled); boolean isSuccess = exception == null && !wasCancelled; endTimeMs = System.currentTimeMillis(); maybeLogResult(isSuccess, exception, wasCancelled, buffer); if (isSuccess) { notifySuccess(buffer); } else { notifyFailure(exception); } if (dataLogger != null) { dataLogger.logNetworkData(info, startTime, responseStartTimeMs, endTimeMs); } builder = null; jobPool.put(this); } private void notifySuccess(ByteBuffer buffer) { ByteBuffer toNotify = buffer; /* Locking here isn't necessary and is potentially dangerous. There's an optimization in * Glide that avoids re-posting results if the callback onRequestComplete triggers is called * on the calling thread. If that were ever to happen here (the request is cached in memory?), * this might block all requests for a while. Locking isn't necessary because the Job is * removed from the serializer's job set at the beginning of onRequestFinished. After that * point, whatever thread we're on is the only one that has access to the Job. Subsequent * requests for the same image would trigger an additional RPC/Job. */ for (int i = 0, size = listeners.size(); i < size; i++) { Listener listener = listeners.get(i); listener.onRequestComplete(toNotify); toNotify = (ByteBuffer) toNotify.asReadOnlyBuffer().position(0); } } private void notifyFailure(Exception exception) { /* Locking here isn't necessary and is potentially dangerous. There's an optimization in * Glide that avoids re-posting results if the callback onRequestComplete triggers is called * on the calling thread. If that were ever to happen here (the request is cached in memory?), * this might block all requests for a while. Locking isn't necessary because the Job is * removed from the serializer's job set at the beginning of onRequestFinished. After that * point, whatever thread we're on is the only one that has access to the Job. Subsequent * requests for the same image would trigger an additional RPC/Job. */ for (int i = 0, size = listeners.size(); i < size; i++) { Listener listener = listeners.get(i); listener.onRequestFailed(exception); } } private void maybeLogResult( boolean isSuccess, Exception exception, boolean wasCancelled, ByteBuffer buffer) { if (isSuccess && Log.isLoggable(TAG, Log.VERBOSE)) { Log.v( TAG, "Successfully completed request" + ", url: " + glideUrl + ", duration: " + (System.currentTimeMillis() - startTime) + ", file size: " + (buffer.limit() / 1024) + "kb"); } else if (!isSuccess && Log.isLoggable(TAG, Log.ERROR) && !wasCancelled) { Log.e(TAG, "Request failed", exception); } } private void clearListeners() { synchronized (ChromiumRequestSerializer.this) { listeners.clear(); request = null; isCancelled = false; } } } private class JobPool { private static final int MAX_POOL_SIZE = 50; private final ArrayDeque pool = new ArrayDeque<>(); public synchronized Job get(GlideUrl glideUrl) { Job job = pool.poll(); if (job == null) { job = new Job(); } job.init(glideUrl); return job; } public void put(Job job) { job.clearListeners(); synchronized (this) { if (pool.size() < MAX_POOL_SIZE) { pool.offer(job); } } } } interface Listener { void onRequestComplete(ByteBuffer byteBuffer); void onRequestFailed(@Nullable Exception e); } }





  • © 2015 - 2024 Weber Informatics LLC | Privacy Policy