aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/actions/LocalHostCapacity.java
blob: 5fc6013490112fffaccf4fb3e28b40fdefe938ff (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
// 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 = new ResourceSet(3000.0, 2.0, 1.0);

  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 new ResourceSet(
         ramMb,
         logicalCpuCount * (hyperthreading ? EFFECTIVE_CPUS_PER_HYPERTHREADED_CPU
                                          : 1.0),
         1.0);
    } 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;
  }
}