From eb98a357c39c17f591075c2bed6f3c2791a6cccf Mon Sep 17 00:00:00 2001 From: Lakshman Annadorai Date: Wed, 30 Nov 2022 15:07:24 -0800 Subject: [PATCH 1/2] Read cpusets and max CPU frequencies from devfs and sysfs. Cpuset information and max CPU frequencies don't change after system bootup. So, read them only once during init. If these information are not available, the reader cannot generate CPU info. So, disable the collector when these cannot be read. Unit tests are added in one of the following changes. Test: m Bug: 217422127 Bug: 242722241 Change-Id: Icc201b65fdfe054da5258e8136750d518182d207 (cherry picked from commit 6a9c8e6063dc8503be23f6f9d94b5a04fafe0c09) --- .../com/android/server/cpu/CpuInfoReader.java | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 services/core/java/com/android/server/cpu/CpuInfoReader.java diff --git a/services/core/java/com/android/server/cpu/CpuInfoReader.java b/services/core/java/com/android/server/cpu/CpuInfoReader.java new file mode 100644 index 0000000000000..12ef88b1b67fd --- /dev/null +++ b/services/core/java/com/android/server/cpu/CpuInfoReader.java @@ -0,0 +1,226 @@ +/* + * 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.server.cpu; + +import android.annotation.IntDef; +import android.util.SparseArray; +import android.util.SparseIntArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.utils.Slogf; + +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; + +/** Reader to read CPU information from proc and sys fs files exposed by the Kernel. */ +public final class CpuInfoReader { + static final String TAG = CpuInfoReader.class.getSimpleName(); + 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 MAX_CPUFREQ_FILE = "cpuinfo_max_freq"; + private static final String MAX_SCALING_FREQ_FILE = "scaling_max_freq"; + 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"; + + @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 File mCpuFreqDir; + private final File mProcStatFile; + private final SparseIntArray mCpusetCategoriesByCpus = new SparseIntArray(); + private final SparseArray mMaxCpuFrequenciesByCpus = new SparseArray<>(); + + private File[] mCpuFreqPolicyDirs; + + 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; + } + return true; + } + + 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 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 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 static long readCpuFreqKHz(File file) { + if (!file.exists()) { + Slogf.e(TAG, "CPU frequency file %s doesn't exist", file.getAbsolutePath()); + return 0; + } + try { + List 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; + } + + /** + * Reads the list of CPU cores from the given file. + * + * Reads CPU cores represented in one of the below formats. + *
    + *
  • Single core id. Eg: 1 + *
  • Core id range. Eg: 1-4 + *
  • Comma separated values. Eg: 1, 3-5, 7 + *
+ */ + private static List 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 lines = Files.readAllLines(file.toPath()); + List 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(); + } +} From 9187f3fad7189a7b372642d4a30f32eef50f45f2 Mon Sep 17 00:00:00 2001 From: Lakshman Annadorai Date: Wed, 30 Nov 2022 16:17:13 -0800 Subject: [PATCH 2/2] Read latest CPU usage stats from procfs. Unit tests are added in one of the following changes. Test: m Bug: 217422127 Bug: 242722241 Change-Id: Ie3bac338c1ba5f3fe56e0c7925a21a94fb8bb8c1 (cherry picked from commit 8023f7ccc1f3768ac5c6c5ec1790e78c877f31a3) --- .../com/android/server/cpu/CpuInfoReader.java | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) diff --git a/services/core/java/com/android/server/cpu/CpuInfoReader.java b/services/core/java/com/android/server/cpu/CpuInfoReader.java index 12ef88b1b67fd..680829d2bfc30 100644 --- a/services/core/java/com/android/server/cpu/CpuInfoReader.java +++ b/services/core/java/com/android/server/cpu/CpuInfoReader.java @@ -17,6 +17,9 @@ package com.android.server.cpu; import android.annotation.IntDef; +import android.annotation.Nullable; +import android.system.Os; +import android.system.OsConstants; import android.util.SparseArray; import android.util.SparseIntArray; @@ -30,6 +33,9 @@ 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 { @@ -47,6 +53,14 @@ public final class CpuInfoReader { 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(?[0-9]+)\\s(?[0-9]+)\\s" + + "(?[0-9]+)\\s(?[0-9]+)\\s" + + "(?[0-9]+)\\s(?[0-9]+)\\s" + + "(?[0-9]+)\\s(?[0-9]+)\\s" + + "(?[0-9]+)\\s(?[0-9]+)\\s" + + "(?[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 = { @@ -62,6 +76,8 @@ public final class CpuInfoReader { private final SparseArray mMaxCpuFrequenciesByCpus = new SparseArray<>(); private File[] mCpuFreqPolicyDirs; + private SparseArray mCumulativeCpuUsageStats = new SparseArray<>(); + private boolean mIsEnabled; public CpuInfoReader() { this(new File(CPUSET_DIR_PATH), new File(CPUFREQ_DIR_PATH), new File(PROC_STAT_FILE_PATH)); @@ -99,9 +115,24 @@ public final class CpuInfoReader { mCpuFreqDir.getAbsolutePath()); return false; } + mIsEnabled = true; return true; } + /** Reads CPU information from proc and sys fs files exposed by the Kernel. */ + public List readCpuInfos() { + if (!mIsEnabled) { + return Collections.emptyList(); + } + SparseArray latestCpuUsageStats = readLatestCpuUsageStats(); + if (latestCpuUsageStats == null) { + Slogf.e(TAG, "Failed to read latest CPU usage stats"); + return Collections.emptyList(); + } + // TODO(b/217422127): Read current CPU frequencies and populate the CpuInfo. + return Collections.emptyList(); + } + private void readCpusetCategories() { File[] cpusetDirs = mCpusetDir.listFiles(File::isDirectory); if (cpusetDirs == null) { @@ -223,4 +254,200 @@ public final class CpuInfoReader { } return Collections.emptyList(); } + + @Nullable + private SparseArray readLatestCpuUsageStats() { + SparseArray cumulativeCpuUsageStats = readCumulativeCpuUsageStats(); + if (cumulativeCpuUsageStats.size() == 0) { + Slogf.e(TAG, "Failed to read cumulative CPU usage stats"); + return null; + } + SparseArray 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 readCumulativeCpuUsageStats() { + SparseArray cpuUsageStats = new SparseArray<>(); + try { + List 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(Objects.requireNonNull(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(Objects.requireNonNull(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; + } + } }