/*
 * 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.internal.os;

import static com.android.internal.util.Preconditions.checkNotNull;

import android.annotation.Nullable;
import android.util.ArrayMap;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * Delegates per-thread CPU collection to {@link KernelCpuThreadReader}, and calculates the
 * difference between CPU usage at each call of {@link #getProcessCpuUsageDiffed()}.
 *
 * <p>Some notes on the diff calculation:
 *
 * <ul>
 *   <li>The diffing is done between each call of {@link #getProcessCpuUsageDiffed()}, i.e. call N
 *       of this method will return CPU used by threads between call N-1 and N.
 *   <li>The first call of {@link #getProcessCpuUsageDiffed()} will return no processes ("first
 *       call" is the first call in the lifetime of a {@link KernelCpuThreadReaderDiff} object).
 *   <li>If a thread does not exist at call N, but does exist at call N+1, the diff will assume that
 *       the CPU usage at call N was zero. Thus, the diff reported will be equivalent to the value
 *       returned by {@link KernelCpuThreadReader#getProcessCpuUsage()} at call N+1.
 *   <li>If an error occurs in {@link KernelCpuThreadReader} at call N, we will return no
 *       information for CPU usage between call N-1 and N (as we don't know the start value) and
 *       between N and N+1 (as we don't know the end value). Assuming all other calls are
 *       successful, the next call to return data will be N+2, for the period between N+1 and N+2.
 *   <li>If an error occurs in this class (but not in {@link KernelCpuThreadReader}) at call N, the
 *       data will only be dropped for call N, as we can still use the CPU data for the surrounding
 *       calls.
 * </ul>
 *
 * <p>Additionally to diffing, this class also contains logic for thresholding reported threads. A
 * thread will not be reported unless its total CPU usage is at least equal to the value set in
 * {@link #setMinimumTotalCpuUsageMillis}. Filtered thread CPU usage is summed and reported under
 * one "other threads" thread. This reduces the cardinality of the {@link
 * #getProcessCpuUsageDiffed()} result.
 *
 * <p>Thresholding is done in this class, instead of {@link KernelCpuThreadReader}, and instead of
 * statsd, because the thresholding should be done after diffing, not before. This is because of
 * two issues with thresholding before diffing:
 *
 * <ul>
 *   <li>We would threshold less and less threads as thread uptime increases.
 *   <li>We would encounter errors as the filtered threads become unfiltered, as the "other threads"
 *       result could have negative diffs, and the newly unfiltered threads would have incorrect
 *       diffs that include CPU usage from when they were filtered.
 * </ul>
 *
 * @hide Only for use within the system server
 */
@SuppressWarnings("ForLoopReplaceableByForEach")
public class KernelCpuThreadReaderDiff {
    private static final String TAG = "KernelCpuThreadReaderDiff";

    /** Thread ID used when reporting CPU used by other threads */
    private static final int OTHER_THREADS_ID = -1;

    /** Thread name used when reporting CPU used by other threads */
    private static final String OTHER_THREADS_NAME = "__OTHER_THREADS";

    private final KernelCpuThreadReader mReader;

    /**
     * CPU usage from the previous call of {@link #getProcessCpuUsageDiffed()}. Null if there was no
     * previous call, or if the previous call failed
     *
     * <p>Maps the thread's identifier to the per-frequency CPU usage for that thread. The
     * identifier contains the minimal amount of information to identify a thread (see {@link
     * ThreadKey} for more information), thus reducing memory consumption.
     */
    @Nullable private Map<ThreadKey, int[]> mPreviousCpuUsage;

    /**
     * If a thread has strictly less than {@code minimumTotalCpuUsageMillis} total CPU usage, it
     * will not be reported
     */
    private int mMinimumTotalCpuUsageMillis;

    @VisibleForTesting
    public KernelCpuThreadReaderDiff(KernelCpuThreadReader reader, int minimumTotalCpuUsageMillis) {
        mReader = checkNotNull(reader);
        mMinimumTotalCpuUsageMillis = minimumTotalCpuUsageMillis;
        mPreviousCpuUsage = null;
    }

    /**
     * Returns the difference in CPU usage since the last time this method was called.
     *
     * @see KernelCpuThreadReader#getProcessCpuUsage()
     */
    @Nullable
    public ArrayList<KernelCpuThreadReader.ProcessCpuUsage> getProcessCpuUsageDiffed() {
        Map<ThreadKey, int[]> newCpuUsage = null;
        try {
            // Get the thread CPU usage and index them by ThreadKey
            final ArrayList<KernelCpuThreadReader.ProcessCpuUsage> processCpuUsages =
                    mReader.getProcessCpuUsage();
            newCpuUsage = createCpuUsageMap(processCpuUsages);
            // If there is no previous CPU usage, return nothing
            if (mPreviousCpuUsage == null) {
                return null;
            }

            // Do diffing and thresholding for each process
            for (int i = 0; i < processCpuUsages.size(); i++) {
                KernelCpuThreadReader.ProcessCpuUsage processCpuUsage = processCpuUsages.get(i);
                changeToDiffs(mPreviousCpuUsage, processCpuUsage);
                applyThresholding(processCpuUsage);
            }
            return processCpuUsages;
        } finally {
            // Always update the previous CPU usage. If we haven't got an update, it will be set to
            // null, so the next call knows there no previous values
            mPreviousCpuUsage = newCpuUsage;
        }
    }

    /** @see KernelCpuThreadReader#getCpuFrequenciesKhz() */
    @Nullable
    public int[] getCpuFrequenciesKhz() {
        return mReader.getCpuFrequenciesKhz();
    }

    /**
     * If a thread has strictly less than {@code minimumTotalCpuUsageMillis} total CPU usage, it
     * will not be reported
     */
    void setMinimumTotalCpuUsageMillis(int minimumTotalCpuUsageMillis) {
        if (minimumTotalCpuUsageMillis < 0) {
            Slog.w(TAG, "Negative minimumTotalCpuUsageMillis: " + minimumTotalCpuUsageMillis);
            return;
        }
        mMinimumTotalCpuUsageMillis = minimumTotalCpuUsageMillis;
    }

    /**
     * Create a map of a thread's identifier to a thread's CPU usage. Used for fast indexing when
     * calculating diffs
     */
    private static Map<ThreadKey, int[]> createCpuUsageMap(
            List<KernelCpuThreadReader.ProcessCpuUsage> processCpuUsages) {
        final Map<ThreadKey, int[]> cpuUsageMap = new ArrayMap<>();
        for (int i = 0; i < processCpuUsages.size(); i++) {
            KernelCpuThreadReader.ProcessCpuUsage processCpuUsage = processCpuUsages.get(i);
            for (int j = 0; j < processCpuUsage.threadCpuUsages.size(); j++) {
                KernelCpuThreadReader.ThreadCpuUsage threadCpuUsage =
                        processCpuUsage.threadCpuUsages.get(j);
                cpuUsageMap.put(
                        new ThreadKey(
                                processCpuUsage.processId,
                                threadCpuUsage.threadId,
                                processCpuUsage.processName,
                                threadCpuUsage.threadName),
                        threadCpuUsage.usageTimesMillis);
            }
        }
        return cpuUsageMap;
    }

    /**
     * Calculate the difference in per-frequency CPU usage for all threads in a process
     *
     * @param previousCpuUsage CPU usage from the last call, the base of the diff
     * @param processCpuUsage CPU usage from the current call, this value is modified to contain the
     *     diffed values
     */
    private static void changeToDiffs(
            Map<ThreadKey, int[]> previousCpuUsage,
            KernelCpuThreadReader.ProcessCpuUsage processCpuUsage) {
        for (int i = 0; i < processCpuUsage.threadCpuUsages.size(); i++) {
            KernelCpuThreadReader.ThreadCpuUsage threadCpuUsage =
                    processCpuUsage.threadCpuUsages.get(i);
            final ThreadKey key =
                    new ThreadKey(
                            processCpuUsage.processId,
                            threadCpuUsage.threadId,
                            processCpuUsage.processName,
                            threadCpuUsage.threadName);
            int[] previous = previousCpuUsage.get(key);
            if (previous == null) {
                // If there's no previous CPU usage, assume that it's zero
                previous = new int[threadCpuUsage.usageTimesMillis.length];
            }
            threadCpuUsage.usageTimesMillis =
                    cpuTimeDiff(threadCpuUsage.usageTimesMillis, previous);
        }
    }

    /**
     * Filter out any threads with less than {@link #mMinimumTotalCpuUsageMillis} total CPU usage
     *
     * <p>The sum of the CPU usage of filtered threads is added under a single thread, labeled with
     * {@link #OTHER_THREADS_ID} and {@link #OTHER_THREADS_NAME}.
     *
     * @param processCpuUsage CPU usage to apply thresholding to, this value is modified to change
     *     the threads it contains
     */
    private void applyThresholding(KernelCpuThreadReader.ProcessCpuUsage processCpuUsage) {
        int[] filteredThreadsCpuUsage = null;
        final ArrayList<KernelCpuThreadReader.ThreadCpuUsage> thresholded = new ArrayList<>();
        for (int i = 0; i < processCpuUsage.threadCpuUsages.size(); i++) {
            KernelCpuThreadReader.ThreadCpuUsage threadCpuUsage =
                    processCpuUsage.threadCpuUsages.get(i);
            if (mMinimumTotalCpuUsageMillis > totalCpuUsage(threadCpuUsage.usageTimesMillis)) {
                if (filteredThreadsCpuUsage == null) {
                    filteredThreadsCpuUsage = new int[threadCpuUsage.usageTimesMillis.length];
                }
                addToCpuUsage(filteredThreadsCpuUsage, threadCpuUsage.usageTimesMillis);
                continue;
            }
            thresholded.add(threadCpuUsage);
        }
        if (filteredThreadsCpuUsage != null) {
            thresholded.add(
                    new KernelCpuThreadReader.ThreadCpuUsage(
                            OTHER_THREADS_ID, OTHER_THREADS_NAME, filteredThreadsCpuUsage));
        }
        processCpuUsage.threadCpuUsages = thresholded;
    }

    /** Get the sum of all CPU usage across all frequencies */
    private static int totalCpuUsage(int[] cpuUsage) {
        int total = 0;
        for (int i = 0; i < cpuUsage.length; i++) {
            total += cpuUsage[i];
        }
        return total;
    }

    /** Add two CPU frequency usages together */
    private static void addToCpuUsage(int[] a, int[] b) {
        for (int i = 0; i < a.length; i++) {
            a[i] += b[i];
        }
    }

    /** Subtract two CPU frequency usages from each other */
    private static int[] cpuTimeDiff(int[] a, int[] b) {
        int[] difference = new int[a.length];
        for (int i = 0; i < a.length; i++) {
            difference[i] = a[i] - b[i];
        }
        return difference;
    }

    /**
     * Identifies a thread
     *
     * <p>Only stores the minimum amount of information to identify a thread. This includes the
     * PID/TID, but as both are recycled as processes/threads end and begin, we also store the hash
     * of the name of the process/thread.
     */
    private static class ThreadKey {
        private final int mProcessId;
        private final int mThreadId;
        private final int mProcessNameHash;
        private final int mThreadNameHash;

        ThreadKey(int processId, int threadId, String processName, String threadName) {
            this.mProcessId = processId;
            this.mThreadId = threadId;
            // Only store the hash to reduce memory consumption
            this.mProcessNameHash = Objects.hash(processName);
            this.mThreadNameHash = Objects.hash(threadName);
        }

        @Override
        public int hashCode() {
            return Objects.hash(mProcessId, mThreadId, mProcessNameHash, mThreadNameHash);
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof ThreadKey)) {
                return false;
            }
            ThreadKey other = (ThreadKey) obj;
            return mProcessId == other.mProcessId
                    && mThreadId == other.mThreadId
                    && mProcessNameHash == other.mProcessNameHash
                    && mThreadNameHash == other.mThreadNameHash;
        }
    }
}
