/*
 * Copyright (C) 2022 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.car.os;

import static com.android.car.os.CarPerformanceService.TAG;

import android.annotation.IntDef;
import android.annotation.Nullable;
import android.car.builtin.util.Slogf;
import android.system.Os;
import android.system.OsConstants;
import android.util.ArrayMap;
import android.util.SparseArray;
import android.util.SparseIntArray;

import com.android.internal.annotations.VisibleForTesting;

import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Reader to read CPU information from proc and sys fs files exposed by the Kernel. */
public final class CpuInfoReader {
    static final int FLAG_CPUSET_CATEGORY_TOP_APP = 1 << 0;
    static final int FLAG_CPUSET_CATEGORY_BACKGROUND = 1 << 1;

    private static final String CPUFREQ_DIR_PATH = "/sys/devices/system/cpu/cpufreq";
    private static final String POLICY_DIR_PREFIX = "policy";
    private static final String RELATED_CPUS_FILE = "related_cpus";
    private static final String AFFECTED_CPUS_FILE = "affected_cpus";
    private static final String CUR_CPUFREQ_FILE = "cpuinfo_cur_freq";
    private static final String MAX_CPUFREQ_FILE = "cpuinfo_max_freq";
    private static final String CUR_SCALING_FREQ_FILE = "scaling_cur_freq";
    private static final String MAX_SCALING_FREQ_FILE = "scaling_max_freq";
    private static final String TIME_IN_STATE_FILE = "stats/time_in_state";
    private static final String CPUSET_DIR_PATH = "/dev/cpuset";
    private static final String CPUSET_TOP_APP_DIR = "top-app";
    private static final String CPUSET_BACKGROUND_DIR = "background";
    private static final String CPUS_FILE = "cpus";
    private static final String PROC_STAT_FILE_PATH = "/proc/stat";
    private static final Pattern PROC_STAT_PATTERN =
            Pattern.compile("cpu(?<core>[0-9]+)\\s(?<userClockTicks>[0-9]+)\\s"
                    + "(?<niceClockTicks>[0-9]+)\\s(?<sysClockTicks>[0-9]+)\\s"
                    + "(?<idleClockTicks>[0-9]+)\\s(?<iowaitClockTicks>[0-9]+)\\s"
                    + "(?<irqClockTicks>[0-9]+)\\s(?<softirqClockTicks>[0-9]+)\\s"
                    + "(?<stealClockTicks>[0-9]+)\\s(?<guestClockTicks>[0-9]+)\\s"
                    + "(?<guestNiceClockTicks>[0-9]+)");
    private static final Pattern TIME_IN_STATE_PATTERN =
            Pattern.compile("(?<freqKHz>[0-9]+)\\s(?<time>[0-9]+)");
    private static final long MILLIS_PER_JIFFY = 1000L / Os.sysconf(OsConstants._SC_CLK_TCK);

    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = {"FLAG_CPUSET_CATEGORY_"}, flag = true, value = {
            FLAG_CPUSET_CATEGORY_TOP_APP,
            FLAG_CPUSET_CATEGORY_BACKGROUND
    })
    private @interface CpusetCategory{}

    private final File mCpusetDir;
    private final SparseIntArray mCpusetCategoriesByCpus = new SparseIntArray();
    private final SparseArray<Long> mMaxCpuFrequenciesByCpus = new SparseArray<>();
    private final ArrayMap<String, ArrayMap<Long, Long>> mTimeInStateByPolicy = new ArrayMap<>();

    private File mCpuFreqDir;
    private File mProcStatFile;
    private File[] mCpuFreqPolicyDirs;
    private SparseArray<CpuUsageStats> mCumulativeCpuUsageStats = new SparseArray<>();
    private boolean mIsEnabled;
    private boolean mHasTimeInStateFile;

    public CpuInfoReader() {
        this(new File(CPUSET_DIR_PATH), new File(CPUFREQ_DIR_PATH), new File(PROC_STAT_FILE_PATH));
    }

    @VisibleForTesting
    CpuInfoReader(File cpusetDir, File cpuFreqDir, File procStatFile) {
        mCpusetDir = cpusetDir;
        mCpuFreqDir = cpuFreqDir;
        mProcStatFile = procStatFile;
    }

    /** Inits CpuInfoReader and returns a boolean to indicate whether the reader is enabled. */
    public boolean init() {
        mCpuFreqPolicyDirs = mCpuFreqDir.listFiles(
                file -> file.isDirectory() && file.getName().startsWith(POLICY_DIR_PREFIX));
        if (mCpuFreqPolicyDirs == null || mCpuFreqPolicyDirs.length == 0) {
            Slogf.w(TAG, "Missing CPU frequency policy directories at %s",
                    mCpuFreqDir.getAbsolutePath());
            return false;
        }
        if (!mProcStatFile.exists()) {
            Slogf.e(TAG, "Missing proc stat file at %s", mProcStatFile.getAbsolutePath());
            return false;
        }
        readCpusetCategories();
        if (mCpusetCategoriesByCpus.size() == 0) {
            Slogf.e(TAG, "Failed to read cpuset information read from %s",
                    mCpusetDir.getAbsolutePath());
            return false;
        }
        readMaxCpuFrequencies();
        if (mMaxCpuFrequenciesByCpus.size() == 0) {
            Slogf.e(TAG, "Failed to read max CPU frequencies from policy directories at %s",
                    mCpuFreqDir.getAbsolutePath());
            return false;
        }
        // The Kernel must be configured to generate the `time_in_state` file. If the Kernel is not
        // configured to generate this file, this file won't be available for the entire system
        // uptime. Thus, check for the presence of this file only during init.
        mHasTimeInStateFile = new File(mCpuFreqPolicyDirs[0], TIME_IN_STATE_FILE).exists();
        mIsEnabled = true;
        return true;
    }

    /** Reads CPU information from proc and sys fs files exposed by the Kernel. */
    public List<CpuInfo> readCpuInfos() {
        if (!mIsEnabled) {
            return Collections.emptyList();
        }
        SparseArray<CpuUsageStats> latestCpuUsageStats = readLatestCpuUsageStats();
        if (latestCpuUsageStats == null) {
            Slogf.e(TAG, "Failed to read latest CPU usage stats");
            return Collections.emptyList();
        }
        SparseArray<Long> cpuFrequenciesByCpus = readCurrentCpuFrequencies();
        List<CpuInfo> cpuInfos = new ArrayList<>();
        for (int i = 0; i < cpuFrequenciesByCpus.size(); i++) {
            int cpu = cpuFrequenciesByCpus.keyAt(i);
            long curFrequency = cpuFrequenciesByCpus.valueAt(i);
            if (!mMaxCpuFrequenciesByCpus.contains(cpu) || !latestCpuUsageStats.contains(cpu)) {
                Slogf.w(TAG, "Missing max CPU frequency or CPU usage stats for CPU core %d", cpu);
                continue;
            }
            int cpuCategories = mCpusetCategoriesByCpus.get(cpu, -1);
            if (cpuCategories == -1) {
                Slogf.w(TAG, "Missing cpuset information for CPU core %d", cpu);
                continue;
            }
            cpuInfos.add(new CpuInfo(cpu, cpuCategories, curFrequency,
                    mMaxCpuFrequenciesByCpus.get(cpu), latestCpuUsageStats.get(cpu)));
        }
        return cpuInfos;
    }

    @VisibleForTesting
    void setCpuFreqDir(File cpuFreqDir) {
        File[] cpuFreqPolicyDirs = cpuFreqDir.listFiles(
                file -> file.isDirectory() && file.getName().startsWith(POLICY_DIR_PREFIX));
        if (mCpuFreqPolicyDirs == null || mCpuFreqPolicyDirs.length == 0) {
            Slogf.w(TAG, "Failed to set CPU frequency directory. Missing policy directories at %s",
                    mCpuFreqDir.getAbsolutePath());
            return;
        }
        mCpuFreqDir = cpuFreqDir;
        mCpuFreqPolicyDirs = cpuFreqPolicyDirs;
        Slogf.i(TAG, "Set CPU frequency directory to %s", cpuFreqDir.getAbsolutePath());
    }

    @VisibleForTesting
    void setProcStatFile(File procStatFile) {
        mProcStatFile = procStatFile;
        Slogf.i(TAG, "Set proc stat file to %s", procStatFile.getAbsolutePath());
    }

    private void readCpusetCategories() {
        File[] cpusetDirs = mCpusetDir.listFiles(File::isDirectory);
        if (cpusetDirs == null) {
            Slogf.e(TAG, "Missing cpuset directories at %s", mCpusetDir.getAbsolutePath());
            return;
        }
        for (int i = 0; i < cpusetDirs.length; i++) {
            File dir = cpusetDirs[i];
            @CpusetCategory int cpusetCategory;
            switch (dir.getName()) {
                case CPUSET_TOP_APP_DIR:
                    cpusetCategory = FLAG_CPUSET_CATEGORY_TOP_APP;
                    break;
                case CPUSET_BACKGROUND_DIR:
                    cpusetCategory = FLAG_CPUSET_CATEGORY_BACKGROUND;
                    break;
                default:
                    continue;
            }
            File cpuCoresFile = new File(dir.getPath(), CPUS_FILE);
            List<Integer> cpuCores = readCpuCores(cpuCoresFile);
            if (cpuCores.isEmpty()) {
                Slogf.e(TAG, "Failed to read CPU cores from %s", cpuCoresFile.getAbsolutePath());
                continue;
            }
            for (int j = 0; j < cpuCores.size(); j++) {
                int categories = mCpusetCategoriesByCpus.get(cpuCores.get(j));
                categories |= cpusetCategory;
                mCpusetCategoriesByCpus.append(cpuCores.get(j), categories);
            }
        }
    }

    private void readMaxCpuFrequencies() {
        for (int i = 0; i < mCpuFreqPolicyDirs.length; i++) {
            File policyDir = mCpuFreqPolicyDirs[i];
            long maxCpuFreqKHz = readMaxCpuFrequency(policyDir);
            if (maxCpuFreqKHz == 0) {
                Slogf.w(TAG, "Invalid max CPU frequency read from %s", policyDir.getAbsolutePath());
                continue;
            }
            File cpuCoresFile = new File(policyDir, RELATED_CPUS_FILE);
            List<Integer> cpuCores = readCpuCores(cpuCoresFile);
            if (cpuCores.isEmpty()) {
                Slogf.e(TAG, "Failed to read CPU cores from %s", cpuCoresFile.getAbsolutePath());
                continue;
            }
            for (int j = 0; j < cpuCores.size(); j++) {
                mMaxCpuFrequenciesByCpus.append(cpuCores.get(j), maxCpuFreqKHz);
            }
        }
    }

    private long readMaxCpuFrequency(File policyDir) {
        long curCpuFreqKHz = readCpuFreqKHz(new File(policyDir, MAX_CPUFREQ_FILE));
        return curCpuFreqKHz > 0 ? curCpuFreqKHz
                : readCpuFreqKHz(new File(policyDir, MAX_SCALING_FREQ_FILE));
    }

    private SparseArray<Long> readCurrentCpuFrequencies() {
        SparseArray<Long> curCpuFrequenciesByCpus = new SparseArray<>();
        for (int i = 0; i < mCpuFreqPolicyDirs.length; i++) {
            File policyDir = mCpuFreqPolicyDirs[i];
            long curCpuFreqKHz = readCurrentCpuFrequency(policyDir);
            if (curCpuFreqKHz == 0) {
                Slogf.w(TAG, "Missing current frequency information at %s",
                        policyDir.getAbsolutePath());
                continue;
            }
            File cpuCoresFile = new File(policyDir, AFFECTED_CPUS_FILE);
            List<Integer> cpuCores = readCpuCores(cpuCoresFile);
            if (cpuCores.isEmpty()) {
                Slogf.e(TAG, "Failed to read CPU cores from %s", cpuCoresFile.getAbsolutePath());
                continue;
            }
            for (int j = 0; j < cpuCores.size(); j++) {
                curCpuFrequenciesByCpus.append(cpuCores.get(j), curCpuFreqKHz);
            }
        }
        return curCpuFrequenciesByCpus;
    }

    private long readCurrentCpuFrequency(File policyDir) {
        ArrayMap<Long, Long> latestTimeInState = readTimeInState(policyDir);
        if (latestTimeInState == null) {
            long curCpuFreqKHz = readCpuFreqKHz(new File(policyDir, CUR_CPUFREQ_FILE));
            return curCpuFreqKHz > 0 ? curCpuFreqKHz :
                    readCpuFreqKHz(new File(policyDir, CUR_SCALING_FREQ_FILE));
        }
        String policyDirName = policyDir.getName();
        if (mTimeInStateByPolicy.containsKey(policyDirName)) {
            ArrayMap<Long, Long> prevTimeInState = mTimeInStateByPolicy.get(policyDirName);
            ArrayMap<Long, Long> deltaTimeInState =
                    calculateDeltaTimeInState(prevTimeInState, latestTimeInState);
            mTimeInStateByPolicy.put(policyDirName, latestTimeInState);
            return calculateAvgCpuFreq(deltaTimeInState);
        }
        mTimeInStateByPolicy.put(policyDirName, latestTimeInState);
        return calculateAvgCpuFreq(latestTimeInState);
    }

    @Nullable
    private ArrayMap<Long, Long> readTimeInState(File policyDir) {
        if (!mHasTimeInStateFile) {
            return null;
        }
        File timeInStateFile = new File(policyDir, TIME_IN_STATE_FILE);
        try {
            List<String> lines = Files.readAllLines(timeInStateFile.toPath());
            if (lines.isEmpty()) {
                Slogf.w(TAG, "Empty time in state file at %s", timeInStateFile.getAbsolutePath());
                return null;
            }
            ArrayMap<Long, Long> cpuTimeByFrequencies = new ArrayMap<>();
            for (int i = 0; i < lines.size(); i++) {
                Matcher m = TIME_IN_STATE_PATTERN.matcher(lines.get(i).trim());
                if (!m.find()) {
                    continue;
                }
                cpuTimeByFrequencies.put(Long.parseLong(m.group("freqKHz")),
                        jiffyStrToMillis(m.group("time")));
            }
            return cpuTimeByFrequencies;
        } catch (Exception e) {
            Slogf.e(TAG, e, "Failed to read CPU time in state from file: %s",
                    timeInStateFile.getAbsolutePath());
        }
        return null;
    }

    private static long readCpuFreqKHz(File file) {
        if (!file.exists()) {
            Slogf.e(TAG, "CPU frequency file %s doesn't exist", file.getAbsolutePath());
            return 0;
        }
        try {
            List<String> lines = Files.readAllLines(file.toPath());
            if (!lines.isEmpty()) {
                long frequency = Long.parseLong(lines.get(0).trim());
                return frequency > 0 ? frequency : 0;
            }
        } catch (Exception e) {
            Slogf.e(TAG, e, "Failed to read integer content from file: %s", file.getAbsolutePath());
        }
        return 0;
    }

    private static ArrayMap<Long, Long> calculateDeltaTimeInState(
            ArrayMap<Long, Long> prevTimeInState, ArrayMap<Long, Long> latestTimeInState) {
        ArrayMap<Long, Long> deltaTimeInState = new ArrayMap();
        for (int i = 0; i < latestTimeInState.size(); i++) {
            long freq = latestTimeInState.keyAt(i);
            long durationMillis = latestTimeInState.valueAt(i);
            long deltaDurationMillis;
            if (prevTimeInState.containsKey(freq)) {
                long prevDurationMillis = prevTimeInState.get(freq);
                deltaDurationMillis = durationMillis > prevDurationMillis
                        ? (durationMillis - prevDurationMillis) : durationMillis;
            } else {
                deltaDurationMillis = durationMillis;
            }
            deltaTimeInState.put(freq, deltaDurationMillis);
        }
        return deltaTimeInState;
    }

    private static long calculateAvgCpuFreq(ArrayMap<Long, Long> timeInState) {
        double totalTimeInState = 0;
        for (int i = 0; i < timeInState.size(); i++) {
            totalTimeInState += timeInState.valueAt(i);
        }
        double avgFreqKHz = 0;
        for (int i = 0; i < timeInState.size(); i++) {
            avgFreqKHz += (timeInState.keyAt(i) * timeInState.valueAt(i)) / totalTimeInState;
        }
        return (long) avgFreqKHz;
    }

    /**
     * Reads the list of CPU cores from the given file.
     *
     * Reads CPU cores represented in one of the below formats.
     * <ul>
     * <li> Single core id. Eg: 1
     * <li> Core id range. Eg: 1-4
     * <li> Comma separated values. Eg: 1, 3-5, 7
     * </ul>
     */
    private static List<Integer> readCpuCores(File file) {
        if (!file.exists()) {
            Slogf.e(TAG, "Failed to read CPU cores as the file '%s' doesn't exist",
                    file.getAbsolutePath());
            return Collections.emptyList();
        }
        try {
            List<String> lines = Files.readAllLines(file.toPath());
            List<Integer> cpuCores = new ArrayList<>();
            for (int i = 0; i < lines.size(); i++) {
                String[] pairs = lines.get(i).trim().split(",");
                for (int j = 0; j < pairs.length; j++) {
                    String[] minMaxPairs = pairs[j].split("-");
                    if (minMaxPairs.length >= 2) {
                        int min = Integer.parseInt(minMaxPairs[0]);
                        int max = Integer.parseInt(minMaxPairs[1]);
                        if (min > max) {
                            continue;
                        }
                        for (int id = min; id <= max; id++) {
                            cpuCores.add(id);
                        }
                    } else if (minMaxPairs.length == 1) {
                        cpuCores.add(Integer.parseInt(minMaxPairs[0]));
                    } else {
                        Slogf.w(TAG, "Invalid CPU core range format %s", pairs[j]);
                    }
                }
            }
            return cpuCores;
        } catch (Exception e) {
            Slogf.e(TAG, e, "Failed to read CPU cores from %s", file.getAbsolutePath());
        }
        return Collections.emptyList();
    }

    @Nullable
    private SparseArray<CpuUsageStats> readLatestCpuUsageStats() {
        SparseArray<CpuUsageStats> cumulativeCpuUsageStats = readCumulativeCpuUsageStats();
        if (cumulativeCpuUsageStats.size() == 0) {
            Slogf.e(TAG, "Failed to read cumulative CPU usage stats");
            return null;
        }
        SparseArray<CpuUsageStats> deltaCpuUsageStats = new SparseArray();
        for (int i = 0; i < cumulativeCpuUsageStats.size(); i++) {
            int cpu = cumulativeCpuUsageStats.keyAt(i);
            CpuUsageStats newStats = cumulativeCpuUsageStats.valueAt(i);
            CpuUsageStats oldStats = mCumulativeCpuUsageStats.get(cpu);
            deltaCpuUsageStats.append(cpu, oldStats == null ? newStats : newStats.delta(oldStats));
        }
        mCumulativeCpuUsageStats = cumulativeCpuUsageStats;
        return deltaCpuUsageStats;
    }

    private SparseArray<CpuUsageStats> readCumulativeCpuUsageStats() {
        SparseArray<CpuUsageStats> cpuUsageStats = new SparseArray<>();
        try {
            List<String> lines = Files.readAllLines(mProcStatFile.toPath());
            for (int i = 0; i < lines.size(); i++) {
                Matcher m = PROC_STAT_PATTERN.matcher(lines.get(i).trim());
                if (!m.find()) {
                    continue;
                }
                cpuUsageStats.append(Integer.parseInt(m.group("core")),
                        new CpuUsageStats(jiffyStrToMillis(m.group("userClockTicks")),
                                jiffyStrToMillis(m.group("niceClockTicks")),
                                jiffyStrToMillis(m.group("sysClockTicks")),
                                jiffyStrToMillis(m.group("idleClockTicks")),
                                jiffyStrToMillis(m.group("iowaitClockTicks")),
                                jiffyStrToMillis(m.group("irqClockTicks")),
                                jiffyStrToMillis(m.group("softirqClockTicks")),
                                jiffyStrToMillis(m.group("stealClockTicks")),
                                jiffyStrToMillis(m.group("guestClockTicks")),
                                jiffyStrToMillis(m.group("guestNiceClockTicks"))));
            }
        } catch (Exception e) {
            Slogf.e(TAG, e, "Failed to read cpu usage stats from %s",
                    mProcStatFile.getAbsolutePath());
        }
        return cpuUsageStats;
    }

    private static long jiffyStrToMillis(String jiffyStr) {
        return Long.parseLong(jiffyStr) * MILLIS_PER_JIFFY;
    }

    /** Contains information for each CPU core on the system. */
    public static final class CpuInfo {
        public final int cpuCore;
        public final @CpusetCategory int cpusetCategories;
        public final long curCpuFreqKHz;
        public final long maxCpuFreqKHz;
        public final CpuUsageStats latestCpuUsageStats;

        CpuInfo(int cpuCore, @CpusetCategory int cpusetCategories, long curCpuFreqKHz,
                long maxCpuFreqKHz, CpuUsageStats latestCpuUsageStats) {
            this.cpuCore = cpuCore;
            this.cpusetCategories = cpusetCategories;
            this.curCpuFreqKHz = curCpuFreqKHz;
            this.maxCpuFreqKHz = maxCpuFreqKHz;
            this.latestCpuUsageStats = latestCpuUsageStats;
        }

        @Override
        public String toString() {
            return new StringBuilder("CpuInfo{ cpuCore = ").append(cpuCore)
                    .append(", cpusetCategories = ").append(cpusetCategories)
                    .append(", curCpuFreqKHz = ").append(curCpuFreqKHz)
                    .append(", maxCpuFreqKHz = ").append(maxCpuFreqKHz)
                    .append(", latestCpuUsageStats = ").append(latestCpuUsageStats)
                    .append(" }").toString();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof CpuInfo)) {
                return false;
            }
            CpuInfo other = (CpuInfo) obj;
            return cpuCore == other.cpuCore && cpusetCategories == other.cpusetCategories
                    && curCpuFreqKHz == other.curCpuFreqKHz
                    && maxCpuFreqKHz == other.maxCpuFreqKHz
                    && latestCpuUsageStats.equals(other.latestCpuUsageStats);
        }

        @Override
        public int hashCode() {
            return Objects.hash(cpuCore, cpusetCategories, curCpuFreqKHz, maxCpuFreqKHz,
                    latestCpuUsageStats);
        }
    }

    /** CPU time spent in different modes. */
    public static final class CpuUsageStats {
        public final long userTimeMillis;
        public final long niceTimeMillis;
        public final long systemTimeMillis;
        public final long idleTimeMillis;
        public final long iowaitTimeMillis;
        public final long irqTimeMillis;
        public final long softirqTimeMillis;
        public final long stealTimeMillis;
        public final long guestTimeMillis;
        public final long guestNiceTimeMillis;

        public CpuUsageStats(long userTimeMillis, long niceTimeMillis, long systemTimeMillis,
                long idleTimeMillis, long iowaitTimeMillis, long irqTimeMillis,
                long softirqTimeMillis, long stealTimeMillis, long guestTimeMillis,
                long guestNiceTimeMillis) {
            this.userTimeMillis = userTimeMillis;
            this.niceTimeMillis = niceTimeMillis;
            this.systemTimeMillis = systemTimeMillis;
            this.idleTimeMillis = idleTimeMillis;
            this.iowaitTimeMillis = iowaitTimeMillis;
            this.irqTimeMillis = irqTimeMillis;
            this.softirqTimeMillis = softirqTimeMillis;
            this.stealTimeMillis = stealTimeMillis;
            this.guestTimeMillis = guestTimeMillis;
            this.guestNiceTimeMillis = guestNiceTimeMillis;
        }

        public long getTotalTime() {
            return userTimeMillis + niceTimeMillis + systemTimeMillis + idleTimeMillis
                    + iowaitTimeMillis + irqTimeMillis + softirqTimeMillis + stealTimeMillis
                    + guestTimeMillis + guestNiceTimeMillis;
        }

        @Override
        public String toString() {
            return new StringBuilder("CpuUsageStats{ userTimeMillis = ")
                    .append(userTimeMillis)
                    .append(", niceTimeMillis = ").append(niceTimeMillis)
                    .append(", systemTimeMillis = ").append(systemTimeMillis)
                    .append(", idleTimeMillis = ").append(idleTimeMillis)
                    .append(", iowaitTimeMillis = ").append(iowaitTimeMillis)
                    .append(", irqTimeMillis = ").append(irqTimeMillis)
                    .append(", softirqTimeMillis = ").append(softirqTimeMillis)
                    .append(", stealTimeMillis = ").append(stealTimeMillis)
                    .append(", guestTimeMillis = ").append(guestTimeMillis)
                    .append(", guestNiceTimeMillis = ").append(guestNiceTimeMillis)
                    .append(" }").toString();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof CpuUsageStats)) {
                return false;
            }
            CpuUsageStats other = (CpuUsageStats) obj;
            return userTimeMillis == other.userTimeMillis && niceTimeMillis == other.niceTimeMillis
                    && systemTimeMillis == other.systemTimeMillis
                    && idleTimeMillis == other.idleTimeMillis
                    && iowaitTimeMillis == other.iowaitTimeMillis
                    && irqTimeMillis == other.irqTimeMillis
                    && softirqTimeMillis == other.softirqTimeMillis
                    && stealTimeMillis == other.stealTimeMillis
                    && guestTimeMillis == other.guestTimeMillis
                    && guestNiceTimeMillis == other.guestNiceTimeMillis;
        }

        @Override
        public int hashCode() {
            return Objects.hash(userTimeMillis, niceTimeMillis, systemTimeMillis, idleTimeMillis,
                    iowaitTimeMillis, irqTimeMillis, softirqTimeMillis, stealTimeMillis,
                    guestTimeMillis,
                    guestNiceTimeMillis);
        }

        CpuUsageStats delta(CpuUsageStats rhs) {
            return new CpuUsageStats(diff(userTimeMillis, rhs.userTimeMillis),
                    diff(niceTimeMillis, rhs.niceTimeMillis),
                    diff(systemTimeMillis, rhs.systemTimeMillis),
                    diff(idleTimeMillis, rhs.idleTimeMillis),
                    diff(iowaitTimeMillis, rhs.iowaitTimeMillis),
                    diff(irqTimeMillis, rhs.irqTimeMillis),
                    diff(softirqTimeMillis, rhs.softirqTimeMillis),
                    diff(stealTimeMillis, rhs.stealTimeMillis),
                    diff(guestTimeMillis, rhs.guestTimeMillis),
                    diff(guestNiceTimeMillis, rhs.guestNiceTimeMillis));
        }

        private static long diff(long lhs, long rhs) {
            return lhs > rhs ? lhs - rhs : 0;
        }
    }
}
