Use a separate executor for network loads to avoid stalling cached loads. Optimize background cache fill loads.

This commit is contained in:
Cameron Gutman 2015-03-02 18:34:21 -05:00
parent 56c8a9e6fe
commit 899387caa1
4 changed files with 73 additions and 61 deletions

View File

@ -13,16 +13,17 @@ import com.limelight.nvstream.http.NvApp;
import java.io.InputStream; import java.io.InputStream;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class CachedAppAssetLoader { public class CachedAppAssetLoader {
private static final int MAX_CONCURRENT_FOREGROUND_LOADS = 8; private static final int MAX_CONCURRENT_DISK_LOADS = 4;
private static final int MAX_CONCURRENT_NETWORK_LOADS = 4;
private static final int MAX_CONCURRENT_CACHE_LOADS = 2; private static final int MAX_CONCURRENT_CACHE_LOADS = 2;
private static final int MAX_PENDING_CACHE_LOADS = 100; private static final int MAX_PENDING_CACHE_LOADS = 100;
private static final int MAX_PENDING_FOREGROUND_LOADS = 30; private static final int MAX_PENDING_NETWORK_LOADS = 40;
private static final int MAX_PENDING_DISK_LOADS = 40;
private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor( private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS,
@ -31,9 +32,15 @@ public class CachedAppAssetLoader {
new ThreadPoolExecutor.DiscardOldestPolicy()); new ThreadPoolExecutor.DiscardOldestPolicy());
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor( private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_FOREGROUND_LOADS, MAX_CONCURRENT_FOREGROUND_LOADS, MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS, Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_FOREGROUND_LOADS), new LinkedBlockingQueue<Runnable>(MAX_PENDING_DISK_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_NETWORK_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy()); new ThreadPoolExecutor.DiscardOldestPolicy());
private final ComputerDetails computer; private final ComputerDetails computer;
@ -63,9 +70,14 @@ public class CachedAppAssetLoader {
public void cancelForegroundLoads() { public void cancelForegroundLoads() {
Runnable r; Runnable r;
while ((r = foregroundExecutor.getQueue().poll()) != null) { while ((r = foregroundExecutor.getQueue().poll()) != null) {
foregroundExecutor.remove(r); foregroundExecutor.remove(r);
} }
while ((r = networkExecutor.getQueue().poll()) != null) {
networkExecutor.remove(r);
}
} }
public void freeCacheMemory() { public void freeCacheMemory() {
@ -73,8 +85,6 @@ public class CachedAppAssetLoader {
} }
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) { private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
Bitmap bmp;
// Try 3 times // Try 3 times
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
// Check again whether we've been cancelled or the image view is gone // Check again whether we've been cancelled or the image view is gone
@ -87,10 +97,13 @@ public class CachedAppAssetLoader {
// Write the stream straight to disk // Write the stream straight to disk
diskLoader.populateCacheWithStream(tuple, in); diskLoader.populateCacheWithStream(tuple, in);
// Read it back scaled // If there's a task associated with this load, we should return the bitmap
bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider); if (task != null) {
if (bmp != null) { return diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
return bmp; }
else {
// Otherwise it's a background load and we return nothing
return null;
} }
} }
@ -107,11 +120,13 @@ public class CachedAppAssetLoader {
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> { private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
private final WeakReference<ImageView> imageViewRef; private final WeakReference<ImageView> imageViewRef;
private LoaderTuple tuple; private final boolean diskOnly;
private boolean loadFinished;
public LoaderTask(ImageView imageView) { private LoaderTuple tuple;
imageViewRef = new WeakReference<ImageView>(imageView);
public LoaderTask(ImageView imageView, boolean diskOnly) {
this.imageViewRef = new WeakReference<ImageView>(imageView);
this.diskOnly = diskOnly;
} }
@Override @Override
@ -120,22 +135,23 @@ public class CachedAppAssetLoader {
// Check whether it has been cancelled or the image view is gone // Check whether it has been cancelled or the image view is gone
if (isCancelled() || imageViewRef.get() == null) { if (isCancelled() || imageViewRef.get() == null) {
System.out.println("Cancelled or no image view in doInBackground");
return null; return null;
} }
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider); Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp == null) { if (bmp == null) {
// Report progress to display the placeholder if (!diskOnly) {
publishProgress();
// Try to load the asset from the network // Try to load the asset from the network
bmp = doNetworkAssetLoad(tuple, this); bmp = doNetworkAssetLoad(tuple, this);
} else {
// Report progress to display the placeholder and spin
// off the network-capable task
publishProgress();
}
} }
// Cache the bitmap // Cache the bitmap
if (bmp != null) { if (bmp != null) {
loadFinished = true;
memoryLoader.populateCache(tuple, bmp); memoryLoader.populateCache(tuple, bmp);
} }
@ -144,25 +160,20 @@ public class CachedAppAssetLoader {
@Override @Override
protected void onProgressUpdate(Void... nothing) { protected void onProgressUpdate(Void... nothing) {
// Do nothing if the load has already completed
if (loadFinished) {
return;
}
// Do nothing if cancelled // Do nothing if cancelled
if (isCancelled()) { if (isCancelled()) {
return; return;
} }
final ImageView imageView = imageViewRef.get();
if (imageView != null) {
// If the current loader task for this view isn't us, do nothing // If the current loader task for this view isn't us, do nothing
if (getLoaderTask(imageView) != this) { final ImageView imageView = imageViewRef.get();
return; if (getLoaderTask(imageView) == this) {
} // Set off another loader task on the network executor
LoaderTask task = new LoaderTask(imageView, false);
// Show the placeholder by setting alpha to 1.0 AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
imageView.setAlpha(1.0f); imageView.setAlpha(1.0f);
imageView.setImageDrawable(asyncDrawable);
task.executeOnExecutor(networkExecutor, tuple);
} }
} }
@ -174,12 +185,7 @@ public class CachedAppAssetLoader {
} }
final ImageView imageView = imageViewRef.get(); final ImageView imageView = imageViewRef.get();
if (imageView != null) { if (getLoaderTask(imageView) == this) {
// If the current loader task for this view isn't us, do nothing
if (getLoaderTask(imageView) != this) {
return;
}
// Set the bitmap // Set the bitmap
if (bitmap != null) { if (bitmap != null) {
imageView.setImageBitmap(bitmap); imageView.setImageBitmap(bitmap);
@ -206,6 +212,10 @@ public class CachedAppAssetLoader {
} }
private static LoaderTask getLoaderTask(ImageView imageView) { private static LoaderTask getLoaderTask(ImageView imageView) {
if (imageView == null) {
return null;
}
final Drawable drawable = imageView.getDrawable(); final Drawable drawable = imageView.getDrawable();
// If our drawable is in play, get the loader task // If our drawable is in play, get the loader task
@ -249,20 +259,13 @@ public class CachedAppAssetLoader {
cacheExecutor.execute(new Runnable() { cacheExecutor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
Bitmap bmp;
// Check if the image is cached on disk // Check if the image is cached on disk
bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider); if (diskLoader.checkCacheExists(tuple)) {
if (bmp == null) { return;
// Try to load the asset from the network and cache on disk
bmp = doNetworkAssetLoad(tuple, null);
} }
// If the bitmap was loaded, recycle it immediately. We can do this // Try to load the asset from the network and cache result on disk
// because it's not loaded into any image views or cached in memory doNetworkAssetLoad(tuple, null);
if (bmp != null) {
bmp.recycle();
}
} }
}); });
} }
@ -270,14 +273,6 @@ public class CachedAppAssetLoader {
public void populateImageView(NvApp app, ImageView view) { public void populateImageView(NvApp app, ImageView view) {
LoaderTuple tuple = new LoaderTuple(computer, app); LoaderTuple tuple = new LoaderTuple(computer, app);
// First, try the memory cache in the current context
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
if (bmp != null) {
// Show the bitmap immediately
view.setImageBitmap(bmp);
return;
}
// If there's already a task in progress for this view, // If there's already a task in progress for this view,
// cancel it. If the task is already loading the same image, // cancel it. If the task is already loading the same image,
// we return and let that load finish. // we return and let that load finish.
@ -285,9 +280,18 @@ public class CachedAppAssetLoader {
return; return;
} }
// First, try the memory cache in the current context
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
if (bmp != null) {
// Show the bitmap immediately
view.setAlpha(1.0f);
view.setImageBitmap(bmp);
return;
}
// If it's not in memory, create an async task to load it. This task will be attached // If it's not in memory, create an async task to load it. This task will be attached
// via AsyncDrawable to this view. // via AsyncDrawable to this view.
final LoaderTask task = new LoaderTask(view); final LoaderTask task = new LoaderTask(view, true);
final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task); final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task);
view.setAlpha(0.0f); view.setAlpha(0.0f);
view.setImageDrawable(asyncDrawable); view.setImageDrawable(asyncDrawable);

View File

@ -19,6 +19,10 @@ public class DiskAssetLoader {
this.cacheDir = cacheDir; this.cacheDir = cacheDir;
} }
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) { public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
InputStream in = null; InputStream in = null;
Bitmap bmp = null; Bitmap bmp = null;

View File

@ -7,7 +7,7 @@ import com.limelight.LimeLog;
public class MemoryAssetLoader { public class MemoryAssetLoader {
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 12) { private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
@Override @Override
protected int sizeOf(String key, Bitmap bitmap) { protected int sizeOf(String key, Bitmap bitmap) {
// Sizeof returns kilobytes // Sizeof returns kilobytes

View File

@ -30,6 +30,10 @@ public class CacheHelper {
return f; return f;
} }
public static boolean cacheFileExists(File root, String... path) {
return openPath(false, root, path).exists();
}
public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException { public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
return new BufferedInputStream(new FileInputStream(openPath(false, root, path))); return new BufferedInputStream(new FileInputStream(openPath(false, root, path)));
} }