| // Copyright 2014 Google Inc. All rights reserved. |
| // |
| // 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.google.devtools.build.lib.actions; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Splitter; |
| import com.google.common.io.Files; |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible; |
| import com.google.devtools.build.lib.util.BlazeClock; |
| import com.google.devtools.build.lib.util.Clock; |
| import com.google.devtools.build.lib.util.LoggingUtil; |
| import com.google.devtools.build.lib.util.ProcMeminfoParser; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.charset.Charset; |
| import java.util.HashSet; |
| import java.util.Set; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| /** |
| * This class estimates the local host's resource capacity. |
| */ |
| @ThreadCompatible |
| public final class LocalHostCapacity { |
| |
| private static final Logger LOG = Logger.getLogger(LocalHostCapacity.class.getName()); |
| |
| /** |
| * Stores parsed /proc/stat CPU time counters. |
| * See {@link LocalHostCapacity#getCpuTimes(String)} for details. |
| */ |
| @Immutable |
| private final static class CpuTimes { |
| private final long idleJiffies; |
| private final long totalJiffies; |
| |
| CpuTimes(long idleJiffies, long totalJiffies) { |
| this.idleJiffies = idleJiffies; |
| this.totalJiffies = totalJiffies; |
| } |
| |
| /** |
| * Return idle CPU ratio using current and previous CPU readings or 0 if |
| * ratio is undefined. |
| */ |
| double getIdleRatio(CpuTimes prevTimes) { |
| if (prevTimes.totalJiffies == 0 || totalJiffies == prevTimes.totalJiffies) { |
| return 0; |
| } |
| return ((double)(idleJiffies - prevTimes.idleJiffies) / |
| (double)(totalJiffies - prevTimes.totalJiffies)); |
| } |
| } |
| |
| /** |
| * Used to store available local CPU and RAM resources information. |
| * See {@link LocalHostCapacity#getFreeResources(FreeResources)} for details. |
| */ |
| public static final class FreeResources { |
| |
| private final Clock clock; |
| private final CpuTimes cpuTimes; |
| private final long lastTimestamp; |
| private final double freeCpu; |
| private final double freeMb; |
| private final long interval; |
| |
| private FreeResources(Clock localClock, ProcMeminfoParser memInfo, String statContent, |
| FreeResources prevStats) { |
| clock = localClock; |
| lastTimestamp = localClock.nanoTime(); |
| freeMb = ProcMeminfoParser.kbToMb(memInfo.getFreeRamKb()); |
| cpuTimes = getCpuTimes(statContent); |
| if (prevStats == null) { |
| interval = 0; |
| freeCpu = 0.0; |
| } else { |
| interval = lastTimestamp - prevStats.lastTimestamp; |
| freeCpu = getLocalHostCapacity().getCpuUsage() * cpuTimes.getIdleRatio(prevStats.cpuTimes); |
| } |
| } |
| |
| /** |
| * Returns amount of available RAM in MB. |
| */ |
| public double getFreeMb() { return freeMb; } |
| |
| /** |
| * Returns average available CPU resources (as a fraction of the CPU core, |
| * so one fully CPU-bound thread should consume exactly 1.0 CPU resource). |
| */ |
| public double getAvgFreeCpu() { return freeCpu; } |
| |
| /** |
| * Returns interval in ms between CPU load measurements used to calculate |
| * average available CPU resources. |
| */ |
| public long getInterval() { return interval / 1000000; } |
| |
| /** |
| * Returns age of available resource data in ms. |
| */ |
| public long getReadingAge() { |
| return (clock.nanoTime() - lastTimestamp) / 1000000; |
| } |
| } |
| |
| // Disables getFreeResources() if error occured during reading or parsing |
| // /proc/* information. |
| @VisibleForTesting |
| static boolean isDisabled; |
| |
| // If /proc/* information is not available, assume 3000 MB and 2 CPUs. |
| private static ResourceSet DEFAULT_RESOURCES = ResourceSet.create(3000.0, 2.0, 1.0, |
| Integer.MAX_VALUE); |
| |
| private LocalHostCapacity() {} |
| |
| /** |
| * Estimates of the local host's resource capacity, |
| * obtained by reading /proc/cpuinfo and /proc/meminfo. |
| */ |
| private static ResourceSet localHostCapacity; |
| |
| /** |
| * Estimates of the local host's resource capacity, |
| * obtained by reading /proc/cpuinfo and /proc/meminfo. |
| */ |
| public static ResourceSet getLocalHostCapacity() { |
| if (localHostCapacity == null) { |
| localHostCapacity = getLocalHostCapacity("/proc/cpuinfo", "/proc/meminfo"); |
| } |
| return localHostCapacity; |
| } |
| |
| /** |
| * Returns new FreeResources object populated with free RAM information from |
| * /proc/meminfo and CPU load information from the /proc/stat. First call |
| * should be made with null parameter to instantiate new FreeResources object. |
| * Subsequent calls will use information inside it to calculate average CPU |
| * load over the time between calls and to calculate amount of free CPU |
| * resources and generate new FreeResources() instance. |
| * |
| * If information is not available due to error, functionality will be disabled |
| * and method will always return null. |
| */ |
| public static FreeResources getFreeResources(FreeResources stats) { |
| return getFreeResources(BlazeClock.instance(), "/proc/meminfo", "/proc/stat", stats); |
| } |
| |
| private static final Splitter NEWLINE_SPLITTER = Splitter.on('\n').omitEmptyStrings(); |
| |
| @VisibleForTesting |
| static int getLogicalCpuCount(String cpuinfoContent) { |
| Iterable<String> lines = NEWLINE_SPLITTER.split(cpuinfoContent); |
| int count = 0; |
| for (String line : lines) { |
| if(line.startsWith("processor")) { |
| count++; |
| } |
| } |
| if (count == 0) { |
| throw new IllegalArgumentException("Can't locate processor in the /proc/cpuinfo"); |
| } |
| return count; |
| } |
| |
| @VisibleForTesting |
| static int getPhysicalCpuCount(String cpuinfoContent, int logicalCpuCount) { |
| Iterable<String> lines = NEWLINE_SPLITTER.split(cpuinfoContent); |
| Set<String> uniq = new HashSet<>(); |
| for (String line : lines) { |
| if(line.startsWith("physical id")) { |
| uniq.add(line); |
| } |
| } |
| int physicalCpuCount = uniq.size(); |
| if (physicalCpuCount == 0) { |
| physicalCpuCount = logicalCpuCount; |
| } |
| return physicalCpuCount; |
| } |
| |
| @VisibleForTesting |
| static int getCoresPerCpu(String cpuinfoFileContent) { |
| Iterable<String> lines = NEWLINE_SPLITTER.split(cpuinfoFileContent); |
| Set<String> uniq = new HashSet<>(); |
| for (String line : lines) { |
| if(line.startsWith("core id")) { |
| uniq.add(line); |
| } |
| } |
| int coresPerCpu = uniq.size(); |
| if (coresPerCpu == 0) { |
| coresPerCpu = 1; |
| } |
| return coresPerCpu; |
| } |
| |
| /** |
| * Parses cpu line of the /proc/stats, calculates number of idle and total |
| * CPU jiffies and returns CpuTimes instance with that information. |
| * |
| * Total CPU time includes <b>all</b> time reported to be spent by the CPUs, |
| * including so-called "stolen" time - time spent by other VMs on the same |
| * workstation. |
| */ |
| private static CpuTimes getCpuTimes(String statContent) { |
| String[] cpuStats = statContent.substring(0, statContent.indexOf('\n')).trim().split(" +"); |
| // Supported versions of /proc/stat (Linux kernel 2.6.x) must contain either |
| // 9 or 10 fields: |
| // "cpu" utime ultime stime idle iowait irq softirq steal(since 2.6.11) 0 |
| // We are interested in total time (sum of all columns) and idle time. |
| if (cpuStats.length < 9 | cpuStats.length > 10) { |
| throw new IllegalArgumentException("Unrecognized /proc/stat format"); |
| } |
| if (!cpuStats[0].equals("cpu")) { |
| throw new IllegalArgumentException("/proc/stat does not start with cpu keyword"); |
| } |
| long idleCpuJiffies = Long.parseLong(cpuStats[4]); // "idle" column. |
| long totalJiffies = 0; |
| for (int i = 1; i < cpuStats.length; i++) { |
| totalJiffies += Long.parseLong(cpuStats[i]); |
| } |
| long totalCpuJiffies = totalJiffies; |
| return new CpuTimes(idleCpuJiffies, totalCpuJiffies); |
| } |
| |
| @VisibleForTesting |
| static ResourceSet getLocalHostCapacity(String cpuinfoFile, String meminfoFile) { |
| try { |
| String cpuinfoContent = readContent(cpuinfoFile); |
| ProcMeminfoParser memInfo = new ProcMeminfoParser(meminfoFile); |
| int logicalCpuCount = getLogicalCpuCount(cpuinfoContent); |
| int physicalCpuCount = getPhysicalCpuCount(cpuinfoContent, logicalCpuCount); |
| int coresPerCpu = getCoresPerCpu(cpuinfoContent); |
| int totalCores = coresPerCpu * physicalCpuCount; |
| boolean hyperthreading = (logicalCpuCount != totalCores); |
| double ramMb = ProcMeminfoParser.kbToMb(memInfo.getTotalKb()); |
| final double EFFECTIVE_CPUS_PER_HYPERTHREADED_CPU = 0.6; |
| return ResourceSet.create( |
| ramMb, |
| logicalCpuCount * (hyperthreading ? EFFECTIVE_CPUS_PER_HYPERTHREADED_CPU : 1.0), |
| 1.0, |
| Integer.MAX_VALUE); |
| } catch (IOException | IllegalArgumentException e) { |
| disableProcFsUse(e); |
| return DEFAULT_RESOURCES; |
| } |
| } |
| |
| @VisibleForTesting |
| static FreeResources getFreeResources(Clock localClock, String meminfoFile, String statFile, |
| FreeResources prevStats) { |
| if (isDisabled) { return null; } |
| try { |
| String statContent = readContent(statFile); |
| return new FreeResources(localClock, new ProcMeminfoParser(meminfoFile), |
| statContent, prevStats); |
| } catch (IOException | IllegalArgumentException e) { |
| disableProcFsUse(e); |
| return null; |
| } |
| } |
| |
| /** |
| * For testing purposes only. Do not use it. |
| */ |
| @VisibleForTesting |
| static void setLocalHostCapacity(ResourceSet resources) { |
| localHostCapacity = resources; |
| isDisabled = false; |
| } |
| |
| private static String readContent(String filename) throws IOException { |
| return Files.toString(new File(filename), Charset.defaultCharset()); |
| } |
| |
| /** |
| * Disables use of /proc filesystem. Called internally when unexpected |
| * exception is caught. |
| */ |
| private static void disableProcFsUse(Throwable cause) { |
| LoggingUtil.logToRemote(Level.WARNING, "Unable to read system load or capacity", cause); |
| LOG.log(Level.WARNING, "Unable to read system load or capacity", cause); |
| isDisabled = true; |
| } |
| } |