/*
 * Copyright (C) 2018 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.ActivityManager;
import android.app.job.JobInfo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.PowerManager;
import android.os.RemoteException;
import android.util.Slog;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.procstats.ProcessStats;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.StatLogger;
import com.android.server.job.JobSchedulerService.Constants;
import com.android.server.job.JobSchedulerService.MaxJobCountsPerMemoryTrimLevel;
import com.android.server.job.controllers.JobStatus;
import com.android.server.job.controllers.StateController;

import java.util.Iterator;
import java.util.List;

/**
 * This class decides, given the various configuration and the system status, how many more jobs
 * can start.
 */
class JobConcurrencyManager {
    private static final String TAG = JobSchedulerService.TAG;
    private static final boolean DEBUG = JobSchedulerService.DEBUG;

    private final Object mLock;
    private final JobSchedulerService mService;
    private final JobSchedulerService.Constants mConstants;
    private final Context mContext;
    private final Handler mHandler;

    private PowerManager mPowerManager;

    private boolean mCurrentInteractiveState;
    private boolean mEffectiveInteractiveState;

    private long mLastScreenOnRealtime;
    private long mLastScreenOffRealtime;

    private static final int MAX_JOB_CONTEXTS_COUNT = JobSchedulerService.MAX_JOB_CONTEXTS_COUNT;

    /**
     * This array essentially stores the state of mActiveServices array.
     * The ith index stores the job present on the ith JobServiceContext.
     * We manipulate this array until we arrive at what jobs should be running on
     * what JobServiceContext.
     */
    JobStatus[] mRecycledAssignContextIdToJobMap = new JobStatus[MAX_JOB_CONTEXTS_COUNT];

    boolean[] mRecycledSlotChanged = new boolean[MAX_JOB_CONTEXTS_COUNT];

    int[] mRecycledPreferredUidForContext = new int[MAX_JOB_CONTEXTS_COUNT];

    /** Max job counts according to the current system state. */
    private JobSchedulerService.MaxJobCounts mMaxJobCounts;

    private final JobCountTracker mJobCountTracker = new JobCountTracker();

    /** Current memory trim level. */
    private int mLastMemoryTrimLevel;

    /** Used to throttle heavy API calls. */
    private long mNextSystemStateRefreshTime;
    private static final int SYSTEM_STATE_REFRESH_MIN_INTERVAL = 1000;

    private final StatLogger mStatLogger = new StatLogger(new String[]{
            "assignJobsToContexts",
            "refreshSystemState",
    });

    interface Stats {
        int ASSIGN_JOBS_TO_CONTEXTS = 0;
        int REFRESH_SYSTEM_STATE = 1;

        int COUNT = REFRESH_SYSTEM_STATE + 1;
    }

    JobConcurrencyManager(JobSchedulerService service) {
        mService = service;
        mLock = mService.mLock;
        mConstants = service.mConstants;
        mContext = service.getContext();

        mHandler = BackgroundThread.getHandler();
    }

    public void onSystemReady() {
        mPowerManager = mContext.getSystemService(PowerManager.class);

        final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
        filter.addAction(Intent.ACTION_SCREEN_OFF);
        mContext.registerReceiver(mReceiver, filter);

        onInteractiveStateChanged(mPowerManager.isInteractive());
    }

    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            switch (intent.getAction()) {
                case Intent.ACTION_SCREEN_ON:
                    onInteractiveStateChanged(true);
                    break;
                case Intent.ACTION_SCREEN_OFF:
                    onInteractiveStateChanged(false);
                    break;
            }
        }
    };

    /**
     * Called when the screen turns on / off.
     */
    private void onInteractiveStateChanged(boolean interactive) {
        synchronized (mLock) {
            if (mCurrentInteractiveState == interactive) {
                return;
            }
            mCurrentInteractiveState = interactive;
            if (DEBUG) {
                Slog.d(TAG, "Interactive: " + interactive);
            }

            final long nowRealtime = JobSchedulerService.sElapsedRealtimeClock.millis();
            if (interactive) {
                mLastScreenOnRealtime = nowRealtime;
                mEffectiveInteractiveState = true;

                mHandler.removeCallbacks(mRampUpForScreenOff);
            } else {
                mLastScreenOffRealtime = nowRealtime;

                // Set mEffectiveInteractiveState to false after the delay, when we may increase
                // the concurrency.
                // We don't need a wakeup alarm here. When there's a pending job, there should
                // also be jobs running too, meaning the device should be awake.

                // Note: we can't directly do postDelayed(this::rampUpForScreenOn), because
                // we need the exact same instance for removeCallbacks().
                mHandler.postDelayed(mRampUpForScreenOff,
                        mConstants.SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.getValue());
            }
        }
    }

    private final Runnable mRampUpForScreenOff = this::rampUpForScreenOff;

    /**
     * Called in {@link Constants#SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS} after
     * the screen turns off, in order to increase concurrency.
     */
    private void rampUpForScreenOff() {
        synchronized (mLock) {
            // Make sure the screen has really been off for the configured duration.
            // (There could be a race.)
            if (!mEffectiveInteractiveState) {
                return;
            }
            if (mLastScreenOnRealtime > mLastScreenOffRealtime) {
                return;
            }
            final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
            if ((mLastScreenOffRealtime
                    + mConstants.SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.getValue())
                    > now) {
                return;
            }

            mEffectiveInteractiveState = false;

            if (DEBUG) {
                Slog.d(TAG, "Ramping up concurrency");
            }

            mService.maybeRunPendingJobsLocked();
        }
    }

    private boolean isFgJob(JobStatus job) {
        return job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP;
    }

    @GuardedBy("mLock")
    private void refreshSystemStateLocked() {
        final long nowUptime = JobSchedulerService.sUptimeMillisClock.millis();

        // Only refresh the information every so often.
        if (nowUptime < mNextSystemStateRefreshTime) {
            return;
        }

        final long start = mStatLogger.getTime();
        mNextSystemStateRefreshTime = nowUptime + SYSTEM_STATE_REFRESH_MIN_INTERVAL;

        mLastMemoryTrimLevel = ProcessStats.ADJ_MEM_FACTOR_NORMAL;
        try {
            mLastMemoryTrimLevel = ActivityManager.getService().getMemoryTrimLevel();
        } catch (RemoteException e) {
        }

        mStatLogger.logDurationStat(Stats.REFRESH_SYSTEM_STATE, start);
    }

    @GuardedBy("mLock")
    private void updateMaxCountsLocked() {
        refreshSystemStateLocked();

        final MaxJobCountsPerMemoryTrimLevel jobCounts = mEffectiveInteractiveState
                ? mConstants.MAX_JOB_COUNTS_SCREEN_ON
                : mConstants.MAX_JOB_COUNTS_SCREEN_OFF;


        switch (mLastMemoryTrimLevel) {
            case ProcessStats.ADJ_MEM_FACTOR_MODERATE:
                mMaxJobCounts = jobCounts.moderate;
                break;
            case ProcessStats.ADJ_MEM_FACTOR_LOW:
                mMaxJobCounts = jobCounts.low;
                break;
            case ProcessStats.ADJ_MEM_FACTOR_CRITICAL:
                mMaxJobCounts = jobCounts.critical;
                break;
            default:
                mMaxJobCounts = jobCounts.normal;
                break;
        }
    }

    /**
     * Takes jobs from pending queue and runs them on available contexts.
     * If no contexts are available, preempts lower priority jobs to
     * run higher priority ones.
     * Lock on mJobs before calling this function.
     */
    @GuardedBy("mLock")
    void assignJobsToContextsLocked() {
        final long start = mStatLogger.getTime();

        assignJobsToContextsInternalLocked();

        mStatLogger.logDurationStat(Stats.ASSIGN_JOBS_TO_CONTEXTS, start);
    }

    @GuardedBy("mLock")
    private void assignJobsToContextsInternalLocked() {
        if (DEBUG) {
            Slog.d(TAG, printPendingQueueLocked());
        }

        final JobPackageTracker tracker = mService.mJobPackageTracker;
        final List<JobStatus> pendingJobs = mService.mPendingJobs;
        final List<JobServiceContext> activeServices = mService.mActiveServices;
        final List<StateController> controllers = mService.mControllers;

        updateMaxCountsLocked();

        // To avoid GC churn, we recycle the arrays.
        JobStatus[] contextIdToJobMap = mRecycledAssignContextIdToJobMap;
        boolean[] slotChanged = mRecycledSlotChanged;
        int[] preferredUidForContext = mRecycledPreferredUidForContext;


        // Initialize the work variables and also count running jobs.
        mJobCountTracker.reset(
                mMaxJobCounts.getMaxTotal(),
                mMaxJobCounts.getMaxBg(),
                mMaxJobCounts.getMinBg());

        for (int i=0; i<MAX_JOB_CONTEXTS_COUNT; i++) {
            final JobServiceContext js = mService.mActiveServices.get(i);
            final JobStatus status = js.getRunningJobLocked();

            if ((contextIdToJobMap[i] = status) != null) {
                mJobCountTracker.incrementRunningJobCount(isFgJob(status));
            }

            slotChanged[i] = false;
            preferredUidForContext[i] = js.getPreferredUid();
        }
        if (DEBUG) {
            Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs initial"));
        }

        // Next, update the job priorities, and also count the pending FG / BG jobs.
        for (int i = 0; i < pendingJobs.size(); i++) {
            final JobStatus pending = pendingJobs.get(i);

            // If job is already running, go to next job.
            int jobRunningContext = findJobContextIdFromMap(pending, contextIdToJobMap);
            if (jobRunningContext != -1) {
                continue;
            }

            final int priority = mService.evaluateJobPriorityLocked(pending);
            pending.lastEvaluatedPriority = priority;

            mJobCountTracker.incrementPendingJobCount(isFgJob(pending));
        }

        mJobCountTracker.onCountDone();

        for (int i = 0; i < pendingJobs.size(); i++) {
            final JobStatus nextPending = pendingJobs.get(i);

            // Unfortunately we need to repeat this relatively expensive check.
            int jobRunningContext = findJobContextIdFromMap(nextPending, contextIdToJobMap);
            if (jobRunningContext != -1) {
                continue;
            }

            final boolean isPendingFg = isFgJob(nextPending);

            // Find an available slot for nextPending. The context should be available OR
            // it should have lowest priority among all running jobs
            // (sharing the same Uid as nextPending)
            int minPriorityForPreemption = Integer.MAX_VALUE;
            int selectedContextId = -1;
            boolean startingJob = false;
            for (int j=0; j<MAX_JOB_CONTEXTS_COUNT; j++) {
                JobStatus job = contextIdToJobMap[j];
                int preferredUid = preferredUidForContext[j];
                if (job == null) {
                    final boolean preferredUidOkay = (preferredUid == nextPending.getUid())
                            || (preferredUid == JobServiceContext.NO_PREFERRED_UID);

                    if (preferredUidOkay && mJobCountTracker.canJobStart(isPendingFg)) {
                        // This slot is free, and we haven't yet hit the limit on
                        // concurrent jobs...  we can just throw the job in to here.
                        selectedContextId = j;
                        startingJob = true;
                        break;
                    }
                    // 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()) {
                    continue;
                }

                final int jobPriority = mService.evaluateJobPriorityLocked(job);
                if (jobPriority >= nextPending.lastEvaluatedPriority) {
                    continue;
                }

                // TODO lastEvaluatedPriority should be evaluateJobPriorityLocked. (double check it)
                if (minPriorityForPreemption > nextPending.lastEvaluatedPriority) {
                    minPriorityForPreemption = nextPending.lastEvaluatedPriority;
                    selectedContextId = j;
                    // In this case, we're just going to preempt a low priority job, we're not
                    // actually starting a job, so don't set startingJob.
                }
            }
            if (selectedContextId != -1) {
                contextIdToJobMap[selectedContextId] = nextPending;
                slotChanged[selectedContextId] = true;
            }
            if (startingJob) {
                // Increase the counters when we're going to start a job.
                mJobCountTracker.onStartingNewJob(isPendingFg);
            }
        }
        if (DEBUG) {
            Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs final"));
        }

        mJobCountTracker.logStatus();

        tracker.noteConcurrency(mJobCountTracker.getTotalRunningJobCountToNote(),
                mJobCountTracker.getFgRunningJobCountToNote());

        for (int i=0; i<MAX_JOB_CONTEXTS_COUNT; i++) {
            boolean preservePreferredUid = false;
            if (slotChanged[i]) {
                JobStatus js = activeServices.get(i).getRunningJobLocked();
                if (js != null) {
                    if (DEBUG) {
                        Slog.d(TAG, "preempting job: "
                                + activeServices.get(i).getRunningJobLocked());
                    }
                    // preferredUid will be set to uid of currently running job.
                    activeServices.get(i).preemptExecutingJobLocked();
                    preservePreferredUid = true;
                } else {
                    final JobStatus pendingJob = contextIdToJobMap[i];
                    if (DEBUG) {
                        Slog.d(TAG, "About to run job on context "
                                + i + ", job: " + pendingJob);
                    }
                    for (int ic=0; ic<controllers.size(); ic++) {
                        controllers.get(ic).prepareForExecutionLocked(pendingJob);
                    }
                    if (!activeServices.get(i).executeRunnableJob(pendingJob)) {
                        Slog.d(TAG, "Error executing " + pendingJob);
                    }
                    if (pendingJobs.remove(pendingJob)) {
                        tracker.noteNonpending(pendingJob);
                    }
                }
            }
            if (!preservePreferredUid) {
                activeServices.get(i).clearPreferredUid();
            }
        }
    }

    private static int findJobContextIdFromMap(JobStatus jobStatus, JobStatus[] map) {
        for (int i=0; i<map.length; i++) {
            if (map[i] != null && map[i].matches(jobStatus.getUid(), jobStatus.getJobId())) {
                return i;
            }
        }
        return -1;
    }

    @GuardedBy("mLock")
    private String printPendingQueueLocked() {
        StringBuilder s = new StringBuilder("Pending queue: ");
        Iterator<JobStatus> it = mService.mPendingJobs.iterator();
        while (it.hasNext()) {
            JobStatus js = it.next();
            s.append("(")
                    .append(js.getJob().getId())
                    .append(", ")
                    .append(js.getUid())
                    .append(") ");
        }
        return s.toString();
    }

    private static String printContextIdToJobMap(JobStatus[] map, String initial) {
        StringBuilder s = new StringBuilder(initial + ": ");
        for (int i=0; i<map.length; i++) {
            s.append("(")
                    .append(map[i] == null? -1: map[i].getJobId())
                    .append(map[i] == null? -1: map[i].getUid())
                    .append(")" );
        }
        return s.toString();
    }


    public void dumpLocked(IndentingPrintWriter pw, long now, long nowRealtime) {
        pw.println("Concurrency:");

        pw.increaseIndent();
        try {
            pw.print("Screen state: current ");
            pw.print(mCurrentInteractiveState ? "ON" : "OFF");
            pw.print("  effective ");
            pw.print(mEffectiveInteractiveState ? "ON" : "OFF");
            pw.println();

            pw.print("Last screen ON : ");
            TimeUtils.dumpTimeWithDelta(pw, now - nowRealtime + mLastScreenOnRealtime, now);
            pw.println();

            pw.print("Last screen OFF: ");
            TimeUtils.dumpTimeWithDelta(pw, now - nowRealtime + mLastScreenOffRealtime, now);
            pw.println();

            pw.println();

            pw.println("Current max jobs:");
            pw.println("  ");
            pw.println(mJobCountTracker);

            pw.println();

            pw.print("mLastMemoryTrimLevel: ");
            pw.print(mLastMemoryTrimLevel);
            pw.println();

            mStatLogger.dump(pw);
        } finally {
            pw.decreaseIndent();
        }
    }

    public void dumpProtoLocked(ProtoOutputStream proto, long tag, long now, long nowRealtime) {
        final long token = proto.start(tag);

        proto.write(JobConcurrencyManagerProto.CURRENT_INTERACTIVE,
                mCurrentInteractiveState);
        proto.write(JobConcurrencyManagerProto.EFFECTIVE_INTERACTIVE,
                mEffectiveInteractiveState);

        proto.write(JobConcurrencyManagerProto.TIME_SINCE_LAST_SCREEN_ON_MS,
                nowRealtime - mLastScreenOnRealtime);
        proto.write(JobConcurrencyManagerProto.TIME_SINCE_LAST_SCREEN_OFF_MS,
                nowRealtime - mLastScreenOffRealtime);

        mJobCountTracker.dumpProto(proto, JobConcurrencyManagerProto.JOB_COUNT_TRACKER);

        proto.write(JobConcurrencyManagerProto.MEMORY_TRIM_LEVEL,
                mLastMemoryTrimLevel);

        proto.end(token);
    }

    /**
     * This class decides, taking into account {@link #mMaxJobCounts} and how mny jos are running /
     * pending, how many more job can start.
     *
     * Extracted for testing and logging.
     */
    @VisibleForTesting
    static class JobCountTracker {
        private int mConfigNumMaxTotalJobs;
        private int mConfigNumMaxBgJobs;
        private int mConfigNumMinBgJobs;

        private int mNumRunningFgJobs;
        private int mNumRunningBgJobs;

        private int mNumPendingFgJobs;
        private int mNumPendingBgJobs;

        private int mNumStartingFgJobs;
        private int mNumStartingBgJobs;

        private int mNumReservedForBg;
        private int mNumActualMaxFgJobs;
        private int mNumActualMaxBgJobs;

        void reset(int numTotalMaxJobs, int numMaxBgJobs, int numMinBgJobs) {
            mConfigNumMaxTotalJobs = numTotalMaxJobs;
            mConfigNumMaxBgJobs = numMaxBgJobs;
            mConfigNumMinBgJobs = numMinBgJobs;

            mNumRunningFgJobs = 0;
            mNumRunningBgJobs = 0;

            mNumPendingFgJobs = 0;
            mNumPendingBgJobs = 0;

            mNumStartingFgJobs = 0;
            mNumStartingBgJobs = 0;

            mNumReservedForBg = 0;
            mNumActualMaxFgJobs = 0;
            mNumActualMaxBgJobs = 0;
        }

        void incrementRunningJobCount(boolean isFg) {
            if (isFg) {
                mNumRunningFgJobs++;
            } else {
                mNumRunningBgJobs++;
            }
        }

        void incrementPendingJobCount(boolean isFg) {
            if (isFg) {
                mNumPendingFgJobs++;
            } else {
                mNumPendingBgJobs++;
            }
        }

        void onStartingNewJob(boolean isFg) {
            if (isFg) {
                mNumStartingFgJobs++;
            } else {
                mNumStartingBgJobs++;
            }
        }

        void onCountDone() {
            // Note some variables are used only here but are made class members in order to have
            // them on logcat / dumpsys.

            // How many slots should we allocate to BG jobs at least?
            // That's basically "getMinBg()", but if there are less jobs, decrease it.
            // (e.g. even if min-bg is 2, if there's only 1 running+pending job, this has to be 1.)
            final int reservedForBg = Math.min(
                    mConfigNumMinBgJobs,
                    mNumRunningBgJobs + mNumPendingBgJobs);

            // However, if there are FG jobs already running, we have to adjust it.
            mNumReservedForBg = Math.min(reservedForBg,
                    mConfigNumMaxTotalJobs - mNumRunningFgJobs);

            // Max FG is [total - [number needed for BG jobs]]
            // [number needed for BG jobs] is the bigger one of [running BG] or [reserved BG]
            final int maxFg =
                    mConfigNumMaxTotalJobs - Math.max(mNumRunningBgJobs, mNumReservedForBg);

            // The above maxFg is the theoretical max. If there are less FG jobs, the actual
            // max FG will be lower accordingly.
            mNumActualMaxFgJobs = Math.min(
                    maxFg,
                    mNumRunningFgJobs + mNumPendingFgJobs);

            // Max BG is [total - actual max FG], but cap at [config max BG].
            final int maxBg = Math.min(
                    mConfigNumMaxBgJobs,
                    mConfigNumMaxTotalJobs - mNumActualMaxFgJobs);

            // If there are less BG jobs than maxBg, then reduce the actual max BG accordingly.
            // This isn't needed for the logic to work, but this will give consistent output
            // on logcat and dumpsys.
            mNumActualMaxBgJobs = Math.min(
                    maxBg,
                    mNumRunningBgJobs + mNumPendingBgJobs);
        }

        boolean canJobStart(boolean isFg) {
            if (isFg) {
                return mNumRunningFgJobs + mNumStartingFgJobs < mNumActualMaxFgJobs;
            } else {
                return mNumRunningBgJobs + mNumStartingBgJobs < mNumActualMaxBgJobs;
            }
        }

        public int getNumStartingFgJobs() {
            return mNumStartingFgJobs;
        }

        public int getNumStartingBgJobs() {
            return mNumStartingBgJobs;
        }

        int getTotalRunningJobCountToNote() {
            return mNumRunningFgJobs + mNumRunningBgJobs
                    + mNumStartingFgJobs + mNumStartingBgJobs;
        }

        int getFgRunningJobCountToNote() {
            return mNumRunningFgJobs + mNumStartingFgJobs;
        }

        void logStatus() {
            if (DEBUG) {
                Slog.d(TAG, "assignJobsToContexts: " + this);
            }
        }

        public String toString() {
            final int totalFg = mNumRunningFgJobs + mNumStartingFgJobs;
            final int totalBg = mNumRunningBgJobs + mNumStartingBgJobs;
            return String.format(
                    "Config={tot=%d bg min/max=%d/%d}"
                            + " Running[FG/BG (total)]: %d / %d (%d)"
                            + " Pending: %d / %d (%d)"
                            + " Actual max: %d%s / %d%s (%d%s)"
                            + " Res BG: %d"
                            + " Starting: %d / %d (%d)"
                            + " Total: %d%s / %d%s (%d%s)",
                    mConfigNumMaxTotalJobs,
                    mConfigNumMinBgJobs,
                    mConfigNumMaxBgJobs,

                    mNumRunningFgJobs, mNumRunningBgJobs,
                    mNumRunningFgJobs + mNumRunningBgJobs,

                    mNumPendingFgJobs, mNumPendingBgJobs,
                    mNumPendingFgJobs + mNumPendingBgJobs,

                    mNumActualMaxFgJobs, (totalFg <= mConfigNumMaxTotalJobs) ? "" : "*",
                    mNumActualMaxBgJobs, (totalBg <= mConfigNumMaxBgJobs) ? "" : "*",

                    mNumActualMaxFgJobs + mNumActualMaxBgJobs,
                    (mNumActualMaxFgJobs + mNumActualMaxBgJobs <= mConfigNumMaxTotalJobs)
                            ? "" : "*",

                    mNumReservedForBg,

                    mNumStartingFgJobs, mNumStartingBgJobs, mNumStartingFgJobs + mNumStartingBgJobs,

                    totalFg, (totalFg <= mNumActualMaxFgJobs) ? "" : "*",
                    totalBg, (totalBg <= mNumActualMaxBgJobs) ? "" : "*",
                    totalFg + totalBg, (totalFg + totalBg <= mConfigNumMaxTotalJobs) ? "" : "*"
            );
        }

        public void dumpProto(ProtoOutputStream proto, long fieldId) {
            final long token = proto.start(fieldId);

            proto.write(JobCountTrackerProto.CONFIG_NUM_MAX_TOTAL_JOBS, mConfigNumMaxTotalJobs);
            proto.write(JobCountTrackerProto.CONFIG_NUM_MAX_BG_JOBS, mConfigNumMaxBgJobs);
            proto.write(JobCountTrackerProto.CONFIG_NUM_MIN_BG_JOBS, mConfigNumMinBgJobs);

            proto.write(JobCountTrackerProto.NUM_RUNNING_FG_JOBS, mNumRunningFgJobs);
            proto.write(JobCountTrackerProto.NUM_RUNNING_BG_JOBS, mNumRunningBgJobs);

            proto.write(JobCountTrackerProto.NUM_PENDING_FG_JOBS, mNumPendingFgJobs);
            proto.write(JobCountTrackerProto.NUM_PENDING_BG_JOBS, mNumPendingBgJobs);

            proto.end(token);
        }
    }
}
