/*
 * Copyright (C) 2020 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 android.annotation.Nullable;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.expresslog.Counter;

import java.io.IOException;
import java.util.Arrays;

/**
 * Iterates over all threads owned by a given process, and return the CPU usage for
 * each thread. The CPU usage statistics contain the amount of time spent in a frequency band. CPU
 * usage is collected using {@link ProcTimeInStateReader}.
 */
public class KernelSingleProcessCpuThreadReader {

    private static final String TAG = "KernelSingleProcCpuThreadRdr";

    private static final boolean DEBUG = false;

    private final int mPid;

    private final CpuTimeInStateReader mCpuTimeInStateReader;

    private int[] mSelectedThreadNativeTids = new int[0];  // Sorted

    /**
     * Count of frequencies read from the {@code time_in_state} file.
     */
    private int mFrequencyCount;

    private boolean mIsTracking;

    /**
     * A CPU time-in-state provider for testing.  Imitates the behavior of the corresponding
     * methods in frameworks/native/libs/cputimeinstate/cputimeinstate.c
     */
    @VisibleForTesting
    public interface CpuTimeInStateReader {
        /**
         * Returns the overall number of cluster-frequency combinations.
         */
        int getCpuFrequencyCount();

        /**
         * Returns true to indicate success.
         *
         * Called from native.
         */
        boolean startTrackingProcessCpuTimes(int tgid);

        /**
         * Returns true to indicate success.
         *
         * Called from native.
         */
        boolean startAggregatingTaskCpuTimes(int pid, int aggregationKey);

        /**
         * Must return an array of strings formatted like this:
         * "aggKey:t0_0 t0_1...:t1_0 t1_1..."
         * Times should be provided in nanoseconds.
         *
         * Called from native.
         */
        String[] getAggregatedTaskCpuFreqTimes(int pid);
    }

    /**
     * Create with a path where `proc` is mounted. Used primarily for testing
     *
     * @param pid      PID of the process whose threads are to be read.
     */
    @VisibleForTesting
    public KernelSingleProcessCpuThreadReader(int pid,
            @Nullable CpuTimeInStateReader cpuTimeInStateReader) throws IOException {
        mPid = pid;
        mCpuTimeInStateReader = cpuTimeInStateReader;
    }

    /**
     * Create the reader and handle exceptions during creation
     *
     * @return the reader, null if an exception was thrown during creation
     */
    @Nullable
    public static KernelSingleProcessCpuThreadReader create(int pid) {
        try {
            return new KernelSingleProcessCpuThreadReader(pid, null);
        } catch (IOException e) {
            Slog.e(TAG, "Failed to initialize KernelSingleProcessCpuThreadReader", e);
            return null;
        }
    }

    /**
     * Starts tracking aggregated CPU time-in-state of all threads of the process with the PID
     * supplied in the constructor.
     */
    public void startTrackingThreadCpuTimes() {
        if (!mIsTracking) {
            if (!startTrackingProcessCpuTimes(mPid, mCpuTimeInStateReader)) {
                Slog.wtf(TAG, "Failed to start tracking process CPU times for " + mPid);
                Counter.logIncrement("cpu.value_process_tracking_start_failure_count");
            }
            if (mSelectedThreadNativeTids.length > 0) {
                if (!startAggregatingThreadCpuTimes(mSelectedThreadNativeTids,
                        mCpuTimeInStateReader)) {
                    Slog.wtf(TAG, "Failed to start tracking aggregated thread CPU times for "
                            + Arrays.toString(mSelectedThreadNativeTids));
                    Counter.logIncrement(
                            "cpu.value_aggregated_thread_tracking_start_failure_count");
                }
            }
            mIsTracking = true;
        }
    }

    /**
     * @param nativeTids an array of native Thread IDs whose CPU times should
     *                   be aggregated as a group.  This is expected to be a subset
     *                   of all thread IDs owned by the process.
     */
    public void setSelectedThreadIds(int[] nativeTids) {
        mSelectedThreadNativeTids = nativeTids.clone();
        if (mIsTracking) {
            startAggregatingThreadCpuTimes(mSelectedThreadNativeTids, mCpuTimeInStateReader);
        }
    }

    /**
     * Get the CPU frequencies that correspond to the times reported in {@link ProcessCpuUsage}.
     */
    public int getCpuFrequencyCount() {
        if (mFrequencyCount == 0) {
            mFrequencyCount = getCpuFrequencyCount(mCpuTimeInStateReader);
        }
        return mFrequencyCount;
    }

    /**
     * Get the total CPU usage of the process with the PID specified in the
     * constructor. The CPU usage time is aggregated across all threads and may
     * exceed the time the entire process has been running.
     */
    @Nullable
    public ProcessCpuUsage getProcessCpuUsage() {
        if (DEBUG) {
            Slog.d(TAG, "Reading CPU thread usages for PID " + mPid);
        }

        ProcessCpuUsage processCpuUsage = new ProcessCpuUsage(getCpuFrequencyCount());

        boolean result = readProcessCpuUsage(mPid,
                processCpuUsage.threadCpuTimesMillis,
                processCpuUsage.selectedThreadCpuTimesMillis,
                mCpuTimeInStateReader);
        if (!result) {
            return null;
        }

        if (DEBUG) {
            Slog.d(TAG, "threadCpuTimesMillis = "
                    + Arrays.toString(processCpuUsage.threadCpuTimesMillis));
            Slog.d(TAG, "selectedThreadCpuTimesMillis = "
                    + Arrays.toString(processCpuUsage.selectedThreadCpuTimesMillis));
        }

        return processCpuUsage;
    }

    /** CPU usage of a process, all of its threads and a selected subset of its threads */
    public static class ProcessCpuUsage {
        public long[] threadCpuTimesMillis;
        public long[] selectedThreadCpuTimesMillis;

        public ProcessCpuUsage(int cpuFrequencyCount) {
            threadCpuTimesMillis = new long[cpuFrequencyCount];
            selectedThreadCpuTimesMillis = new long[cpuFrequencyCount];
        }
    }

    private native int getCpuFrequencyCount(CpuTimeInStateReader reader);

    private native boolean startTrackingProcessCpuTimes(int pid, CpuTimeInStateReader reader);

    private native boolean startAggregatingThreadCpuTimes(int[] selectedThreadIds,
            CpuTimeInStateReader reader);

    private native boolean readProcessCpuUsage(int pid,
            long[] threadCpuTimesMillis,
            long[] selectedThreadCpuTimesMillis,
            CpuTimeInStateReader reader);
}
