Merge "Change context assignment mechanism." into tm-dev

This commit is contained in:
Kweku Adams
2022-03-24 14:35:10 +00:00
committed by Android (Google) Code Review
3 changed files with 231 additions and 123 deletions

View File

@@ -273,23 +273,22 @@ class JobConcurrencyManager {
*/ */
JobStatus[] mRecycledAssignContextIdToJobMap = new JobStatus[MAX_JOB_CONTEXTS_COUNT]; JobStatus[] mRecycledAssignContextIdToJobMap = new JobStatus[MAX_JOB_CONTEXTS_COUNT];
boolean[] mRecycledSlotChanged = new boolean[MAX_JOB_CONTEXTS_COUNT]; private final ArraySet<ContextAssignment> mRecycledChanged = new ArraySet<>();
private final ArraySet<ContextAssignment> mRecycledIdle = new ArraySet<>();
private final ArraySet<ContextAssignment> mRecycledPreferredUidOnly = new ArraySet<>();
private final ArraySet<ContextAssignment> mRecycledStoppable = new ArraySet<>();
int[] mRecycledPreferredUidForContext = new int[MAX_JOB_CONTEXTS_COUNT]; private final Pools.Pool<ContextAssignment> mContextAssignmentPool =
new Pools.SimplePool<>(MAX_JOB_CONTEXTS_COUNT);
int[] mRecycledWorkTypeForContext = new int[MAX_JOB_CONTEXTS_COUNT];
String[] mRecycledPreemptReasonForContext = new String[MAX_JOB_CONTEXTS_COUNT];
int[] mRecycledPreemptReasonCodeForContext = new int[MAX_JOB_CONTEXTS_COUNT];
String[] mRecycledShouldStopJobReason = new String[MAX_JOB_CONTEXTS_COUNT];
/** /**
* Set of JobServiceContexts that we use to run jobs. * Set of JobServiceContexts that are actively running jobs.
*/ */
final List<JobServiceContext> mActiveServices = new ArrayList<>(); final List<JobServiceContext> mActiveServices = new ArrayList<>();
/** Set of JobServiceContexts that aren't currently running any jobs. */
final ArraySet<JobServiceContext> mIdleContexts = new ArraySet<>();
private final ArraySet<JobStatus> mRunningJobs = new ArraySet<>(); private final ArraySet<JobStatus> mRunningJobs = new ArraySet<>();
private final WorkCountTracker mWorkCountTracker = new WorkCountTracker(); private final WorkCountTracker mWorkCountTracker = new WorkCountTracker();
@@ -378,7 +377,7 @@ class JobConcurrencyManager {
final IBatteryStats batteryStats = IBatteryStats.Stub.asInterface( final IBatteryStats batteryStats = IBatteryStats.Stub.asInterface(
ServiceManager.getService(BatteryStats.SERVICE_NAME)); ServiceManager.getService(BatteryStats.SERVICE_NAME));
for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) { for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) {
mActiveServices.add( mIdleContexts.add(
new JobServiceContext(mService, this, batteryStats, new JobServiceContext(mService, this, batteryStats,
mService.mJobPackageTracker, mContext.getMainLooper())); mService.mJobPackageTracker, mContext.getMainLooper()));
} }
@@ -582,12 +581,10 @@ class JobConcurrencyManager {
// To avoid GC churn, we recycle the arrays. // To avoid GC churn, we recycle the arrays.
JobStatus[] contextIdToJobMap = mRecycledAssignContextIdToJobMap; JobStatus[] contextIdToJobMap = mRecycledAssignContextIdToJobMap;
boolean[] slotChanged = mRecycledSlotChanged; final ArraySet<ContextAssignment> changed = mRecycledChanged;
int[] preferredUidForContext = mRecycledPreferredUidForContext; final ArraySet<ContextAssignment> idle = mRecycledIdle;
int[] workTypeForContext = mRecycledWorkTypeForContext; final ArraySet<ContextAssignment> preferredUidOnly = mRecycledPreferredUidOnly;
String[] preemptReasonForContext = mRecycledPreemptReasonForContext; final ArraySet<ContextAssignment> stoppable = mRecycledStoppable;
int[] preemptReasonCodeForContext = mRecycledPreemptReasonCodeForContext;
String[] shouldStopJobReason = mRecycledShouldStopJobReason;
updateCounterConfigLocked(); updateCounterConfigLocked();
// Reset everything since we'll re-evaluate the current state. // Reset everything since we'll re-evaluate the current state.
@@ -598,20 +595,47 @@ class JobConcurrencyManager {
// shouldStopRunningJobLocked(). // shouldStopRunningJobLocked().
updateNonRunningPrioritiesLocked(pendingJobQueue, true); updateNonRunningPrioritiesLocked(pendingJobQueue, true);
for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) { final int numRunningJobs = activeServices.size();
final JobServiceContext js = activeServices.get(i); for (int i = 0; i < numRunningJobs; ++i) {
final JobStatus status = js.getRunningJobLocked(); final JobServiceContext jsc = activeServices.get(i);
final JobStatus js = jsc.getRunningJobLocked();
if ((contextIdToJobMap[i] = status) != null) { ContextAssignment assignment = mContextAssignmentPool.acquire();
mWorkCountTracker.incrementRunningJobCount(js.getRunningJobWorkType()); if (assignment == null) {
workTypeForContext[i] = js.getRunningJobWorkType(); assignment = new ContextAssignment();
} }
slotChanged[i] = false; assignment.context = jsc;
preferredUidForContext[i] = js.getPreferredUid();
preemptReasonForContext[i] = null; if (js != null) {
preemptReasonCodeForContext[i] = JobParameters.STOP_REASON_UNDEFINED; mWorkCountTracker.incrementRunningJobCount(jsc.getRunningJobWorkType());
shouldStopJobReason[i] = shouldStopRunningJobLocked(js); assignment.workType = jsc.getRunningJobWorkType();
}
assignment.preferredUid = jsc.getPreferredUid();
if ((assignment.shouldStopJobReason = shouldStopRunningJobLocked(jsc)) != null) {
stoppable.add(assignment);
} else {
preferredUidOnly.add(assignment);
}
}
for (int i = numRunningJobs; i < MAX_JOB_CONTEXTS_COUNT; ++i) {
final JobServiceContext jsc;
final int numIdleContexts = mIdleContexts.size();
if (numIdleContexts > 0) {
jsc = mIdleContexts.removeAt(numIdleContexts - 1);
} else {
Slog.wtf(TAG, "Had fewer than " + MAX_JOB_CONTEXTS_COUNT + " in existence");
jsc = createNewJobServiceContext();
}
ContextAssignment assignment = mContextAssignmentPool.acquire();
if (assignment == null) {
assignment = new ContextAssignment();
}
assignment.context = jsc;
idle.add(assignment);
} }
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs initial")); Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs initial"));
@@ -626,83 +650,97 @@ class JobConcurrencyManager {
continue; continue;
} }
// Find an available slot for nextPending. The context should be available OR // Find an available slot for nextPending. The context should be one of the following:
// it should have the lowest bias among all running jobs // 1. Unused
// (sharing the same Uid as nextPending) // 2. Its job should have used up its minimum execution guarantee so it
int minBiasForPreemption = Integer.MAX_VALUE; // 3. Its job should have the lowest bias among all running jobs (sharing the same UID
int selectedContextId = -1; // as nextPending)
int allWorkTypes = getJobWorkTypes(nextPending); ContextAssignment selectedContext = null;
int workType = mWorkCountTracker.canJobStart(allWorkTypes); final int allWorkTypes = getJobWorkTypes(nextPending);
boolean startingJob = false;
int preemptReasonCode = JobParameters.STOP_REASON_UNDEFINED;
String preemptReason = null;
final boolean pkgConcurrencyOkay = !isPkgConcurrencyLimitedLocked(nextPending); final boolean pkgConcurrencyOkay = !isPkgConcurrencyLimitedLocked(nextPending);
// TODO(141645789): rewrite this to look at empty contexts first so we don't boolean startingJob = false;
// unnecessarily preempt if (idle.size() > 0) {
for (int j = 0; j < MAX_JOB_CONTEXTS_COUNT; j++) { final int idx = idle.size() - 1;
JobStatus job = contextIdToJobMap[j]; final ContextAssignment assignment = idle.valueAt(idx);
int preferredUid = preferredUidForContext[j]; final boolean preferredUidOkay = (assignment.preferredUid == nextPending.getUid())
if (job == null) { || (assignment.preferredUid == JobServiceContext.NO_PREFERRED_UID);
final boolean preferredUidOkay = (preferredUid == nextPending.getUid()) int workType = mWorkCountTracker.canJobStart(allWorkTypes);
|| (preferredUid == JobServiceContext.NO_PREFERRED_UID);
if (preferredUidOkay && pkgConcurrencyOkay && workType != WORK_TYPE_NONE) { if (preferredUidOkay && pkgConcurrencyOkay && workType != WORK_TYPE_NONE) {
// This slot is free, and we haven't yet hit the limit on // This slot is free, and we haven't yet hit the limit on
// concurrent jobs... we can just throw the job in to here. // concurrent jobs... we can just throw the job in to here.
selectedContextId = j; selectedContext = assignment;
startingJob = true; startingJob = true;
break; idle.removeAt(idx);
assignment.newJob = nextPending;
assignment.newWorkType = workType;
} }
// No job on this context, but nextPending can't run here because
// the context has a preferred Uid or we have reached the limit on
// concurrent jobs.
continue;
} }
if (job.getUid() != nextPending.getUid()) { if (selectedContext == null) {
for (int s = stoppable.size() - 1; s >= 0; --s) {
ContextAssignment assignment = stoppable.valueAt(s);
JobStatus runningJob = assignment.context.getRunningJobLocked();
// Maybe stop the job if it has had its day in the sun. Don't let a different // Maybe stop the job if it has had its day in the sun. Don't let a different
// app preempt jobs started for TOP apps though. // app preempt jobs started for TOP apps though.
final String reason = shouldStopJobReason[j]; if (runningJob.lastEvaluatedBias < JobInfo.BIAS_TOP_APP
if (job.lastEvaluatedBias < JobInfo.BIAS_TOP_APP && assignment.shouldStopJobReason != null) {
&& reason != null && mWorkCountTracker.canJobStart(allWorkTypes, int replaceWorkType = mWorkCountTracker.canJobStart(allWorkTypes,
activeServices.get(j).getRunningJobWorkType()) != WORK_TYPE_NONE) { assignment.context.getRunningJobWorkType());
if (replaceWorkType != WORK_TYPE_NONE) {
// Right now, the way the code is set up, we don't need to explicitly // Right now, the way the code is set up, we don't need to explicitly
// assign the new job to this context since we'll reassign when the // assign the new job to this context since we'll reassign when the
// preempted job finally stops. // preempted job finally stops.
preemptReason = reason; assignment.preemptReason = assignment.shouldStopJobReason;
preemptReasonCode = JobParameters.STOP_REASON_DEVICE_STATE; assignment.preemptReasonCode = JobParameters.STOP_REASON_DEVICE_STATE;
selectedContext = assignment;
stoppable.removeAt(s);
assignment.newJob = nextPending;
assignment.newWorkType = replaceWorkType;
// Don't preserve the UID since we're stopping the job because
// something is pending (eg. EJs).
assignment.context.clearPreferredUid();
break;
} }
}
}
}
if (selectedContext == null) {
int lowestBiasSeen = Integer.MAX_VALUE;
for (int p = preferredUidOnly.size() - 1; p >= 0; --p) {
final ContextAssignment assignment = preferredUidOnly.valueAt(p);
final JobStatus runningJob = assignment.context.getRunningJobLocked();
if (runningJob.getUid() != nextPending.getUid()) {
continue; continue;
} }
final int jobBias = mService.evaluateJobBiasLocked(runningJob);
final int jobBias = mService.evaluateJobBiasLocked(job);
if (jobBias >= nextPending.lastEvaluatedBias) { if (jobBias >= nextPending.lastEvaluatedBias) {
continue; continue;
} }
if (minBiasForPreemption > jobBias) { if (selectedContext == null || lowestBiasSeen > jobBias) {
// Step down the preemption threshold - wind up replacing // Step down the preemption threshold - wind up replacing
// the lowest-bias running job // the lowest-bias running job
minBiasForPreemption = jobBias; lowestBiasSeen = jobBias;
selectedContextId = j; selectedContext = assignment;
preemptReason = "higher bias job found"; assignment.preemptReason = "higher bias job found";
preemptReasonCode = JobParameters.STOP_REASON_PREEMPT; assignment.preemptReasonCode = JobParameters.STOP_REASON_PREEMPT;
// In this case, we're just going to preempt a low bias job, we're not // In this case, we're just going to preempt a low bias job, we're not
// actually starting a job, so don't set startingJob. // actually starting a job, so don't set startingJob to true.
}
}
if (selectedContext != null) {
selectedContext.newJob = nextPending;
preferredUidOnly.remove(selectedContext);
} }
} }
final PackageStats packageStats = getPkgStatsLocked( final PackageStats packageStats = getPkgStatsLocked(
nextPending.getSourceUserId(), nextPending.getSourcePackageName()); nextPending.getSourceUserId(), nextPending.getSourcePackageName());
if (selectedContextId != -1) { if (selectedContext != null) {
contextIdToJobMap[selectedContextId] = nextPending; changed.add(selectedContext);
slotChanged[selectedContextId] = true;
preemptReasonCodeForContext[selectedContextId] = preemptReasonCode;
preemptReasonForContext[selectedContextId] = preemptReason;
packageStats.adjustStagedCount(true, nextPending.shouldTreatAsExpeditedJob()); packageStats.adjustStagedCount(true, nextPending.shouldTreatAsExpeditedJob());
} }
if (startingJob) { if (startingJob) {
// Increase the counters when we're going to start a job. // Increase the counters when we're going to start a job.
workTypeForContext[selectedContextId] = workType; mWorkCountTracker.stageJob(selectedContext.newWorkType, allWorkTypes);
mWorkCountTracker.stageJob(workType, allWorkTypes);
mActivePkgStats.add( mActivePkgStats.add(
nextPending.getSourceUserId(), nextPending.getSourcePackageName(), nextPending.getSourceUserId(), nextPending.getSourcePackageName(),
packageStats); packageStats);
@@ -714,37 +752,52 @@ class JobConcurrencyManager {
Slog.d(TAG, "assignJobsToContexts: " + mWorkCountTracker.toString()); Slog.d(TAG, "assignJobsToContexts: " + mWorkCountTracker.toString());
} }
for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) { for (int c = changed.size() - 1; c >= 0; --c) {
boolean preservePreferredUid = false; final ContextAssignment assignment = changed.valueAt(c);
if (slotChanged[i]) { final JobStatus js = assignment.context.getRunningJobLocked();
JobStatus js = activeServices.get(i).getRunningJobLocked();
if (js != null) { if (js != null) {
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "preempting job: " Slog.d(TAG, "preempting job: " + js);
+ activeServices.get(i).getRunningJobLocked());
} }
// preferredUid will be set to uid of currently running job. // preferredUid will be set to uid of currently running job.
activeServices.get(i).cancelExecutingJobLocked( assignment.context.cancelExecutingJobLocked(
preemptReasonCodeForContext[i], assignment.preemptReasonCode,
JobParameters.INTERNAL_STOP_REASON_PREEMPT, preemptReasonForContext[i]); JobParameters.INTERNAL_STOP_REASON_PREEMPT, assignment.preemptReason);
// Only preserve the UID if we're preempting for the same UID. If we're stopping
// the job because something is pending (eg. EJs), then we shouldn't preserve
// the UID.
preservePreferredUid =
preemptReasonCodeForContext[i] == JobParameters.STOP_REASON_PREEMPT;
} else { } else {
final JobStatus pendingJob = contextIdToJobMap[i]; final JobStatus pendingJob = assignment.newJob;
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "About to run job on context " Slog.d(TAG, "About to run job on context "
+ i + ", job: " + pendingJob); + assignment.context.getId() + ", job: " + pendingJob);
} }
startJobLocked(activeServices.get(i), pendingJob, workTypeForContext[i]); startJobLocked(assignment.context, pendingJob, assignment.newWorkType);
} }
assignment.clear();
mContextAssignmentPool.release(assignment);
} }
if (!preservePreferredUid) { for (int s = stoppable.size() - 1; s >= 0; --s) {
activeServices.get(i).clearPreferredUid(); final ContextAssignment assignment = stoppable.valueAt(s);
assignment.context.clearPreferredUid();
assignment.clear();
mContextAssignmentPool.release(assignment);
} }
for (int p = preferredUidOnly.size() - 1; p >= 0; --p) {
final ContextAssignment assignment = preferredUidOnly.valueAt(p);
assignment.context.clearPreferredUid();
assignment.clear();
mContextAssignmentPool.release(assignment);
} }
for (int i = idle.size() - 1; i >= 0; --i) {
final ContextAssignment assignment = idle.valueAt(i);
mIdleContexts.add(assignment.context);
assignment.context.clearPreferredUid();
assignment.clear();
mContextAssignmentPool.release(assignment);
}
changed.clear();
idle.clear();
stoppable.clear();
preferredUidOnly.clear();
mWorkCountTracker.resetStagingCount(); mWorkCountTracker.resetStagingCount();
mActivePkgStats.forEach(mPackageStatsStagingCountClearer); mActivePkgStats.forEach(mPackageStatsStagingCountClearer);
noteConcurrency(); noteConcurrency();
@@ -787,7 +840,7 @@ class JobConcurrencyManager {
@GuardedBy("mLock") @GuardedBy("mLock")
private void stopLongRunningJobsLocked(@NonNull String debugReason) { private void stopLongRunningJobsLocked(@NonNull String debugReason) {
for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; ++i) { for (int i = 0; i < mActiveServices.size(); ++i) {
final JobServiceContext jsc = mActiveServices.get(i); final JobServiceContext jsc = mActiveServices.get(i);
final JobStatus jobStatus = jsc.getRunningJobLocked(); final JobStatus jobStatus = jsc.getRunningJobLocked();
@@ -931,6 +984,8 @@ class JobConcurrencyManager {
} }
} else { } else {
mRunningJobs.add(jobStatus); mRunningJobs.add(jobStatus);
mActiveServices.add(worker);
mIdleContexts.remove(worker);
mWorkCountTracker.onJobStarted(workType); mWorkCountTracker.onJobStarted(workType);
packageStats.adjustRunningCount(true, jobStatus.shouldTreatAsExpeditedJob()); packageStats.adjustRunningCount(true, jobStatus.shouldTreatAsExpeditedJob());
mActivePkgStats.add( mActivePkgStats.add(
@@ -950,6 +1005,8 @@ class JobConcurrencyManager {
@WorkType final int workType) { @WorkType final int workType) {
mWorkCountTracker.onJobFinished(workType); mWorkCountTracker.onJobFinished(workType);
mRunningJobs.remove(jobStatus); mRunningJobs.remove(jobStatus);
mActiveServices.remove(worker);
mIdleContexts.add(worker);
final PackageStats packageStats = final PackageStats packageStats =
mActivePkgStats.get(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName()); mActivePkgStats.get(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
if (packageStats == null) { if (packageStats == null) {
@@ -1093,7 +1150,7 @@ class JobConcurrencyManager {
/** /**
* Returns {@code null} if the job can continue running and a non-null String if the job should * Returns {@code null} if the job can continue running and a non-null String if the job should
* be stopped. The non-null String details the reason for stopping the job. A job will generally * be stopped. The non-null String details the reason for stopping the job. A job will generally
* be stopped if there similar job types waiting to be run and stopping this job would allow * be stopped if there are similar job types waiting to be run and stopping this job would allow
* another job to run, or if system state suggests the job should stop. * another job to run, or if system state suggests the job should stop.
*/ */
@Nullable @Nullable
@@ -1202,6 +1259,14 @@ class JobConcurrencyManager {
return foundSome; return foundSome;
} }
@NonNull
private JobServiceContext createNewJobServiceContext() {
return new JobServiceContext(mService, this,
IBatteryStats.Stub.asInterface(
ServiceManager.getService(BatteryStats.SERVICE_NAME)),
mService.mJobPackageTracker, mContext.getMainLooper());
}
@GuardedBy("mLock") @GuardedBy("mLock")
private String printPendingQueueLocked() { private String printPendingQueueLocked() {
StringBuilder s = new StringBuilder("Pending queue: "); StringBuilder s = new StringBuilder("Pending queue: ");
@@ -1327,10 +1392,13 @@ class JobConcurrencyManager {
} }
@GuardedBy("mLock") @GuardedBy("mLock")
void dumpActiveJobsLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate, void dumpContextInfoLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate,
long nowElapsed, long nowUptime) { long nowElapsed, long nowUptime) {
pw.println("Active jobs:"); pw.println("Active jobs:");
pw.increaseIndent(); pw.increaseIndent();
if (mActiveServices.size() == 0) {
pw.println("N/A");
}
for (int i = 0; i < mActiveServices.size(); i++) { for (int i = 0; i < mActiveServices.size(); i++) {
JobServiceContext jsc = mActiveServices.get(i); JobServiceContext jsc = mActiveServices.get(i);
final JobStatus job = jsc.getRunningJobLocked(); final JobStatus job = jsc.getRunningJobLocked();
@@ -1339,7 +1407,8 @@ class JobConcurrencyManager {
continue; continue;
} }
pw.print("Slot #"); pw.print(i); pw.print(": "); pw.print("Slot #"); pw.print(i);
pw.print("(ID="); pw.print(jsc.getId()); pw.print("): ");
jsc.dumpLocked(pw, nowElapsed); jsc.dumpLocked(pw, nowElapsed);
if (job != null) { if (job != null) {
@@ -1361,6 +1430,19 @@ class JobConcurrencyManager {
} }
} }
pw.decreaseIndent(); pw.decreaseIndent();
pw.println();
pw.print("Idle contexts (");
pw.print(mIdleContexts.size());
pw.println("):");
pw.increaseIndent();
for (int i = 0; i < mIdleContexts.size(); i++) {
JobServiceContext jsc = mIdleContexts.valueAt(i);
pw.print("ID="); pw.print(jsc.getId()); pw.print(": ");
jsc.dumpLocked(pw, nowElapsed);
}
pw.decreaseIndent();
} }
public void dumpProtoLocked(ProtoOutputStream proto, long tag, long now, long nowRealtime) { public void dumpProtoLocked(ProtoOutputStream proto, long tag, long now, long nowRealtime) {
@@ -1989,4 +2071,26 @@ class JobConcurrencyManager {
pw.println("}"); pw.println("}");
} }
} }
private static final class ContextAssignment {
public JobServiceContext context;
public int preferredUid = JobServiceContext.NO_PREFERRED_UID;
public int workType = WORK_TYPE_NONE;
public String preemptReason;
public int preemptReasonCode = JobParameters.STOP_REASON_UNDEFINED;
public String shouldStopJobReason;
public JobStatus newJob;
public int newWorkType = WORK_TYPE_NONE;
void clear() {
context = null;
preferredUid = JobServiceContext.NO_PREFERRED_UID;
workType = WORK_TYPE_NONE;
preemptReason = null;
preemptReasonCode = JobParameters.STOP_REASON_UNDEFINED;
shouldStopJobReason = null;
newJob = null;
newWorkType = WORK_TYPE_NONE;
}
}
} }

View File

@@ -3770,7 +3770,7 @@ public class JobSchedulerService extends com.android.server.SystemService
pw.decreaseIndent(); pw.decreaseIndent();
pw.println(); pw.println();
mConcurrencyManager.dumpActiveJobsLocked(pw, predicate, nowElapsed, nowUptime); mConcurrencyManager.dumpContextInfoLocked(pw, predicate, nowElapsed, nowUptime);
pw.println(); pw.println();
boolean recentPrinted = false; boolean recentPrinted = false;

View File

@@ -433,6 +433,10 @@ public final class JobServiceContext implements ServiceConnection {
mPreferredUid = NO_PREFERRED_UID; mPreferredUid = NO_PREFERRED_UID;
} }
int getId() {
return hashCode();
}
long getExecutionStartTimeElapsed() { long getExecutionStartTimeElapsed() {
return mExecutionStartTimeElapsed; return mExecutionStartTimeElapsed;
} }