/*
* 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.concurrent.GuardedBy;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import android.os.SystemClock;
import com.facebook.common.internal.VisibleForTesting;
import com.facebook.imagepipeline.image.EncodedImage;
/**
* Manages jobs so that only one can be executed at a time and no more often than once in
* <code>mMinimumJobIntervalMs</code> milliseconds.
*/
public class JobScheduler {
static final String QUEUE_TIME_KEY = "queueTime";
@VisibleForTesting
static class JobStartExecutorSupplier {
private static ScheduledExecutorService sJobStarterExecutor;
static ScheduledExecutorService get() {
if (sJobStarterExecutor == null) {
sJobStarterExecutor = Executors.newSingleThreadScheduledExecutor();
}
return sJobStarterExecutor;
}
}
public interface JobRunnable {
void run(EncodedImage encodedImage, boolean isLast);
}
private final Executor mExecutor;
private final JobRunnable mJobRunnable;
private final Runnable mDoJobRunnable;
private final Runnable mSubmitJobRunnable;
private final int mMinimumJobIntervalMs;
@VisibleForTesting
enum JobState { IDLE, QUEUED, RUNNING, RUNNING_AND_PENDING }
// job data
@GuardedBy("this")
@VisibleForTesting EncodedImage mEncodedImage;
@GuardedBy("this")
@VisibleForTesting boolean mIsLast;
// job state
@GuardedBy("this")
@VisibleForTesting JobState mJobState;
@GuardedBy("this")
@VisibleForTesting long mJobSubmitTime;
@GuardedBy("this")
@VisibleForTesting long mJobStartTime;
public JobScheduler(Executor executor, JobRunnable jobRunnable, int minimumJobIntervalMs) {
mExecutor = executor;
mJobRunnable = jobRunnable;
mMinimumJobIntervalMs = minimumJobIntervalMs;
mDoJobRunnable = new Runnable() {
@Override
public void run() {
doJob();
}
};
mSubmitJobRunnable = new Runnable() {
@Override
public void run() {
submitJob();
}
};
mEncodedImage = null;
mIsLast = false;
mJobState = JobState.IDLE;
mJobSubmitTime = 0;
mJobStartTime = 0;
}
/**
* Clears the currently set job.
*
* <p> In case the currently set job has been scheduled but not started yet, the job won't be
* executed.
*/
public void clearJob() {
EncodedImage oldEncodedImage;
synchronized (this) {
oldEncodedImage = mEncodedImage;
mEncodedImage = null;
mIsLast = false;
}
EncodedImage.closeSafely(oldEncodedImage);
}
/**
* Updates the job.
*
* <p> This just updates the job, but it doesn't schedule it. In order to be executed, the job has
* to be scheduled after being set. In case there was a previous job scheduled that has not yet
* started, this new job will be executed instead.
*
* @return whether the job was successfully updated.
*/
public boolean updateJob(EncodedImage encodedImage, boolean isLast) {
if (!shouldProcess(encodedImage, isLast)) {
return false;
}
EncodedImage oldEncodedImage;
synchronized (this) {
oldEncodedImage = mEncodedImage;
mEncodedImage = EncodedImage.cloneOrNull(encodedImage);
mIsLast = isLast;
}
EncodedImage.closeSafely(oldEncodedImage);
return true;
}
/**
* Schedules the currently set job (if any).
*
* <p> This method can be called multiple times. It is guaranteed that each job set will be
* executed no more than once. It is guaranteed that the last job set will be executed, unless
* the job was cleared first.
* <p> The job will be scheduled no sooner than <code>minimumJobIntervalMs</code> milliseconds
* since the last job started.
*
* @return true if the job was scheduled, false if there was no valid job to be scheduled
*/
public boolean scheduleJob() {
long now = SystemClock.uptimeMillis();
long when = 0;
boolean shouldEnqueue = false;
synchronized (this) {
if (!shouldProcess(mEncodedImage, mIsLast)) {
return false;
}
switch (mJobState) {
case IDLE:
when = Math.max(mJobStartTime + mMinimumJobIntervalMs, now);
shouldEnqueue = true;
mJobSubmitTime = now;
mJobState = JobState.QUEUED;
break;
case QUEUED:
// do nothing, the job is already queued
break;
case RUNNING:
mJobState = JobState.RUNNING_AND_PENDING;
break;
case RUNNING_AND_PENDING:
// do nothing, the next job is already pending
break;
}
}
if (shouldEnqueue) {
enqueueJob(when - now);
}
return true;
}
private void enqueueJob(long delay) {
// If we make mExecutor be a {@link ScheduledexecutorService}, we could just have
// `mExecutor.schedule(mDoJobRunnable, delay)` and avoid mSubmitJobRunnable and
// JobStartExecutorSupplier altogether. That would require some refactoring though.
if (delay > 0) {
JobStartExecutorSupplier.get().schedule(mSubmitJobRunnable, delay, TimeUnit.MILLISECONDS);
} else {
mSubmitJobRunnable.run();
}
}
private void submitJob() {
mExecutor.execute(mDoJobRunnable);
}
private void doJob() {
long now = SystemClock.uptimeMillis();
EncodedImage input;
boolean isLast;
synchronized (this) {
input = mEncodedImage;
isLast = mIsLast;
mEncodedImage = null;
mIsLast = false;
mJobState = JobState.RUNNING;
mJobStartTime = now;
}
try {
// we need to do a check in case the job got cleared in the meantime
if (shouldProcess(input, isLast)) {
mJobRunnable.run(input, isLast);
}
} finally {
EncodedImage.closeSafely(input);
onJobFinished();
}
}
private void onJobFinished() {
long now = SystemClock.uptimeMillis();
long when = 0;
boolean shouldEnqueue = false;
synchronized (this) {
if (mJobState == JobState.RUNNING_AND_PENDING) {
when = Math.max(mJobStartTime + mMinimumJobIntervalMs, now);
shouldEnqueue = true;
mJobSubmitTime = now;
mJobState = JobState.QUEUED;
} else {
mJobState = JobState.IDLE;
}
}
if (shouldEnqueue) {
enqueueJob(when - now);
}
}
private static boolean shouldProcess(EncodedImage encodedImage, boolean isLast) {
// the last result should always be processed, whereas
// an intermediate result should be processed only if valid
return isLast || EncodedImage.isValid(encodedImage);
}
/**
* Gets the queued time in milliseconds for the currently running job.
*
* <p> The result is only valid if called from {@link JobRunnable#run}.
*/
public synchronized long getQueuedTime() {
return mJobStartTime - mJobSubmitTime;
}
}