diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java index 9b4f43ae9feb2..09050b644d232 100644 --- a/core/java/android/app/job/JobInfo.java +++ b/core/java/android/app/job/JobInfo.java @@ -138,6 +138,20 @@ public class JobInfo implements Parcelable { */ public static final int PRIORITY_TOP_APP = 40; + /** + * Adjustment of {@link #getPriority} if the app has often (50% or more of the time) + * been running jobs. + * @hide + */ + public static final int PRIORITY_ADJ_OFTEN_RUNNING = -40; + + /** + * Adjustment of {@link #getPriority} if the app has always (90% or more of the time) + * been running jobs. + * @hide + */ + public static final int PRIORITY_ADJ_ALWAYS_RUNNING = -80; + private final int jobId; private final PersistableBundle extras; private final ComponentName service; diff --git a/services/core/java/com/android/server/job/JobPackageTracker.java b/services/core/java/com/android/server/job/JobPackageTracker.java new file mode 100644 index 0000000000000..e5a20954dcc2f --- /dev/null +++ b/services/core/java/com/android/server/job/JobPackageTracker.java @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.job; + +import android.app.job.JobInfo; +import android.os.SystemClock; +import android.os.UserHandle; +import android.text.format.DateFormat; +import android.util.ArrayMap; +import android.util.SparseArray; +import android.util.TimeUtils; +import com.android.server.job.controllers.JobStatus; + +import java.io.PrintWriter; + +public final class JobPackageTracker { + // We batch every 30 minutes. + static final long BATCHING_TIME = 30*60*1000; + // Number of historical data sets we keep. + static final int NUM_HISTORY = 5; + + DataSet mCurDataSet = new DataSet(); + DataSet[] mLastDataSets = new DataSet[NUM_HISTORY]; + + final static class PackageEntry { + long pastActiveTime; + long activeStartTime; + int activeCount; + boolean hadActive; + long pastActiveTopTime; + long activeTopStartTime; + int activeTopCount; + boolean hadActiveTop; + long pastPendingTime; + long pendingStartTime; + int pendingCount; + boolean hadPending; + + public long getActiveTime(long now) { + long time = pastActiveTime; + if (activeCount > 0) { + time += now - activeStartTime; + } + return time; + } + + public long getActiveTopTime(long now) { + long time = pastActiveTopTime; + if (activeTopCount > 0) { + time += now - activeTopStartTime; + } + return time; + } + + public long getPendingTime(long now) { + long time = pastPendingTime; + if (pendingCount > 0) { + time += now - pendingStartTime; + } + return time; + } + } + + final static class DataSet { + final SparseArray> mEntries = new SparseArray<>(); + final long mStartUptimeTime; + final long mStartElapsedTime; + final long mStartClockTime; + long mSummedTime; + + public DataSet(DataSet otherTimes) { + mStartUptimeTime = otherTimes.mStartUptimeTime; + mStartElapsedTime = otherTimes.mStartElapsedTime; + mStartClockTime = otherTimes.mStartClockTime; + } + + public DataSet() { + mStartUptimeTime = SystemClock.uptimeMillis(); + mStartElapsedTime = SystemClock.elapsedRealtime(); + mStartClockTime = System.currentTimeMillis(); + } + + private PackageEntry getOrCreateEntry(int uid, String pkg) { + ArrayMap uidMap = mEntries.get(uid); + if (uidMap == null) { + uidMap = new ArrayMap<>(); + mEntries.put(uid, uidMap); + } + PackageEntry entry = uidMap.get(pkg); + if (entry == null) { + entry = new PackageEntry(); + uidMap.put(pkg, entry); + } + return entry; + } + + public PackageEntry getEntry(int uid, String pkg) { + ArrayMap uidMap = mEntries.get(uid); + if (uidMap == null) { + return null; + } + return uidMap.get(pkg); + } + + long getTotalTime(long now) { + if (mSummedTime > 0) { + return mSummedTime; + } + return now - mStartUptimeTime; + } + + void incPending(int uid, String pkg, long now) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.pendingCount == 0) { + pe.pendingStartTime = now; + } + pe.pendingCount++; + } + + void decPending(int uid, String pkg, long now) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.pendingCount == 1) { + pe.pastPendingTime += now - pe.pendingStartTime; + } + pe.pendingCount--; + } + + void incActive(int uid, String pkg, long now) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.activeCount == 0) { + pe.activeStartTime = now; + } + pe.activeCount++; + } + + void decActive(int uid, String pkg, long now) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.activeCount == 1) { + pe.pastActiveTime += now - pe.activeStartTime; + } + pe.activeCount--; + } + + void incActiveTop(int uid, String pkg, long now) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.activeTopCount == 0) { + pe.activeTopStartTime = now; + } + pe.activeTopCount++; + } + + void decActiveTop(int uid, String pkg, long now) { + PackageEntry pe = getOrCreateEntry(uid, pkg); + if (pe.activeTopCount == 1) { + pe.pastActiveTopTime += now - pe.activeTopStartTime; + } + pe.activeTopCount--; + } + + void finish(DataSet next, long now) { + for (int i = mEntries.size() - 1; i >= 0; i--) { + ArrayMap uidMap = mEntries.valueAt(i); + for (int j = uidMap.size() - 1; j >= 0; j--) { + PackageEntry pe = uidMap.valueAt(j); + if (pe.activeCount > 0 || pe.activeTopCount > 0 || pe.pendingCount > 0) { + // Propagate existing activity in to next data set. + PackageEntry nextPe = next.getOrCreateEntry(mEntries.keyAt(i), uidMap.keyAt(j)); + nextPe.activeStartTime = now; + nextPe.activeCount = pe.activeCount; + nextPe.activeTopStartTime = now; + nextPe.activeTopCount = pe.activeTopCount; + nextPe.pendingStartTime = now; + nextPe.pendingCount = pe.pendingCount; + // Finish it off. + if (pe.activeCount > 0) { + pe.pastActiveTime += now - pe.activeStartTime; + pe.activeCount = 0; + } + if (pe.activeTopCount > 0) { + pe.pastActiveTopTime += now - pe.activeTopStartTime; + pe.activeTopCount = 0; + } + if (pe.pendingCount > 0) { + pe.pastPendingTime += now - pe.pendingStartTime; + pe.pendingCount = 0; + } + } + } + } + } + + void addTo(DataSet out, long now) { + out.mSummedTime += getTotalTime(now); + for (int i = mEntries.size() - 1; i >= 0; i--) { + ArrayMap uidMap = mEntries.valueAt(i); + for (int j = uidMap.size() - 1; j >= 0; j--) { + PackageEntry pe = uidMap.valueAt(j); + PackageEntry outPe = out.getOrCreateEntry(mEntries.keyAt(i), uidMap.keyAt(j)); + outPe.pastActiveTime += pe.pastActiveTime; + outPe.pastActiveTopTime += pe.pastActiveTopTime; + outPe.pastPendingTime += pe.pastPendingTime; + if (pe.activeCount > 0) { + outPe.pastActiveTime += now - pe.activeStartTime; + outPe.hadActive = true; + } + if (pe.activeTopCount > 0) { + outPe.pastActiveTopTime += now - pe.activeTopStartTime; + outPe.hadActiveTop = true; + } + if (pe.pendingCount > 0) { + outPe.pastPendingTime += now - pe.pendingStartTime; + outPe.hadPending = true; + } + } + } + } + + void printDuration(PrintWriter pw, long period, long duration, String suffix) { + float fraction = duration / (float) period; + int percent = (int) ((fraction * 100) + .5f); + if (percent > 0) { + pw.print(" "); + pw.print(percent); + pw.print("% "); + pw.print(suffix); + } + } + + void dump(PrintWriter pw, String header, String prefix, long now, long nowEllapsed) { + final long period = getTotalTime(now); + pw.print(prefix); pw.print(header); pw.print(" at "); + pw.print(DateFormat.format("yyyy-MM-dd-HH-mm-ss", mStartClockTime).toString()); + pw.print(" ("); + TimeUtils.formatDuration(mStartElapsedTime, nowEllapsed, pw); + pw.print(") over "); + TimeUtils.formatDuration(period, pw); + pw.println(":"); + final int NE = mEntries.size(); + for (int i = 0; i < NE; i++) { + ArrayMap uidMap = mEntries.valueAt(i); + final int NP = uidMap.size(); + for (int j = 0; j < NP; j++) { + PackageEntry pe = uidMap.valueAt(j); + pw.print(prefix); pw.print(" "); + UserHandle.formatUid(pw, mEntries.keyAt(i)); + pw.print(" / "); pw.print(uidMap.keyAt(j)); + pw.print(":"); + printDuration(pw, period, pe.getPendingTime(now), "pending"); + printDuration(pw, period, pe.getActiveTime(now), "active"); + printDuration(pw, period, pe.getActiveTopTime(now), "active-top"); + if (pe.pendingCount > 0 || pe.hadPending) { + pw.print(" (pending)"); + } + if (pe.activeCount > 0 || pe.hadActive) { + pw.print(" (active)"); + } + if (pe.activeTopCount > 0 || pe.hadActiveTop) { + pw.print(" (active-top)"); + } + pw.println(); + } + } + } + } + + void rebatchIfNeeded(long now) { + long totalTime = mCurDataSet.getTotalTime(now); + if (totalTime > BATCHING_TIME) { + DataSet last = mCurDataSet; + last.mSummedTime = totalTime; + mCurDataSet = new DataSet(); + last.finish(mCurDataSet, now); + System.arraycopy(mLastDataSets, 0, mLastDataSets, 1, mLastDataSets.length-1); + mLastDataSets[0] = last; + } + } + + public void notePending(JobStatus job) { + final long now = SystemClock.uptimeMillis(); + rebatchIfNeeded(now); + mCurDataSet.incPending(job.getSourceUid(), job.getSourcePackageName(), now); + } + + public void noteNonpending(JobStatus job) { + final long now = SystemClock.uptimeMillis(); + mCurDataSet.decPending(job.getSourceUid(), job.getSourcePackageName(), now); + rebatchIfNeeded(now); + } + + public void noteActive(JobStatus job) { + final long now = SystemClock.uptimeMillis(); + rebatchIfNeeded(now); + if (job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP) { + mCurDataSet.incActiveTop(job.getSourceUid(), job.getSourcePackageName(), now); + } else { + mCurDataSet.incActive(job.getSourceUid(), job.getSourcePackageName(), now); + } + } + + public void noteInactive(JobStatus job) { + final long now = SystemClock.uptimeMillis(); + if (job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP) { + mCurDataSet.decActiveTop(job.getSourceUid(), job.getSourcePackageName(), now); + } else { + mCurDataSet.decActive(job.getSourceUid(), job.getSourcePackageName(), now); + } + rebatchIfNeeded(now); + } + + public float getLoadFactor(JobStatus job) { + final int uid = job.getSourceUid(); + final String pkg = job.getSourcePackageName(); + PackageEntry cur = mCurDataSet.getEntry(uid, pkg); + PackageEntry last = mLastDataSets[0] != null ? mLastDataSets[0].getEntry(uid, pkg) : null; + if (cur == null && last == null) { + return 0; + } + final long now = SystemClock.uptimeMillis(); + long time = cur.getActiveTime(now) + cur.getPendingTime(now); + long period = mCurDataSet.getTotalTime(now); + if (last != null) { + time += last.getActiveTime(now) + last.getPendingTime(now); + period += mLastDataSets[0].getTotalTime(now); + } + return time / (float)period; + } + + public void dump(PrintWriter pw, String prefix) { + final long now = SystemClock.uptimeMillis(); + final long nowEllapsed = SystemClock.elapsedRealtime(); + final DataSet total; + if (mLastDataSets[0] != null) { + total = new DataSet(mLastDataSets[0]); + mLastDataSets[0].addTo(total, now); + } else { + total = new DataSet(mCurDataSet); + } + mCurDataSet.addTo(total, now); + for (int i = 1; i < mLastDataSets.length; i++) { + if (mLastDataSets[i] != null) { + mLastDataSets[i].dump(pw, "Historical stats", prefix, now, nowEllapsed); + pw.println(); + } + } + total.dump(pw, "Current stats", prefix, now, nowEllapsed); + } +} diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java index b2350025152fa..c4a2c73126007 100644 --- a/services/core/java/com/android/server/job/JobSchedulerService.java +++ b/services/core/java/com/android/server/job/JobSchedulerService.java @@ -93,16 +93,24 @@ public final class JobSchedulerService extends com.android.server.SystemService public static final boolean DEBUG = false; /** The maximum number of concurrent jobs we run at one time. */ - private static final int MAX_JOB_CONTEXTS_COUNT = 8; + private static final int MAX_JOB_CONTEXTS_COUNT = 12; + /** The number of MAX_JOB_CONTEXTS_COUNT we reserve for the foreground app. */ + private static final int FG_JOB_CONTEXTS_COUNT = 4; /** Enforce a per-app limit on scheduled jobs? */ private static final boolean ENFORCE_MAX_JOBS = true; /** The maximum number of jobs that we allow an unprivileged app to schedule */ private static final int MAX_JOBS_PER_APP = 100; + /** This is the job execution factor that is considered to be heavy use of the system. */ + private static final float HEAVY_USE_FACTOR = .9f; + /** This is the job execution factor that is considered to be moderate use of the system. */ + private static final float MODERATE_USE_FACTOR = .5f; /** Global local for all job scheduler state. */ final Object mLock = new Object(); /** Master list of jobs. */ final JobStore mJobs; + /** Tracking amount of time each package runs for. */ + final JobPackageTracker mJobPackageTracker = new JobPackageTracker(); static final int MSG_JOB_EXPIRED = 0; static final int MSG_CHECK_JOB = 1; @@ -173,7 +181,7 @@ public final class JobSchedulerService extends com.android.server.SystemService * Current limit on the number of concurrent JobServiceContext entries we want to * keep actively running a job. */ - int mMaxActiveJobs = MAX_JOB_CONTEXTS_COUNT - 2; + int mMaxActiveJobs = MAX_JOB_CONTEXTS_COUNT - FG_JOB_CONTEXTS_COUNT; /** * Which uids are currently in the foreground. @@ -386,7 +394,9 @@ public final class JobSchedulerService extends com.android.server.SystemService stopTrackingJob(cancelled, incomingJob, true /* writeBack */); synchronized (mLock) { // Remove from pending queue. - mPendingJobs.remove(cancelled); + if (mPendingJobs.remove(cancelled)) { + mJobPackageTracker.noteNonpending(cancelled); + } // Cancel if running. stopJobOnServiceContextLocked(cancelled, JobParameters.REASON_CANCELED); reportActive(); @@ -518,7 +528,7 @@ public final class JobSchedulerService extends com.android.server.SystemService // Create the "runners". for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) { mActiveServices.add( - new JobServiceContext(this, mBatteryStats, + new JobServiceContext(this, mBatteryStats, mJobPackageTracker, getContext().getMainLooper())); } // Attach jobs to their controllers. @@ -604,6 +614,20 @@ public final class JobSchedulerService extends com.android.server.SystemService return false; } + void noteJobsPending(List jobs) { + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus job = jobs.get(i); + mJobPackageTracker.notePending(job); + } + } + + void noteJobsNonpending(List jobs) { + for (int i = jobs.size() - 1; i >= 0; i--) { + JobStatus job = jobs.get(i); + mJobPackageTracker.noteNonpending(job); + } + } + /** * Reschedules the given job based on the job's backoff policy. It doesn't make sense to * specify an override deadline on a failed job (the failed job will run even though it's not @@ -759,6 +783,7 @@ public final class JobSchedulerService extends com.android.server.SystemService // state is such that all ready jobs should be run immediately. if (runNow != null && !mPendingJobs.contains(runNow) && mJobs.containsJob(runNow)) { + mJobPackageTracker.notePending(runNow); mPendingJobs.add(runNow); } queueReadyJobsForExecutionLockedH(); @@ -797,6 +822,7 @@ public final class JobSchedulerService extends com.android.server.SystemService if (DEBUG) { Slog.d(TAG, "queuing all ready jobs for execution:"); } + noteJobsNonpending(mPendingJobs); mPendingJobs.clear(); mJobs.forEachJob(mReadyQueueFunctor); mReadyQueueFunctor.postProcess(); @@ -832,6 +858,7 @@ public final class JobSchedulerService extends com.android.server.SystemService public void postProcess() { if (newReadyJobs != null) { + noteJobsPending(newReadyJobs); mPendingJobs.addAll(newReadyJobs); } newReadyJobs = null; @@ -910,6 +937,7 @@ public final class JobSchedulerService extends com.android.server.SystemService if (DEBUG) { Slog.d(TAG, "maybeQueueReadyJobsForExecutionLockedH: Running jobs."); } + noteJobsPending(runnableJobs); mPendingJobs.addAll(runnableJobs); } else { if (DEBUG) { @@ -935,6 +963,7 @@ public final class JobSchedulerService extends com.android.server.SystemService private void maybeQueueReadyJobsForExecutionLockedH() { if (DEBUG) Slog.d(TAG, "Maybe queuing ready jobs..."); + noteJobsNonpending(mPendingJobs); mPendingJobs.clear(); mJobs.forEachJob(mMaybeQueueFunctor); mMaybeQueueFunctor.postProcess(); @@ -998,16 +1027,28 @@ public final class JobSchedulerService extends com.android.server.SystemService } } + private int adjustJobPriority(int curPriority, JobStatus job) { + if (curPriority < JobInfo.PRIORITY_TOP_APP) { + float factor = mJobPackageTracker.getLoadFactor(job); + if (factor >= HEAVY_USE_FACTOR) { + curPriority += JobInfo.PRIORITY_ADJ_ALWAYS_RUNNING; + } else if (factor >= MODERATE_USE_FACTOR) { + curPriority += JobInfo.PRIORITY_ADJ_OFTEN_RUNNING; + } + } + return curPriority; + } + private int evaluateJobPriorityLocked(JobStatus job) { int priority = job.getPriority(); if (priority >= JobInfo.PRIORITY_FOREGROUND_APP) { - return priority; + return adjustJobPriority(priority, job); } int override = mUidPriorityOverride.get(job.getSourceUid(), 0); if (override != 0) { - return override; + return adjustJobPriority(override, job); } - return priority; + return adjustJobPriority(priority, job); } /** @@ -1029,16 +1070,16 @@ public final class JobSchedulerService extends com.android.server.SystemService } switch (memLevel) { case ProcessStats.ADJ_MEM_FACTOR_MODERATE: - mMaxActiveJobs = ((MAX_JOB_CONTEXTS_COUNT - 2) * 2) / 3; + mMaxActiveJobs = ((MAX_JOB_CONTEXTS_COUNT - FG_JOB_CONTEXTS_COUNT) * 2) / 3; break; case ProcessStats.ADJ_MEM_FACTOR_LOW: - mMaxActiveJobs = (MAX_JOB_CONTEXTS_COUNT - 2) / 3; + mMaxActiveJobs = (MAX_JOB_CONTEXTS_COUNT - FG_JOB_CONTEXTS_COUNT) / 3; break; case ProcessStats.ADJ_MEM_FACTOR_CRITICAL: mMaxActiveJobs = 1; break; default: - mMaxActiveJobs = MAX_JOB_CONTEXTS_COUNT - 2; + mMaxActiveJobs = MAX_JOB_CONTEXTS_COUNT - FG_JOB_CONTEXTS_COUNT; break; } @@ -1134,7 +1175,9 @@ public final class JobSchedulerService extends com.android.server.SystemService if (!mActiveServices.get(i).executeRunnableJob(pendingJob)) { Slog.d(TAG, "Error executing " + pendingJob); } - mPendingJobs.remove(pendingJob); + if (mPendingJobs.remove(pendingJob)) { + mJobPackageTracker.noteNonpending(pendingJob); + } } } if (!preservePreferredUid) { @@ -1444,6 +1487,8 @@ public final class JobSchedulerService extends com.android.server.SystemService pw.print(": "); pw.println(mUidPriorityOverride.valueAt(i)); } pw.println(); + mJobPackageTracker.dump(pw, ""); + pw.println(); pw.println("Pending queue:"); for (int i=0; i